diff options
Diffstat (limited to 'src/ide/complete.rs')
| -rw-r--r-- | src/ide/complete.rs | 1201 |
1 files changed, 0 insertions, 1201 deletions
diff --git a/src/ide/complete.rs b/src/ide/complete.rs deleted file mode 100644 index 16cba1bc..00000000 --- a/src/ide/complete.rs +++ /dev/null @@ -1,1201 +0,0 @@ -use std::collections::{BTreeSet, HashSet}; - -use ecow::{eco_format, EcoString}; -use if_chain::if_chain; -use unscanny::Scanner; - -use super::analyze::analyze_labels; -use super::{analyze_expr, analyze_import, plain_docs_sentence, summarize_font_family}; -use crate::doc::Frame; -use crate::eval::{format_str, methods_on, CastInfo, Library, Scope, Value}; -use crate::syntax::{ - ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind, -}; -use crate::util::separated_list; -use crate::World; - -/// Autocomplete a cursor position in a source file. -/// -/// Returns the position from which the completions apply and a list of -/// completions. -/// -/// When `explicit` is `true`, the user requested the completion by pressing -/// control and space or something similar. -pub fn autocomplete( - world: &(dyn World + 'static), - frames: &[Frame], - source: &Source, - cursor: usize, - explicit: bool, -) -> Option<(usize, Vec<Completion>)> { - let mut ctx = CompletionContext::new(world, frames, source, cursor, explicit)?; - - let _ = complete_comments(&mut ctx) - || complete_field_accesses(&mut ctx) - || complete_imports(&mut ctx) - || complete_rules(&mut ctx) - || complete_params(&mut ctx) - || complete_markup(&mut ctx) - || complete_math(&mut ctx) - || complete_code(&mut ctx); - - Some((ctx.from, ctx.completions)) -} - -/// An autocompletion option. -#[derive(Debug, Clone)] -pub struct Completion { - /// The kind of item this completes to. - pub kind: CompletionKind, - /// The label the completion is shown with. - pub label: EcoString, - /// The completed version of the input, possibly described with snippet - /// syntax like `${lhs} + ${rhs}`. - /// - /// Should default to the `label` if `None`. - pub apply: Option<EcoString>, - /// An optional short description, at most one sentence. - pub detail: Option<EcoString>, -} - -/// A kind of item that can be completed. -#[derive(Debug, Clone)] -pub enum CompletionKind { - /// A syntactical structure. - Syntax, - /// A function. - Func, - /// A function parameter. - Param, - /// A constant. - Constant, - /// A symbol. - Symbol(char), -} - -/// Complete in comments. Or rather, don't! -fn complete_comments(ctx: &mut CompletionContext) -> bool { - matches!(ctx.leaf.kind(), SyntaxKind::LineComment | SyntaxKind::BlockComment) -} - -/// Complete in markup mode. -fn complete_markup(ctx: &mut CompletionContext) -> bool { - // Bail if we aren't even in markup. - if !matches!( - ctx.leaf.parent_kind(), - None | Some(SyntaxKind::Markup) | Some(SyntaxKind::Ref) - ) { - return false; - } - - // Start of an interpolated identifier: "#|". - if ctx.leaf.kind() == SyntaxKind::Hashtag { - ctx.from = ctx.cursor; - code_completions(ctx, true); - return true; - } - - // An existing identifier: "#pa|". - if ctx.leaf.kind() == SyntaxKind::Ident { - ctx.from = ctx.leaf.offset(); - code_completions(ctx, true); - return true; - } - - // Start of an reference: "@|" or "@he|". - if ctx.leaf.kind() == SyntaxKind::RefMarker { - ctx.from = ctx.leaf.offset() + 1; - ctx.label_completions(); - return true; - } - - // Behind a half-completed binding: "#let x = |". - if_chain! { - if let Some(prev) = ctx.leaf.prev_leaf(); - if prev.kind() == SyntaxKind::Eq; - if prev.parent_kind() == Some(SyntaxKind::LetBinding); - then { - ctx.from = ctx.cursor; - code_completions(ctx, false); - return true; - } - } - - // Directly after a raw block. - let mut s = Scanner::new(ctx.text); - s.jump(ctx.leaf.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() == ctx.cursor { - ctx.from = start; - ctx.raw_completions(); - } - return true; - } - - // Anywhere: "|". - if ctx.explicit { - ctx.from = ctx.cursor; - markup_completions(ctx); - return true; - } - - false -} - -/// Add completions for markup snippets. -#[rustfmt::skip] -fn markup_completions(ctx: &mut CompletionContext) { - ctx.snippet_completion( - "expression", - "#${}", - "Variables, function calls, blocks, and more.", - ); - - ctx.snippet_completion( - "linebreak", - "\\\n${}", - "Inserts a forced linebreak.", - ); - - ctx.snippet_completion( - "strong text", - "*${strong}*", - "Strongly emphasizes content by increasing the font weight.", - ); - - ctx.snippet_completion( - "emphasized text", - "_${emphasized}_", - "Emphasizes content by setting it in italic font style.", - ); - - ctx.snippet_completion( - "raw text", - "`${text}`", - "Displays text verbatim, in monospace.", - ); - - ctx.snippet_completion( - "code listing", - "```${lang}\n${code}\n```", - "Inserts computer code with syntax highlighting.", - ); - - ctx.snippet_completion( - "hyperlink", - "https://${example.com}", - "Links to a URL.", - ); - - ctx.snippet_completion( - "label", - "<${name}>", - "Makes the preceding element referenceable.", - ); - - ctx.snippet_completion( - "reference", - "@${name}", - "Inserts a reference to a label.", - ); - - ctx.snippet_completion( - "heading", - "= ${title}", - "Inserts a section heading.", - ); - - ctx.snippet_completion( - "list item", - "- ${item}", - "Inserts an item of a bullet list.", - ); - - ctx.snippet_completion( - "enumeration item", - "+ ${item}", - "Inserts an item of a numbered list.", - ); - - ctx.snippet_completion( - "enumeration item (numbered)", - "${number}. ${item}", - "Inserts an explicitly numbered list item.", - ); - - ctx.snippet_completion( - "term list item", - "/ ${term}: ${description}", - "Inserts an item of a term list.", - ); - - ctx.snippet_completion( - "math (inline)", - "$${x}$", - "Inserts an inline-level mathematical equation.", - ); - - ctx.snippet_completion( - "math (block)", - "$ ${sum_x^2} $", - "Inserts a block-level mathematical equation.", - ); -} - -/// Complete in math mode. -fn complete_math(ctx: &mut CompletionContext) -> bool { - if !matches!( - ctx.leaf.parent_kind(), - Some(SyntaxKind::Equation) - | Some(SyntaxKind::Math) - | Some(SyntaxKind::MathFrac) - | Some(SyntaxKind::MathAttach) - ) { - return false; - } - - // Start of an interpolated identifier: "#|". - if ctx.leaf.kind() == SyntaxKind::Hashtag { - ctx.from = ctx.cursor; - code_completions(ctx, true); - return true; - } - - // Behind existing atom or identifier: "$a|$" or "$abc|$". - if matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathIdent) { - ctx.from = ctx.leaf.offset(); - math_completions(ctx); - return true; - } - - // Anywhere: "$|$". - if ctx.explicit { - ctx.from = ctx.cursor; - math_completions(ctx); - return true; - } - - false -} - -/// Add completions for math snippets. -#[rustfmt::skip] -fn math_completions(ctx: &mut CompletionContext) { - ctx.scope_completions(true, |_| true); - - ctx.snippet_completion( - "subscript", - "${x}_${2:2}", - "Sets something in subscript.", - ); - - ctx.snippet_completion( - "superscript", - "${x}^${2:2}", - "Sets something in superscript.", - ); - - ctx.snippet_completion( - "fraction", - "${x}/${y}", - "Inserts a fraction.", - ); -} - -/// Complete field accesses. -fn complete_field_accesses(ctx: &mut CompletionContext) -> bool { - // Behind an expression plus dot: "emoji.|". - if_chain! { - if ctx.leaf.kind() == SyntaxKind::Dot - || (ctx.leaf.kind() == SyntaxKind::Text - && ctx.leaf.text() == "."); - if ctx.leaf.range().end == ctx.cursor; - if let Some(prev) = ctx.leaf.prev_sibling(); - if prev.is::<ast::Expr>(); - if prev.parent_kind() != Some(SyntaxKind::Markup) || - prev.prev_sibling_kind() == Some(SyntaxKind::Hashtag); - if let Some(value) = analyze_expr(ctx.world, &prev).into_iter().next(); - then { - ctx.from = ctx.cursor; - field_access_completions(ctx, &value); - return true; - } - } - - // Behind a started field access: "emoji.fa|". - if_chain! { - if ctx.leaf.kind() == SyntaxKind::Ident; - if let Some(prev) = ctx.leaf.prev_sibling(); - if prev.kind() == SyntaxKind::Dot; - if let Some(prev_prev) = prev.prev_sibling(); - if prev_prev.is::<ast::Expr>(); - if let Some(value) = analyze_expr(ctx.world, &prev_prev).into_iter().next(); - then { - ctx.from = ctx.leaf.offset(); - field_access_completions(ctx, &value); - return true; - } - } - - false -} - -/// Add completions for all fields on a value. -fn field_access_completions(ctx: &mut CompletionContext, value: &Value) { - for &(method, args) in methods_on(value.type_name()) { - ctx.completions.push(Completion { - kind: CompletionKind::Func, - label: method.into(), - apply: Some(if args { - eco_format!("{method}(${{}})") - } else { - eco_format!("{method}()${{}}") - }), - detail: None, - }) - } - - match value { - Value::Symbol(symbol) => { - for modifier in symbol.modifiers() { - if let Ok(modified) = symbol.clone().modified(modifier) { - ctx.completions.push(Completion { - kind: CompletionKind::Symbol(modified.get()), - label: modifier.into(), - apply: None, - detail: None, - }); - } - } - } - Value::Content(content) => { - for (name, value) in content.fields() { - ctx.value_completion(Some(name.clone()), &value, false, None); - } - } - Value::Dict(dict) => { - for (name, value) in dict.iter() { - ctx.value_completion(Some(name.clone().into()), value, false, None); - } - } - Value::Module(module) => { - for (name, value) in module.scope().iter() { - ctx.value_completion(Some(name.clone()), value, true, None); - } - } - Value::Func(func) => { - if let Some(info) = func.info() { - // Consider all names from the function's scope. - for (name, value) in info.scope.iter() { - ctx.value_completion(Some(name.clone()), value, true, None); - } - } - } - _ => {} - } -} - -/// Complete imports. -fn complete_imports(ctx: &mut CompletionContext) -> bool { - // In an import path for a package: - // "#import "@|", - if_chain! { - if matches!( - ctx.leaf.parent_kind(), - Some(SyntaxKind::ModuleImport | SyntaxKind::ModuleInclude) - ); - if let Some(ast::Expr::Str(str)) = ctx.leaf.cast(); - if str.get().starts_with('@'); - then { - ctx.from = ctx.leaf.offset(); - ctx.package_completions(); - return true; - } - } - - // Behind an import list: - // "#import "path.typ": |", - // "#import "path.typ": a, b, |". - if_chain! { - if let Some(prev) = ctx.leaf.prev_sibling(); - if let Some(ast::Expr::Import(import)) = prev.cast(); - if let Some(ast::Imports::Items(items)) = import.imports(); - if let Some(source) = prev.children().find(|child| child.is::<ast::Expr>()); - if let Some(value) = analyze_expr(ctx.world, &source).into_iter().next(); - then { - ctx.from = ctx.cursor; - import_item_completions(ctx, &items, &value); - return true; - } - } - - // Behind a half-started identifier in an import list: - // "#import "path.typ": thi|", - if_chain! { - if ctx.leaf.kind() == SyntaxKind::Ident; - if let Some(parent) = ctx.leaf.parent(); - if parent.kind() == SyntaxKind::ImportItems; - if let Some(grand) = parent.parent(); - if let Some(ast::Expr::Import(import)) = grand.cast(); - if let Some(ast::Imports::Items(items)) = import.imports(); - if let Some(source) = grand.children().find(|child| child.is::<ast::Expr>()); - if let Some(value) = analyze_expr(ctx.world, &source).into_iter().next(); - then { - ctx.from = ctx.leaf.offset(); - import_item_completions(ctx, &items, &value); - return true; - } - } - - false -} - -/// Add completions for all exports of a module. -fn import_item_completions( - ctx: &mut CompletionContext, - existing: &[ast::Ident], - value: &Value, -) { - let module = match value { - Value::Str(path) => match analyze_import(ctx.world, ctx.source, path) { - Some(module) => module, - None => return, - }, - Value::Module(module) => module.clone(), - _ => return, - }; - - if existing.is_empty() { - ctx.snippet_completion("*", "*", "Import everything."); - } - - for (name, value) in module.scope().iter() { - if existing.iter().all(|ident| ident.as_str() != name) { - ctx.value_completion(Some(name.clone()), value, false, None); - } - } -} - -/// Complete set and show rules. -fn complete_rules(ctx: &mut CompletionContext) -> bool { - // We don't want to complete directly behind the keyword. - if !ctx.leaf.kind().is_trivia() { - return false; - } - - let Some(prev) = ctx.leaf.prev_leaf() else { return false }; - - // Behind the set keyword: "set |". - if matches!(prev.kind(), SyntaxKind::Set) { - ctx.from = ctx.cursor; - set_rule_completions(ctx); - return true; - } - - // Behind the show keyword: "show |". - if matches!(prev.kind(), SyntaxKind::Show) { - ctx.from = ctx.cursor; - show_rule_selector_completions(ctx); - return true; - } - - // Behind a half-completed show rule: "show strong: |". - if_chain! { - if let Some(prev) = ctx.leaf.prev_leaf(); - if matches!(prev.kind(), SyntaxKind::Colon); - if matches!(prev.parent_kind(), Some(SyntaxKind::ShowRule)); - then { - ctx.from = ctx.cursor; - show_rule_recipe_completions(ctx); - return true; - } - } - - false -} - -/// Add completions for all functions from the global scope. -fn set_rule_completions(ctx: &mut CompletionContext) { - ctx.scope_completions(true, |value| { - matches!( - value, - Value::Func(func) if func.info().map_or(false, |info| { - info.params.iter().any(|param| param.settable) - }), - ) - }); -} - -/// Add completions for selectors. -fn show_rule_selector_completions(ctx: &mut CompletionContext) { - ctx.scope_completions( - false, - |value| matches!(value, Value::Func(func) if func.element().is_some()), - ); - - ctx.enrich("", ": "); - - ctx.snippet_completion( - "text selector", - "\"${text}\": ${}", - "Replace occurrences of specific text.", - ); - - ctx.snippet_completion( - "regex selector", - "regex(\"${regex}\"): ${}", - "Replace matches of a regular expression.", - ); -} - -/// Add completions for recipes. -fn show_rule_recipe_completions(ctx: &mut CompletionContext) { - ctx.snippet_completion( - "replacement", - "[${content}]", - "Replace the selected element with content.", - ); - - ctx.snippet_completion( - "replacement (string)", - "\"${text}\"", - "Replace the selected element with a string of text.", - ); - - ctx.snippet_completion( - "transformation", - "element => [${content}]", - "Transform the element with a function.", - ); - - ctx.scope_completions(false, |value| matches!(value, Value::Func(_))); -} - -/// Complete call and set rule parameters. -fn complete_params(ctx: &mut CompletionContext) -> bool { - // Ensure that we are in a function call or set rule's argument list. - let (callee, set, args) = if_chain! { - if let Some(parent) = ctx.leaf.parent(); - if let Some(parent) = match parent.kind() { - SyntaxKind::Named => parent.parent(), - _ => Some(parent), - }; - if let Some(args) = parent.cast::<ast::Args>(); - if let Some(grand) = parent.parent(); - if let Some(expr) = grand.cast::<ast::Expr>(); - let set = matches!(expr, ast::Expr::Set(_)); - if let Some(ast::Expr::Ident(callee)) = match expr { - ast::Expr::FuncCall(call) => Some(call.callee()), - ast::Expr::Set(set) => Some(set.target()), - _ => None, - }; - then { - (callee, set, args) - } else { - return false; - } - }; - - // Find the piece of syntax that decides what we're completing. - let mut deciding = ctx.leaf.clone(); - while !matches!( - deciding.kind(), - SyntaxKind::LeftParen | SyntaxKind::Comma | SyntaxKind::Colon - ) { - let Some(prev) = deciding.prev_leaf() else { break }; - deciding = prev; - } - - // Parameter values: "func(param:|)", "func(param: |)". - if_chain! { - if deciding.kind() == SyntaxKind::Colon; - if let Some(prev) = deciding.prev_leaf(); - if let Some(param) = prev.cast::<ast::Ident>(); - then { - if let Some(next) = deciding.next_leaf() { - ctx.from = ctx.cursor.min(next.offset()); - } - - named_param_value_completions(ctx, &callee, ¶m); - return true; - } - } - - // Parameters: "func(|)", "func(hi|)", "func(12,|)". - if_chain! { - if matches!(deciding.kind(), SyntaxKind::LeftParen | SyntaxKind::Comma); - if deciding.kind() != SyntaxKind::Comma || deciding.range().end < ctx.cursor; - then { - if let Some(next) = deciding.next_leaf() { - ctx.from = ctx.cursor.min(next.offset()); - } - - // Exclude arguments which are already present. - let exclude: Vec<_> = args.items().filter_map(|arg| match arg { - ast::Arg::Named(named) => Some(named.name()), - _ => None, - }).collect(); - - param_completions(ctx, &callee, set, &exclude); - return true; - } - } - - false -} - -/// Add completions for the parameters of a function. -fn param_completions( - ctx: &mut CompletionContext, - callee: &ast::Ident, - set: bool, - exclude: &[ast::Ident], -) { - let info = if_chain! { - if let Some(Value::Func(func)) = ctx.global.get(callee); - if let Some(info) = func.info(); - then { info } - else { return; } - }; - - for param in &info.params { - if exclude.iter().any(|ident| ident.as_str() == param.name) { - continue; - } - - if set && !param.settable { - continue; - } - - if param.named { - ctx.completions.push(Completion { - kind: CompletionKind::Param, - label: param.name.into(), - apply: Some(eco_format!("{}: ${{}}", param.name)), - detail: Some(plain_docs_sentence(param.docs)), - }); - } - - if param.positional { - ctx.cast_completions(¶m.cast); - } - } - - if ctx.before.ends_with(',') { - ctx.enrich(" ", ""); - } -} - -/// Add completions for the values of a named function parameter. -fn named_param_value_completions( - ctx: &mut CompletionContext, - callee: &ast::Ident, - name: &str, -) { - let param = if_chain! { - if let Some(Value::Func(func)) = ctx.global.get(callee); - if let Some(info) = func.info(); - if let Some(param) = info.param(name); - if param.named; - then { param } - else { return; } - }; - - ctx.cast_completions(¶m.cast); - - if callee.as_str() == "text" && name == "font" { - ctx.font_completions(); - } - - if ctx.before.ends_with(':') { - ctx.enrich(" ", ""); - } -} - -/// Complete in code mode. -fn complete_code(ctx: &mut CompletionContext) -> bool { - if matches!( - ctx.leaf.parent_kind(), - None | Some(SyntaxKind::Markup) - | Some(SyntaxKind::Math) - | Some(SyntaxKind::MathFrac) - | Some(SyntaxKind::MathAttach) - | Some(SyntaxKind::MathRoot) - ) { - return false; - } - - // An existing identifier: "{ pa| }". - if ctx.leaf.kind() == SyntaxKind::Ident { - ctx.from = ctx.leaf.offset(); - code_completions(ctx, false); - return true; - } - - // Anywhere: "{ | }". - // But not within or after an expression. - if ctx.explicit - && (ctx.leaf.kind().is_trivia() - || matches!(ctx.leaf.kind(), SyntaxKind::LeftParen | SyntaxKind::LeftBrace)) - { - ctx.from = ctx.cursor; - code_completions(ctx, false); - return true; - } - - false -} - -/// Add completions for expression snippets. -#[rustfmt::skip] -fn code_completions(ctx: &mut CompletionContext, hashtag: bool) { - ctx.scope_completions(true, |value| !hashtag || { - matches!(value, Value::Symbol(_) | Value::Func(_) | Value::Module(_)) - }); - - ctx.snippet_completion( - "function call", - "${function}(${arguments})[${body}]", - "Evaluates a function.", - ); - - ctx.snippet_completion( - "code block", - "{ ${} }", - "Inserts a nested code block.", - ); - - ctx.snippet_completion( - "content block", - "[${content}]", - "Switches into markup mode.", - ); - - ctx.snippet_completion( - "set rule", - "set ${}", - "Sets style properties on an element.", - ); - - ctx.snippet_completion( - "show rule", - "show ${}", - "Redefines the look of an element.", - ); - - ctx.snippet_completion( - "show rule (everything)", - "show: ${}", - "Transforms everything that follows.", - ); - - ctx.snippet_completion( - "let binding", - "let ${name} = ${value}", - "Saves a value in a variable.", - ); - - ctx.snippet_completion( - "let binding (function)", - "let ${name}(${params}) = ${output}", - "Defines a function.", - ); - - ctx.snippet_completion( - "if conditional", - "if ${1 < 2} {\n\t${}\n}", - "Computes or inserts something conditionally.", - ); - - ctx.snippet_completion( - "if-else conditional", - "if ${1 < 2} {\n\t${}\n} else {\n\t${}\n}", - "Computes or inserts different things based on a condition.", - ); - - ctx.snippet_completion( - "while loop", - "while ${1 < 2} {\n\t${}\n}", - "Computes or inserts something while a condition is met.", - ); - - ctx.snippet_completion( - "for loop", - "for ${value} in ${(1, 2, 3)} {\n\t${}\n}", - "Computes or inserts something for each value in a collection.", - ); - - ctx.snippet_completion( - "for loop (with key)", - "for ${key}, ${value} in ${(a: 1, b: 2)} {\n\t${}\n}", - "Computes or inserts something for each key and value in a collection.", - ); - - ctx.snippet_completion( - "break", - "break", - "Exits early from a loop.", - ); - - ctx.snippet_completion( - "continue", - "continue", - "Continues with the next iteration of a loop.", - ); - - ctx.snippet_completion( - "return", - "return ${output}", - "Returns early from a function.", - ); - - ctx.snippet_completion( - "import (file)", - "import \"${file}.typ\": ${items}", - "Imports variables from another file.", - ); - - ctx.snippet_completion( - "import (package)", - "import \"@${}\": ${items}", - "Imports variables from another file.", - ); - - ctx.snippet_completion( - "include (file)", - "include \"${file}.typ\"", - "Includes content from another file.", - ); - - ctx.snippet_completion( - "include (package)", - "include \"@${}\"", - "Includes content from another file.", - ); - - ctx.snippet_completion( - "array", - "(${1, 2, 3})", - "Creates a sequence of values.", - ); - - ctx.snippet_completion( - "dictionary", - "(${a: 1, b: 2})", - "Creates a mapping from names to value.", - ); - - if !hashtag { - ctx.snippet_completion( - "function", - "(${params}) => ${output}", - "Creates an unnamed function.", - ); - } -} - -/// Context for autocompletion. -struct CompletionContext<'a> { - world: &'a (dyn World + 'static), - frames: &'a [Frame], - library: &'a Library, - source: &'a Source, - global: &'a Scope, - math: &'a Scope, - text: &'a str, - before: &'a str, - after: &'a str, - leaf: LinkedNode<'a>, - cursor: usize, - explicit: bool, - from: usize, - completions: Vec<Completion>, - seen_casts: HashSet<u128>, -} - -impl<'a> CompletionContext<'a> { - /// Create a new autocompletion context. - fn new( - world: &'a (dyn World + 'static), - frames: &'a [Frame], - source: &'a Source, - cursor: usize, - explicit: bool, - ) -> Option<Self> { - let text = source.text(); - let library = world.library(); - let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?; - Some(Self { - world, - frames, - library, - source, - global: library.global.scope(), - math: library.math.scope(), - text, - before: &text[..cursor], - after: &text[cursor..], - leaf, - cursor, - explicit, - from: cursor, - completions: vec![], - seen_casts: HashSet::new(), - }) - } - - /// Add a prefix and suffix to all applications. - fn enrich(&mut self, prefix: &str, suffix: &str) { - for Completion { label, apply, .. } in &mut self.completions { - let current = apply.as_ref().unwrap_or(label); - *apply = Some(eco_format!("{prefix}{current}{suffix}")); - } - } - - /// Add a snippet completion. - fn snippet_completion( - &mut self, - label: &'static str, - snippet: &'static str, - docs: &'static str, - ) { - self.completions.push(Completion { - kind: CompletionKind::Syntax, - label: label.into(), - apply: Some(snippet.into()), - detail: Some(docs.into()), - }); - } - - /// Add completions for all font families. - fn font_completions(&mut self) { - let equation = self.before[self.cursor.saturating_sub(25)..].contains("equation"); - for (family, iter) in self.world.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) { - for (package, description) in self.world.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 (self.library.items.raw_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.completions.push(Completion { - kind: CompletionKind::Constant, - label: name.into(), - apply: Some(tags[0].into()), - detail: Some(separated_list(&tags, " or ").into()), - }); - } - } - - /// Add completions for all labels. - fn label_completions(&mut self) { - for (label, detail) in analyze_labels(self.world, self.frames).0 { - self.completions.push(Completion { - kind: CompletionKind::Constant, - label: label.0, - apply: None, - detail, - }); - } - } - - /// Add a completion for a specific value. - fn value_completion( - &mut self, - label: Option<EcoString>, - value: &Value, - parens: bool, - docs: Option<&str>, - ) { - let label = label.unwrap_or_else(|| value.repr().into()); - let mut apply = None; - - if label.starts_with('"') && self.after.starts_with('"') { - if let Some(trimmed) = label.strip_suffix('"') { - apply = Some(trimmed.into()); - } - } - - let detail = docs.map(Into::into).or_else(|| match value { - Value::Symbol(_) => None, - Value::Func(func) => func.info().map(|info| plain_docs_sentence(info.docs)), - v => { - let repr = v.repr(); - (repr.as_str() != label).then(|| repr.into()) - } - }); - - if parens && matches!(value, Value::Func(_)) { - apply = Some(eco_format!("{label}(${{}})")); - } - - self.completions.push(Completion { - kind: match value { - Value::Func(_) => CompletionKind::Func, - Value::Symbol(s) => CompletionKind::Symbol(s.get()), - _ => CompletionKind::Constant, - }, - label, - apply, - detail, - }); - } - - /// Add completions for a castable. - fn cast_completions(&mut self, cast: &'a CastInfo) { - // Prevent duplicate completions from appearing. - if !self.seen_casts.insert(crate::util::hash128(cast)) { - return; - } - - match cast { - CastInfo::Any => {} - CastInfo::Value(value, docs) => { - self.value_completion(None, value, true, Some(docs)); - } - CastInfo::Type("none") => self.snippet_completion("none", "none", "Nothing."), - CastInfo::Type("auto") => { - self.snippet_completion("auto", "auto", "A smart default."); - } - CastInfo::Type("boolean") => { - self.snippet_completion("false", "false", "No / Disabled."); - self.snippet_completion("true", "true", "Yes / Enabled."); - } - CastInfo::Type("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.scope_completions(false, |value| value.type_name() == "color"); - } - CastInfo::Type("function") => { - self.snippet_completion( - "function", - "(${params}) => ${output}", - "A custom function.", - ); - } - CastInfo::Type(ty) => { - self.completions.push(Completion { - kind: CompletionKind::Syntax, - label: (*ty).into(), - apply: Some(eco_format!("${{{ty}}}")), - detail: Some(eco_format!("A value of type {ty}.")), - }); - self.scope_completions(false, |value| value.type_name() == *ty); - } - CastInfo::Union(union) => { - for info in union { - self.cast_completions(info); - } - } - } - } - - /// Add completions for definitions that are available at the cursor. - /// Filters the global/math scope with the given filter. - fn scope_completions(&mut self, parens: bool, filter: impl Fn(&Value) -> bool) { - let mut defined = BTreeSet::new(); - - let mut ancestor = Some(self.leaf.clone()); - while let Some(node) = &ancestor { - let mut sibling = Some(node.clone()); - while let Some(node) = &sibling { - if let Some(v) = node.cast::<ast::LetBinding>() { - for ident in v.kind().idents() { - defined.insert(ident.take()); - } - } - sibling = node.prev_sibling(); - } - - if let Some(parent) = node.parent() { - if let Some(v) = parent.cast::<ast::ForLoop>() { - if node.prev_sibling_kind() != Some(SyntaxKind::In) { - let pattern = v.pattern(); - for ident in pattern.idents() { - defined.insert(ident.take()); - } - } - } - - ancestor = Some(parent.clone()); - continue; - } - - break; - } - - let in_math = matches!( - self.leaf.parent_kind(), - Some(SyntaxKind::Equation) - | Some(SyntaxKind::Math) - | Some(SyntaxKind::MathFrac) - | Some(SyntaxKind::MathAttach) - ); - - let scope = if in_math { self.math } else { self.global }; - for (name, value) in scope.iter() { - if filter(value) && !defined.contains(name) { - self.value_completion(Some(name.clone()), value, parens, None); - } - } - - for name in defined { - if !name.is_empty() { - self.completions.push(Completion { - kind: CompletionKind::Constant, - label: name, - apply: None, - detail: None, - }); - } - } - } -} |
