Skip to main content

nifty_lsp/features/
completion.rs

1//! Completing what is being typed, offering only what fits where the cursor is: statements
2//! that suit the block, elements in UI, argument labels and values in calls, and types after
3//! a `:`.
4
5use std::collections::HashSet;
6
7use lsp_types::{CompletionItem, CompletionItemKind, InsertTextFormat};
8use nifty_check::elements::{self, Element, Param, ParamKind, Place, TrailingBlock};
9use nifty_check::functions::{self, Function};
10use nifty_check::hir::{PRIMITIVE_TYPES, Type};
11use nifty_syntax::TokenKind;
12
13use crate::context::{self, BlockKind, Call, Slot};
14use crate::index::{Symbol, SymbolKind};
15use crate::project::Cursor;
16use crate::signature;
17
18const ELEMENT_DETAIL: &str = "element";
19const FUNCTION_DETAIL: &str = "built-in function";
20const TOP_LEVEL_KEYWORDS: &[TokenKind] = &[
21    TokenKind::State,
22    TokenKind::Fn,
23    TokenKind::View,
24    TokenKind::Struct,
25];
26const CODE_KEYWORDS: &[TokenKind] = &[TokenKind::Let, TokenKind::If, TokenKind::For];
27const UI_KEYWORDS: &[TokenKind] = &[TokenKind::If, TokenKind::For];
28const VALUE_KEYWORDS: &[TokenKind] = &[TokenKind::True, TokenKind::False];
29
30/// Completions for the name ending at the cursor, or `None` where nothing fits.
31pub(crate) fn completions(cursor: Cursor) -> Option<Vec<CompletionItem>> {
32    let document = cursor.document();
33    let word_start = document.word_start(cursor.offset);
34    let items = match context::at(document, word_start).slot {
35        Slot::Statement(block) => statement(cursor, block),
36        Slot::AfterIf => keywords(&[TokenKind::Else]),
37        Slot::Expression => values(cursor),
38        Slot::ArgumentStart(call) => argument_start(cursor, &call),
39        Slot::ArgumentValue(call) => argument_value(cursor, &call),
40        Slot::ImplicitMember(call) => options(cursor, &call)
41            .into_iter()
42            .map(|name| item(name, CompletionItemKind::ENUM_MEMBER))
43            .collect(),
44        Slot::Type => types(cursor),
45        Slot::ForIn => keywords(&[TokenKind::In]),
46        Slot::Nothing => return None,
47    };
48    let mut seen = HashSet::new();
49    Some(
50        items
51            .into_iter()
52            .filter(|item| seen.insert(item.label.clone()))
53            .collect(),
54    )
55}
56
57fn statement(cursor: Cursor, block: BlockKind) -> Vec<CompletionItem> {
58    match block {
59        BlockKind::TopLevel => {
60            let mut items = keywords(TOP_LEVEL_KEYWORDS);
61            items.extend(elements_in(Place::TopLevel));
62            items
63        }
64        BlockKind::Menu => elements_in(Place::Menu).collect(),
65        BlockKind::Ui => {
66            let mut items: Vec<_> = elements_in(Place::Ui).collect();
67            items.extend(symbols(cursor, |kind| kind == SymbolKind::View));
68            items.extend(keywords(UI_KEYWORDS));
69            items
70        }
71        BlockKind::Function | BlockKind::Action | BlockKind::Other => {
72            let mut items: Vec<_> = symbols(cursor, |kind| {
73                matches!(
74                    kind,
75                    SymbolKind::State
76                        | SymbolKind::Parameter
77                        | SymbolKind::Local
78                        | SymbolKind::Function
79                )
80            })
81            .collect();
82            items.extend(functions::FUNCTIONS.iter().map(function));
83            items.extend(keywords(CODE_KEYWORDS));
84            if block == BlockKind::Function {
85                items.extend(keywords(&[TokenKind::Return]));
86            }
87            items
88        }
89        BlockKind::Struct => Vec::new(),
90    }
91}
92
93/// The elements that may be written in `place`.
94fn elements_in(place: Place) -> impl Iterator<Item = CompletionItem> {
95    elements::ELEMENTS
96        .iter()
97        .filter(move |candidate| candidate.places.contains(&place))
98        .map(element)
99}
100
101/// Everything an expression can start with.
102fn values(cursor: Cursor) -> Vec<CompletionItem> {
103    let mut items: Vec<_> = symbols(cursor, |kind| {
104        matches!(
105            kind,
106            SymbolKind::State
107                | SymbolKind::Parameter
108                | SymbolKind::Local
109                | SymbolKind::Function
110                | SymbolKind::Struct
111        )
112    })
113    .collect();
114    items.extend(keywords(VALUE_KEYWORDS));
115    items
116}
117
118/// Labels for the parameters not given yet, then values for a positional argument.
119fn argument_start(cursor: Cursor, call: &Call) -> Vec<CompletionItem> {
120    let Some(signature) = signature_of(cursor, call) else {
121        return values(cursor);
122    };
123    let mut items: Vec<_> = signature
124        .params
125        .iter()
126        .enumerate()
127        .filter(|(index, param)| {
128            param.named && *index >= call.positional && !call.labels.contains(&param.name)
129        })
130        .map(|(_, param)| CompletionItem {
131            insert_text: Some(format!("{}: ", param.name)),
132            detail: Some(param.detail.clone()),
133            ..item(&format!("{}:", param.name), CompletionItemKind::PROPERTY)
134        })
135        .collect();
136    if let Some(index) = signature.active(None, call.positional) {
137        items.extend(argument_values(cursor, signature.params[index].kind));
138    }
139    items
140}
141
142/// Values for a labelled argument.
143fn argument_value(cursor: Cursor, call: &Call) -> Vec<CompletionItem> {
144    match labelled_param(cursor, call) {
145        Some(param) => argument_values(cursor, param.kind),
146        None => values(cursor),
147    }
148}
149
150/// What fits an argument: anything for a declared callable's parameter, and for an element
151/// parameter only what the element accepts there.
152fn argument_values(cursor: Cursor, kind: Option<ParamKind>) -> Vec<CompletionItem> {
153    match kind {
154        None | Some(ParamKind::Condition) => values(cursor),
155        Some(ParamKind::Text | ParamKind::TextList | ParamKind::Number) => {
156            symbols(cursor, |kind| {
157                matches!(
158                    kind,
159                    SymbolKind::State
160                        | SymbolKind::Parameter
161                        | SymbolKind::Local
162                        | SymbolKind::Function
163                )
164            })
165            .collect()
166        }
167        Some(
168            ParamKind::FixedText | ParamKind::Points | ParamKind::FixedNumber | ParamKind::Shortcut,
169        ) => Vec::new(),
170        Some(ParamKind::Flag) => keywords(VALUE_KEYWORDS),
171        Some(ParamKind::Window) => symbols(cursor, |kind| kind == SymbolKind::Window).collect(),
172        Some(ParamKind::Binding(_)) => symbols(cursor, |kind| {
173            matches!(kind, SymbolKind::State | SymbolKind::Local)
174        })
175        .map(|binding| CompletionItem {
176            label: format!("${}", binding.label),
177            ..binding
178        })
179        .collect(),
180        Some(ParamKind::Option(options)) => options
181            .names()
182            .into_iter()
183            .map(|name| item(&format!(".{name}"), CompletionItemKind::ENUM_MEMBER))
184            .collect(),
185    }
186}
187
188/// The `.name` values the labelled element argument takes, if it takes a set of them.
189fn options(cursor: Cursor, call: &Call) -> Vec<&'static str> {
190    match labelled_param(cursor, call).and_then(|param| param.kind) {
191        Some(ParamKind::Option(options)) => options.names(),
192        _ => Vec::new(),
193    }
194}
195
196/// The parameter the current label names, when the callee and label are known.
197fn labelled_param(cursor: Cursor, call: &Call) -> Option<signature::Param> {
198    let mut signature = signature_of(cursor, call)?;
199    let index = signature.active(call.label.as_deref(), call.positional)?;
200    Some(signature.params.swap_remove(index))
201}
202
203fn signature_of(cursor: Cursor, call: &Call) -> Option<signature::Signature> {
204    signature::of(cursor.project, call.callee.as_deref()?)
205}
206
207fn types(cursor: Cursor) -> Vec<CompletionItem> {
208    let primitives = PRIMITIVE_TYPES
209        .iter()
210        .filter_map(Type::primitive_name)
211        .map(|name| item(name, CompletionItemKind::TYPE_PARAMETER));
212    let structs = symbols(cursor, |kind| kind == SymbolKind::Struct);
213    primitives.chain(structs).collect()
214}
215
216fn symbols(
217    cursor: Cursor,
218    wanted: impl Fn(SymbolKind) -> bool,
219) -> impl Iterator<Item = CompletionItem> {
220    cursor
221        .project
222        .index
223        .in_scope(cursor.file, cursor.offset)
224        .filter(move |symbol| wanted(symbol.kind))
225        .map(symbol_item)
226}
227
228fn symbol_item(symbol: &Symbol) -> CompletionItem {
229    let kind = match symbol.kind {
230        SymbolKind::State | SymbolKind::Parameter | SymbolKind::Local => {
231            CompletionItemKind::VARIABLE
232        }
233        SymbolKind::Struct => CompletionItemKind::STRUCT,
234        SymbolKind::Field => CompletionItemKind::FIELD,
235        SymbolKind::Function => CompletionItemKind::FUNCTION,
236        SymbolKind::View => CompletionItemKind::CLASS,
237        SymbolKind::Window => CompletionItemKind::CONSTANT,
238    };
239    item(&symbol.name, kind)
240}
241
242/// An element, inserted with placeholders for its required arguments and its block.
243fn element(element: &Element) -> CompletionItem {
244    let block = match element.block {
245        TrailingBlock::None | TrailingBlock::OptionalAction => "$0",
246        TrailingBlock::Children | TrailingBlock::Action | TrailingBlock::Menu => " {\n\t$0\n}",
247    };
248    call(
249        element.name,
250        element.params,
251        block,
252        element.summary,
253        ELEMENT_DETAIL,
254    )
255}
256
257/// A built-in function, inserted with placeholders for its required arguments.
258fn function(function: &Function) -> CompletionItem {
259    call(
260        function.name,
261        function.params,
262        "$0",
263        function.summary,
264        FUNCTION_DETAIL,
265    )
266}
267
268/// A built-in call, with numbered placeholders for its required arguments, then `tail`.
269fn call(name: &str, params: &[Param], tail: &str, summary: &str, detail: &str) -> CompletionItem {
270    let args: Vec<_> = params
271        .iter()
272        .filter(|param| param.required)
273        .enumerate()
274        .map(|(index, param)| {
275            let placeholder = format!("${{{}:{}}}", index + 1, param.name);
276            if param.positional {
277                placeholder
278            } else {
279                format!("{}: {placeholder}", param.name)
280            }
281        })
282        .collect();
283    CompletionItem {
284        detail: Some(detail.to_owned()),
285        documentation: Some(lsp_types::Documentation::String(summary.to_owned())),
286        insert_text: Some(format!("{name}({}){tail}", args.join(", "))),
287        insert_text_format: Some(InsertTextFormat::SNIPPET),
288        ..item(name, CompletionItemKind::FUNCTION)
289    }
290}
291
292fn keywords(keywords: &[TokenKind]) -> Vec<CompletionItem> {
293    keywords
294        .iter()
295        .filter_map(|keyword| keyword.text())
296        .map(|keyword| item(keyword, CompletionItemKind::KEYWORD))
297        .collect()
298}
299
300fn item(label: &str, kind: CompletionItemKind) -> CompletionItem {
301    CompletionItem {
302        label: label.to_owned(),
303        kind: Some(kind),
304        ..CompletionItem::default()
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use nifty_check::elements::Options;
311
312    use super::*;
313    use crate::project::Project;
314
315    /// The elements that may be written in `place`.
316    fn elements_at(place: Place) -> Vec<&'static str> {
317        elements::ELEMENTS
318            .iter()
319            .filter(|element| element.places.contains(&place))
320            .map(|element| element.name)
321            .collect()
322    }
323
324    fn ui_elements() -> Vec<&'static str> {
325        elements_at(Place::Ui)
326    }
327
328    fn function_names() -> Vec<&'static str> {
329        functions::FUNCTIONS
330            .iter()
331            .map(|function| function.name)
332            .collect()
333    }
334
335    const DECLARATIONS: &str = "state count = 0\nstruct Todo { title: string }\n\
336                                fn bump(by: int) {}\nview Badge(text: string) {}\n";
337
338    /// The labels offered where `|` stands in `source`, which is removed before parsing.
339    fn labels(source: &str) -> Vec<String> {
340        let (project, offset) = Project::with_cursor(source);
341        completions(project.cursor(offset))
342            .unwrap_or_default()
343            .into_iter()
344            .map(|item| item.label)
345            .collect()
346    }
347
348    fn offers(source: &str, expected: &[&str]) {
349        let found = labels(source);
350        for label in expected {
351            assert!(
352                found.iter().any(|found| found == label),
353                "`{label}` missing from {found:?}"
354            );
355        }
356    }
357
358    fn omits(source: &str, unexpected: &[&str]) {
359        let found = labels(source);
360        for label in unexpected {
361            assert!(
362                !found.iter().any(|found| found == label),
363                "`{label}` offered in {found:?}"
364            );
365        }
366    }
367
368    #[test]
369    fn a_ui_block_offers_elements_views_if_and_for_only() {
370        let source = format!("{DECLARATIONS}window(\"App\") {{\n  r|\n}}");
371        offers(&source, &ui_elements());
372        offers(&source, &["Badge", "if", "for"]);
373        omits(&source, &function_names());
374        omits(
375            &source,
376            &[
377                "return",
378                "let",
379                elements::WINDOW,
380                "count",
381                "bump",
382                "true",
383                "struct",
384            ],
385        );
386    }
387
388    #[test]
389    fn a_menu_offers_only_what_goes_in_a_menu() {
390        let source = format!("{DECLARATIONS}menu(\"File\") {{\n  m|\n}}");
391        offers(&source, &elements_at(Place::Menu));
392        omits(&source, &["label", "window", "if", "for", "Badge"]);
393    }
394
395    #[test]
396    fn opening_a_window_offers_the_windows_names() {
397        let source = "window main(\"Main\") {\n  button(\"Go\") { open_window(|) }\n}\nwindow settings(\"Settings\", open: false) {\n}";
398        offers(source, &["main", "settings"]);
399        omits(source, &["count", "true"]);
400    }
401
402    #[test]
403    fn a_context_menu_offers_what_goes_in_a_menu() {
404        let source =
405            format!("{DECLARATIONS}window(\"App\") {{\n  context_menu() {{\n    m|\n  }}\n}}");
406        offers(&source, &elements_at(Place::Menu));
407        omits(&source, &["label", "context_menu", "if", "for", "Badge"]);
408    }
409
410    #[test]
411    fn the_top_level_offers_declarations_and_window() {
412        offers("w|", &["window", "menu", "state", "fn", "view", "struct"]);
413        omits("w|", &["row", "return", "let", "if"]);
414    }
415
416    #[test]
417    fn functions_offer_statements_and_actions_skip_return() {
418        let function = format!("{DECLARATIONS}fn f() {{\n  |\n}}");
419        offers(&function, &["let", "if", "for", "return", "count", "bump"]);
420        offers(&function, &function_names());
421        omits(&function, &ui_elements());
422        omits(&function, &["Badge", "state"]);
423        let action = format!("{DECLARATIONS}window(\"App\") {{\n  button(\"Go\") {{ |}}\n}}");
424        offers(&action, &["count", "bump", "let"]);
425        offers(&action, &function_names());
426        omits(&action, &["return", "row"]);
427    }
428
429    #[test]
430    fn an_argument_list_offers_the_labels_not_given_yet() {
431        offers("window(|", &["width:", "height:"]);
432        omits("window(|", &["title:", "row", "return", "true"]);
433        let binding = "state draft = \"\"\nwindow(\"App\") {\n  text_field(|";
434        offers(binding, &["$draft", "placeholder:"]);
435        let after_width = labels("window(\"App\", width: 300, |");
436        assert!(after_width.contains(&"height:".to_owned()));
437        assert!(!after_width.contains(&"width:".to_owned()));
438        offers(&format!("{DECLARATIONS}bump(|"), &["by:", "count"]);
439        let download = format!("{DECLARATIONS}fn f() {{\n  download(\"x\", |");
440        offers(
441            &download,
442            &[
443                "to:",
444                "progress:",
445                "status:",
446                "received:",
447                "total:",
448                "speed:",
449            ],
450        );
451        offers(
452            &format!("{DECLARATIONS}fn f() {{\n  download(\"x\", progress: |"),
453            &["$count"],
454        );
455    }
456
457    #[test]
458    fn element_settings_offer_their_options() {
459        let source = "window(\"App\") {\n  row(align: |";
460        let alignments: Vec<_> = Options::Alignment
461            .names()
462            .iter()
463            .map(|name| format!(".{name}"))
464            .collect();
465        assert_eq!(labels(source), alignments);
466        assert_eq!(
467            labels("window(\"App\") {\n  label(\"x\", style: .|"),
468            Options::TextStyle.names()
469        );
470        assert_eq!(
471            labels("window(\"App\") {\n  label(\"x\", align: .|"),
472            Options::TextAlignment.names()
473        );
474        assert_eq!(
475            labels("window(\"App\") {\n  label(\"x\", selectable: |"),
476            ["true", "false"]
477        );
478    }
479
480    #[test]
481    fn expressions_offer_values_in_scope() {
482        let source = format!("{DECLARATIONS}fn f() {{\n  let x = |");
483        offers(&source, &["count", "bump", "Todo", "true"]);
484        omits(&source, &["Badge", "row", "let"]);
485    }
486
487    #[test]
488    fn hides_locals_outside_their_block() {
489        omits(
490            "fn f() {\n  for item in [1] { }\n  let later = 1\n}\nfn g() {\n  |\n}",
491            &["item", "later"],
492        );
493    }
494
495    #[test]
496    fn offers_types_after_a_colon_and_nothing_where_a_name_is_declared() {
497        let types = format!("{DECLARATIONS}state todos: [|");
498        let primitives: Vec<_> = PRIMITIVE_TYPES
499            .iter()
500            .filter_map(Type::primitive_name)
501            .collect();
502        offers(&types, &primitives);
503        offers(&types, &["Todo"]);
504        assert_eq!(labels("state |"), Vec::<String>::new());
505        assert_eq!(labels("fn f(a|"), Vec::<String>::new());
506        assert_eq!(labels("state s = 1\nfn f() { s.|"), Vec::<String>::new());
507    }
508
509    #[test]
510    fn offers_nothing_in_strings_or_comments() {
511        assert_eq!(labels("window(\"Ap|\")"), Vec::<String>::new());
512        assert_eq!(labels("// wi|"), Vec::<String>::new());
513    }
514
515    #[test]
516    fn offers_else_after_an_if_block_and_in_after_a_for_item() {
517        assert_eq!(labels("fn f() {\n  if true { } |"), ["else"]);
518        assert_eq!(labels("fn f() {\n  for item |"), ["in"]);
519        assert_eq!(labels("fn f() {\n  for index, item |"), ["in"]);
520        assert_eq!(labels("fn f() {\n  for index, |"), Vec::<String>::new());
521    }
522}