Skip to main content

nifty_lsp/features/
hover.rs

1//! What the name under the cursor is: a callable's signature and, for built-ins, the
2//! summary from their table.
3
4use lsp_types::{Hover, HoverContents, MarkupContent, MarkupKind};
5use nifty_check::methods;
6
7use crate::document::Encoding;
8use crate::index::{Role, SymbolKind, Unresolved};
9use crate::project::{Cursor, Project};
10use crate::signature;
11
12pub(crate) fn hover(cursor: Cursor, encoding: Encoding) -> Option<Hover> {
13    let project = cursor.project;
14    let occurrence = project.index.occurrence_at(cursor.file, cursor.offset)?;
15    let name = project.text(occurrence.span);
16    let (code, summary) = match occurrence.role {
17        Role::Unresolved(Unresolved::Method) => {
18            let method = methods::method(name)?;
19            let code = format!("{}({})", method.name, method.params.join(", "));
20            (code, Some(method.summary))
21        }
22        Role::Unresolved(Unresolved::Function) => callable(project, name)?,
23        Role::Declaration(id) | Role::Reference(id)
24            if matches!(
25                project.index.symbol(id).kind,
26                SymbolKind::Function | SymbolKind::View | SymbolKind::Struct
27            ) =>
28        {
29            callable(project, name)?
30        }
31        _ => return None,
32    };
33    let mut markdown = format!("```nifty\n{code}\n```");
34    if let Some(summary) = summary {
35        markdown.push_str("\n\n");
36        markdown.push_str(summary);
37    }
38    Some(Hover {
39        contents: HoverContents::Markup(MarkupContent {
40            kind: MarkupKind::Markdown,
41            value: markdown,
42        }),
43        range: Some(cursor.document().range(occurrence.span, encoding)),
44    })
45}
46
47fn callable(project: &Project, name: &str) -> Option<(String, Option<&'static str>)> {
48    let signature = signature::of(project, name)?;
49    Some((signature.label().0, signature.summary))
50}
51
52#[cfg(test)]
53mod tests {
54    use nifty_check::elements;
55    use nifty_check::functions::FUNCTIONS;
56    use nifty_check::methods::METHODS;
57
58    use super::*;
59
60    /// The hover text where `|` stands in `source`.
61    fn text(source: &str) -> Option<String> {
62        let (project, offset) = Project::with_cursor(source);
63        let hover = hover(project.cursor(offset), Encoding::Utf16)?;
64        let HoverContents::Markup(markup) = hover.contents else {
65            panic!("hover is markdown");
66        };
67        Some(markup.value)
68    }
69
70    #[test]
71    fn built_ins_show_their_signature_and_summary() {
72        let label = elements::element("label").expect("label is an element");
73        let shown = text("window(\"A\") {\n  la|bel(\"hi\")\n}").expect("a hover");
74        assert!(shown.starts_with("```nifty\nlabel(text: string"), "{shown}");
75        assert!(shown.ends_with(label.summary), "{shown}");
76
77        let download = &FUNCTIONS[0];
78        let shown = text(&format!("fn f() {{\n  {}|(\"u\")\n}}", download.name)).expect("a hover");
79        assert!(shown.ends_with(download.summary), "{shown}");
80    }
81
82    #[test]
83    fn methods_show_their_parameters_and_summary() {
84        let push = METHODS
85            .iter()
86            .find(|method| method.name == "push")
87            .expect("push is a method");
88        let shown = text("state xs: [int] = []\nfn f() {\n  xs.pu|sh(1)\n}").expect("a hover");
89        assert_eq!(
90            shown,
91            format!("```nifty\npush(item)\n```\n\n{}", push.summary)
92        );
93    }
94
95    #[test]
96    fn declared_callables_show_their_signature() {
97        let shown = text("fn add(a: int) -> int { return a }\nlet x = ad|d(1)");
98        assert_eq!(shown.as_deref(), Some("```nifty\nadd(a: int)\n```"));
99    }
100
101    #[test]
102    fn values_and_unknown_names_show_nothing() {
103        assert_eq!(text("state count = 0\nlet x = cou|nt"), None);
104        assert_eq!(text("fn f() {\n  nope|()\n}"), None);
105    }
106}