1use 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
30pub(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
93fn 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
101fn 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
118fn 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(¶m.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
142fn 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
150fn 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
188fn 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
196fn 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
242fn 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
257fn function(function: &Function) -> CompletionItem {
259 call(
260 function.name,
261 function.params,
262 "$0",
263 function.summary,
264 FUNCTION_DETAIL,
265 )
266}
267
268fn 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 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 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}