1// Copyright ยฉ SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
3
4// cSpell: ignore rfind
5
6use super::component_catalog::all_exported_components;
7use super::DocumentCache;
8use crate::common::ComponentInformation;
9use crate::util::{lookup_current_element_type, map_position, with_lookup_ctx};
10
11#[cfg(target_arch = "wasm32")]
12use crate::wasm_prelude::*;
13use i_slint_compiler::diagnostics::Spanned;
14use i_slint_compiler::expression_tree::Expression;
15use i_slint_compiler::langtype::{ElementType, Type};
16use i_slint_compiler::lookup::{LookupCtx, LookupObject, LookupResult};
17use i_slint_compiler::object_tree::ElementRc;
18use i_slint_compiler::parser::{syntax_nodes, SyntaxKind, SyntaxToken};
19use lsp_types::{
20 CompletionClientCapabilities, CompletionItem, CompletionItemKind, InsertTextFormat, Position,
21 Range, TextEdit,
22};
23use std::borrow::Cow;
24use std::collections::{HashMap, HashSet};
25use std::path::Path;
26
27pub(crate) fn completion_at(
28 document_cache: &mut DocumentCache,
29 token: SyntaxToken,
30 offset: u32,
31 client_caps: Option<&CompletionClientCapabilities>,
32) -> Option<Vec<CompletionItem>> {
33 let node = token.parent();
34
35 let snippet_support = client_caps
36 .and_then(|caps| caps.completion_item.as_ref())
37 .and_then(|caps| caps.snippet_support)
38 .unwrap_or(false);
39
40 if token.kind() == SyntaxKind::StringLiteral {
41 if matches!(node.kind(), SyntaxKind::ImportSpecifier | SyntaxKind::AtImageUrl) {
42 return complete_path_in_string(
43 token.source_file()?.path(),
44 token.text(),
45 offset.checked_sub(token.text_range().start().into())?,
46 )
47 .map(|mut r| {
48 if node.kind() == SyntaxKind::ImportSpecifier && !token.text().contains('/') {
49 let mut c =
50 CompletionItem::new_simple("std-widgets.slint".into(), String::new());
51
52 c.kind = Some(CompletionItemKind::FILE);
53 r.push(c)
54 }
55 r
56 });
57 }
58 } else if let Some(element) = syntax_nodes::Element::new(node.clone()) {
59 if token.kind() == SyntaxKind::At
60 || (token.kind() == SyntaxKind::Identifier
61 && token.prev_token().map_or(false, |t| t.kind() == SyntaxKind::At))
62 {
63 return Some(vec![CompletionItem::new_simple("children".into(), String::new())]);
64 }
65
66 return resolve_element_scope(element, document_cache).map(|mut r| {
67 let mut available_types = HashSet::new();
68 if snippet_support {
69 for c in r.iter_mut() {
70 c.insert_text_format = Some(InsertTextFormat::SNIPPET);
71 match c.kind {
72 Some(CompletionItemKind::PROPERTY) => {
73 c.insert_text = Some(format!("{}: $1;", c.label))
74 }
75 Some(CompletionItemKind::METHOD) => {
76 c.insert_text = Some(format!("{} => {{$1}}", c.label))
77 }
78 Some(CompletionItemKind::CLASS) => {
79 available_types.insert(c.label.clone());
80 if !is_followed_by_brace(&token) {
81 c.insert_text = Some(format!("{} {{$1}}", c.label))
82 }
83 }
84 _ => (),
85 }
86 }
87 }
88
89 let is_global = node
90 .parent()
91 .and_then(|n| n.child_text(SyntaxKind::Identifier))
92 .map_or(false, |k| k == "global");
93
94 // add keywords
95 r.extend(
96 [
97 ("property", "property <${1:int}> ${2:name};"),
98 ("in property", "in property <${1:int}> ${2:name};"),
99 ("in-out property", "in-out property <${1:int}> ${2:name};"),
100 ("out property", "out property <${1:int}> ${2:name};"),
101 ("private property", "private property <${1:int}> ${2:name};"),
102 ("function", "function ${1:name}($2) {\n $0\n}"),
103 ("public function", "public function ${1:name}($2) {\n $0\n}"),
104 ("callback", "callback ${1:name}($2);"),
105 ]
106 .iter()
107 .map(|(kw, ins_tex)| {
108 let mut c = CompletionItem::new_simple(kw.to_string(), String::new());
109 c.kind = Some(CompletionItemKind::KEYWORD);
110 with_insert_text(c, ins_tex, snippet_support)
111 }),
112 );
113
114 if !is_global {
115 r.extend(
116 [
117 ("animate", "animate ${1:prop} {\n $0\n}"),
118 ("states", "states [\n $0\n]"),
119 ("for", "for $1 in $2: ${3:Rectangle} {\n $0\n}"),
120 ("if", "if $1: ${2:Rectangle} {\n $0\n}"),
121 ("@children", "@children"),
122 ]
123 .iter()
124 .map(|(kw, ins_tex)| {
125 let mut c = CompletionItem::new_simple(kw.to_string(), String::new());
126 c.kind = Some(CompletionItemKind::KEYWORD);
127 with_insert_text(c, ins_tex, snippet_support)
128 }),
129 );
130 }
131
132 if !is_global && snippet_support {
133 add_components_to_import(&token, document_cache, available_types, &mut r);
134 }
135
136 r
137 });
138 } else if let Some(n) = syntax_nodes::Binding::new(node.clone()) {
139 if let Some(colon) = n.child_token(SyntaxKind::Colon) {
140 if offset >= colon.text_range().end().into() {
141 return with_lookup_ctx(&document_cache.documents, node, |ctx| {
142 resolve_expression_scope(ctx).map(Into::into)
143 })?;
144 }
145 }
146 if token.kind() != SyntaxKind::Identifier {
147 return None;
148 }
149 let all = resolve_element_scope(syntax_nodes::Element::new(n.parent()?)?, document_cache)?;
150 return Some(
151 all.into_iter()
152 .filter(|ce| ce.kind == Some(CompletionItemKind::PROPERTY))
153 .collect::<Vec<_>>(),
154 );
155 } else if let Some(n) = syntax_nodes::TwoWayBinding::new(node.clone()) {
156 let double_arrow_range =
157 n.children_with_tokens().find(|n| n.kind() == SyntaxKind::DoubleArrow)?.text_range();
158 if offset < double_arrow_range.end().into() {
159 return None;
160 }
161 return with_lookup_ctx(&document_cache.documents, node, |ctx| {
162 resolve_expression_scope(ctx)
163 })?;
164 } else if let Some(n) = syntax_nodes::CallbackConnection::new(node.clone()) {
165 if token.kind() != SyntaxKind::Identifier {
166 return None;
167 }
168 let mut parent = n.parent()?;
169 let element = loop {
170 if let Some(e) = syntax_nodes::Element::new(parent.clone()) {
171 break e;
172 }
173 parent = parent.parent()?;
174 };
175 let all = resolve_element_scope(element, document_cache)?;
176 return Some(
177 all.into_iter()
178 .filter(|ce| ce.kind == Some(CompletionItemKind::METHOD))
179 .collect::<Vec<_>>(),
180 );
181 } else if matches!(
182 node.kind(),
183 SyntaxKind::Type | SyntaxKind::ArrayType | SyntaxKind::ObjectType | SyntaxKind::ReturnType
184 ) {
185 return resolve_type_scope(token, document_cache).map(Into::into);
186 } else if syntax_nodes::PropertyDeclaration::new(node.clone()).is_some() {
187 if token.kind() == SyntaxKind::LAngle {
188 return resolve_type_scope(token, document_cache).map(Into::into);
189 }
190 } else if let Some(n) = syntax_nodes::CallbackDeclaration::new(node.clone()) {
191 let paren = n.child_token(SyntaxKind::LParent)?;
192 if token.token.text_range().start() >= paren.token.text_range().end() {
193 return resolve_type_scope(token, document_cache).map(Into::into);
194 }
195 } else if matches!(
196 node.kind(),
197 SyntaxKind::BindingExpression
198 | SyntaxKind::CodeBlock
199 | SyntaxKind::ReturnStatement
200 | SyntaxKind::Expression
201 | SyntaxKind::FunctionCallExpression
202 | SyntaxKind::SelfAssignment
203 | SyntaxKind::ConditionalExpression
204 | SyntaxKind::BinaryExpression
205 | SyntaxKind::UnaryOpExpression
206 | SyntaxKind::Array
207 | SyntaxKind::AtGradient
208 | SyntaxKind::StringTemplate
209 | SyntaxKind::IndexExpression
210 ) {
211 if token.kind() == SyntaxKind::At
212 || (token.kind() == SyntaxKind::Identifier
213 && token.prev_token().map_or(false, |t| t.kind() == SyntaxKind::At))
214 {
215 return Some(
216 [
217 ("tr", "tr(\"$1\")"),
218 ("image-url", "image-url(\"$1\")"),
219 ("linear-gradient", "linear-gradient($1)"),
220 ("radial-gradient", "radial-gradient(circle, $1)"),
221 ]
222 .into_iter()
223 .map(|(label, insert)| {
224 with_insert_text(
225 CompletionItem::new_simple(label.into(), String::new()),
226 insert,
227 snippet_support,
228 )
229 })
230 .collect::<Vec<_>>(),
231 );
232 }
233
234 return with_lookup_ctx(&document_cache.documents, node, |ctx| {
235 resolve_expression_scope(ctx).map(Into::into)
236 })?;
237 } else if let Some(q) = syntax_nodes::QualifiedName::new(node.clone()) {
238 match q.parent()?.kind() {
239 SyntaxKind::Element => {
240 // auto-complete the components
241 let global_tr = document_cache.documents.global_type_registry.borrow();
242 let tr = q
243 .source_file()
244 .and_then(|sf| document_cache.documents.get_document(sf.path()))
245 .map(|doc| &doc.local_registry)
246 .unwrap_or(&global_tr);
247
248 let mut result = tr
249 .all_elements()
250 .into_iter()
251 .filter_map(|(k, t)| {
252 match t {
253 ElementType::Component(c) if !c.is_global() => (),
254 ElementType::Builtin(b) if !b.is_internal && !b.is_global => (),
255 _ => return None,
256 };
257 let mut c = CompletionItem::new_simple(k, "element".into());
258 c.kind = Some(CompletionItemKind::CLASS);
259 Some(c)
260 })
261 .collect::<Vec<_>>();
262
263 drop(global_tr);
264
265 if snippet_support {
266 let available_types = result.iter().map(|c| c.label.clone()).collect();
267 add_components_to_import(&token, document_cache, available_types, &mut result);
268 }
269
270 return Some(result);
271 }
272 SyntaxKind::Type => {
273 return resolve_type_scope(token, document_cache).map(Into::into);
274 }
275 SyntaxKind::Expression => {
276 return with_lookup_ctx(&document_cache.documents, node, |ctx| {
277 let it = q.children_with_tokens().filter_map(|t| t.into_token());
278 let mut it = it.skip_while(|t| {
279 t.kind() != SyntaxKind::Identifier && t.token != token.token
280 });
281 let first = it.next();
282 if first.as_ref().map_or(true, |f| f.token == token.token) {
283 return resolve_expression_scope(ctx).map(Into::into);
284 }
285 let first = i_slint_compiler::parser::normalize_identifier(first?.text());
286 let global = i_slint_compiler::lookup::global_lookup();
287 let mut expr_it = global.lookup(ctx, &first)?;
288 let mut has_dot = false;
289 for t in it {
290 has_dot |= t.kind() == SyntaxKind::Dot;
291 if t.token == token.token {
292 break;
293 };
294 if t.kind() != SyntaxKind::Identifier {
295 continue;
296 }
297 has_dot = false;
298 let str = i_slint_compiler::parser::normalize_identifier(t.text());
299 expr_it = expr_it.lookup(ctx, &str)?;
300 }
301 has_dot.then(|| {
302 let mut r = Vec::new();
303 expr_it.for_each_entry(ctx, &mut |str, expr| -> Option<()> {
304 r.push(completion_item_from_expression(str, expr));
305 None
306 });
307 r
308 })
309 })?;
310 }
311 _ => (),
312 }
313 } else if node.kind() == SyntaxKind::ImportIdentifierList {
314 let import = syntax_nodes::ImportSpecifier::new(node.parent()?)?;
315
316 let path = document_cache
317 .documents
318 .resolve_import_path(
319 Some(&token.into()),
320 import.child_text(SyntaxKind::StringLiteral)?.trim_matches('\"'),
321 )?
322 .0;
323 let doc = document_cache.documents.get_document(&path)?;
324 return Some(
325 doc.exports
326 .iter()
327 .map(|(exported_name, _)| CompletionItem {
328 label: exported_name.name.clone(),
329 ..Default::default()
330 })
331 .collect(),
332 );
333 } else if node.kind() == SyntaxKind::Document {
334 let mut r: Vec<_> = [
335 // the $1 is first in the quote so the filename can be completed before the import names
336 ("import", "import { ${2:Component} } from \"${1:std-widgets.slint}\";"),
337 ("component", "component ${1:Component} {\n $0\n}"),
338 ("struct", "struct ${1:Name} {\n $0\n}"),
339 ("global", "global ${1:Name} {\n $0\n}"),
340 ("export", "export { $0 }"),
341 ("export component", "export component ${1:ExportedComponent} {\n $0\n}"),
342 ("export struct", "export struct ${1:Name} {\n $0\n}"),
343 ("export global", "export global ${1:Name} {\n $0\n}"),
344 ]
345 .iter()
346 .map(|(kw, ins_tex)| {
347 let mut c = CompletionItem::new_simple(kw.to_string(), String::new());
348 c.kind = Some(CompletionItemKind::KEYWORD);
349 with_insert_text(c, ins_tex, snippet_support)
350 })
351 .collect();
352 if let Some(component) = token
353 .prev_sibling_or_token()
354 .filter(|x| x.kind() == SyntaxKind::Component)
355 .and_then(|x| x.into_node())
356 {
357 let has_child = |kind| {
358 !component.children().find(|n| n.kind() == kind).unwrap().text_range().is_empty()
359 };
360 if has_child(SyntaxKind::DeclaredIdentifier) && !has_child(SyntaxKind::Element) {
361 let mut c = CompletionItem::new_simple("inherits".into(), String::new());
362 c.kind = Some(CompletionItemKind::KEYWORD);
363 r.push(c)
364 }
365 }
366 return Some(r);
367 } else if let Some(c) = syntax_nodes::Component::new(node.clone()) {
368 let id_range = c.DeclaredIdentifier().text_range();
369 if !id_range.is_empty()
370 && offset >= id_range.end().into()
371 && !c
372 .children_with_tokens()
373 .any(|c| c.as_token().map_or(false, |t| t.text() == "inherits"))
374 {
375 let mut c = CompletionItem::new_simple("inherits".into(), String::new());
376 c.kind = Some(CompletionItemKind::KEYWORD);
377 return Some(vec![c]);
378 }
379 } else if node.kind() == SyntaxKind::State {
380 let r: Vec<_> = [("when", "when $1: {\n $0\n}")]
381 .iter()
382 .map(|(kw, ins_tex)| {
383 let mut c = CompletionItem::new_simple(kw.to_string(), String::new());
384 c.kind = Some(CompletionItemKind::KEYWORD);
385 with_insert_text(c, ins_tex, snippet_support)
386 })
387 .collect();
388 return Some(r);
389 } else if node.kind() == SyntaxKind::PropertyAnimation {
390 let global_tr = document_cache.documents.global_type_registry.borrow();
391 let r = global_tr
392 .property_animation_type_for_property(Type::Float32)
393 .property_list()
394 .into_iter()
395 .map(|(k, t)| {
396 let mut c = CompletionItem::new_simple(k, t.to_string());
397 c.kind = Some(CompletionItemKind::PROPERTY);
398 if snippet_support {
399 c.insert_text_format = Some(InsertTextFormat::SNIPPET);
400 c.insert_text = Some(format!("{}: $1;", c.label));
401 }
402 c
403 })
404 .collect::<Vec<_>>();
405 return Some(r);
406 }
407 None
408}
409
410fn with_insert_text(
411 mut c: CompletionItem,
412 ins_text: &str,
413 snippet_support: bool,
414) -> CompletionItem {
415 if snippet_support {
416 c.insert_text_format = Some(InsertTextFormat::SNIPPET);
417 c.insert_text = Some(ins_text.to_string());
418 }
419 c
420}
421
422fn resolve_element_scope(
423 element: syntax_nodes::Element,
424 document_cache: &DocumentCache,
425) -> Option<Vec<CompletionItem>> {
426 let global_tr = document_cache.documents.global_type_registry.borrow();
427 let tr = element
428 .source_file()
429 .and_then(|sf| document_cache.documents.get_document(sf.path()))
430 .map(|doc| &doc.local_registry)
431 .unwrap_or(&global_tr);
432 let element_type = lookup_current_element_type((*element).clone(), tr).unwrap_or_default();
433 let mut result = element_type
434 .property_list()
435 .into_iter()
436 .map(|(k, t)| {
437 let k = de_normalize_property_name(&element_type, &k).into_owned();
438 let mut c = CompletionItem::new_simple(k, t.to_string());
439 c.kind = Some(if matches!(t, Type::InferredCallback | Type::Callback { .. }) {
440 CompletionItemKind::METHOD
441 } else {
442 CompletionItemKind::PROPERTY
443 });
444 c.sort_text = Some(format!("#{}", c.label));
445 c
446 })
447 .chain(element.PropertyDeclaration().filter_map(|pr| {
448 let mut c = CompletionItem::new_simple(
449 pr.DeclaredIdentifier().child_text(SyntaxKind::Identifier)?,
450 pr.Type().map(|t| t.text().into()).unwrap_or_else(|| "property".to_owned()),
451 );
452 c.kind = Some(CompletionItemKind::PROPERTY);
453 c.sort_text = Some(format!("#{}", c.label));
454 Some(c)
455 }))
456 .chain(element.CallbackDeclaration().filter_map(|cd| {
457 let mut c = CompletionItem::new_simple(
458 cd.DeclaredIdentifier().child_text(SyntaxKind::Identifier)?,
459 "callback".into(),
460 );
461 c.kind = Some(CompletionItemKind::METHOD);
462 c.sort_text = Some(format!("#{}", c.label));
463 Some(c)
464 }))
465 .collect::<Vec<_>>();
466
467 if !matches!(element_type, ElementType::Global) {
468 result.extend(
469 i_slint_compiler::typeregister::reserved_properties()
470 .filter_map(|(k, t, _)| {
471 if matches!(t, Type::Function { .. }) {
472 return None;
473 }
474 let mut c = CompletionItem::new_simple(k.into(), t.to_string());
475 c.kind = Some(if matches!(t, Type::InferredCallback | Type::Callback { .. }) {
476 CompletionItemKind::METHOD
477 } else {
478 CompletionItemKind::PROPERTY
479 });
480 Some(c)
481 })
482 .chain(tr.all_elements().into_iter().filter_map(|(k, t)| {
483 match t {
484 ElementType::Component(c) if !c.is_global() => (),
485 ElementType::Builtin(b) if !b.is_internal && !b.is_global => (),
486 _ => return None,
487 };
488 let mut c = CompletionItem::new_simple(k, "element".into());
489 c.kind = Some(CompletionItemKind::CLASS);
490 Some(c)
491 })),
492 );
493 };
494 Some(result)
495}
496
497/// Given a property name in the specified element, give the non-normalized name (so that the '_' and '-' fits the definition of the property)
498fn de_normalize_property_name<'a>(element_type: &ElementType, prop: &'a str) -> Cow<'a, str> {
499 match element_type {
500 ElementType::Component(base: &Rc) => {
501 de_normalize_property_name_with_element(&base.root_element, prop)
502 }
503 _ => prop.into(),
504 }
505}
506
507// Same as de_normalize_property_name, but use a `ElementRc`
508fn de_normalize_property_name_with_element<'a>(element: &ElementRc, prop: &'a str) -> Cow<'a, str> {
509 if let Some(d: &PropertyDeclaration) = element.borrow().property_declarations.get(key:prop) {
510 d.node
511 .as_ref()
512 .and_then(|n| n.child_node(SyntaxKind::DeclaredIdentifier))
513 .and_then(|n| n.child_text(SyntaxKind::Identifier))
514 .map_or(default:prop.into(), |x: String| x.into())
515 } else {
516 de_normalize_property_name(&element.borrow().base_type, prop)
517 }
518}
519
520fn resolve_expression_scope(lookup_context: &LookupCtx) -> Option<Vec<CompletionItem>> {
521 let mut r: Vec = Vec::new();
522 let global: impl LookupObject = i_slint_compiler::lookup::global_lookup();
523 global.for_each_entry(ctx:lookup_context, &mut |str: &str, expr: LookupResult| -> Option<()> {
524 r.push(completion_item_from_expression(str, lookup_result:expr));
525 None
526 });
527 Some(r)
528}
529
530fn completion_item_from_expression(str: &str, lookup_result: LookupResult) -> CompletionItem {
531 match lookup_result {
532 LookupResult::Expression { expression, .. } => {
533 let label = match &expression {
534 Expression::CallbackReference(nr, ..)
535 | Expression::FunctionReference(nr, ..)
536 | Expression::PropertyReference(nr) => {
537 de_normalize_property_name_with_element(&nr.element(), str).into_owned()
538 }
539 _ => str.to_string(),
540 };
541
542 let mut c = CompletionItem::new_simple(label, expression.ty().to_string());
543 c.kind = match expression {
544 Expression::BoolLiteral(_) => Some(CompletionItemKind::CONSTANT),
545 Expression::CallbackReference(..) => Some(CompletionItemKind::METHOD),
546 Expression::FunctionReference(..) => Some(CompletionItemKind::FUNCTION),
547 Expression::PropertyReference(_) => Some(CompletionItemKind::PROPERTY),
548 Expression::BuiltinFunctionReference(..) => Some(CompletionItemKind::FUNCTION),
549 Expression::BuiltinMacroReference(..) => Some(CompletionItemKind::FUNCTION),
550 Expression::ElementReference(_) => Some(CompletionItemKind::CLASS),
551 Expression::RepeaterIndexReference { .. } => Some(CompletionItemKind::VARIABLE),
552 Expression::RepeaterModelReference { .. } => Some(CompletionItemKind::VARIABLE),
553 Expression::FunctionParameterReference { .. } => Some(CompletionItemKind::VARIABLE),
554 Expression::Cast { .. } => Some(CompletionItemKind::CONSTANT),
555 Expression::EasingCurve(_) => Some(CompletionItemKind::CONSTANT),
556 Expression::EnumerationValue(_) => Some(CompletionItemKind::ENUM_MEMBER),
557 _ => None,
558 };
559 c
560 }
561 LookupResult::Enumeration(e) => {
562 let mut c = CompletionItem::new_simple(str.to_string(), e.name.clone());
563 c.kind = Some(CompletionItemKind::ENUM);
564 c
565 }
566 LookupResult::Namespace(_) => CompletionItem {
567 label: str.to_string(),
568 kind: Some(CompletionItemKind::MODULE),
569 ..CompletionItem::default()
570 },
571 }
572}
573
574fn resolve_type_scope(
575 token: SyntaxToken,
576 document_cache: &DocumentCache,
577) -> Option<Vec<CompletionItem>> {
578 let global_tr: Ref<'_, TypeRegister> = document_cache.documents.global_type_registry.borrow();
579 let tr: &TypeRegister = token
580 .source_file()
581 .and_then(|sf| document_cache.documents.get_document(sf.path()))
582 .map(|doc| &doc.local_registry)
583 .unwrap_or(&global_tr);
584 Some(
585 trimpl Iterator.all_types()
586 .into_iter()
587 .filter_map(|(k: String, t: Type)| {
588 t.is_property_type().then(|| {
589 let mut c: CompletionItem = CompletionItem::new_simple(label:k, detail:String::new());
590 c.kind = Some(CompletionItemKind::TYPE_PARAMETER);
591 c
592 })
593 })
594 .collect(),
595 )
596}
597
598fn complete_path_in_string(base: &Path, text: &str, offset: u32) -> Option<Vec<CompletionItem>> {
599 if offset as usize > text.len() || offset == 0 {
600 return None;
601 }
602 let mut text = text.strip_prefix('\"')?;
603 text = &text[..(offset - 1) as usize];
604 let base = i_slint_compiler::typeloader::base_directory(base);
605 let path = if let Some(last_slash) = text.rfind('/') {
606 base.join(Path::new(&text[..last_slash]))
607 } else {
608 base
609 };
610 let dir = std::fs::read_dir(path).ok()?;
611 Some(
612 dir.filter_map(|x| {
613 let entry = x.ok()?;
614 let mut c =
615 CompletionItem::new_simple(entry.file_name().into_string().ok()?, String::new());
616 if entry.file_type().ok()?.is_dir() {
617 c.kind = Some(CompletionItemKind::FOLDER);
618 c.insert_text = Some(format!("{}/", c.label));
619 } else {
620 c.kind = Some(CompletionItemKind::FILE);
621 }
622 Some(c)
623 })
624 .collect(),
625 )
626}
627
628/// Add the components that are available when adding import to the `result`
629///
630/// `available_types` are the component which are already available and need no
631/// import and should already be in result
632fn add_components_to_import(
633 token: &SyntaxToken,
634 document_cache: &mut DocumentCache,
635 mut available_types: HashSet<String>,
636 result: &mut Vec<CompletionItem>,
637) {
638 build_import_statements_edits(
639 token,
640 document_cache,
641 &mut |exported_name| {
642 if available_types.contains(exported_name) {
643 false
644 } else {
645 available_types.insert(exported_name.to_string());
646 true
647 }
648 },
649 &mut |exported_name, file, the_import| {
650 result.push(CompletionItem {
651 label: format!("{} (import from \"{}\")", exported_name, file),
652 insert_text: if is_followed_by_brace(token) {
653 Some(exported_name.to_string())
654 } else {
655 Some(format!("{} {{$1}}", exported_name))
656 },
657 insert_text_format: Some(InsertTextFormat::SNIPPET),
658 filter_text: Some(exported_name.to_string()),
659 kind: Some(CompletionItemKind::CLASS),
660 detail: Some(format!("(import from \"{}\")", file)),
661 additional_text_edits: Some(vec![the_import]),
662 ..Default::default()
663 });
664 },
665 );
666}
667
668/// Find the insert location for new imports in the `document`
669///
670/// The result is a tuple with the first element pointing to the place new import statements should
671/// get added. The second element in the tuple is a HashMap mapping import file names to the
672/// correct location to enter more components into the existing import statement.
673fn find_import_locations(
674 document: &syntax_nodes::Document,
675) -> (Position, HashMap<String, Position>) {
676 let mut import_locations = HashMap::new();
677 let mut last = 0u32;
678 for import in document.ImportSpecifier() {
679 if let Some((loc, file)) = import.ImportIdentifierList().and_then(|list| {
680 let node = list.ImportIdentifier().last()?;
681 let id = crate::util::last_non_ws_token(&node).or_else(|| node.first_token())?;
682 Some((
683 map_position(id.source_file()?, id.text_range().end()),
684 import.child_token(SyntaxKind::StringLiteral)?,
685 ))
686 }) {
687 import_locations.insert(file.text().to_string().trim_matches('\"').to_string(), loc);
688 }
689 last = import.text_range().end().into();
690 }
691
692 let new_import_position = if last == 0 {
693 // There are currently no input statement, place it at the location of the first non-empty token.
694 // This should also work in the slint! macro.
695 // consider this file: We want to insert before the doc1 position
696 // ```
697 // //not doc (eg, license header)
698 //
699 // //doc1
700 // //doc2
701 // component Foo {
702 // ```
703 let mut offset = None;
704 for it in document.children_with_tokens() {
705 match it.kind() {
706 SyntaxKind::Comment => {
707 if offset.is_none() {
708 offset = Some(it.text_range().start());
709 }
710 }
711 SyntaxKind::Whitespace => {
712 // Single newline is just considered part of the comment
713 // but more new lines means it splits that comment
714 if it.as_token().unwrap().text() != "\n" {
715 offset = None;
716 }
717 }
718 _ => {
719 if offset.is_none() {
720 offset = Some(it.text_range().start());
721 }
722 break;
723 }
724 }
725 }
726 map_position(&document.source_file, offset.unwrap_or_default())
727 } else {
728 Position::new(map_position(&document.source_file, last.into()).line + 1, 0)
729 };
730
731 (new_import_position, import_locations)
732}
733
734fn create_import_edit_impl(
735 component: &str,
736 import_path: &str,
737 missing_import_location: &Position,
738 known_import_locations: &HashMap<String, Position>,
739) -> TextEdit {
740 known_import_locations.get(import_path).map_or_else(
741 || {
742 TextEdit::new(
743 Range::new(*missing_import_location, *missing_import_location),
744 format!("import {{ {} }} from \"{}\";\n", component, import_path),
745 )
746 },
747 |pos: &Position| TextEdit::new(Range::new(*pos, *pos), new_text:format!(", {}", component)),
748 )
749}
750
751/// Creates a text edit
752#[cfg(any(feature = "preview-external", feature = "preview-engine"))]
753pub fn create_import_edit(
754 document: &i_slint_compiler::object_tree::Document,
755 component: &str,
756 import_path: &Option<String>,
757) -> Option<TextEdit> {
758 let import_path: &String = import_path.as_ref()?;
759 let doc_node: &Document = document.node.as_ref().unwrap();
760
761 if document.local_registry.lookup_element(name:component).is_ok() {
762 None // already known, no import needed
763 } else {
764 let (missing_import_location: Position, known_import_locations: HashMap) = find_import_locations(document:doc_node);
765
766 Some(create_import_edit_impl(
767 component,
768 import_path,
769 &missing_import_location,
770 &known_import_locations,
771 ))
772 }
773}
774
775/// Try to generate `import { XXX } from "foo.slint";` for every component
776///
777/// This is used for auto-completion and also for fixup diagnostics
778///
779/// Call `add_edit` with the component name and file name and TextEdit for every component for which the `filter` callback returns true
780pub fn build_import_statements_edits(
781 token: &SyntaxToken,
782 document_cache: &mut DocumentCache,
783 filter: &mut dyn FnMut(&str) -> bool,
784 add_edit: &mut dyn FnMut(&str, &str, TextEdit),
785) -> Option<()> {
786 // Find out types that can be imported
787 let current_file = token.source_file.path().to_owned();
788 let current_uri = lsp_types::Url::from_file_path(&current_file).ok();
789 let current_doc = document_cache.documents.get_document(&current_file)?.node.as_ref()?;
790 let (missing_import_location, known_import_locations) = find_import_locations(current_doc);
791
792 let exports = {
793 let mut tmp = Vec::new();
794 all_exported_components(
795 document_cache,
796 &mut move |ci: &ComponentInformation| {
797 !filter(&ci.name) || ci.is_global || !ci.is_exported
798 },
799 &mut tmp,
800 );
801 tmp
802 };
803
804 for ci in &exports {
805 let Some(file) = ci.import_file_name(&current_uri) else {
806 continue;
807 };
808
809 let the_import = create_import_edit_impl(
810 &ci.name,
811 &file,
812 &missing_import_location,
813 &known_import_locations,
814 );
815 add_edit(&ci.name, &file, the_import);
816 }
817
818 Some(())
819}
820
821fn is_followed_by_brace(token: &SyntaxToken) -> bool {
822 let mut next_token: Option = token.next_token();
823 while let Some(ref t: &SyntaxToken) = next_token {
824 if t.kind() != SyntaxKind::Whitespace {
825 break;
826 }
827 next_token = t.next_token();
828 }
829 next_token.is_some_and(|x: SyntaxToken| x.kind() == SyntaxKind::LBrace)
830}
831
832#[cfg(test)]
833mod tests {
834 use super::*;
835 use crate::language::uri_to_file;
836
837 /// Given a source text containing the unicode emoji `๐Ÿ”บ`, the emoji will be removed and then an autocompletion request will be done as if the cursor was there
838 fn get_completions(file: &str) -> Option<Vec<CompletionItem>> {
839 const CURSOR_EMOJI: char = '๐Ÿ”บ';
840 let offset = file.find(CURSOR_EMOJI).unwrap() as u32;
841 let source = file.replace(CURSOR_EMOJI, "");
842 let (mut dc, uri, _) = crate::language::test::loaded_document_cache(source);
843
844 let doc = dc.documents.get_document(&uri_to_file(&uri).unwrap()).unwrap();
845 let token = crate::language::token_at_offset(doc.node.as_ref().unwrap(), offset)?;
846 let caps = CompletionClientCapabilities {
847 completion_item: Some(lsp_types::CompletionItemCapability {
848 snippet_support: Some(true),
849 ..Default::default()
850 }),
851 ..Default::default()
852 };
853
854 completion_at(&mut dc, token, offset, Some(&caps))
855 }
856
857 #[test]
858 fn in_expression() {
859 let with_semi = r#"
860 component Bar inherits Text { nope := Rectangle {} property <string> red; }
861 global Glib { property <int> gama; }
862 component Foo {
863 property <int> alpha;
864 pure function funi() {}
865 bobo := Bar {
866 property <int> beta;
867 width: ๐Ÿ”บ;
868 }
869 }
870 "#;
871 let without_semi = r#"
872 component Bar inherits Text { nope := Rectangle {} property <string> red; }
873 global Glib { property <int> gama; }
874 component Foo {
875 property <int> alpha;
876 pure function funi() {}
877 bobo := Bar {
878 property <int> beta;
879 width: ๐Ÿ”บ
880 }
881 }
882 "#;
883 for source in [with_semi, without_semi] {
884 let res = get_completions(source).unwrap();
885 res.iter().find(|ci| ci.label == "alpha").unwrap();
886 res.iter().find(|ci| ci.label == "beta").unwrap();
887 res.iter().find(|ci| ci.label == "funi").unwrap();
888 res.iter().find(|ci| ci.label == "Glib").unwrap();
889 res.iter().find(|ci| ci.label == "Colors").unwrap();
890 res.iter().find(|ci| ci.label == "Math").unwrap();
891 res.iter().find(|ci| ci.label == "animation-tick").unwrap();
892 res.iter().find(|ci| ci.label == "bobo").unwrap();
893 res.iter().find(|ci| ci.label == "true").unwrap();
894 res.iter().find(|ci| ci.label == "self").unwrap();
895 res.iter().find(|ci| ci.label == "root").unwrap();
896 res.iter().find(|ci| ci.label == "TextInputInterface").unwrap();
897
898 assert!(!res.iter().any(|ci| ci.label == "text"));
899 assert!(!res.iter().any(|ci| ci.label == "red"));
900 assert!(!res.iter().any(|ci| ci.label == "nope"));
901
902 assert!(!res.iter().any(|ci| ci.label == "Rectangle"));
903 assert!(!res.iter().any(|ci| ci.label == "Clip"));
904 assert!(!res.iter().any(|ci| ci.label == "NativeStyleMetrics"));
905 assert!(!res.iter().any(|ci| ci.label == "SlintInternal"));
906 }
907 }
908
909 #[test]
910 fn dashes_and_underscores() {
911 let in_element = r#"
912 component Bar { property <string> super_property-1; }
913 component Foo {
914 Bar {
915 function nope() {}
916 property<int> hello_world;
917 pure callback with_underscores-and_dash();
918 ๐Ÿ”บ
919 }
920 }
921 "#;
922 let in_expr1 = r#"
923 component Bar { property <string> nope; }
924 component Foo {
925 function hello_world() {}
926 Bar {
927 property <string> super_property-1;
928 pure callback with_underscores-and_dash();
929 width: ๐Ÿ”บ
930 }
931 }
932 "#;
933 let in_expr2 = r#"
934 component Bar { property <string> super_property-1; }
935 component Foo {
936 property <int> nope;
937 Bar {
938 function hello_world() {}
939 pure callback with_underscores-and_dash();
940 width: self.๐Ÿ”บ
941 }
942 }
943 "#;
944 for source in [in_element, in_expr1, in_expr2] {
945 let res = get_completions(source).unwrap();
946 assert!(!res.iter().any(|ci| ci.label == "nope"));
947 res.iter().find(|ci| ci.label == "with_underscores-and_dash").unwrap();
948 res.iter().find(|ci| ci.label == "super_property-1").unwrap();
949 res.iter().find(|ci| ci.label == "hello_world").unwrap();
950 }
951 }
952
953 #[test]
954 fn arguments_struct() {
955 let source = r#"
956 struct S1 { foo: int, bar: {xx: int, yy: string} }
957 component Bar { callback c(S1) }
958 component Foo {
959 Bar {
960 c(param) => { param.bar.๐Ÿ”บ }
961 }
962 }
963 "#;
964 let res = get_completions(source).unwrap();
965 res.iter().find(|ci| ci.label == "xx").unwrap();
966 res.iter().find(|ci| ci.label == "yy").unwrap();
967 assert_eq!(res.len(), 2);
968 }
969
970 #[test]
971 fn function_args() {
972 let source = r#"
973 component Foo {
974 function xxx(alpha: int, beta_gamma: string) -> color {
975 ๐Ÿ”บ
976 }
977 }
978 "#;
979 let res = get_completions(source).unwrap();
980 res.iter().find(|ci| ci.label == "alpha").unwrap();
981 res.iter().find(|ci| ci.label == "beta-gamma").unwrap();
982 res.iter().find(|ci| ci.label == "red").unwrap();
983 assert!(!res.iter().any(|ci| ci.label == "width"));
984 }
985
986 #[test]
987 fn function_no_when_in_empty_state() {
988 let source = r#"
989 component Foo {
990 states [
991 ๐Ÿ”บ
992 ]
993 }
994 "#;
995 assert!(get_completions(source).is_none());
996 }
997
998 #[test]
999 fn function_no_when_in_state() {
1000 let source = r#"
1001 component Foo {
1002 property<bool> bar: false;
1003 states [
1004 foo when root.bar: { }
1005 ๐Ÿ”บ
1006 baz when !root.bar: { }
1007 ]
1008 }
1009 "#;
1010 assert!(get_completions(source).is_none());
1011 }
1012
1013 #[test]
1014 fn function_when_after_state_name() {
1015 let source = r#"
1016 component Foo {
1017 states [
1018 foo ๐Ÿ”บ
1019 ]
1020 }
1021 "#;
1022 let res = get_completions(source).unwrap();
1023 res.iter().find(|ci| ci.label == "when").unwrap();
1024 }
1025
1026 #[test]
1027 fn function_when_after_state_name_between_more_states() {
1028 let source = r#"
1029 component Foo {
1030 states [
1031 foo when root.bar: { }
1032 barbar ๐Ÿ”บ
1033 baz when !root.bar: { }
1034 ]
1035 }
1036 "#;
1037 let res = get_completions(source).unwrap();
1038 res.iter().find(|ci| ci.label == "when").unwrap();
1039 }
1040
1041 #[test]
1042 fn import_component() {
1043 let source = r#"
1044 import {๐Ÿ”บ} from "std-widgets.slint"
1045 "#;
1046 let res = get_completions(source).unwrap();
1047 res.iter().find(|ci| ci.label == "LineEdit").unwrap();
1048 res.iter().find(|ci| ci.label == "StyleMetrics").unwrap();
1049
1050 let source = r#"
1051 import { Foo, ๐Ÿ”บ} from "std-widgets.slint"
1052 "#;
1053 let res = get_completions(source).unwrap();
1054 res.iter().find(|ci| ci.label == "TextEdit").unwrap();
1055 }
1056
1057 #[test]
1058 fn animation_completion() {
1059 let source = r#"
1060 component Foo {
1061 Text {
1062 width: 20px;
1063 animate width {
1064 ๐Ÿ”บ
1065 }
1066 }
1067 }
1068 "#;
1069 let res = get_completions(source).unwrap();
1070 res.iter().find(|ci| ci.label == "delay").unwrap();
1071 res.iter().find(|ci| ci.label == "duration").unwrap();
1072 res.iter().find(|ci| ci.label == "iteration-count").unwrap();
1073 res.iter().find(|ci| ci.label == "easing").unwrap();
1074 }
1075
1076 #[test]
1077 fn animation_easing_completion() {
1078 let source = r#"
1079 component Foo {
1080 Text {
1081 width: 20px;
1082 animate width {
1083 easing: ๐Ÿ”บ;
1084 }
1085 }
1086 }
1087 "#;
1088 let res = get_completions(source).unwrap();
1089 res.iter().find(|ci| ci.label == "ease-in-quad").unwrap();
1090 res.iter().find(|ci| ci.label == "ease-out-quad").unwrap();
1091 res.iter().find(|ci| ci.label == "ease-in-out-quad").unwrap();
1092 res.iter().find(|ci| ci.label == "ease").unwrap();
1093 res.iter().find(|ci| ci.label == "ease-in").unwrap();
1094 res.iter().find(|ci| ci.label == "ease-out").unwrap();
1095 res.iter().find(|ci| ci.label == "ease-in-out").unwrap();
1096 res.iter().find(|ci| ci.label == "ease-in-quart").unwrap();
1097 res.iter().find(|ci| ci.label == "ease-out-quart").unwrap();
1098 res.iter().find(|ci| ci.label == "ease-in-out-quart").unwrap();
1099 res.iter().find(|ci| ci.label == "ease-in-quint").unwrap();
1100 res.iter().find(|ci| ci.label == "ease-out-quint").unwrap();
1101 res.iter().find(|ci| ci.label == "ease-in-out-quint").unwrap();
1102 res.iter().find(|ci| ci.label == "ease-in-expo").unwrap();
1103 res.iter().find(|ci| ci.label == "ease-out-expo").unwrap();
1104 res.iter().find(|ci| ci.label == "ease-in-out-expo").unwrap();
1105 res.iter().find(|ci| ci.label == "ease-in-sine").unwrap();
1106 res.iter().find(|ci| ci.label == "ease-out-sine").unwrap();
1107 res.iter().find(|ci| ci.label == "ease-in-out-sine").unwrap();
1108 res.iter().find(|ci| ci.label == "ease-in-back").unwrap();
1109 res.iter().find(|ci| ci.label == "ease-out-back").unwrap();
1110 res.iter().find(|ci| ci.label == "ease-in-out-back").unwrap();
1111 res.iter().find(|ci| ci.label == "ease-in-elastic").unwrap();
1112 res.iter().find(|ci| ci.label == "ease-out-elastic").unwrap();
1113 res.iter().find(|ci| ci.label == "ease-in-out-elastic").unwrap();
1114 res.iter().find(|ci| ci.label == "ease-in-bounce").unwrap();
1115 res.iter().find(|ci| ci.label == "ease-out-bounce").unwrap();
1116 res.iter().find(|ci| ci.label == "ease-in-out-bounce").unwrap();
1117 res.iter().find(|ci| ci.label == "linear").unwrap();
1118 res.iter().find(|ci| ci.label == "cubic-bezier").unwrap();
1119 }
1120
1121 #[test]
1122 fn element_snippet_without_braces() {
1123 let source = r#"
1124 component Foo {
1125 ๐Ÿ”บ
1126 }
1127 "#;
1128 let res = get_completions(source)
1129 .unwrap()
1130 .into_iter()
1131 .filter(|ci| {
1132 matches!(
1133 ci,
1134 CompletionItem {
1135 insert_text_format: Some(InsertTextFormat::SNIPPET),
1136 detail: Some(detail),
1137 ..
1138 }
1139 if detail == "element"
1140 )
1141 })
1142 .collect::<Vec<_>>();
1143 assert!(!res.is_empty());
1144 assert!(res.iter().all(|ci| ci.insert_text.as_ref().is_some_and(|t| t.ends_with("{$1}"))));
1145 }
1146
1147 #[test]
1148 fn element_snippet_before_braces() {
1149 let source = r#"
1150 component Foo {
1151 ๐Ÿ”บ {}
1152 }
1153 "#;
1154 let res = get_completions(source)
1155 .unwrap()
1156 .into_iter()
1157 .filter(|ci| {
1158 matches!(
1159 ci,
1160 CompletionItem {
1161 insert_text_format: Some(InsertTextFormat::SNIPPET),
1162 detail: Some(detail),
1163 ..
1164 }
1165 if detail == "element"
1166 )
1167 })
1168 .collect::<Vec<_>>();
1169 assert!(!res.is_empty());
1170 assert!(res.iter().all(|ci| ci.insert_text.is_none()));
1171 }
1172
1173 #[test]
1174 fn import_completed_component() {
1175 let source = r#"
1176 import { VerticalBox } from "std-widgets.slint";
1177
1178 export component Test {
1179 VerticalBox {
1180 ๐Ÿ”บ
1181 }
1182 }
1183
1184 "#;
1185 let res = get_completions(source).unwrap();
1186 let about = res.iter().find(|ci| ci.label.starts_with("AboutSlint")).unwrap();
1187
1188 let additional_edits = about.additional_text_edits.as_ref().unwrap();
1189 let edit = additional_edits.first().unwrap();
1190
1191 assert_eq!(edit.range.start.line, 1);
1192 assert_eq!(edit.range.start.character, 32);
1193 assert_eq!(edit.range.end.line, 1);
1194 assert_eq!(edit.range.end.character, 32);
1195 assert_eq!(edit.new_text, ", AboutSlint");
1196 }
1197
1198 #[test]
1199 fn inherits() {
1200 let sources = [
1201 "component Bar ๐Ÿ”บ",
1202 "component Bar in๐Ÿ”บ",
1203 "component Bar ๐Ÿ”บ {}",
1204 "component Bar in๐Ÿ”บ Window {}",
1205 ];
1206 for source in sources {
1207 eprintln!("Test for inherits in {source:?}");
1208 let res = get_completions(source).unwrap();
1209 res.iter().find(|ci| ci.label == "inherits").unwrap();
1210 }
1211
1212 let sources = ["component ๐Ÿ”บ", "component Bar {}๐Ÿ”บ", "component Bar inherits ๐Ÿ”บ {}", "๐Ÿ”บ"];
1213 for source in sources {
1214 let Some(res) = get_completions(source) else { continue };
1215 assert!(
1216 !res.iter().any(|ci| ci.label == "inherits"),
1217 "completion for {source:?} contains 'inherits'"
1218 );
1219 }
1220 }
1221
1222 #[test]
1223 fn two_way_bindings() {
1224 let sources = [
1225 "component X { property<string> prop; elem := Text{} property foo <=> ๐Ÿ”บ",
1226 "component X { property<string> prop; elem := Text{} property<string> foo <=> e๐Ÿ”บ; }",
1227 "component X { property<string> prop; elem := Text{} prop <=> ๐Ÿ”บ",
1228 "component X { property<string> prop; elem := Text{} prop <=> e๐Ÿ”บ; }",
1229 ];
1230 for source in sources {
1231 eprintln!("Test for two ways in {source:?}");
1232 let res = get_completions(source).unwrap();
1233 res.iter().find(|ci| ci.label == "prop").unwrap();
1234 res.iter().find(|ci| ci.label == "self").unwrap();
1235 res.iter().find(|ci| ci.label == "root").unwrap();
1236 res.iter().find(|ci| ci.label == "elem").unwrap();
1237 }
1238
1239 let sources = [
1240 "component X { elem := Text{ property<int> prop; } property foo <=> elem.๐Ÿ”บ",
1241 "component X { elem := Text{ property<int> prop; } property <string> foo <=> elem.t๐Ÿ”บ",
1242 "component X { elem := Text{ property<int> prop; } property foo <=> elem.๐Ÿ”บ; }",
1243 "component X { elem := Text{ property<string> prop; } title <=> elem.t๐Ÿ”บ",
1244 "component X { elem := Text{ property<string> prop; } title <=> elem.๐Ÿ”บ; }",
1245 ];
1246 for source in sources {
1247 eprintln!("Test for two ways in {source:?}");
1248 let res = get_completions(source).unwrap();
1249 res.iter().find(|ci| ci.label == "text").unwrap();
1250 res.iter().find(|ci| ci.label == "prop").unwrap();
1251 assert!(!res.iter().any(|ci| ci.label == "elem"));
1252 }
1253 }
1254}
1255