Skip to main content

gruel_lsp/
hover.rs

1//! Hover (ADR-0091 Phase 3).
2//!
3//! Given an LSP `Position`, find the smallest AST item whose span contains
4//! the position and return a markdown hover with the item's signature
5//! followed by its `///` docstring (rendered through `gruel_doc`).
6//!
7//! Phase 3 covers top-level items (`fn`, `struct`, `enum`, `interface`,
8//! `derive`, `const`, fields, variants, methods, parameters, type
9//! references). Hover for arbitrary expressions / locals lands in Phase 4
10//! when sema's expr-type side-table is wired up.
11
12use gruel_compiler::{Type, TypeInternPool};
13use gruel_parser::ast::{
14    Ast, ConstDecl, DeriveDecl, EnumDecl, EnumVariant, EnumVariantKind, FieldDecl, Function, Ident,
15    InterfaceDecl, Item, LinkExternBlock, Method, MethodSig, Param, StructDecl, TypeExpr,
16};
17use gruel_util::Span;
18use lasso::ThreadedRodeo;
19use rustc_hash::FxHashMap;
20
21/// Hover content rendered to Markdown (plus the AST node's span for the
22/// returned LSP `Hover.range` field).
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct HoverContent {
25    pub markdown: String,
26    pub span: Span,
27}
28
29/// Find an item under the cursor and produce hover content for it.
30///
31/// `file_id` constrains the search to one file: items from other files
32/// (via the merged AST) are skipped because their spans live in another
33/// `FileId` namespace.
34pub fn hover_at(
35    ast: &Ast,
36    interner: &ThreadedRodeo,
37    file_id: gruel_util::FileId,
38    byte: u32,
39) -> Option<HoverContent> {
40    let target = SmallestSpanFinder::new(file_id, byte).find(ast)?;
41    target.render(interner)
42}
43
44/// Like [`hover_at`], but also consults the AIR expression-type side
45/// table. If the cursor is inside an expression whose type sema computed,
46/// we return the type as fallback hover content (Phase 4).
47pub fn hover_at_with_expr_types(
48    ast: &Ast,
49    interner: &ThreadedRodeo,
50    expr_types: &FxHashMap<Span, Type>,
51    type_pool: Option<&TypeInternPool>,
52    file_id: gruel_util::FileId,
53    byte: u32,
54) -> Option<HoverContent> {
55    if let Some(content) = hover_at(ast, interner, file_id, byte) {
56        return Some(content);
57    }
58    // Fall back to the smallest span in `expr_types` covering `byte`.
59    let mut best: Option<(Span, Type)> = None;
60    for (span, ty) in expr_types {
61        if span.file_id != file_id {
62            continue;
63        }
64        if byte < span.start || byte >= span.end {
65            continue;
66        }
67        if best.map_or(true, |(b, _)| span.end - span.start < b.end - b.start) {
68            best = Some((*span, *ty));
69        }
70    }
71    let (span, ty) = best?;
72    let display = type_pool
73        .map(|p| p.format_type_name(ty))
74        .unwrap_or_else(|| format!("{:?}", ty));
75    Some(HoverContent {
76        markdown: format!("```gruel\n{}\n```", display),
77        span,
78    })
79}
80
81/// Visitor that finds the smallest AST node whose span contains a target
82/// byte. Walks declaratively over a single AST; returns the most specific
83/// match.
84struct SmallestSpanFinder {
85    file_id: gruel_util::FileId,
86    byte: u32,
87    best: Option<HoverTarget>,
88    best_size: u32,
89}
90
91/// The kind of AST node the finder landed on. Hover rendering reads this.
92#[derive(Debug, Clone)]
93enum HoverTarget {
94    Function(Function),
95    Struct(StructDecl),
96    Enum(EnumDecl),
97    Interface(InterfaceDecl),
98    Derive(DeriveDecl),
99    Const(ConstDecl),
100    LinkExtern(LinkExternBlock),
101    Field(FieldDecl),
102    EnumVariant(EnumVariant),
103    Method(Method),
104    MethodSig(MethodSig),
105    Param(Param),
106    /// A type reference (e.g. `i32` in a parameter list). Rendered as
107    /// `: T` where T is the type display string.
108    TypeRef(TypeExpr),
109    /// Identifier reference (Phase 4 will resolve to a definition; Phase 3
110    /// just emits the bare identifier name).
111    Identifier(Ident),
112}
113
114impl SmallestSpanFinder {
115    fn new(file_id: gruel_util::FileId, byte: u32) -> Self {
116        Self {
117            file_id,
118            byte,
119            best: None,
120            best_size: u32::MAX,
121        }
122    }
123
124    fn find(mut self, ast: &Ast) -> Option<HoverTarget> {
125        for item in &ast.items {
126            self.visit_item(item);
127        }
128        self.best
129    }
130
131    fn span_matches(&self, span: Span) -> bool {
132        span.file_id == self.file_id && self.byte >= span.start && self.byte < span.end
133    }
134
135    fn consider(&mut self, span: Span, target: HoverTarget) {
136        if !self.span_matches(span) {
137            return;
138        }
139        let size = span.end.saturating_sub(span.start);
140        if size <= self.best_size {
141            self.best_size = size;
142            self.best = Some(target);
143        }
144    }
145
146    fn visit_item(&mut self, item: &Item) {
147        match item {
148            Item::Function(f) => {
149                self.consider(f.span, HoverTarget::Function(f.clone()));
150                // Re-record the function on its name's span specifically so
151                // hovering the name itself wins over (and is shorter than)
152                // the larger body span — but renders the same content.
153                self.consider(f.name.span, HoverTarget::Function(f.clone()));
154                for p in &f.params {
155                    self.visit_param(p);
156                }
157                if let Some(rt) = &f.return_type {
158                    self.visit_type_expr(rt);
159                }
160            }
161            Item::Struct(s) => {
162                self.consider(s.span, HoverTarget::Struct(s.clone()));
163                self.consider(s.name.span, HoverTarget::Struct(s.clone()));
164                for field in &s.fields {
165                    self.visit_field(field);
166                }
167                for m in &s.methods {
168                    self.visit_method(m);
169                }
170            }
171            Item::Enum(e) => {
172                self.consider(e.span, HoverTarget::Enum(e.clone()));
173                self.consider(e.name.span, HoverTarget::Enum(e.clone()));
174                for v in &e.variants {
175                    self.visit_variant(v);
176                }
177                for m in &e.methods {
178                    self.visit_method(m);
179                }
180            }
181            Item::Interface(i) => {
182                self.consider(i.span, HoverTarget::Interface(i.clone()));
183                self.consider(i.name.span, HoverTarget::Interface(i.clone()));
184                for sig in &i.methods {
185                    self.visit_method_sig(sig);
186                }
187            }
188            Item::Derive(d) => {
189                self.consider(d.span, HoverTarget::Derive(d.clone()));
190                self.consider(d.name.span, HoverTarget::Derive(d.clone()));
191                for m in &d.methods {
192                    self.visit_method(m);
193                }
194            }
195            Item::Const(c) => {
196                self.consider(c.span, HoverTarget::Const(c.clone()));
197                self.consider(c.name.span, HoverTarget::Const(c.clone()));
198                if let Some(ty) = &c.ty {
199                    self.visit_type_expr(ty);
200                }
201            }
202            Item::LinkExtern(b) => {
203                self.consider(b.span, HoverTarget::LinkExtern(b.clone()));
204                for ext in &b.items {
205                    for p in &ext.params {
206                        self.visit_param(p);
207                    }
208                    if let Some(rt) = &ext.return_type {
209                        self.visit_type_expr(rt);
210                    }
211                }
212            }
213            Item::Error(_) => {}
214        }
215    }
216
217    fn visit_field(&mut self, field: &FieldDecl) {
218        self.consider(field.span, HoverTarget::Field(field.clone()));
219        self.consider(field.name.span, HoverTarget::Field(field.clone()));
220        self.visit_type_expr(&field.ty);
221    }
222
223    fn visit_variant(&mut self, v: &EnumVariant) {
224        self.consider(v.span, HoverTarget::EnumVariant(v.clone()));
225        self.consider(v.name.span, HoverTarget::EnumVariant(v.clone()));
226        match &v.kind {
227            EnumVariantKind::Unit => {}
228            EnumVariantKind::Tuple(tys) => {
229                for ty in tys {
230                    self.visit_type_expr(ty);
231                }
232            }
233            EnumVariantKind::Struct(fields) => {
234                for f in fields {
235                    self.visit_type_expr(&f.ty);
236                }
237            }
238        }
239    }
240
241    fn visit_method(&mut self, m: &Method) {
242        self.consider(m.span, HoverTarget::Method(m.clone()));
243        self.consider(m.name.span, HoverTarget::Method(m.clone()));
244        for p in &m.params {
245            self.visit_param(p);
246        }
247        if let Some(rt) = &m.return_type {
248            self.visit_type_expr(rt);
249        }
250    }
251
252    fn visit_method_sig(&mut self, m: &MethodSig) {
253        self.consider(m.span, HoverTarget::MethodSig(m.clone()));
254        self.consider(m.name.span, HoverTarget::MethodSig(m.clone()));
255        for p in &m.params {
256            self.visit_param(p);
257        }
258        if let Some(rt) = &m.return_type {
259            self.visit_type_expr(rt);
260        }
261    }
262
263    fn visit_param(&mut self, p: &Param) {
264        self.consider(p.span, HoverTarget::Param(p.clone()));
265        self.consider(p.name.span, HoverTarget::Param(p.clone()));
266        self.visit_type_expr(&p.ty);
267    }
268
269    fn visit_type_expr(&mut self, ty: &TypeExpr) {
270        let span = ty.span();
271        self.consider(span, HoverTarget::TypeRef(ty.clone()));
272        if let TypeExpr::Named(ident) = ty {
273            self.consider(ident.span, HoverTarget::Identifier(*ident));
274        }
275        // Recurse into compound types.
276        match ty {
277            TypeExpr::Array { element, .. } => self.visit_type_expr(element),
278            TypeExpr::Tuple { elems, .. } => {
279                for e in elems {
280                    self.visit_type_expr(e);
281                }
282            }
283            TypeExpr::TypeCall { args, .. } => {
284                for a in args {
285                    self.visit_type_expr(a);
286                }
287            }
288            _ => {}
289        }
290    }
291}
292
293impl HoverTarget {
294    fn render(&self, interner: &ThreadedRodeo) -> Option<HoverContent> {
295        match self {
296            HoverTarget::Function(f) => Some(render_function(f, interner)),
297            HoverTarget::Struct(s) => Some(render_struct(s, interner)),
298            HoverTarget::Enum(e) => Some(render_enum(e, interner)),
299            HoverTarget::Interface(i) => Some(render_interface(i, interner)),
300            HoverTarget::Derive(d) => Some(render_derive(d, interner)),
301            HoverTarget::Const(c) => Some(render_const(c, interner)),
302            HoverTarget::LinkExtern(b) => Some(render_link_extern(b, interner)),
303            HoverTarget::Field(f) => Some(render_field(f, interner)),
304            HoverTarget::EnumVariant(v) => Some(render_variant(v, interner)),
305            HoverTarget::Method(m) => Some(render_method(m, interner)),
306            HoverTarget::MethodSig(m) => Some(render_method_sig(m, interner)),
307            HoverTarget::Param(p) => Some(render_param(p, interner)),
308            HoverTarget::TypeRef(ty) => Some(render_type_ref(ty, interner)),
309            HoverTarget::Identifier(i) => Some(HoverContent {
310                markdown: format!("`{}`", interner.resolve(&i.name)),
311                span: i.span,
312            }),
313        }
314    }
315}
316
317fn render_function(f: &Function, interner: &ThreadedRodeo) -> HoverContent {
318    let mut sig = String::new();
319    sig.push_str("fn ");
320    sig.push_str(interner.resolve(&f.name.name));
321    sig.push('(');
322    for (i, p) in f.params.iter().enumerate() {
323        if i > 0 {
324            sig.push_str(", ");
325        }
326        sig.push_str(interner.resolve(&p.name.name));
327        sig.push_str(": ");
328        sig.push_str(&type_expr_display(&p.ty, interner));
329    }
330    sig.push(')');
331    if let Some(rt) = &f.return_type {
332        sig.push_str(" -> ");
333        sig.push_str(&type_expr_display(rt, interner));
334    }
335    HoverContent {
336        markdown: markdown_with_doc(&sig, f.doc.as_ref()),
337        span: f.span,
338    }
339}
340
341fn render_struct(s: &StructDecl, interner: &ThreadedRodeo) -> HoverContent {
342    let sig = format!("struct {}", interner.resolve(&s.name.name));
343    HoverContent {
344        markdown: markdown_with_doc(&sig, s.doc.as_ref()),
345        span: s.span,
346    }
347}
348
349fn render_enum(e: &EnumDecl, interner: &ThreadedRodeo) -> HoverContent {
350    let sig = format!("enum {}", interner.resolve(&e.name.name));
351    HoverContent {
352        markdown: markdown_with_doc(&sig, e.doc.as_ref()),
353        span: e.span,
354    }
355}
356
357fn render_interface(i: &InterfaceDecl, interner: &ThreadedRodeo) -> HoverContent {
358    let sig = format!("interface {}", interner.resolve(&i.name.name));
359    HoverContent {
360        markdown: markdown_with_doc(&sig, i.doc.as_ref()),
361        span: i.span,
362    }
363}
364
365fn render_derive(d: &DeriveDecl, interner: &ThreadedRodeo) -> HoverContent {
366    let sig = format!("derive {}", interner.resolve(&d.name.name));
367    HoverContent {
368        markdown: markdown_with_doc(&sig, d.doc.as_ref()),
369        span: d.span,
370    }
371}
372
373fn render_const(c: &ConstDecl, interner: &ThreadedRodeo) -> HoverContent {
374    let mut sig = format!("const {}", interner.resolve(&c.name.name));
375    if let Some(ty) = &c.ty {
376        sig.push_str(": ");
377        sig.push_str(&type_expr_display(ty, interner));
378    }
379    HoverContent {
380        markdown: markdown_with_doc(&sig, c.doc.as_ref()),
381        span: c.span,
382    }
383}
384
385fn render_link_extern(b: &LinkExternBlock, interner: &ThreadedRodeo) -> HoverContent {
386    let library = interner.resolve(&b.library.value);
387    let sig = format!("link_extern(\"{}\")", library);
388    HoverContent {
389        markdown: markdown_with_doc(&sig, b.doc.as_ref()),
390        span: b.span,
391    }
392}
393
394fn render_field(f: &FieldDecl, interner: &ThreadedRodeo) -> HoverContent {
395    let sig = format!(
396        "{}: {}",
397        interner.resolve(&f.name.name),
398        type_expr_display(&f.ty, interner)
399    );
400    HoverContent {
401        markdown: markdown_with_doc(&sig, f.doc.as_ref()),
402        span: f.span,
403    }
404}
405
406fn render_variant(v: &EnumVariant, interner: &ThreadedRodeo) -> HoverContent {
407    let mut sig = interner.resolve(&v.name.name).to_string();
408    match &v.kind {
409        EnumVariantKind::Unit => {}
410        EnumVariantKind::Tuple(tys) => {
411            sig.push('(');
412            for (i, ty) in tys.iter().enumerate() {
413                if i > 0 {
414                    sig.push_str(", ");
415                }
416                sig.push_str(&type_expr_display(ty, interner));
417            }
418            sig.push(')');
419        }
420        EnumVariantKind::Struct(fields) => {
421            sig.push_str(" { ");
422            for (i, f) in fields.iter().enumerate() {
423                if i > 0 {
424                    sig.push_str(", ");
425                }
426                sig.push_str(interner.resolve(&f.name.name));
427                sig.push_str(": ");
428                sig.push_str(&type_expr_display(&f.ty, interner));
429            }
430            sig.push_str(" }");
431        }
432    }
433    HoverContent {
434        markdown: markdown_with_doc(&sig, v.doc.as_ref()),
435        span: v.span,
436    }
437}
438
439fn render_method(m: &Method, interner: &ThreadedRodeo) -> HoverContent {
440    let mut sig = format!("fn {}(", interner.resolve(&m.name.name));
441    if let Some(_recv) = &m.receiver {
442        sig.push_str("self");
443        if !m.params.is_empty() {
444            sig.push_str(", ");
445        }
446    }
447    for (i, p) in m.params.iter().enumerate() {
448        if i > 0 {
449            sig.push_str(", ");
450        }
451        sig.push_str(interner.resolve(&p.name.name));
452        sig.push_str(": ");
453        sig.push_str(&type_expr_display(&p.ty, interner));
454    }
455    sig.push(')');
456    if let Some(rt) = &m.return_type {
457        sig.push_str(" -> ");
458        sig.push_str(&type_expr_display(rt, interner));
459    }
460    HoverContent {
461        markdown: markdown_with_doc(&sig, m.doc.as_ref()),
462        span: m.span,
463    }
464}
465
466fn render_method_sig(m: &MethodSig, interner: &ThreadedRodeo) -> HoverContent {
467    let mut sig = format!("fn {}(self", interner.resolve(&m.name.name));
468    for p in &m.params {
469        sig.push_str(", ");
470        sig.push_str(interner.resolve(&p.name.name));
471        sig.push_str(": ");
472        sig.push_str(&type_expr_display(&p.ty, interner));
473    }
474    sig.push(')');
475    if let Some(rt) = &m.return_type {
476        sig.push_str(" -> ");
477        sig.push_str(&type_expr_display(rt, interner));
478    }
479    sig.push(';');
480    HoverContent {
481        markdown: markdown_with_doc(&sig, m.doc.as_ref()),
482        span: m.span,
483    }
484}
485
486fn render_param(p: &Param, interner: &ThreadedRodeo) -> HoverContent {
487    let sig = format!(
488        "{}: {}",
489        interner.resolve(&p.name.name),
490        type_expr_display(&p.ty, interner)
491    );
492    HoverContent {
493        markdown: markdown_with_doc(&sig, None),
494        span: p.span,
495    }
496}
497
498fn render_type_ref(ty: &TypeExpr, interner: &ThreadedRodeo) -> HoverContent {
499    HoverContent {
500        markdown: format!("```gruel\n{}\n```", type_expr_display(ty, interner)),
501        span: ty.span(),
502    }
503}
504
505fn markdown_with_doc(sig: &str, doc: Option<&gruel_parser::ast::Doc>) -> String {
506    let mut out = String::from("```gruel\n");
507    out.push_str(sig);
508    out.push_str("\n```");
509    if let Some(d) = doc {
510        out.push_str("\n\n");
511        out.push_str(d.body.trim());
512    }
513    out
514}
515
516/// Pretty-print a `TypeExpr` resolving named idents through the interner.
517fn type_expr_display(ty: &TypeExpr, interner: &ThreadedRodeo) -> String {
518    match ty {
519        TypeExpr::Named(ident) => interner.resolve(&ident.name).to_string(),
520        TypeExpr::Unit(_) => "()".to_string(),
521        TypeExpr::Never(_) => "!".to_string(),
522        TypeExpr::Array {
523            element, length, ..
524        } => {
525            format!("[{}; {}]", type_expr_display(element, interner), length)
526        }
527        TypeExpr::AnonymousStruct { .. } => "struct { … }".to_string(),
528        TypeExpr::AnonymousEnum { .. } => "enum { … }".to_string(),
529        TypeExpr::AnonymousInterface { .. } => "interface { … }".to_string(),
530        TypeExpr::TypeCall { callee, args, .. } => {
531            let mut s = interner.resolve(&callee.name).to_string();
532            s.push('(');
533            for (i, a) in args.iter().enumerate() {
534                if i > 0 {
535                    s.push_str(", ");
536                }
537                s.push_str(&type_expr_display(a, interner));
538            }
539            s.push(')');
540            s
541        }
542        TypeExpr::Tuple { elems, .. } => {
543            let mut s = String::from("(");
544            for (i, e) in elems.iter().enumerate() {
545                if i > 0 {
546                    s.push_str(", ");
547                }
548                s.push_str(&type_expr_display(e, interner));
549            }
550            if elems.len() == 1 {
551                s.push(',');
552            }
553            s.push(')');
554            s
555        }
556    }
557}
558
559#[cfg(test)]
560mod tests {
561    use super::*;
562    use gruel_compiler::{
563        FileId, PreviewFeatures, SourceFile, merge_symbols, parse_all_files_with_preview,
564    };
565
566    fn parse(source: &str) -> (Ast, ThreadedRodeo) {
567        let sources = vec![SourceFile::new("main.gruel", source, FileId::new(1))];
568        let parsed = parse_all_files_with_preview(&sources, &PreviewFeatures::default()).unwrap();
569        let merged = merge_symbols(parsed).unwrap();
570        (merged.ast, merged.interner)
571    }
572
573    #[test]
574    fn hover_function_name() {
575        let src = "fn main() -> i32 { 0 }";
576        let (ast, interner) = parse(src);
577        // Cursor on the `m` of `main` (byte 3).
578        let h = hover_at(&ast, &interner, FileId::new(1), 3).unwrap();
579        assert!(h.markdown.contains("fn main"), "got: {}", h.markdown);
580        assert!(h.markdown.contains("-> i32"));
581    }
582
583    #[test]
584    fn hover_function_with_doc() {
585        let src = "/// Does the thing.\nfn main() -> i32 { 0 }";
586        let (ast, interner) = parse(src);
587        // The function name starts after "/// Does the thing.\nfn ".
588        let byte = src.find("main").unwrap() as u32;
589        let h = hover_at(&ast, &interner, FileId::new(1), byte).unwrap();
590        assert!(h.markdown.contains("Does the thing"), "got: {}", h.markdown);
591    }
592
593    #[test]
594    fn hover_struct_name() {
595        let src = "struct Point { x: i32, y: i32 }";
596        let (ast, interner) = parse(src);
597        let byte = src.find("Point").unwrap() as u32 + 1;
598        let h = hover_at(&ast, &interner, FileId::new(1), byte).unwrap();
599        assert!(h.markdown.contains("struct Point"));
600    }
601
602    #[test]
603    fn hover_struct_field() {
604        let src = "struct Point { x: i32, y: i32 }";
605        let (ast, interner) = parse(src);
606        let byte = src.find(": i32").unwrap() as u32 - 1; // on 'x'
607        let h = hover_at(&ast, &interner, FileId::new(1), byte).unwrap();
608        assert!(
609            h.markdown.contains("x: i32") || h.markdown.contains("x"),
610            "got: {}",
611            h.markdown
612        );
613    }
614
615    #[test]
616    fn hover_type_reference() {
617        let src = "fn id(x: i32) -> i32 { x }";
618        let (ast, interner) = parse(src);
619        // Position on first 'i32' (the parameter type).
620        let byte = src.find("i32").unwrap() as u32;
621        let h = hover_at(&ast, &interner, FileId::new(1), byte).unwrap();
622        assert!(h.markdown.contains("i32"), "got: {}", h.markdown);
623    }
624
625    #[test]
626    fn hover_enum_with_variants() {
627        let src = "enum Color { Red, Green, Blue }";
628        let (ast, interner) = parse(src);
629        let byte = src.find("Red").unwrap() as u32;
630        let h = hover_at(&ast, &interner, FileId::new(1), byte).unwrap();
631        assert!(h.markdown.contains("Red"), "got: {}", h.markdown);
632    }
633
634    #[test]
635    fn hover_const_with_doc() {
636        let src = "/// The answer.\nconst N: i32 = 42;";
637        let (ast, interner) = parse(src);
638        let byte = src.find('N').unwrap() as u32;
639        let h = hover_at(&ast, &interner, FileId::new(1), byte).unwrap();
640        assert!(h.markdown.contains("const N"));
641        assert!(h.markdown.contains("The answer"));
642    }
643
644    #[test]
645    fn hover_outside_returns_none() {
646        let src = "fn main() -> i32 { 0 }";
647        let (ast, interner) = parse(src);
648        let byte = src.len() as u32 + 100;
649        assert!(hover_at(&ast, &interner, FileId::new(1), byte).is_none());
650    }
651}