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 | |
6 | use super::component_catalog::all_exported_components; |
7 | use super::DocumentCache; |
8 | use crate::common::ComponentInformation; |
9 | use crate::util::{lookup_current_element_type, map_position, with_lookup_ctx}; |
10 | |
11 | #[cfg (target_arch = "wasm32" )] |
12 | use crate::wasm_prelude::*; |
13 | use i_slint_compiler::diagnostics::Spanned; |
14 | use i_slint_compiler::expression_tree::Expression; |
15 | use i_slint_compiler::langtype::{ElementType, Type}; |
16 | use i_slint_compiler::lookup::{LookupCtx, LookupObject, LookupResult}; |
17 | use i_slint_compiler::object_tree::ElementRc; |
18 | use i_slint_compiler::parser::{syntax_nodes, SyntaxKind, SyntaxToken}; |
19 | use lsp_types::{ |
20 | CompletionClientCapabilities, CompletionItem, CompletionItemKind, InsertTextFormat, Position, |
21 | Range, TextEdit, |
22 | }; |
23 | use std::borrow::Cow; |
24 | use std::collections::{HashMap, HashSet}; |
25 | use std::path::Path; |
26 | |
27 | pub(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 | |
410 | fn 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 | |
422 | fn 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) |
498 | fn 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` |
508 | fn 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 | |
520 | fn 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 | |
530 | fn 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 | |
574 | fn 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 | |
598 | fn 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 |
632 | fn 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. |
673 | fn 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 | |
734 | fn 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" ))] |
753 | pub 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 |
780 | pub 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(¤t_file).ok(); |
789 | let current_doc = document_cache.documents.get_document(¤t_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(¤t_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 | |
821 | fn 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)] |
833 | mod 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 | |