feat: check surrounding syntax for elements/selectors (#236)

* feat: check surrounding syntax for elements/selectors

* dev: update snapshot

* dev: update snapshot
This commit is contained in:
Myriad-Dreamin 2024-05-05 19:36:30 +08:00 committed by GitHub
parent d774304574
commit bf8a505135
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 186 additions and 83 deletions

View file

@ -236,6 +236,7 @@ mod tests {
.into_iter() .into_iter()
.map(|item| CompletionItem { .map(|item| CompletionItem {
label: item.label, label: item.label,
label_details: item.label_details,
sort_text: item.sort_text, sort_text: item.sort_text,
kind: item.kind, kind: item.kind,
text_edit: item.text_edit, text_edit: item.text_edit,

View file

@ -65,7 +65,7 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_args.typ
{ {
"kind": 7, "kind": 7,
"label": "content", "label": "content",
"sortText": "031", "sortText": "050",
"textEdit": { "textEdit": {
"newText": "content", "newText": "content",
"range": { "range": {

View file

@ -65,7 +65,7 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_with_args.typ
{ {
"kind": 7, "kind": 7,
"label": "content", "label": "content",
"sortText": "031", "sortText": "050",
"textEdit": { "textEdit": {
"newText": "content", "newText": "content",
"range": { "range": {

View file

@ -25,23 +25,6 @@ input_file: crates/tinymist-query/src/fixtures/completion/import_star.typ
} }
} }
}, },
{
"kind": 3,
"label": "aa.where",
"textEdit": {
"newText": "aa.where(${1:})",
"range": {
"end": {
"character": 4,
"line": 2
},
"start": {
"character": 1,
"line": 2
}
}
}
},
{ {
"kind": 3, "kind": 3,
"label": "aa.with", "label": "aa.with",

View file

@ -1212,12 +1212,7 @@ impl<'a, 'w> CompletionContext<'a, 'w> {
} }
self.completions.push(Completion { self.completions.push(Completion {
kind: match value { kind: value_to_completion_kind(value),
Value::Func(_) => CompletionKind::Func,
Value::Type(_) => CompletionKind::Type,
Value::Symbol(s) => CompletionKind::Symbol(s.get()),
_ => CompletionKind::Constant,
},
label, label,
apply, apply,
detail, detail,

View file

@ -48,9 +48,10 @@ impl<'a, 'w> CompletionContext<'a, 'w> {
pub fn scope_completions_(&mut self, parens: bool, filter: impl Fn(Option<&Value>) -> bool) { pub fn scope_completions_(&mut self, parens: bool, filter: impl Fn(Option<&Value>) -> bool) {
let mut defined = BTreeMap::new(); let mut defined = BTreeMap::new();
#[derive(Debug, Clone)]
enum DefKind { enum DefKind {
Syntax(Span), Syntax(Span),
Instance(Spanned<Value>), Instance(Span, Value),
} }
let mut try_insert = |name: EcoString, kind: (CompletionKind, DefKind)| { let mut try_insert = |name: EcoString, kind: (CompletionKind, DefKind)| {
@ -114,12 +115,12 @@ impl<'a, 'w> CompletionContext<'a, 'w> {
})(); })();
let def_kind = analyzed.clone().map(|module_ins| { let def_kind = analyzed.clone().map(|module_ins| {
DefKind::Instance(Spanned::new( DefKind::Instance(
module_ins,
v.new_name() v.new_name()
.map(|n| n.span()) .map(|n| n.span())
.unwrap_or_else(Span::detached), .unwrap_or_else(Span::detached),
)) module_ins,
)
}); });
if let Some((name, def_kind)) = name.zip(def_kind) { if let Some((name, def_kind)) = name.zip(def_kind) {
@ -159,32 +160,23 @@ impl<'a, 'w> CompletionContext<'a, 'w> {
if let Some(scope) = module_ins.scope() { if let Some(scope) = module_ins.scope() {
for (name, v) in scope.iter() { for (name, v) in scope.iter() {
let kind = match v { let kind = value_to_completion_kind(v);
Value::Func(..) => CompletionKind::Func,
Value::Module(..) => CompletionKind::Module,
Value::Type(..) => CompletionKind::Type,
_ => CompletionKind::Constant,
};
let def_kind = match &import_filter { let def_kind = match &import_filter {
Some(import_filter) => { Some(import_filter) => {
let w = import_filter.get(name); let w = import_filter.get(name);
match w { match w {
Some(DefKind::Syntax(span)) => { Some(DefKind::Syntax(span)) => {
Some(DefKind::Instance(Spanned::new( Some(DefKind::Instance(*span, v.clone()))
v.clone(),
*span,
)))
} }
Some(DefKind::Instance(v)) => { Some(DefKind::Instance(span, v)) => {
Some(DefKind::Instance(v.clone())) Some(DefKind::Instance(*span, v.clone()))
} }
None => None, None => None,
} }
} }
None => Some(DefKind::Instance(Spanned::new( None => {
v.clone(), Some(DefKind::Instance(Span::detached(), v.clone()))
Span::detached(), }
))),
}; };
if let Some(def_kind) = def_kind { if let Some(def_kind) = def_kind {
try_insert(name.clone(), (kind, def_kind)); try_insert(name.clone(), (kind, def_kind));
@ -263,55 +255,177 @@ impl<'a, 'w> CompletionContext<'a, 'w> {
.clone(); .clone();
for (name, value) in scope.iter() { for (name, value) in scope.iter() {
if filter(Some(value)) && !defined.contains_key(name) { if filter(Some(value)) && !defined.contains_key(name) {
self.value_completion(Some(name.clone()), value, parens, None); defined.insert(
name.clone(),
(
value_to_completion_kind(value),
DefKind::Instance(Span::detached(), value.clone()),
),
);
} }
} }
enum SurroundingSyntax {
Regular,
Selector,
SetRule,
}
let surrounding_syntax = check_surrounding_syntax(&self.leaf)
.or_else(|| check_previous_syntax(&self.leaf))
.unwrap_or(SurroundingSyntax::Regular);
for (name, (kind, def_kind)) in defined { for (name, (kind, def_kind)) in defined {
if filter(None) && !name.is_empty() { if !filter(None) || name.is_empty() {
let _ = types; continue;
let _ = def_kind; }
if kind == CompletionKind::Func { let _ = types;
let apply = eco_format!("{}.with(${{}})", name); if kind == CompletionKind::Func {
let base = Completion {
kind: kind.clone(),
// todo: only vscode and neovim (0.9.1) support this
command: Some("editor.action.triggerSuggest"),
..Default::default()
};
let zero_args = match &def_kind {
DefKind::Instance(_, Value::Func(func)) => func
.params()
.is_some_and(|params| params.iter().all(|param| param.name == "self")),
_ => false,
};
let is_element = match &def_kind {
DefKind::Instance(_, Value::Func(func)) => func.element().is_some(),
_ => false,
};
log::debug!("is_element: {} {:?} -> {:?}", name, def_kind, is_element);
if !zero_args && matches!(surrounding_syntax, SurroundingSyntax::Regular) {
self.completions.push(Completion { self.completions.push(Completion {
kind: kind.clone(),
label: eco_format!("{}.with", name), label: eco_format!("{}.with", name),
apply: Some(apply), apply: Some(eco_format!("{}.with(${{}})", name)),
// todo: only vscode and neovim (0.9.1) support this ..base.clone()
command: Some("editor.action.triggerSuggest"),
..Default::default()
});
let apply = eco_format!("{}.where(${{}})", name);
self.completions.push(Completion {
kind: kind.clone(),
label: eco_format!("{}.where", name),
apply: Some(apply),
// todo: only vscode and neovim (0.9.1) support this
command: Some("editor.action.triggerSuggest"),
..Default::default()
});
// todo: check arguments, if empty, jump to after the parens
let apply = eco_format!("{}(${{}})", name);
self.completions.push(Completion {
kind: kind.clone(),
label: name,
apply: Some(apply),
// todo: only vscode and neovim (0.9.1) support this
command: Some("editor.action.triggerSuggest"),
..Completion::default()
});
} else {
self.completions.push(Completion {
kind,
label: name,
..Completion::default()
}); });
} }
if is_element && !matches!(surrounding_syntax, SurroundingSyntax::SetRule) {
self.completions.push(Completion {
label: eco_format!("{}.where", name),
apply: Some(eco_format!("{}.where(${{}})", name)),
..base.clone()
});
}
let bad_instantiate = matches!(
surrounding_syntax,
SurroundingSyntax::Selector | SurroundingSyntax::SetRule
) && !is_element;
if !bad_instantiate {
if !parens {
self.completions.push(Completion {
label: name,
..base
});
} else if zero_args {
self.completions.push(Completion {
apply: Some(eco_format!("{}()${{}}", name)),
label: name,
..base
});
} else {
self.completions.push(Completion {
apply: Some(eco_format!("{}(${{}})", name)),
label: name,
..base
});
}
}
} else if let DefKind::Instance(_, v) = def_kind {
let bad_instantiate = matches!(
surrounding_syntax,
SurroundingSyntax::Selector | SurroundingSyntax::SetRule
) && !matches!(&v, Value::Func(func) if func.element().is_some());
if !bad_instantiate {
self.value_completion(Some(name), &v, parens, None);
}
} else {
self.completions.push(Completion {
kind,
label: name,
..Completion::default()
});
} }
} }
fn check_surrounding_syntax(mut leaf: &LinkedNode) -> Option<SurroundingSyntax> {
use SurroundingSyntax::*;
let mut met_args = false;
while let Some(parent) = leaf.parent() {
log::debug!(
"check_surrounding_syntax: {:?}::{:?}",
parent.kind(),
leaf.kind()
);
match parent.kind() {
SyntaxKind::CodeBlock | SyntaxKind::ContentBlock | SyntaxKind::Equation => {
return Some(Regular);
}
SyntaxKind::Named => {
return Some(Regular);
}
SyntaxKind::Args => {
met_args = true;
}
SyntaxKind::SetRule => {
let rule = parent.get().cast::<ast::SetRule>()?;
if met_args || encolsed_by(parent, rule.condition().map(|s| s.span()), leaf)
{
return Some(Regular);
} else {
return Some(SetRule);
}
}
SyntaxKind::ShowRule => {
let rule = parent.get().cast::<ast::ShowRule>()?;
if encolsed_by(parent, Some(rule.transform().span()), leaf) {
return Some(Regular);
} else {
return Some(Selector); // query's first argument
}
}
_ => {}
}
leaf = parent;
}
None
}
fn check_previous_syntax(leaf: &LinkedNode) -> Option<SurroundingSyntax> {
let mut leaf = leaf.clone();
if leaf.kind().is_trivia() {
leaf = leaf.prev_sibling()?;
}
if matches!(leaf.kind(), SyntaxKind::ShowRule | SyntaxKind::SetRule) {
return check_surrounding_syntax(&leaf.rightmost_leaf()?);
}
if matches!(leaf.kind(), SyntaxKind::Show) {
return Some(SurroundingSyntax::Selector);
}
if matches!(leaf.kind(), SyntaxKind::Set) {
return Some(SurroundingSyntax::SetRule);
}
None
}
} }
} }
fn encolsed_by(parent: &LinkedNode, s: Option<Span>, leaf: &LinkedNode) -> bool {
s.and_then(|s| parent.find(s)?.find(leaf.span())).is_some()
}
fn sort_and_explicit_code_completion(ctx: &mut CompletionContext) { fn sort_and_explicit_code_completion(ctx: &mut CompletionContext) {
let mut completions = std::mem::take(&mut ctx.completions); let mut completions = std::mem::take(&mut ctx.completions);
let explict = ctx.explicit; let explict = ctx.explicit;
@ -364,6 +478,16 @@ fn sort_and_explicit_code_completion(ctx: &mut CompletionContext) {
log::debug!("sort_and_explicit_code_completion: {:?}", ctx.completions); log::debug!("sort_and_explicit_code_completion: {:?}", ctx.completions);
} }
pub fn value_to_completion_kind(value: &Value) -> CompletionKind {
match value {
Value::Func(..) => CompletionKind::Func,
Value::Module(..) => CompletionKind::Module,
Value::Type(..) => CompletionKind::Type,
Value::Symbol(s) => CompletionKind::Symbol(s.get()),
_ => CompletionKind::Constant,
}
}
/// Add completions for the parameters of a function. /// Add completions for the parameters of a function.
pub fn param_completions<'a>( pub fn param_completions<'a>(
ctx: &mut CompletionContext<'a, '_>, ctx: &mut CompletionContext<'a, '_>,

View file

@ -374,7 +374,7 @@ fn e2e() {
}); });
let hash = replay_log(&tinymist_binary, &root.join("neovim")); let hash = replay_log(&tinymist_binary, &root.join("neovim"));
insta::assert_snapshot!(hash, @"siphash128_13:35e217e61e97a024b17ba8020374a65f"); insta::assert_snapshot!(hash, @"siphash128_13:10b6c633caf6174b1b1728fe79be4c7a");
} }
{ {
@ -385,7 +385,7 @@ fn e2e() {
}); });
let hash = replay_log(&tinymist_binary, &root.join("vscode")); let hash = replay_log(&tinymist_binary, &root.join("vscode"));
insta::assert_snapshot!(hash, @"siphash128_13:e40d55d49e7012058d1623c8ea1a1573"); insta::assert_snapshot!(hash, @"siphash128_13:fffd39b26d0e52d1b274290e378df265");
} }
} }