1use 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#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct HoverContent {
25 pub markdown: String,
26 pub span: Span,
27}
28
29pub 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
44pub 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 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
81struct SmallestSpanFinder {
85 file_id: gruel_util::FileId,
86 byte: u32,
87 best: Option<HoverTarget>,
88 best_size: u32,
89}
90
91#[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 TypeRef(TypeExpr),
109 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 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 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
516fn 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 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 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; 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 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}