LSP: changed callbacks auto-completion

ChangeLog: LSP: auto-completion of changed callbacks
This commit is contained in:
Olivier Goffart 2024-09-26 15:13:54 +02:00
parent 61565e1ba9
commit cfa115affa
4 changed files with 151 additions and 6 deletions

View file

@ -101,10 +101,15 @@ pub fn parse_element_content(p: &mut impl Parser) {
parse_transitions(&mut *p);
}
_ => {
p.consume();
if !had_parse_error {
p.error("Parse error");
had_parse_error = true;
if p.peek().as_str() == "changed" {
// Try to recover some errors
parse_changed_callback(&mut *p);
} else {
p.consume();
if !had_parse_error {
p.error("Parse error");
had_parse_error = true;
}
}
}
},

View file

@ -14,7 +14,7 @@ use i_slint_compiler::expression_tree::Expression;
use i_slint_compiler::langtype::{ElementType, Type};
use i_slint_compiler::lookup::{LookupCtx, LookupObject, LookupResult};
use i_slint_compiler::object_tree::ElementRc;
use i_slint_compiler::parser::{syntax_nodes, SyntaxKind, SyntaxToken, TextSize};
use i_slint_compiler::parser::{syntax_nodes, SyntaxKind, SyntaxNode, SyntaxToken, TextSize};
use lsp_types::{
CompletionClientCapabilities, CompletionItem, CompletionItemKind, InsertTextFormat, Position,
Range, TextEdit,
@ -114,6 +114,7 @@ pub(crate) fn completion_at(
r.extend(
[
("animate", "animate ${1:prop} {\n $0\n}"),
("changed", "changed ${1:prop} => {$0}"),
("states", "states [\n $0\n]"),
("for", "for $1 in $2: ${3:Rectangle} {\n $0\n}"),
("if", "if $1: ${2:Rectangle} {\n $0\n}"),
@ -161,7 +162,16 @@ pub(crate) fn completion_at(
resolve_expression_scope(ctx, document_cache, snippet_support)
})?;
} else if let Some(n) = syntax_nodes::CallbackConnection::new(node.clone()) {
if token.kind() != SyntaxKind::Identifier {
if token.kind() == SyntaxKind::Whitespace || token.kind() == SyntaxKind::FatArrow {
let ident = n.child_token(SyntaxKind::Identifier)?;
if offset >= ident.text_range().end().into()
&& offset <= n.child_token(SyntaxKind::FatArrow)?.text_range().start().into()
&& ident.text() == "changed"
{
return properties_for_changed_callbacks(node, document_cache);
}
return None;
} else if token.kind() != SyntaxKind::Identifier {
return None;
}
let mut parent = n.parent()?;
@ -402,6 +412,15 @@ pub(crate) fn completion_at(
})
.collect::<Vec<_>>();
return Some(r);
} else if node.kind() == SyntaxKind::DeclaredIdentifier {
let parent = node.parent()?;
if parent.kind() == SyntaxKind::PropertyChangedCallback {
return properties_for_changed_callbacks(parent, document_cache);
}
} else if node.kind() == SyntaxKind::PropertyChangedCallback {
if offset > node.child_token(SyntaxKind::Identifier)?.text_range().end().into() {
return properties_for_changed_callbacks(node, document_cache);
}
}
None
}
@ -418,6 +437,53 @@ fn with_insert_text(
c
}
/// This is different than the properies in resolve_element_scope, because it also include the "out" properties
fn properties_for_changed_callbacks(
mut node: SyntaxNode,
document_cache: &mut DocumentCache,
) -> Option<Vec<CompletionItem>> {
let element = loop {
if let Some(e) = syntax_nodes::Element::new(node.clone()) {
break e;
}
node = node.parent()?;
};
let global_tr = document_cache.global_type_registry();
let tr = element
.source_file()
.and_then(|sf| document_cache.get_document_for_source_file(sf))
.map(|doc| &doc.local_registry)
.unwrap_or(&global_tr);
let element_type = lookup_current_element_type((*element).clone(), tr).unwrap_or_default();
let result = element_type
.property_list()
.into_iter()
.filter(|(_, ty)| ty.is_property_type())
.map(|(k, ty)| {
let k = de_normalize_property_name(&element_type, &k).into_owned();
let mut c = CompletionItem::new_simple(k, ty.to_string());
c.kind = Some(CompletionItemKind::PROPERTY);
c
})
.chain(element.PropertyDeclaration().filter_map(|pr| {
let mut c = CompletionItem::new_simple(
pr.DeclaredIdentifier().child_text(SyntaxKind::Identifier)?,
pr.Type().map(|t| t.text().into()).unwrap_or_else(|| "property".to_owned()),
);
c.kind = Some(CompletionItemKind::PROPERTY);
Some(c)
}))
.chain(i_slint_compiler::typeregister::reserved_properties().filter_map(|(k, ty, _)| {
if !ty.is_property_type() {
return None;
}
let mut c = CompletionItem::new_simple(k.into(), ty.to_string());
c.kind = Some(CompletionItemKind::PROPERTY);
Some(c)
}));
Some(result.collect())
}
/// Try to return the completion items for the location inside an element.
/// `FooBar { /* HERE */ }`
/// So properties and potential sub elements
@ -1012,6 +1078,28 @@ mod tests {
assert_eq!(res.iter().find(|ci| ci.label == "Rectangle").unwrap().kind, class);
assert_eq!(res.iter().find(|ci| ci.label == "TouchArea").unwrap().kind, class);
assert_eq!(res.iter().find(|ci| ci.label == "VerticalLayout").unwrap().kind, class);
// keywords
assert_eq!(
res.iter().find(|ci| ci.label == "changed").unwrap().kind,
Some(CompletionItemKind::KEYWORD)
);
assert_eq!(
res.iter().find(|ci| ci.label == "animate").unwrap().kind,
Some(CompletionItemKind::KEYWORD)
);
assert_eq!(
res.iter().find(|ci| ci.label == "states").unwrap().kind,
Some(CompletionItemKind::KEYWORD)
);
assert_eq!(
res.iter().find(|ci| ci.label == "for").unwrap().kind,
Some(CompletionItemKind::KEYWORD)
);
assert_eq!(
res.iter().find(|ci| ci.label == "if").unwrap().kind,
Some(CompletionItemKind::KEYWORD)
);
}
#[test]
@ -1260,6 +1348,31 @@ mod tests {
res.iter().find(|ci| ci.label == "cubic-bezier").unwrap();
}
#[test]
fn changed_completion() {
let source1 = " component Foo { TextInput { property<int> xyz; changed 🔺 => {} } } ";
let source2 = " component Foo { TextInput { property<int> xyz; changed 🔺 } } ";
let source3 = " component Foo { TextInput { property<int> xyz; changed t🔺 } } ";
let source4 = " component Foo { TextInput { property<int> xyz; changed t🔺 => {} } } ";
let source5 =
" component Foo { TextInput { property<int> xyz; changed 🔺 \n enabled: true; } } ";
let source6 =
" component Foo { TextInput { property<int> xyz; changed t🔺 \n enabled: true; } } ";
for s in [source1, source2, source3, source4, source5, source6] {
eprintln!("changed_completion: {s:?}");
let res = get_completions(s).unwrap();
res.iter().find(|ci| ci.label == "text").unwrap();
res.iter().find(|ci| ci.label == "has-focus").unwrap();
res.iter().find(|ci| ci.label == "width").unwrap();
res.iter().find(|ci| ci.label == "y").unwrap();
res.iter().find(|ci| ci.label == "xyz").unwrap();
assert!(res.iter().find(|ci| ci.label == "Text").is_none());
assert!(res.iter().find(|ci| ci.label == "edited").is_none());
assert!(res.iter().find(|ci| ci.label == "focus").is_none());
}
}
#[test]
fn element_snippet_without_braces() {
let source = r#"

View file

@ -96,6 +96,7 @@ component Abc {
export component Test {
abc := Abc {
hello: "foo";
changed hello => {}
}
btn := Button {
text: abc.hello;
@ -157,6 +158,15 @@ export component Test {
let token = crate::language::token_at_offset(&doc, offset).unwrap();
assert_eq!(token.text(), "text");
assert!(goto_definition(&mut dc, token).is_none());
// Jump from a changed callback
let offset: TextSize = (source.find("changed hello").unwrap() as u32).into();
let token = crate::language::token_at_offset(&doc, offset + TextSize::new(12)).unwrap();
assert_eq!(token.text(), "hello");
let def = goto_definition(&mut dc, token).unwrap();
let link = first_link(&def);
assert_eq!(link.target_uri, uri);
assert_eq!(link.target_range.start.line, 3);
}
#[test]

View file

@ -158,6 +158,23 @@ pub fn token_info(document_cache: &mut DocumentCache, token: SyntaxToken) -> Opt
return Some(TokenInfo::LocalCallback(p));
}
return find_property_declaration_in_base(document_cache, element, &prop_name);
} else if node.kind() == SyntaxKind::DeclaredIdentifier {
let parent = node.parent()?;
if parent.kind() == SyntaxKind::PropertyChangedCallback {
if token.kind() != SyntaxKind::Identifier {
return None;
}
let prop_name = i_slint_compiler::parser::normalize_identifier(token.text());
let element = syntax_nodes::Element::new(parent.parent()?)?;
if let Some(p) = element.PropertyDeclaration().find_map(|p| {
(i_slint_compiler::parser::identifier_text(&p.DeclaredIdentifier())?
== prop_name)
.then_some(p)
}) {
return Some(TokenInfo::LocalProperty(p));
}
return find_property_declaration_in_base(document_cache, element, &prop_name);
}
}
node = node.parent()?;
}