Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LSP: completion of callback snippets including argument names #6731

Merged
merged 2 commits into from
Nov 7, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 111 additions & 71 deletions tools/lsp/language/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ use lsp_types::{
CompletionClientCapabilities, CompletionItem, CompletionItemKind, InsertTextFormat, Position,
Range, TextEdit,
};
use smol_str::SmolStr;
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::path::Path;
Expand Down Expand Up @@ -63,29 +62,8 @@ pub(crate) fn completion_at(
return Some(vec![CompletionItem::new_simple("children".into(), String::new())]);
}

return resolve_element_scope(element, document_cache).map(|mut r| {
let mut available_types = HashSet::new();
if snippet_support {
for c in r.iter_mut() {
c.insert_text_format = Some(InsertTextFormat::SNIPPET);
match c.kind {
Some(CompletionItemKind::PROPERTY) => {
c.insert_text = Some(format!("{}: ", c.label))
}
Some(CompletionItemKind::METHOD) => {
c.insert_text = Some(format!("{} => {{$1}}", c.label))
}
Some(CompletionItemKind::CLASS) => {
available_types.insert(c.label.clone());
if !is_followed_by_brace(&token) {
c.insert_text = Some(format!("{} {{$1}}", c.label))
}
}
_ => (),
}
}
}

let with_snippets = snippet_support && !is_followed_by_brace(&token);
return resolve_element_scope(element, document_cache, with_snippets).map(|mut r| {
let is_global = node
.parent()
.and_then(|n| n.child_text(SyntaxKind::Identifier))
Expand Down Expand Up @@ -131,7 +109,7 @@ pub(crate) fn completion_at(
}

if !is_global && snippet_support {
add_components_to_import(&token, document_cache, available_types, &mut r);
add_components_to_import(&token, document_cache, &mut r);
}

r
Expand All @@ -147,7 +125,8 @@ pub(crate) fn completion_at(
if token.kind() != SyntaxKind::Identifier {
return None;
}
let all = resolve_element_scope(syntax_nodes::Element::new(n.parent()?)?, document_cache)?;
let all =
resolve_element_scope(syntax_nodes::Element::new(n.parent()?)?, document_cache, false)?;
return Some(
all.into_iter()
.filter(|ce| ce.kind == Some(CompletionItemKind::PROPERTY))
Expand Down Expand Up @@ -182,7 +161,7 @@ pub(crate) fn completion_at(
}
parent = parent.parent()?;
};
let all = resolve_element_scope(element, document_cache)?;
let all = resolve_element_scope(element, document_cache, false)?;
return Some(
all.into_iter()
.filter(|ce| ce.kind == Some(CompletionItemKind::METHOD))
Expand Down Expand Up @@ -273,8 +252,7 @@ pub(crate) fn completion_at(
drop(global_tr);

if snippet_support {
let available_types = result.iter().map(|c| c.label.clone()).collect();
add_components_to_import(&token, document_cache, available_types, &mut result);
add_components_to_import(&token, document_cache, &mut result);
}

return Some(result);
Expand Down Expand Up @@ -491,7 +469,24 @@ fn properties_for_changed_callbacks(
fn resolve_element_scope(
element: syntax_nodes::Element,
document_cache: &DocumentCache,
with_snippets: bool,
) -> Option<Vec<CompletionItem>> {
let apply_property_ty =
|mut c: CompletionItem, ty: &Type, cb_args: Option<&[String]>| -> CompletionItem {
if matches!(ty, Type::InferredCallback | Type::Callback { .. }) {
c.kind = Some(CompletionItemKind::METHOD);
let ins_text = match cb_args {
Some(a) => format!("{}({}) => {{$1}}", c.label, a.join(", ")),
None => format!("{} => {{$1}}", c.label),
};
with_insert_text(c, &ins_text, with_snippets)
} else {
c.kind = Some(CompletionItemKind::PROPERTY);
let ins_text = format!("{}: ", c.label);
with_insert_text(c, &ins_text, with_snippets)
}
};

let global_tr = document_cache.global_type_registry();
let tr = element
.source_file()
Expand All @@ -510,57 +505,67 @@ fn resolve_element_scope(
lk.is_local_to_component = false;
return lk.is_valid_for_assignment();
})
.map(|(k, t)| {
let k = de_normalize_property_name(&element_type, &k).into_owned();
let mut c = CompletionItem::new_simple(k, t.to_string());
c.kind = Some(if matches!(t, Type::InferredCallback | Type::Callback { .. }) {
CompletionItemKind::METHOD
.map(|(k, ty)| {
let cb_args = if with_snippets && matches!(ty, Type::Callback { .. }) {
let mut base = element_type.clone();
loop {
let ElementType::Component(c) = base else { break None };
if let Some(p) = c.root_element.borrow().property_declarations.get(&k) {
if let Some(node) =
p.node.clone().and_then(syntax_nodes::CallbackDeclaration::new)
{
if !node
.CallbackDeclarationParameter()
.all(|x| x.DeclaredIdentifier().is_some())
{
break None;
}
let vec = node
.CallbackDeclarationParameter()
.map(|x| x.DeclaredIdentifier().unwrap().text().to_string())
.collect::<Vec<_>>();
break (!vec.is_empty()).then_some(vec);
}
}

base = c.root_element.borrow().base_type.clone();
}
} else {
CompletionItemKind::PROPERTY
});
None
};
let k = de_normalize_property_name(&element_type, &k).into_owned();
let mut c = CompletionItem::new_simple(k, ty.to_string());
c.sort_text = Some(format!("#{}", c.label));
c
apply_property_ty(c, &ty, cb_args.as_deref())
})
.chain(element.PropertyDeclaration().filter_map(|pr| {
let name = pr.DeclaredIdentifier().child_text(SyntaxKind::Identifier)?;
let mut c = CompletionItem::new_simple(
pr.DeclaredIdentifier()
.child_text(SyntaxKind::Identifier)
.as_ref()
.map(SmolStr::to_string)?,
name.to_string(),
pr.Type().map(|t| t.text().into()).unwrap_or_else(|| "property".to_owned()),
);
c.kind = Some(CompletionItemKind::PROPERTY);
c.sort_text = Some(format!("#{}", c.label));
Some(c)
Some(with_insert_text(c, &format!("{name}: "), with_snippets))
}))
.chain(element.CallbackDeclaration().filter_map(|cd| {
let mut c = CompletionItem::new_simple(
cd.DeclaredIdentifier()
.child_text(SyntaxKind::Identifier)
.as_ref()
.map(SmolStr::to_string)?,
"callback".into(),
);
let name = cd.DeclaredIdentifier().child_text(SyntaxKind::Identifier)?;
let mut c = CompletionItem::new_simple(name.to_string(), "callback".into());
c.kind = Some(CompletionItemKind::METHOD);
c.sort_text = Some(format!("#{}", c.label));
Some(c)
Some(with_insert_text(c, &format!("{name} => {{$1}}"), with_snippets))
}))
.collect::<Vec<_>>();

if !matches!(element_type, ElementType::Global) {
result.extend(
i_slint_compiler::typeregister::reserved_properties()
.filter_map(|(k, t, _)| {
if matches!(t, Type::Function { .. }) {
.filter_map(|(k, ty, _)| {
if matches!(ty, Type::Function { .. }) {
return None;
}
let mut c = CompletionItem::new_simple(k.into(), t.to_string());
c.kind = Some(if matches!(t, Type::InferredCallback | Type::Callback { .. }) {
CompletionItemKind::METHOD
} else {
CompletionItemKind::PROPERTY
});
Some(c)
let c = CompletionItem::new_simple(k.into(), ty.to_string());
Some(apply_property_ty(c, &ty, None))
})
.chain(tr.all_elements().into_iter().filter_map(|(k, t)| {
match t {
Expand All @@ -570,7 +575,7 @@ fn resolve_element_scope(
};
let mut c = CompletionItem::new_simple(k.to_string(), "element".into());
c.kind = Some(CompletionItemKind::CLASS);
Some(c)
Some(with_insert_text(c, &format!("{k} {{$1}}"), with_snippets))
})),
);
};
Expand Down Expand Up @@ -768,9 +773,9 @@ fn complete_path_in_string(
fn add_components_to_import(
token: &SyntaxToken,
document_cache: &common::DocumentCache,
mut available_types: HashSet<String>,
result: &mut Vec<CompletionItem>,
) {
let mut available_types: HashSet<_> = result.iter().map(|c| c.label.clone()).collect();
build_import_statements_edits(
token,
document_cache,
Expand Down Expand Up @@ -1066,19 +1071,23 @@ mod tests {
}
"#;
let res = get_completions(source).unwrap();

const P: Option<CompletionItemKind> = Some(CompletionItemKind::PROPERTY);
const M: Option<CompletionItemKind> = Some(CompletionItemKind::METHOD);

// from TouchArea
res.iter().find(|ci| ci.label == "enabled").unwrap();
res.iter().find(|ci| ci.label == "clicked").unwrap();
assert_eq!(res.iter().find(|ci| ci.label == "enabled").unwrap().kind, P);
assert_eq!(res.iter().find(|ci| ci.label == "clicked").unwrap().kind, M);
// general
res.iter().find(|ci| ci.label == "width").unwrap();
res.iter().find(|ci| ci.label == "y").unwrap();
res.iter().find(|ci| ci.label == "accessible-role").unwrap();
res.iter().find(|ci| ci.label == "opacity").unwrap();
assert_eq!(res.iter().find(|ci| ci.label == "width").unwrap().kind, P);
assert_eq!(res.iter().find(|ci| ci.label == "y").unwrap().kind, P);
assert_eq!(res.iter().find(|ci| ci.label == "accessible-role").unwrap().kind, P);
assert_eq!(res.iter().find(|ci| ci.label == "opacity").unwrap().kind, P);
// from Bar
res.iter().find(|ci| ci.label == "in_prop").unwrap();
res.iter().find(|ci| ci.label == "the-callback").unwrap();
assert_eq!(res.iter().find(|ci| ci.label == "in_prop").unwrap().kind, P);
assert_eq!(res.iter().find(|ci| ci.label == "the-callback").unwrap().kind, M);
// local
res.iter().find(|ci| ci.label == "local-prop").unwrap();
assert_eq!(res.iter().find(|ci| ci.label == "local-prop").unwrap().kind, P);
// no functions, no private stuff
assert_eq!(res.iter().find(|ci| ci.label == "out_prop" || ci.label == "out-prop"), None);
assert_eq!(res.iter().find(|ci| ci.label == "priv_prop" || ci.label == "priv-prop"), None);
Expand Down Expand Up @@ -1120,6 +1129,16 @@ mod tests {
res.iter().find(|ci| ci.label == "if").unwrap().kind,
Some(CompletionItemKind::KEYWORD)
);

// check snippets:
assert_eq!(
res.iter().find(|ci| ci.label == "local-prop").unwrap().insert_text,
Some("local-prop: ".into())
);
assert_eq!(
res.iter().find(|ci| ci.label == "the-callback").unwrap().insert_text,
Some("the-callback => {$1}".into())
);
}

#[test]
Expand Down Expand Up @@ -1433,7 +1452,6 @@ mod tests {
matches!(
ci,
CompletionItem {
insert_text_format: Some(InsertTextFormat::SNIPPET),
detail: Some(detail),
..
}
Expand Down Expand Up @@ -1526,4 +1544,26 @@ mod tests {
assert!(!res.iter().any(|ci| ci.label == "elem"));
}
}

#[test]
fn callback_args() {
let source = r#"
import { StandardTableView } from "std-widgets.slint";
component Foo {
StandardTableView {
🔺
}
}
"#;
let res = get_completions(source).unwrap();
assert_eq!(
res.iter().find(|ci| ci.label == "row-pointer-event").unwrap().insert_text,
Some("row-pointer-event(row-index, event, mouse-position) => {$1}".into())
);
// builtin callback don't have named argument yet
assert_eq!(
res.iter().find(|ci| ci.label == "accessible-action-set-value").unwrap().insert_text,
Some("accessible-action-set-value => {$1}".into())
);
}
}
Loading