@@ -12,8 +12,9 @@ use tower_lsp_server::jsonrpc::Result;
1212use tower_lsp_server:: ls_types:: * ;
1313
1414use crate :: lsp:: DocumentState ;
15+ use crate :: lsp:: symbols:: { SymbolTarget , resolve_symbol_target_at_offset} ;
1516use crate :: metadata:: inline_reference_contains;
16- use crate :: syntax:: { AstNode , FootnoteDefinition } ;
17+ use crate :: syntax:: { AstNode , Document , FootnoteDefinition , Heading } ;
1718
1819use super :: super :: { conversions, helpers} ;
1920
@@ -60,10 +61,37 @@ pub(crate) async fn hover(
6061 crate :: salsa:: metadata ( & * db, salsa_file, salsa_config, doc_path. clone ( ) ) . clone ( )
6162 } ;
6263
63- let pending_footnote = {
64+ let target = {
6465 let root = ctx. syntax_root ( ) ;
66+ resolve_symbol_target_at_offset ( & root, offset)
67+ } ;
68+
69+ if let Some ( SymbolTarget :: HeadingLink ( label) ) = target. as_ref ( ) {
70+ let doc_indices = crate :: lsp:: navigation:: project_symbol_documents (
71+ & salsa_db,
72+ salsa_file,
73+ salsa_config,
74+ & doc_path,
75+ uri,
76+ & content_for_offset,
77+ )
78+ . await ;
79+
80+ for doc in & doc_indices {
81+ if let Some ( markdown) = section_hover_markdown ( doc, label) {
82+ return Ok ( Some ( Hover {
83+ contents : HoverContents :: Markup ( MarkupContent {
84+ kind : MarkupKind :: Markdown ,
85+ value : markdown,
86+ } ) ,
87+ range : None ,
88+ } ) ) ;
89+ }
90+ }
91+ }
6592
66- // Find the node at this offset
93+ let pending_footnote = {
94+ let root = ctx. syntax_root ( ) ;
6795 let Some ( mut node) = helpers:: find_node_at_offset ( & root, offset) else {
6896 return Ok ( None ) ;
6997 } ;
@@ -157,6 +185,102 @@ pub(crate) async fn hover(
157185 Ok ( None )
158186}
159187
188+ const HOVER_PREVIEW_MAX_CHARS : usize = 180 ;
189+
190+ fn section_hover_markdown (
191+ doc : & crate :: lsp:: navigation:: IndexedDocument ,
192+ label : & str ,
193+ ) -> Option < String > {
194+ let heading_range = first_heading_definition_range ( & doc. symbol_index , label) ?;
195+ let tree = crate :: parse ( & doc. text , None ) ;
196+ let document = Document :: cast ( tree) ?;
197+
198+ let blocks: Vec < _ > = document. blocks ( ) . collect ( ) ;
199+ let heading_idx = blocks. iter ( ) . position ( |node| {
200+ node. kind ( ) == crate :: syntax:: SyntaxKind :: HEADING && node. text_range ( ) == heading_range
201+ } ) ?;
202+ let heading_node = & blocks[ heading_idx] ;
203+ let heading = Heading :: cast ( heading_node. clone ( ) ) ?;
204+ let title = heading. title_or ( "(empty)" ) ;
205+ let section_end = section_end_offset ( & doc. symbol_index , heading_range, doc. text . len ( ) ) ;
206+
207+ let mut preview = None ;
208+ for block in blocks. iter ( ) . skip ( heading_idx + 1 ) {
209+ let start: usize = block. text_range ( ) . start ( ) . into ( ) ;
210+ if start >= section_end {
211+ break ;
212+ }
213+ let normalized = normalize_preview_text ( block. text ( ) . to_string ( ) . trim ( ) ) ;
214+ if !normalized. is_empty ( ) {
215+ preview = Some ( crop_preview ( & normalized, HOVER_PREVIEW_MAX_CHARS ) ) ;
216+ break ;
217+ }
218+ }
219+
220+ let markdown = match preview {
221+ Some ( snippet) => format ! ( "**Section:** {}\n \n {}" , title, snippet) ,
222+ None => format ! ( "**Section:** {}" , title) ,
223+ } ;
224+ Some ( markdown)
225+ }
226+
227+ fn first_heading_definition_range (
228+ index : & crate :: salsa:: SymbolUsageIndex ,
229+ label : & str ,
230+ ) -> Option < rowan:: TextRange > {
231+ let mut all = Vec :: new ( ) ;
232+ if let Some ( ranges) = index. heading_explicit_definition_ranges ( label) {
233+ all. extend ( ranges. iter ( ) . copied ( ) ) ;
234+ }
235+ if let Some ( ranges) = index. heading_implicit_definition_ranges ( label) {
236+ all. extend ( ranges. iter ( ) . copied ( ) ) ;
237+ }
238+ all. into_iter ( ) . min_by_key ( |range| range. start ( ) )
239+ }
240+
241+ fn section_end_offset (
242+ index : & crate :: salsa:: SymbolUsageIndex ,
243+ heading_range : rowan:: TextRange ,
244+ text_len : usize ,
245+ ) -> usize {
246+ let Some ( ( at, level) ) = index
247+ . heading_sequence ( )
248+ . iter ( )
249+ . enumerate ( )
250+ . find_map ( |( idx, ( range, lvl) ) | ( * range == heading_range) . then_some ( ( idx, * lvl) ) )
251+ else {
252+ return text_len;
253+ } ;
254+
255+ index
256+ . heading_sequence ( )
257+ . iter ( )
258+ . skip ( at + 1 )
259+ . find_map ( |( next_range, next_level) | ( * next_level <= level) . then_some ( next_range. start ( ) ) )
260+ . map ( Into :: < usize > :: into)
261+ . unwrap_or ( text_len)
262+ }
263+
264+ fn normalize_preview_text ( text : & str ) -> String {
265+ text. split_whitespace ( ) . collect :: < Vec < _ > > ( ) . join ( " " )
266+ }
267+
268+ fn crop_preview ( text : & str , max_chars : usize ) -> String {
269+ let mut iter = text. chars ( ) ;
270+ let mut out = String :: new ( ) ;
271+ for _ in 0 ..max_chars {
272+ if let Some ( ch) = iter. next ( ) {
273+ out. push ( ch) ;
274+ } else {
275+ return out;
276+ }
277+ }
278+ if iter. next ( ) . is_some ( ) {
279+ out. push_str ( "..." ) ;
280+ }
281+ out
282+ }
283+
160284/// Format a bibliography entry for hover display.
161285///
162286/// Works with any bibliography format (BibTeX, CSL-JSON, CSL-YAML, RIS).
0 commit comments