From 81d3ea64c2bea7fd10e7e4ab9d5d88c5973a11bf Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin <35292584+Myriad-Dreamin@users.noreply.github.com> Date: Sat, 28 Dec 2024 12:45:45 +0800 Subject: [PATCH] refactor: split completion functions by topics (#1083) --- .../tinymist-query/src/analysis/completion.rs | 2186 +---------------- .../src/analysis/completion/field_access.rs | 123 + .../src/analysis/completion/import.rs | 132 + .../src/analysis/completion/kind.rs | 292 +++ .../src/analysis/completion/mode.rs | 216 ++ .../src/analysis/completion/path.rs | 149 ++ .../src/analysis/completion/scope.rs | 396 +++ .../src/analysis/completion/snippet.rs | 297 +++ .../src/analysis/completion/type.rs | 320 +++ .../src/analysis/completion/typst_specific.rs | 238 ++ crates/tinymist-query/src/completion.rs | 11 +- 11 files changed, 2197 insertions(+), 2163 deletions(-) create mode 100644 crates/tinymist-query/src/analysis/completion/field_access.rs create mode 100644 crates/tinymist-query/src/analysis/completion/import.rs create mode 100644 crates/tinymist-query/src/analysis/completion/kind.rs create mode 100644 crates/tinymist-query/src/analysis/completion/mode.rs create mode 100644 crates/tinymist-query/src/analysis/completion/path.rs create mode 100644 crates/tinymist-query/src/analysis/completion/scope.rs create mode 100644 crates/tinymist-query/src/analysis/completion/snippet.rs create mode 100644 crates/tinymist-query/src/analysis/completion/type.rs create mode 100644 crates/tinymist-query/src/analysis/completion/typst_specific.rs diff --git a/crates/tinymist-query/src/analysis/completion.rs b/crates/tinymist-query/src/analysis/completion.rs index 9eaf77df..41316d16 100644 --- a/crates/tinymist-query/src/analysis/completion.rs +++ b/crates/tinymist-query/src/analysis/completion.rs @@ -48,6 +48,20 @@ use crate::upstream::{plain_docs_sentence, summarize_font_family}; use super::SharedContext; +mod field_access; +mod import; +mod kind; +mod mode; +mod path; +mod scope; +mod snippet; +#[path = "completion/type.rs"] +mod type_; +mod typst_specific; +use kind::*; +use scope::*; +use type_::*; + type LspCompletion = CompletionItem; /// Tinymist's completion features. @@ -289,24 +303,25 @@ type Cursor<'a> = CompletionCursor<'a>; /// enhances the autocompletions. Label completions, for instance, are /// only generated when the document is available. pub struct CompletionWorker<'a> { - /// The analysis local context. - pub ctx: &'a mut LocalContext, - /// The compiled document. - pub document: Option<&'a Document>, - /// Whether the completion was explicitly requested. - pub explicit: bool, - /// The trigger character. - pub trigger_character: Option, /// The completions. pub completions: Vec, /// Whether the completion is incomplete. pub incomplete: bool, + + /// The analysis local context. + ctx: &'a mut LocalContext, + /// The compiled document. + document: Option<&'a Document>, + /// Whether the completion was explicitly requested. + explicit: bool, + /// The trigger character. + trigger_character: Option, /// The set of cast completions seen so far. - pub seen_casts: HashSet, + seen_casts: HashSet, /// The set of type completions seen so far. - pub seen_types: HashSet, + seen_types: HashSet, /// The set of field completions seen so far. - pub seen_fields: HashSet>, + seen_fields: HashSet>, } impl<'a> CompletionWorker<'a> { @@ -356,7 +371,7 @@ impl<'a> CompletionWorker<'a> { // } /// Starts the completion process. - pub(crate) fn work(mut self, cursor: &mut Cursor) -> Option<(bool, Vec)> { + pub(crate) fn work(&mut self, cursor: &mut Cursor) -> Option<()> { // Skip if is the let binding item *directly* if let Some(SyntaxClass::VarAccess(var)) = &cursor.syntax { let node = var.node(); @@ -408,7 +423,7 @@ impl<'a> CompletionWorker<'a> { }; let mut pair = Pair { - worker: &mut self, + worker: self, cursor, }; let _ = pair.complete_root(); @@ -442,7 +457,7 @@ impl<'a> CompletionWorker<'a> { } } - Some((self.incomplete, self.completions)) + Some(()) } } @@ -706,2154 +721,11 @@ impl CompletionPair<'_, '_, '_> { Some(()) } - /// Complete in comments. Or rather, don't! - fn complete_comments(&mut self) -> bool { - let text = self.cursor.leaf.get().text(); - // check if next line defines a function - if_chain! { - if text == "///" || text == "/// "; - // hash node - if let Some(next) = self.cursor.leaf.next_leaf(); - // let node - if let Some(next_next) = next.next_leaf(); - if let Some(next_next) = next_next.next_leaf(); - if matches!(next_next.parent_kind(), Some(SyntaxKind::Closure)); - if let Some(closure) = next_next.parent(); - if let Some(closure) = closure.cast::(); - if let ast::Expr::Closure(c) = closure; - then { - let mut doc_snippet: String = if text == "///" { - " $0\n///".to_string() - } else { - "$0\n///".to_string() - }; - let mut i = 0; - for param in c.params().children() { - // TODO: Properly handle Pos and Spread argument - let param: &EcoString = match param { - Param::Pos(p) => { - match p { - ast::Pattern::Normal(ast::Expr::Ident(ident)) => ident.get(), - _ => &"_".into() - } - } - Param::Named(n) => n.name().get(), - Param::Spread(s) => { - if let Some(ident) = s.sink_ident() { - &eco_format!("{}", ident.get()) - } else { - &EcoString::new() - } - } - }; - log::info!("param: {param}, index: {i}"); - doc_snippet += &format!("\n/// - {param} (${}): ${}", i + 1, i + 2); - i += 2; - } - doc_snippet += &format!("\n/// -> ${}", i + 1); - self.push_completion(Completion { - label: "Document function".into(), - apply: Some(doc_snippet.into()), - ..Completion::default() - }); - } - }; - - true - } - fn push_completion(&mut self, completion: Completion) { self.worker .completions .push(self.cursor.lsp_item_of(&completion)); } - - /// Complete in markup mode. - fn complete_markup(&mut self) -> bool { - let parent_raw = - node_ancestors(&self.cursor.leaf).find(|node| matches!(node.kind(), SyntaxKind::Raw)); - - // Behind a half-completed binding: "#let x = |" or `#let f(x) = |`. - if_chain! { - if let Some(prev) = self.cursor.leaf.prev_leaf(); - if matches!(prev.kind(), SyntaxKind::Eq | SyntaxKind::Arrow); - if matches!( prev.parent_kind(), Some(SyntaxKind::LetBinding | SyntaxKind::Closure)); - then { - self.cursor.from = self.cursor.cursor; - self.code_completions( false); - return true; - } - } - - // Behind a half-completed context block: "#context |". - if_chain! { - if let Some(prev) = self.cursor.leaf.prev_leaf(); - if prev.kind() == SyntaxKind::Context; - then { - self.cursor.from = self.cursor.cursor; - self.code_completions(false); - return true; - } - } - - // Directly after a raw block. - if let Some(parent_raw) = parent_raw { - let mut s = Scanner::new(self.cursor.text); - s.jump(parent_raw.offset()); - if s.eat_if("```") { - s.eat_while('`'); - let start = s.cursor(); - if s.eat_if(is_id_start) { - s.eat_while(is_id_continue); - } - if s.cursor() == self.cursor.cursor { - self.cursor.from = start; - self.raw_completions(); - } - return true; - } - } - - // Anywhere: "|". - if !is_triggered_by_punc(self.worker.trigger_character) && self.worker.explicit { - self.cursor.from = self.cursor.cursor; - self.snippet_completions(Some(InterpretMode::Markup), None); - return true; - } - - false - } - - /// Complete in math mode. - fn complete_math(&mut self) -> bool { - // Behind existing atom or identifier: "$a|$" or "$abc|$". - if !is_triggered_by_punc(self.worker.trigger_character) - && matches!( - self.cursor.leaf.kind(), - SyntaxKind::Text | SyntaxKind::MathIdent - ) - { - self.cursor.from = self.cursor.leaf.offset(); - self.scope_completions(true); - self.snippet_completions(Some(InterpretMode::Math), None); - return true; - } - - // Anywhere: "$|$". - if !is_triggered_by_punc(self.worker.trigger_character) && self.worker.explicit { - self.cursor.from = self.cursor.cursor; - self.scope_completions(true); - self.snippet_completions(Some(InterpretMode::Math), None); - return true; - } - - false - } - - /// Complete in code mode. - fn complete_code(&mut self) -> bool { - // Start of an interpolated identifier: "#|". - if self.cursor.leaf.kind() == SyntaxKind::Hash { - self.cursor.from = self.cursor.cursor; - self.code_completions(true); - - return true; - } - - // Start of an interpolated identifier: "#pa|". - if self.cursor.leaf.kind() == SyntaxKind::Ident { - self.cursor.from = self.cursor.leaf.offset(); - self.code_completions(is_hash_expr(&self.cursor.leaf)); - return true; - } - - // Behind a half-completed context block: "context |". - if_chain! { - if let Some(prev) = self.cursor.leaf.prev_leaf(); - if prev.kind() == SyntaxKind::Context; - then { - self.cursor.from = self.cursor.cursor; - self.code_completions(false); - return true; - } - } - - // An existing identifier: "{ pa| }". - if self.cursor.leaf.kind() == SyntaxKind::Ident - && !matches!( - self.cursor.leaf.parent_kind(), - Some(SyntaxKind::FieldAccess) - ) - { - self.cursor.from = self.cursor.leaf.offset(); - self.code_completions(false); - return true; - } - - // Anywhere: "{ | }". - // But not within or after an expression. - // ctx.explicit && - if self.cursor.leaf.kind().is_trivia() - || (matches!( - self.cursor.leaf.kind(), - SyntaxKind::LeftParen | SyntaxKind::LeftBrace - ) || (matches!(self.cursor.leaf.kind(), SyntaxKind::Colon) - && self.cursor.leaf.parent_kind() == Some(SyntaxKind::ShowRule))) - { - self.cursor.from = self.cursor.cursor; - self.code_completions(false); - return true; - } - - false - } - - /// Add completions for expression snippets. - fn code_completions(&mut self, hash: bool) { - // todo: filter code completions - // matches!(value, Value::Symbol(_) | Value::Func(_) | Value::Type(_) | - // Value::Module(_)) - self.scope_completions(true); - - self.snippet_completions(Some(InterpretMode::Code), None); - - if !hash { - self.snippet_completion( - "function", - "(${params}) => ${output}", - "Creates an unnamed function.", - ); - } - } - - /// Complete imports. - fn complete_imports(&mut self) -> bool { - // On the colon marker of an import list: - // "#import "path.typ":|" - if_chain! { - if matches!(self.cursor.leaf.kind(), SyntaxKind::Colon); - if let Some(parent) = self.cursor.leaf.clone().parent(); - if let Some(ast::Expr::Import(import)) = parent.get().cast(); - if !matches!(import.imports(), Some(ast::Imports::Wildcard)); - if let Some(source) = parent.children().find(|child| child.is::()); - then { - let items = match import.imports() { - Some(ast::Imports::Items(items)) => items, - _ => Default::default(), - }; - - self.cursor.from = self.cursor.cursor; - - self.import_item_completions(items, vec![], &source); - if items.iter().next().is_some() { - self.worker.enrich("", ", "); - } - return true; - } - } - - // Behind an import list: - // "#import "path.typ": |", - // "#import "path.typ": a, b, |". - if_chain! { - if let Some(prev) = self.cursor.leaf.prev_sibling(); - if let Some(ast::Expr::Import(import)) = prev.get().cast(); - if !self.cursor.text[prev.offset()..self.cursor.cursor].contains('\n'); - if let Some(ast::Imports::Items(items)) = import.imports(); - if let Some(source) = prev.children().find(|child| child.is::()); - then { - self. cursor.from = self.cursor.cursor; - self.import_item_completions(items, vec![], &source); - return true; - } - } - - // Behind a comma in an import list: - // "#import "path.typ": this,|". - if_chain! { - if matches!(self.cursor.leaf.kind(), SyntaxKind::Comma); - if let Some(parent) = self.cursor.leaf.clone().parent(); - if parent.kind() == SyntaxKind::ImportItems; - if let Some(grand) = parent.parent(); - if let Some(ast::Expr::Import(import)) = grand.get().cast(); - if let Some(ast::Imports::Items(items)) = import.imports(); - if let Some(source) = grand.children().find(|child| child.is::()); - then { - self.import_item_completions(items, vec![], &source); - self.worker.enrich(" ", ""); - return true; - } - } - - // Behind a half-started identifier in an import list: - // "#import "path.typ": th|". - if_chain! { - if matches!(self.cursor.leaf.kind(), SyntaxKind::Ident | SyntaxKind::Dot); - if let Some(path_ctx) = self.cursor.leaf.clone().parent(); - if path_ctx.kind() == SyntaxKind::ImportItemPath; - if let Some(parent) = path_ctx.parent(); - if parent.kind() == SyntaxKind::ImportItems; - if let Some(grand) = parent.parent(); - if let Some(ast::Expr::Import(import)) = grand.get().cast(); - if let Some(ast::Imports::Items(items)) = import.imports(); - if let Some(source) = grand.children().find(|child| child.is::()); - then { - if self.cursor.leaf.kind() == SyntaxKind::Ident { - self.cursor.from = self.cursor.leaf.offset(); - } - let path = path_ctx.cast::().map(|path| path.iter().take_while(|ident| ident.span() != self.cursor.leaf.span()).collect()); - self.import_item_completions( items, path.unwrap_or_default(), &source); - return true; - } - } - - false - } - - /// Add completions for all exports of a module. - fn import_item_completions( - &mut self, - existing: ast::ImportItems, - comps: Vec, - source: &LinkedNode, - ) { - // Select the source by `comps` - let value = self.worker.ctx.module_by_syntax(source); - let value = comps - .iter() - .fold(value.as_ref(), |value, comp| value?.scope()?.get(comp)); - let Some(scope) = value.and_then(|v| v.scope()) else { - return; - }; - - // Check imported items in the scope - let seen = existing - .iter() - .flat_map(|item| { - let item_comps = item.path().iter().collect::>(); - if item_comps.len() == comps.len() + 1 - && item_comps - .iter() - .zip(comps.as_slice()) - .all(|(l, r)| l.as_str() == r.as_str()) - { - // item_comps.len() >= 1 - item_comps.last().cloned() - } else { - None - } - }) - .collect::>(); - - if existing.iter().next().is_none() { - self.snippet_completion("*", "*", "Import everything."); - } - - for (name, value, _) in scope.iter() { - if seen.iter().all(|item| item.as_str() != name) { - self.value_completion(Some(name.clone()), value, false, None); - } - } - } - - fn complete_path(&mut self, preference: &PathPreference) -> Option> { - let id = self.cursor.source.id(); - if id.package().is_some() { - return None; - } - - let is_in_text; - let text; - let rng; - // todo: the non-str case - if self.cursor.leaf.is::() { - let vr = self.cursor.leaf.range(); - rng = vr.start + 1..vr.end - 1; - if rng.start > rng.end - || (self.cursor.cursor != rng.end && !rng.contains(&self.cursor.cursor)) - { - return None; - } - - let mut w = EcoString::new(); - w.push('"'); - w.push_str(&self.cursor.text[rng.start..self.cursor.cursor]); - w.push('"'); - let partial_str = SyntaxNode::leaf(SyntaxKind::Str, w); - - text = partial_str.cast::()?.get(); - is_in_text = true; - } else { - text = EcoString::default(); - rng = self.cursor.cursor..self.cursor.cursor; - is_in_text = false; - } - crate::log_debug_ct!("complete_path: is_in_text: {is_in_text:?}"); - let path = Path::new(text.as_str()); - let has_root = path.has_root(); - - let src_path = id.vpath(); - let base = id; - let dst_path = src_path.join(path); - let mut compl_path = dst_path.as_rootless_path(); - if !compl_path.is_dir() { - compl_path = compl_path.parent().unwrap_or(Path::new("")); - } - crate::log_debug_ct!("compl_path: {src_path:?} + {path:?} -> {compl_path:?}"); - - if compl_path.is_absolute() { - log::warn!( - "absolute path completion is not supported for security consideration {path:?}" - ); - return None; - } - - // find directory or files in the path - let folder_completions = vec![]; - let mut module_completions = vec![]; - // todo: test it correctly - for path in self.worker.ctx.completion_files(preference) { - crate::log_debug_ct!("compl_check_path: {path:?}"); - - // Skip self smartly - if *path == base { - continue; - } - - let label: EcoString = if has_root { - // diff with root - unix_slash(path.vpath().as_rooted_path()).into() - } else { - let base = base - .vpath() - .as_rooted_path() - .parent() - .unwrap_or(Path::new("/")); - let path = path.vpath().as_rooted_path(); - let w = pathdiff::diff_paths(path, base)?; - unix_slash(&w).into() - }; - crate::log_debug_ct!("compl_label: {label:?}"); - - module_completions.push((label, CompletionKind::File)); - - // todo: looks like the folder completion is broken - // if path.is_dir() { - // folder_completions.push((label, CompletionKind::Folder)); - // } - } - - let replace_range = self.cursor.lsp_range_of(rng); - - fn is_dot_or_slash(ch: &char) -> bool { - matches!(*ch, '.' | '/') - } - - let path_priority_cmp = |lhs: &str, rhs: &str| { - // files are more important than dot started paths - if lhs.starts_with('.') || rhs.starts_with('.') { - // compare consecutive dots and slashes - let a_prefix = lhs.chars().take_while(is_dot_or_slash).count(); - let b_prefix = rhs.chars().take_while(is_dot_or_slash).count(); - if a_prefix != b_prefix { - return a_prefix.cmp(&b_prefix); - } - } - lhs.cmp(rhs) - }; - - module_completions.sort_by(|a, b| path_priority_cmp(&a.0, &b.0)); - // folder_completions.sort_by(|a, b| path_priority_cmp(&a.0, &b.0)); - - let mut sorter = 0; - let digits = (module_completions.len() + folder_completions.len()) - .to_string() - .len(); - let completions = module_completions.into_iter().chain(folder_completions); - Some( - completions - .map(|typst_completion| { - let lsp_snippet = &typst_completion.0; - let text_edit = EcoTextEdit::new( - replace_range, - if is_in_text { - lsp_snippet.clone() - } else { - eco_format!(r#""{lsp_snippet}""#) - }, - ); - - let sort_text = eco_format!("{sorter:0>digits$}"); - sorter += 1; - - // todo: no all clients support label details - LspCompletion { - label: typst_completion.0, - kind: typst_completion.1, - detail: None, - text_edit: Some(text_edit), - // don't sort me - sort_text: Some(sort_text), - filter_text: Some("".into()), - insert_text_format: Some(InsertTextFormat::PLAIN_TEXT), - ..Default::default() - } - }) - .collect_vec(), - ) - } - - fn snippet_completions( - &mut self, - mode: Option, - surrounding_syntax: Option, - ) { - let mut keys = vec![CompletionContextKey::new(mode, surrounding_syntax)]; - if mode.is_some() { - keys.push(CompletionContextKey::new(None, surrounding_syntax)); - } - if surrounding_syntax.is_some() { - keys.push(CompletionContextKey::new(mode, None)); - if mode.is_some() { - keys.push(CompletionContextKey::new(None, None)); - } - } - let applies_to = |snippet: &PrefixSnippet| keys.iter().any(|key| snippet.applies_to(key)); - - for snippet in DEFAULT_PREFIX_SNIPPET.iter() { - if !applies_to(snippet) { - continue; - } - - let analysis = &self.worker.ctx.analysis; - let command = match snippet.command { - Some(CompletionCommand::TriggerSuggest) => analysis.trigger_suggest(true), - None => analysis.trigger_on_snippet(snippet.snippet.contains("${")), - }; - - self.push_completion(Completion { - kind: CompletionKind::Syntax, - label: snippet.label.as_ref().into(), - apply: Some(snippet.snippet.as_ref().into()), - detail: Some(snippet.description.as_ref().into()), - command: command.map(From::from), - ..Completion::default() - }); - } - } - - /// Add a snippet completion. - fn snippet_completion(&mut self, label: &str, snippet: &str, docs: &str) { - self.push_completion(Completion { - kind: CompletionKind::Syntax, - label: label.into(), - apply: Some(snippet.into()), - detail: Some(docs.into()), - command: self - .worker - .ctx - .analysis - .trigger_on_snippet(snippet.contains("${")) - .map(From::from), - ..Completion::default() - }); - } - - /// Add completions for all font families. - fn font_completions(&mut self) { - let equation = self.cursor.before_window(25).contains("equation"); - for (family, iter) in self.worker.world().clone().book().families() { - let detail = summarize_font_family(iter); - if !equation || family.contains("Math") { - self.value_completion( - None, - &Value::Str(family.into()), - false, - Some(detail.as_str()), - ); - } - } - } - - /// Add completions for all available packages. - fn package_completions(&mut self, all_versions: bool) { - let w = self.worker.world().clone(); - let mut packages: Vec<_> = w - .packages() - .iter() - .map(|(spec, desc)| (spec, desc.clone())) - .collect(); - // local_packages to references and add them to the packages - let local_packages_refs = self.worker.ctx.local_packages(); - packages.extend( - local_packages_refs - .iter() - .map(|spec| (spec, Some(eco_format!("{} v{}", spec.name, spec.version)))), - ); - - packages.sort_by_key(|(spec, _)| (&spec.namespace, &spec.name, Reverse(spec.version))); - if !all_versions { - packages.dedup_by_key(|(spec, _)| (&spec.namespace, &spec.name)); - } - for (package, description) in packages { - self.value_completion( - None, - &Value::Str(format_str!("{package}")), - false, - description.as_deref(), - ); - } - } - - /// Add completions for raw block tags. - fn raw_completions(&mut self) { - for (name, mut tags) in RawElem::languages() { - let lower = name.to_lowercase(); - if !tags.contains(&lower.as_str()) { - tags.push(lower.as_str()); - } - - tags.retain(|tag| is_ident(tag)); - if tags.is_empty() { - continue; - } - - self.push_completion(Completion { - kind: CompletionKind::Constant, - label: name.into(), - apply: Some(tags[0].into()), - detail: Some(repr::separated_list(&tags, " or ").into()), - ..Completion::default() - }); - } - } - - /// Add completions for labels and references. - fn ref_completions(&mut self) { - self.label_completions_(false, true); - } - - /// Add completions for labels and references. - fn label_completions(&mut self, only_citation: bool) { - self.label_completions_(only_citation, false); - } - - /// Add completions for labels and references. - fn label_completions_(&mut self, only_citation: bool, ref_label: bool) { - let Some(document) = self.worker.document else { - return; - }; - let (labels, split) = analyze_labels(document); - - let head = &self.cursor.text[..self.cursor.from]; - let at = head.ends_with('@'); - let open = !at && !head.ends_with('<'); - let close = !at && !self.cursor.after.starts_with('>'); - let citation = !at && only_citation; - - let (skip, take) = if at || ref_label { - (0, usize::MAX) - } else if citation { - (split, usize::MAX) - } else { - (0, split) - }; - - for DynLabel { - label, - label_desc, - detail, - bib_title, - } in labels.into_iter().skip(skip).take(take) - { - if !self.worker.seen_casts.insert(hash128(&label)) { - continue; - } - let label: EcoString = label.as_str().into(); - let completion = Completion { - kind: CompletionKind::Reference, - apply: Some(eco_format!( - "{}{}{}", - if open { "<" } else { "" }, - label.as_str(), - if close { ">" } else { "" } - )), - label: label.clone(), - label_details: label_desc.clone(), - filter_text: Some(label.clone()), - detail: detail.clone(), - ..Completion::default() - }; - - if let Some(bib_title) = bib_title { - // Note that this completion re-uses the above `apply` field to - // alter the `bib_title` to the corresponding label. - self.push_completion(Completion { - kind: CompletionKind::Constant, - label: bib_title.clone(), - label_details: Some(label), - filter_text: Some(bib_title), - detail, - ..completion.clone() - }); - } - - self.push_completion(completion); - } - } - - /// Add a completion for a specific value. - fn value_completion( - &mut self, - label: Option, - value: &Value, - parens: bool, - docs: Option<&str>, - ) { - self.value_completion_( - label, - value, - parens, - match value { - Value::Symbol(s) => Some(symbol_label_detail(s.get())), - _ => None, - }, - docs, - ); - } - - /// Add a completion for a specific value. - fn value_completion_( - &mut self, - label: Option, - value: &Value, - parens: bool, - label_details: Option, - docs: Option<&str>, - ) { - // Prevent duplicate completions from appearing. - if !self.worker.seen_casts.insert(hash128(&(&label, &value))) { - return; - } - - let at = label.as_deref().is_some_and(|field| !is_ident(field)); - let label = label.unwrap_or_else(|| value.repr()); - - let detail = docs.map(Into::into).or_else(|| match value { - Value::Symbol(symbol) => Some(symbol_detail(symbol.get())), - Value::Func(func) => func.docs().map(plain_docs_sentence), - Value::Type(ty) => Some(plain_docs_sentence(ty.docs())), - v => { - let repr = v.repr(); - (repr.as_str() != label).then_some(repr) - } - }); - - let mut apply = None; - let mut command = None; - if parens && matches!(value, Value::Func(_)) { - if let Value::Func(func) = value { - command = self.worker.ctx.analysis.trigger_parameter_hints(true); - if func - .params() - .is_some_and(|params| params.iter().all(|param| param.name == "self")) - { - apply = Some(eco_format!("{label}()${{}}")); - } else { - apply = Some(eco_format!("{label}(${{}})")); - } - } - } else if at { - apply = Some(eco_format!("at(\"{label}\")")); - } else { - let apply_label = &mut label.as_str(); - if apply_label.ends_with('"') && self.cursor.after.starts_with('"') { - if let Some(trimmed) = apply_label.strip_suffix('"') { - *apply_label = trimmed; - } - } - let from_before = slice_at(self.cursor.text, 0..self.cursor.from); - if apply_label.starts_with('"') && from_before.ends_with('"') { - if let Some(trimmed) = apply_label.strip_prefix('"') { - *apply_label = trimmed; - } - } - - if apply_label.len() != label.len() { - apply = Some((*apply_label).into()); - } - } - - self.push_completion(Completion { - kind: value_to_completion_kind(value), - label, - apply, - detail, - label_details, - command: command.map(From::from), - ..Completion::default() - }); - } - - fn scope_defs(&mut self) -> Option { - let mut defines = Defines { - types: self.worker.ctx.type_check(&self.cursor.source), - defines: Default::default(), - docs: Default::default(), - }; - - let mode = interpret_mode_at(Some(&self.cursor.leaf)); - let in_math = matches!(mode, InterpretMode::Math); - - let lib = self.worker.world().library(); - let scope = if in_math { &lib.math } else { &lib.global } - .scope() - .clone(); - defines.insert_scope(&scope); - - previous_decls(self.cursor.leaf.clone(), |node| -> Option<()> { - match node { - PreviousDecl::Ident(ident) => { - let ty = self - .worker - .ctx - .type_of_span(ident.span()) - .unwrap_or(Ty::Any); - defines.insert_ty(ty, ident.get()); - } - PreviousDecl::ImportSource(src) => { - let ty = analyze_import_source(self.worker.ctx, &defines.types, src)?; - let name = ty.name().as_ref().into(); - defines.insert_ty(ty, &name); - } - // todo: cache completion items - PreviousDecl::ImportAll(mi) => { - let ty = analyze_import_source(self.worker.ctx, &defines.types, mi.source())?; - ty.iface_surface( - true, - &mut CompletionScopeChecker { - check_kind: ScopeCheckKind::Import, - defines: &mut defines, - ctx: self.worker.ctx, - }, - ); - } - } - None - }); - - Some(defines) - } - - fn postfix_completions(&mut self, node: &LinkedNode, ty: Ty) -> Option<()> { - if !self.worker.ctx.analysis.completion_feat.postfix() { - return None; - } - - let _ = node; - - if !matches!(self.cursor.surrounding_syntax, SurroundingSyntax::Regular) { - return None; - } - - let cursor_mode = interpret_mode_at(Some(node)); - let is_content = ty.is_content(&()); - crate::log_debug_ct!("post snippet is_content: {is_content}"); - - let rng = node.range(); - for snippet in self - .worker - .ctx - .analysis - .completion_feat - .postfix_snippets() - .clone() - { - if !snippet.mode.contains(&cursor_mode) { - continue; - } - - let scope = match snippet.scope { - PostfixSnippetScope::Value => true, - PostfixSnippetScope::Content => is_content, - }; - if !scope { - continue; - } - crate::log_debug_ct!("post snippet: {}", snippet.label); - - static TYPST_SNIPPET_PLACEHOLDER_RE: LazyLock = - LazyLock::new(|| Regex::new(r"\$\{(.*?)\}").unwrap()); - - let parsed_snippet = snippet.parsed_snippet.get_or_init(|| { - let split = TYPST_SNIPPET_PLACEHOLDER_RE - .find_iter(&snippet.snippet) - .map(|s| (&s.as_str()[2..s.as_str().len() - 1], s.start(), s.end())) - .collect::>(); - if split.len() > 2 { - return None; - } - - let split0 = split[0]; - let split1 = split.get(1); - - if split0.0.contains("node") { - Some(ParsedSnippet { - node_before: snippet.snippet[..split0.1].into(), - node_before_before_cursor: None, - node_after: snippet.snippet[split0.2..].into(), - }) - } else { - split1.map(|split1| ParsedSnippet { - node_before_before_cursor: Some(snippet.snippet[..split0.1].into()), - node_before: snippet.snippet[split0.1..split1.1].into(), - node_after: snippet.snippet[split1.2..].into(), - }) - } - }); - crate::log_debug_ct!("post snippet: {} on {:?}", snippet.label, parsed_snippet); - let Some(ParsedSnippet { - node_before, - node_before_before_cursor, - node_after, - }) = parsed_snippet - else { - continue; - }; - - let base = Completion { - kind: CompletionKind::Syntax, - apply: Some("".into()), - label: snippet.label.clone(), - label_details: snippet.label_detail.clone(), - detail: Some(snippet.description.clone()), - // range: Some(range), - ..Default::default() - }; - if let Some(node_before_before_cursor) = &node_before_before_cursor { - let node_content = node.get().clone().into_text(); - let before = EcoTextEdit { - range: self.cursor.lsp_range_of(rng.start..self.cursor.from), - new_text: EcoString::new(), - }; - - self.push_completion(Completion { - apply: Some(eco_format!( - "{node_before_before_cursor}{node_before}{node_content}{node_after}" - )), - additional_text_edits: Some(vec![before]), - ..base - }); - } else { - let before = EcoTextEdit { - range: self.cursor.lsp_range_of(rng.start..rng.start), - new_text: node_before.clone(), - }; - let after = EcoTextEdit { - range: self.cursor.lsp_range_of(rng.end..self.cursor.from), - new_text: "".into(), - }; - self.push_completion(Completion { - apply: Some(node_after.clone()), - additional_text_edits: Some(vec![before, after]), - ..base - }); - } - } - - Some(()) - } - - /// Make ufcs-style completions. Note: you must check that node is a content - /// before calling this. Todo: ufcs completions for other types. - pub fn ufcs_completions(&mut self, node: &LinkedNode) { - if !self.worker.ctx.analysis.completion_feat.any_ufcs() { - return; - } - - if !matches!(self.cursor.surrounding_syntax, SurroundingSyntax::Regular) { - return; - } - - let Some(defines) = self.scope_defs() else { - return; - }; - - crate::log_debug_ct!("defines: {:?}", defines.defines.len()); - let mut kind_checker = CompletionKindChecker { - symbols: HashSet::default(), - functions: HashSet::default(), - }; - - let rng = node.range(); - - let is_content_block = node.kind() == SyntaxKind::ContentBlock; - - let lb = if is_content_block { "" } else { "(" }; - let rb = if is_content_block { "" } else { ")" }; - - // we don't check literal type here for faster completion - for (name, ty) in defines.defines { - // todo: filter ty - if name.is_empty() { - continue; - } - - kind_checker.check(&ty); - - if kind_checker.symbols.iter().min().copied().is_some() { - continue; - } - if kind_checker.functions.is_empty() { - continue; - } - - let label_details = ty.describe().map(From::from).or_else(|| Some("any".into())); - let base = Completion { - kind: CompletionKind::Func, - label_details, - apply: Some("".into()), - // range: Some(range), - command: self - .worker - .ctx - .analysis - .trigger_on_snippet_with_param_hint(true) - .map(From::from), - ..Default::default() - }; - let fn_feat = FnCompletionFeat::default().check(kind_checker.functions.iter()); - - crate::log_debug_ct!("fn_feat: {name} {ty:?} -> {fn_feat:?}"); - - if fn_feat.min_pos() < 1 || !fn_feat.next_arg_is_content { - continue; - } - crate::log_debug_ct!("checked ufcs: {ty:?}"); - if self.worker.ctx.analysis.completion_feat.ufcs() && fn_feat.min_pos() == 1 { - let before = EcoTextEdit { - range: self.cursor.lsp_range_of(rng.start..rng.start), - new_text: eco_format!("{name}{lb}"), - }; - let after = EcoTextEdit { - range: self.cursor.lsp_range_of(rng.end..self.cursor.from), - new_text: rb.into(), - }; - - self.push_completion(Completion { - label: name.clone(), - additional_text_edits: Some(vec![before, after]), - ..base.clone() - }); - } - let more_args = fn_feat.min_pos() > 1 || fn_feat.min_named() > 0; - if self.worker.ctx.analysis.completion_feat.ufcs_left() && more_args { - let node_content = node.get().clone().into_text(); - let before = EcoTextEdit { - range: self.cursor.lsp_range_of(rng.start..self.cursor.from), - new_text: eco_format!("{name}{lb}"), - }; - self.push_completion(Completion { - apply: if is_content_block { - Some(eco_format!("(${{}}){node_content}")) - } else { - Some(eco_format!("${{}}, {node_content})")) - }, - label: eco_format!("{name}("), - additional_text_edits: Some(vec![before]), - ..base.clone() - }); - } - if self.worker.ctx.analysis.completion_feat.ufcs_right() && more_args { - let before = EcoTextEdit { - range: self.cursor.lsp_range_of(rng.start..rng.start), - new_text: eco_format!("{name}("), - }; - let after = EcoTextEdit { - range: self.cursor.lsp_range_of(rng.end..self.cursor.from), - new_text: "".into(), - }; - self.push_completion(Completion { - apply: Some(eco_format!("${{}})")), - label: eco_format!("{name})"), - additional_text_edits: Some(vec![before, after]), - ..base - }); - } - } - } - - /// Add completions for definitions that are available at the cursor. - pub fn scope_completions(&mut self, parens: bool) { - let Some(defines) = self.scope_defs() else { - return; - }; - - self.def_completions(defines, parens); - } - - /// Add completions for definitions. - fn def_completions(&mut self, defines: Defines, parens: bool) { - let default_docs = defines.docs; - let defines = defines.defines; - - let mode = interpret_mode_at(Some(&self.cursor.leaf)); - let surrounding_syntax = self.cursor.surrounding_syntax; - - let mut kind_checker = CompletionKindChecker { - symbols: HashSet::default(), - functions: HashSet::default(), - }; - - let filter = |checker: &CompletionKindChecker| { - match surrounding_syntax { - SurroundingSyntax::Regular => true, - SurroundingSyntax::StringContent | SurroundingSyntax::ImportList => false, - SurroundingSyntax::Selector => 'selector: { - for func in &checker.functions { - if func.element().is_some() { - break 'selector true; - } - } - - false - } - SurroundingSyntax::ShowTransform => !checker.functions.is_empty(), - SurroundingSyntax::SetRule => 'set_rule: { - // todo: user defined elements - for func in &checker.functions { - if let Some(elem) = func.element() { - if elem.params().iter().any(|param| param.settable) { - break 'set_rule true; - } - } - } - - false - } - } - }; - - // we don't check literal type here for faster completion - for (name, ty) in defines { - if name.is_empty() { - continue; - } - - kind_checker.check(&ty); - if !filter(&kind_checker) { - continue; - } - - if let Some(ch) = kind_checker.symbols.iter().min().copied() { - // todo: describe all chars - let kind = CompletionKind::Symbol(ch); - self.push_completion(Completion { - kind, - label: name, - label_details: Some(symbol_label_detail(ch)), - detail: Some(symbol_detail(ch)), - ..Completion::default() - }); - continue; - } - - let docs = default_docs.get(&name).cloned(); - - let label_detail = ty.describe().map(From::from).or_else(|| Some("any".into())); - - crate::log_debug_ct!("scope completions!: {name} {ty:?} {label_detail:?}"); - let detail = docs.or_else(|| label_detail.clone()); - - if !kind_checker.functions.is_empty() { - let base = Completion { - kind: CompletionKind::Func, - label_details: label_detail, - detail, - command: self - .worker - .ctx - .analysis - .trigger_on_snippet_with_param_hint(true) - .map(From::from), - ..Default::default() - }; - - let fn_feat = FnCompletionFeat::default().check(kind_checker.functions.iter()); - - crate::log_debug_ct!("fn_feat: {name} {ty:?} -> {fn_feat:?}"); - - if matches!( - self.cursor.surrounding_syntax, - SurroundingSyntax::ShowTransform - ) && (fn_feat.min_pos() > 0 || fn_feat.min_named() > 0) - { - self.push_completion(Completion { - label: eco_format!("{name}.with"), - apply: Some(eco_format!("{name}.with(${{}})")), - ..base.clone() - }); - } - if fn_feat.is_element - && matches!(self.cursor.surrounding_syntax, SurroundingSyntax::Selector) - { - self.push_completion(Completion { - label: eco_format!("{name}.where"), - apply: Some(eco_format!("{name}.where(${{}})")), - ..base.clone() - }); - } - - let bad_instantiate = matches!( - self.cursor.surrounding_syntax, - SurroundingSyntax::Selector | SurroundingSyntax::SetRule - ) && !fn_feat.is_element; - if !bad_instantiate { - if !parens - || matches!(self.cursor.surrounding_syntax, SurroundingSyntax::Selector) - { - self.push_completion(Completion { - label: name, - ..base - }); - } else if fn_feat.min_pos() < 1 && !fn_feat.has_rest { - self.push_completion(Completion { - apply: Some(eco_format!("{}()${{}}", name)), - label: name, - ..base - }); - } else { - let accept_content_arg = fn_feat.next_arg_is_content && !fn_feat.has_rest; - let scope_reject_content = matches!(mode, InterpretMode::Math) - || matches!( - self.cursor.surrounding_syntax, - SurroundingSyntax::Selector | SurroundingSyntax::SetRule - ); - self.push_completion(Completion { - apply: Some(eco_format!("{name}(${{}})")), - label: name.clone(), - ..base.clone() - }); - if !scope_reject_content && accept_content_arg { - self.push_completion(Completion { - apply: Some(eco_format!("{name}[${{}}]")), - label: eco_format!("{name}.bracket"), - ..base - }); - }; - } - } - continue; - } - - let kind = ty_to_completion_kind(&ty); - self.push_completion(Completion { - kind, - label: name, - label_details: label_detail.clone(), - detail, - ..Completion::default() - }); - } - } - /// Add completions for all fields on a node. - fn field_access_completions(&mut self, target: &LinkedNode) -> Option<()> { - self.value_field_access_completions(target) - .or_else(|| self.type_field_access_completions(target)) - } - - /// Add completions for all fields on a type. - fn type_field_access_completions(&mut self, target: &LinkedNode) -> Option<()> { - let ty = self - .worker - .ctx - .post_type_of_node(target.clone()) - .filter(|ty| !matches!(ty, Ty::Any)); - crate::log_debug_ct!("type_field_access_completions_on: {target:?} -> {ty:?}"); - let mut defines = Defines { - types: self.worker.ctx.type_check(&self.cursor.source), - defines: Default::default(), - docs: Default::default(), - }; - ty?.iface_surface( - true, - &mut CompletionScopeChecker { - check_kind: ScopeCheckKind::FieldAccess, - defines: &mut defines, - ctx: self.worker.ctx, - }, - ); - - self.def_completions(defines, true); - Some(()) - } - - /// Add completions for all fields on a value. - fn value_field_access_completions(&mut self, target: &LinkedNode) -> Option<()> { - let (value, styles) = self.worker.ctx.analyze_expr(target).into_iter().next()?; - for (name, value, _) in value.ty().scope().iter() { - self.value_completion(Some(name.clone()), value, true, None); - } - - if let Some(scope) = value.scope() { - for (name, value, _) in scope.iter() { - self.value_completion(Some(name.clone()), value, true, None); - } - } - - for &field in fields_on(value.ty()) { - // Complete the field name along with its value. Notes: - // 1. No parentheses since function fields cannot currently be called - // with method syntax; - // 2. We can unwrap the field's value since it's a field belonging to - // this value's type, so accessing it should not fail. - self.value_completion( - Some(field.into()), - &value.field(field).unwrap(), - false, - None, - ); - } - - self.postfix_completions(target, Ty::Value(InsTy::new(value.clone()))); - - match value { - Value::Symbol(symbol) => { - for modifier in symbol.modifiers() { - if let Ok(modified) = symbol.clone().modified(modifier) { - self.push_completion(Completion { - kind: CompletionKind::Symbol(modified.get()), - label: modifier.into(), - label_details: Some(symbol_label_detail(modified.get())), - ..Completion::default() - }); - } - } - - self.ufcs_completions(target); - } - Value::Content(content) => { - for (name, value) in content.fields() { - self.value_completion(Some(name.into()), &value, false, None); - } - - self.ufcs_completions(target); - } - Value::Dict(dict) => { - for (name, value) in dict.iter() { - self.value_completion(Some(name.clone().into()), value, false, None); - } - } - Value::Func(func) => { - // Autocomplete get rules. - if let Some((elem, styles)) = func.element().zip(styles.as_ref()) { - for param in elem.params().iter().filter(|param| !param.required) { - if let Some(value) = elem - .field_id(param.name) - .map(|id| elem.field_from_styles(id, StyleChain::new(styles))) - { - self.value_completion( - Some(param.name.into()), - &value.unwrap(), - false, - None, - ); - } - } - } - } - Value::Plugin(plugin) => { - for name in plugin.iter() { - self.push_completion(Completion { - kind: CompletionKind::Func, - label: name.clone(), - ..Completion::default() - }) - } - } - _ => {} - } - - Some(()) - } -} - -struct TypeCompletionWorker<'a, 'b, 'c, 'd> { - base: &'d mut CompletionPair<'a, 'b, 'c>, - filter: &'d dyn Fn(&Ty) -> bool, -} - -impl TypeCompletionWorker<'_, '_, '_, '_> { - fn snippet_completion(&mut self, label: &str, apply: &str, detail: &str) { - if !(self.filter)(&Ty::Any) { - return; - } - - self.base.snippet_completion(label, apply, detail); - } - - fn type_completion(&mut self, infer_type: &Ty, docs: Option<&str>) -> Option<()> { - // Prevent duplicate completions from appearing. - if !self.base.worker.seen_types.insert(infer_type.clone()) { - return Some(()); - } - - crate::log_debug_ct!("type_completion: {infer_type:?}"); - - match infer_type { - Ty::Any => return None, - Ty::Pattern(_) => return None, - Ty::Args(_) => return None, - Ty::Func(_) => return None, - Ty::With(_) => return None, - Ty::Select(_) => return None, - Ty::Var(_) => return None, - Ty::Unary(_) => return None, - Ty::Binary(_) => return None, - Ty::If(_) => return None, - Ty::Union(u) => { - for info in u.as_ref() { - self.type_completion(info, docs); - } - } - Ty::Let(bounds) => { - for ut in bounds.ubs.iter() { - self.type_completion(ut, docs); - } - for lt in bounds.lbs.iter() { - self.type_completion(lt, docs); - } - } - Ty::Tuple(..) | Ty::Array(..) => { - if !(self.filter)(infer_type) { - return None; - } - self.snippet_completion("()", "(${})", "An array."); - } - Ty::Dict(..) => { - if !(self.filter)(infer_type) { - return None; - } - self.snippet_completion("()", "(${})", "A dictionary."); - } - Ty::Boolean(_b) => { - if !(self.filter)(infer_type) { - return None; - } - self.snippet_completion("false", "false", "No / Disabled."); - self.snippet_completion("true", "true", "Yes / Enabled."); - } - Ty::Builtin(v) => { - if !(self.filter)(infer_type) { - return None; - } - self.builtin_type_completion(v, docs); - } - Ty::Value(v) => { - if !(self.filter)(infer_type) { - return None; - } - let docs = v.syntax.as_ref().map(|s| s.doc.as_ref()).or(docs); - - if let Value::Type(ty) = &v.val { - self.type_completion(&Ty::Builtin(BuiltinTy::Type(*ty)), docs); - } else if v.val.ty() == Type::of::() { - self.type_completion(&Ty::Builtin(BuiltinTy::None), docs); - } else if v.val.ty() == Type::of::() { - self.type_completion(&Ty::Builtin(BuiltinTy::Auto), docs); - } else { - self.base.value_completion(None, &v.val, true, docs); - } - } - Ty::Param(param) => { - // todo: variadic - - let docs = docs.or_else(|| param.docs.as_deref()); - if param.attrs.positional { - self.type_completion(¶m.ty, docs); - } - if !param.attrs.named { - return Some(()); - } - - let field = ¶m.name; - if self.base.worker.seen_field(field.clone()) { - return Some(()); - } - if !(self.filter)(infer_type) { - return None; - } - - let mut rev_stream = self.base.cursor.before.chars().rev(); - let ch = rev_stream.find(|ch| !typst::syntax::is_id_continue(*ch)); - // skip label/ref completion. - // todo: more elegant way - if matches!(ch, Some('<' | '@')) { - return Some(()); - } - - self.base.push_completion(Completion { - kind: CompletionKind::Field, - label: field.into(), - apply: Some(eco_format!("{}: ${{}}", field)), - label_details: param.ty.describe(), - detail: docs.map(Into::into), - command: self - .base - .worker - .ctx - .analysis - .trigger_on_snippet_with_param_hint(true) - .map(From::from), - ..Completion::default() - }); - } - }; - - Some(()) - } - - fn builtin_type_completion(&mut self, v: &BuiltinTy, docs: Option<&str>) -> Option<()> { - match v { - BuiltinTy::None => self.snippet_completion("none", "none", "Nothing."), - BuiltinTy::Auto => { - self.snippet_completion("auto", "auto", "A smart default."); - } - BuiltinTy::Clause => return None, - BuiltinTy::Undef => return None, - BuiltinTy::Space => return None, - BuiltinTy::Break => return None, - BuiltinTy::Continue => return None, - BuiltinTy::Content => return None, - BuiltinTy::Infer => return None, - BuiltinTy::FlowNone => return None, - BuiltinTy::Tag(..) => return None, - BuiltinTy::Module(..) => return None, - - BuiltinTy::Path(preference) => { - let items = self.base.complete_path(preference); - self.base - .worker - .completions - .extend(items.into_iter().flatten()); - } - BuiltinTy::Args => return None, - BuiltinTy::Stroke => { - self.snippet_completion("stroke()", "stroke(${})", "Stroke type."); - self.snippet_completion("()", "(${})", "Stroke dictionary."); - self.type_completion(&Ty::Builtin(BuiltinTy::Color), docs); - self.type_completion(&Ty::Builtin(BuiltinTy::Length), docs); - } - BuiltinTy::Color => { - self.snippet_completion("luma()", "luma(${v})", "A custom grayscale color."); - self.snippet_completion( - "rgb()", - "rgb(${r}, ${g}, ${b}, ${a})", - "A custom RGBA color.", - ); - self.snippet_completion( - "cmyk()", - "cmyk(${c}, ${m}, ${y}, ${k})", - "A custom CMYK color.", - ); - self.snippet_completion( - "oklab()", - "oklab(${l}, ${a}, ${b}, ${alpha})", - "A custom Oklab color.", - ); - self.snippet_completion( - "oklch()", - "oklch(${l}, ${chroma}, ${hue}, ${alpha})", - "A custom Oklch color.", - ); - self.snippet_completion( - "color.linear-rgb()", - "color.linear-rgb(${r}, ${g}, ${b}, ${a})", - "A custom linear RGBA color.", - ); - self.snippet_completion( - "color.hsv()", - "color.hsv(${h}, ${s}, ${v}, ${a})", - "A custom HSVA color.", - ); - self.snippet_completion( - "color.hsl()", - "color.hsl(${h}, ${s}, ${l}, ${a})", - "A custom HSLA color.", - ); - } - BuiltinTy::TextSize => return None, - BuiltinTy::TextLang => { - for (&key, desc) in rust_iso639::ALL_MAP.entries() { - let detail = eco_format!("An ISO 639-1/2/3 language code, {}.", desc.name); - self.base.push_completion(Completion { - kind: CompletionKind::Syntax, - label: key.to_lowercase().into(), - apply: Some(eco_format!("\"{}\"", key.to_lowercase())), - detail: Some(detail), - label_details: Some(desc.name.into()), - ..Completion::default() - }); - } - } - BuiltinTy::TextRegion => { - for (&key, desc) in rust_iso3166::ALPHA2_MAP.entries() { - let detail = eco_format!("An ISO 3166-1 alpha-2 region code, {}.", desc.name); - self.base.push_completion(Completion { - kind: CompletionKind::Syntax, - label: key.to_lowercase().into(), - apply: Some(eco_format!("\"{}\"", key.to_lowercase())), - detail: Some(detail), - label_details: Some(desc.name.into()), - ..Completion::default() - }); - } - } - BuiltinTy::Dir => {} - BuiltinTy::TextFont => { - self.base.font_completions(); - } - BuiltinTy::Margin => { - self.snippet_completion("()", "(${})", "Margin dictionary."); - self.type_completion(&Ty::Builtin(BuiltinTy::Length), docs); - } - BuiltinTy::Inset => { - self.snippet_completion("()", "(${})", "Inset dictionary."); - self.type_completion(&Ty::Builtin(BuiltinTy::Length), docs); - } - BuiltinTy::Outset => { - self.snippet_completion("()", "(${})", "Outset dictionary."); - self.type_completion(&Ty::Builtin(BuiltinTy::Length), docs); - } - BuiltinTy::Radius => { - self.snippet_completion("()", "(${})", "Radius dictionary."); - self.type_completion(&Ty::Builtin(BuiltinTy::Length), docs); - } - BuiltinTy::Length => { - self.snippet_completion("pt", "${1}pt", "Point length unit."); - self.snippet_completion("mm", "${1}mm", "Millimeter length unit."); - self.snippet_completion("cm", "${1}cm", "Centimeter length unit."); - self.snippet_completion("in", "${1}in", "Inch length unit."); - self.snippet_completion("em", "${1}em", "Em length unit."); - self.type_completion(&Ty::Builtin(BuiltinTy::Auto), docs); - } - BuiltinTy::Float => { - self.snippet_completion( - "exponential notation", - "${1}e${0}", - "Exponential notation", - ); - } - BuiltinTy::Label => { - self.base.label_completions(false); - } - BuiltinTy::CiteLabel => { - self.base.label_completions(true); - } - BuiltinTy::RefLabel => { - self.base.ref_completions(); - } - BuiltinTy::TypeType(ty) | BuiltinTy::Type(ty) => { - if *ty == Type::of::() { - let docs = docs.or(Some("Nothing.")); - self.type_completion(&Ty::Builtin(BuiltinTy::None), docs); - } else if *ty == Type::of::() { - let docs = docs.or(Some("A smart default.")); - self.type_completion(&Ty::Builtin(BuiltinTy::Auto), docs); - } else if *ty == Type::of::() { - self.snippet_completion("false", "false", "No / Disabled."); - self.snippet_completion("true", "true", "Yes / Enabled."); - } else if *ty == Type::of::() { - self.type_completion(&Ty::Builtin(BuiltinTy::Color), docs); - } else if *ty == Type::of::