summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock10
-rw-r--r--Cargo.toml2
-rw-r--r--src/ide/complete.rs684
-rw-r--r--src/ide/mod.rs2
4 files changed, 697 insertions, 1 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 56ef0976..8d21d48c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -432,6 +432,12 @@ dependencies = [
]
[[package]]
+name = "if_chain"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed"
+
+[[package]]
name = "image"
version = "0.24.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1027,7 +1033,7 @@ dependencies = [
[[package]]
name = "symmie"
version = "0.1.0"
-source = "git+https://github.com/typst/symmie#6280fb20455cb63e6886ba5bb35b95a4b376da68"
+source = "git+https://github.com/typst/symmie#8d620b9854964a261ec954d7d08bfddc9c282b7c"
[[package]]
name = "syn"
@@ -1136,6 +1142,7 @@ dependencies = [
"bytemuck",
"comemo",
"flate2",
+ "if_chain",
"image",
"miniz_oxide 0.5.4",
"once_cell",
@@ -1150,6 +1157,7 @@ dependencies = [
"siphasher",
"subsetter",
"svg2pdf",
+ "symmie",
"thin-vec",
"tiny-skia",
"ttf-parser 0.17.1",
diff --git a/Cargo.toml b/Cargo.toml
index f38fc997..9b7f6860 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,6 +17,7 @@ bitflags = "1"
bytemuck = "1"
comemo = { git = "https://github.com/typst/comemo" }
flate2 = "1"
+if_chain = "1"
image = { version = "0.24", default-features = false, features = ["png", "jpeg", "gif"] }
miniz_oxide = "0.5"
once_cell = "1"
@@ -31,6 +32,7 @@ serde = { version = "1", features = ["derive"] }
siphasher = "0.3"
subsetter = "0.1"
svg2pdf = "0.4"
+symmie = { git = "https://github.com/typst/symmie" }
thin-vec = "0.2"
tiny-skia = "0.6.2"
ttf-parser = "0.17"
diff --git a/src/ide/complete.rs b/src/ide/complete.rs
new file mode 100644
index 00000000..7f312727
--- /dev/null
+++ b/src/ide/complete.rs
@@ -0,0 +1,684 @@
+use if_chain::if_chain;
+
+use crate::model::Value;
+use crate::syntax::{LinkedNode, Source, SyntaxKind};
+use crate::util::{format_eco, EcoString};
+use crate::World;
+
+/// An autocompletion option.
+#[derive(Debug, Clone, Eq, PartialEq)]
+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, defaults to the label.
+ ///
+ /// May use snippet syntax like `${lhs} + ${rhs}`.
+ pub apply: Option<EcoString>,
+ /// Details about the completed item.
+ pub detail: Option<EcoString>,
+}
+
+/// A kind of item that can be completed.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
+pub enum CompletionKind {
+ /// A syntactical structure.
+ Syntax,
+ /// A function name.
+ Function,
+ /// A constant of the given type.
+ Constant,
+ /// A symbol.
+ Symbol,
+}
+
+/// 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,
+ source: &Source,
+ cursor: usize,
+ explicit: bool,
+) -> Option<(usize, Vec<Completion>)> {
+ let mut ctx = CompletionContext::new(world, source, cursor, explicit)?;
+
+ let _ = complete_rules(&mut ctx)
+ || complete_symbols(&mut ctx)
+ || complete_markup(&mut ctx)
+ || complete_math(&mut ctx)
+ || complete_code(&mut ctx);
+
+ Some((ctx.from, ctx.completions))
+}
+
+/// 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.set_rule_completions(ctx.cursor);
+ return true;
+ }
+
+ // Behind the show keyword: "show |".
+ if matches!(prev.kind(), SyntaxKind::Show) {
+ ctx.show_rule_selector_completions(ctx.cursor);
+ 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.show_rule_recipe_completions(ctx.cursor);
+ return true;
+ }
+ }
+
+ false
+}
+
+/// Complete symbols.
+///
+/// Exception: Math identifiers which can also be symbols are handled separately
+/// in `math_completions`.
+fn complete_symbols(ctx: &mut CompletionContext) -> bool {
+ // Whether a colon is necessary.
+ let needs_colon = !ctx.text[ctx.cursor..].starts_with(':');
+
+ // Behind half-completed symbol: "$arrow:|$".
+ if_chain! {
+ if matches!(ctx.leaf.kind(), SyntaxKind::Atom(s) if s == ":");
+ if let Some(prev) = ctx.leaf.prev_leaf();
+ if matches!(prev.kind(), SyntaxKind::Ident(_));
+ then {
+ ctx.symbol_completions(prev.offset(), false);
+ return true;
+ }
+ }
+
+ // Start of a symbol: ":|".
+ // Checking for a text node ensures that "\:" isn't completed.
+ if ctx.text[..ctx.cursor].ends_with(':')
+ && matches!(ctx.leaf.kind(), SyntaxKind::Text(_) | SyntaxKind::Atom(_))
+ {
+ ctx.symbol_completions(ctx.cursor, needs_colon);
+ return true;
+ }
+
+ // An existing symbol: ":arrow:".
+ if matches!(ctx.leaf.kind(), SyntaxKind::Symbol(_)) {
+ // We want to complete behind the colon, therefore plus 1.
+ let has_colon = ctx.text[ctx.leaf.offset()..].starts_with(':');
+ let from = ctx.leaf.offset() + (has_colon as usize);
+ ctx.symbol_completions(from, has_colon && needs_colon);
+ return true;
+ }
+
+ // Behind half-completed symbol: ":bar|" or ":arrow:dou|".
+ if_chain! {
+ if matches!(
+ ctx.leaf.kind(),
+ SyntaxKind::Text(_) | SyntaxKind::Atom(_) | SyntaxKind::Ident(_)
+ );
+ if let Some(prev) = ctx.leaf.prev_leaf();
+ if matches!(prev.kind(), SyntaxKind::Symbol(_)) || matches!(
+ prev.kind(),
+ SyntaxKind::Text(s) | SyntaxKind::Atom(s) if s == ":"
+ );
+ then {
+ // We want to complete behind the colon, therefore plus 1.
+ let from = prev.offset() + 1;
+ ctx.symbol_completions(from, needs_colon);
+ return true;
+ }
+ }
+
+ false
+}
+
+/// 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 { .. })) {
+ return false;
+ }
+
+ // Start of an interpolated identifier: "#|".
+ // Checking for a text node ensures that "\#" isn't completed.
+ if ctx.text[..ctx.cursor].ends_with('#')
+ && matches!(ctx.leaf.kind(), SyntaxKind::Text(_))
+ {
+ ctx.expr_completions(ctx.cursor, true);
+ return true;
+ }
+
+ // An existing identifier: "#pa|".
+ if matches!(ctx.leaf.kind(), SyntaxKind::Ident(_)) {
+ // We want to complete behind the hashtag, therefore plus 1.
+ let from = ctx.leaf.offset() + 1;
+ ctx.expr_completions(from, true);
+ return true;
+ }
+
+ // Behind a half-completed binding: "#let x = |".
+ if_chain! {
+ if let Some(prev) = ctx.leaf.prev_leaf();
+ if matches!(prev.kind(), SyntaxKind::Eq);
+ if matches!(prev.parent_kind(), Some(SyntaxKind::LetBinding));
+ then {
+ ctx.expr_completions(ctx.cursor, false);
+ return true;
+ }
+ }
+
+ // Anywhere: "|".
+ if ctx.explicit {
+ ctx.markup_completions(ctx.cursor);
+ return true;
+ }
+
+ false
+}
+
+/// Complete in math mode.
+fn complete_math(ctx: &mut CompletionContext) -> bool {
+ if !matches!(
+ ctx.leaf.parent_kind(),
+ Some(SyntaxKind::Math) | Some(SyntaxKind::Frac) | Some(SyntaxKind::Script)
+ ) {
+ return false;
+ }
+
+ // Start of an interpolated identifier: "#|".
+ if matches!(ctx.leaf.kind(), SyntaxKind::Atom(s) if s == "#") {
+ ctx.expr_completions(ctx.cursor, true);
+ return true;
+ }
+
+ // Behind existing atom or identifier: "$a|$" or "$abc|$".
+ if matches!(ctx.leaf.kind(), SyntaxKind::Atom(_) | SyntaxKind::Ident(_)) {
+ let from = ctx.leaf.offset();
+ ctx.symbol_completions(from, false);
+ ctx.scope_completions(from);
+ return true;
+ }
+
+ // Anywhere: "$|$".
+ if ctx.explicit {
+ ctx.math_completions(ctx.cursor);
+ return true;
+ }
+
+ false
+}
+
+/// Complete in code mode.
+fn complete_code(ctx: &mut CompletionContext) -> bool {
+ if matches!(
+ ctx.leaf.parent_kind(),
+ None | Some(SyntaxKind::Markup { .. }) | Some(SyntaxKind::Math)
+ ) {
+ return false;
+ }
+
+ // An existing identifier: "{ pa| }".
+ if matches!(ctx.leaf.kind(), SyntaxKind::Ident(_)) {
+ let from = ctx.leaf.offset();
+ ctx.expr_completions(from, true);
+ 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.expr_completions(ctx.cursor, false);
+ return true;
+ }
+
+ false
+}
+
+/// Context for autocompletion.
+struct CompletionContext<'a> {
+ world: &'a dyn World,
+ text: &'a str,
+ leaf: LinkedNode<'a>,
+ cursor: usize,
+ explicit: bool,
+ from: usize,
+ completions: Vec<Completion>,
+}
+
+impl<'a> CompletionContext<'a> {
+ /// Create a new autocompletion context.
+ fn new(
+ world: &'a dyn World,
+ source: &'a Source,
+ cursor: usize,
+ explicit: bool,
+ ) -> Option<Self> {
+ let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?;
+ Some(Self {
+ world,
+ text: source.text(),
+ leaf,
+ cursor,
+ explicit,
+ from: cursor,
+ completions: vec![],
+ })
+ }
+
+ /// Add completions for all functions from the global scope.
+ fn set_rule_completions(&mut self, from: usize) {
+ self.scope_completions_where(
+ from,
+ |value| matches!(value, Value::Func(_)),
+ "(${})",
+ );
+ }
+
+ /// Add completions for selectors.
+ fn show_rule_selector_completions(&mut self, from: usize) {
+ self.snippet(
+ "text selector",
+ "\"${text}\": ${}",
+ "Replace occurances of specific text.",
+ );
+
+ self.snippet(
+ "regex selector",
+ "regex(\"${regex}\"): ${}",
+ "Replace matches of a regular expression.",
+ );
+
+ self.scope_completions_where(
+ from,
+ |value| matches!(value, Value::Func(func) if func.select(None).is_ok()),
+ ": ${}",
+ );
+ }
+
+ /// Add completions for selectors.
+ fn show_rule_recipe_completions(&mut self, from: usize) {
+ self.snippet(
+ "replacement",
+ "[${content}]",
+ "Replace the selected element with content.",
+ );
+
+ self.snippet(
+ "replacement (string)",
+ "\"${text}\"",
+ "Replace the selected element with a string of text.",
+ );
+
+ self.snippet(
+ "transformation",
+ "element => [${content}]",
+ "Transform the element with a function.",
+ );
+
+ self.scope_completions_where(from, |value| matches!(value, Value::Func(_)), "");
+ }
+
+ /// Add completions for the global scope.
+ fn scope_completions(&mut self, from: usize) {
+ self.scope_completions_where(from, |_| true, "");
+ }
+
+ /// Add completions for a subset of the global scope.
+ fn scope_completions_where(
+ &mut self,
+ from: usize,
+ filter: fn(&Value) -> bool,
+ extra: &str,
+ ) {
+ self.from = from;
+ for (name, value) in self.world.library().scope.iter() {
+ if filter(value) {
+ let apply = (!extra.is_empty()).then(|| format_eco!("{name}{extra}"));
+ self.completions.push(match value {
+ Value::Func(func) => Completion {
+ kind: CompletionKind::Function,
+ label: name.clone(),
+ apply,
+ detail: func.doc().map(Into::into),
+ },
+ v => Completion {
+ kind: CompletionKind::Constant,
+ label: name.clone(),
+ apply,
+ detail: Some(format_eco!(
+ "Constant of type `{}`.",
+ v.type_name()
+ )),
+ },
+ });
+ }
+ }
+ }
+
+ /// Add completions for all symbols.
+ fn symbol_completions(&mut self, from: usize, colon: bool) {
+ self.from = from;
+ self.completions.reserve(symmie::list().len());
+ for &(name, c) in symmie::list() {
+ self.completions.push(Completion {
+ kind: CompletionKind::Symbol,
+ label: name.into(),
+ apply: colon.then(|| format_eco!("{name}:")),
+ detail: Some(c.into()),
+ });
+ }
+ }
+
+ /// Add completions for markup snippets.
+ #[rustfmt::skip]
+ fn markup_completions(&mut self, from: usize) {
+ self.from = from;
+
+ self.snippet(
+ "linebreak",
+ "\\\n${}",
+ "Inserts a forced linebreak.",
+ );
+
+ self.snippet(
+ "symbol",
+ ":${}:",
+ "Inserts a symbol.",
+ );
+
+ self.snippet(
+ "strong text",
+ "*${strong}*",
+ "Strongly emphasizes content by increasing the font weight.",
+ );
+
+ self.snippet(
+ "emphasized text",
+ "_${emphasized}_",
+ "Emphasizes content by setting it in italic font style.",
+ );
+
+ self.snippet(
+ "raw text",
+ "`${text}`",
+ "Displays text verbatim, in monospace.",
+ );
+
+ self.snippet(
+ "code listing",
+ "```${lang}\n${code}\n```",
+ "Inserts computer code with syntax highlighting.",
+ );
+
+ self.snippet(
+ "hyperlink",
+ "https://${example.com}",
+ "Links to a URL.",
+ );
+
+ self.snippet(
+ "math (inline)",
+ "$${x}$",
+ "Inserts an inline-level mathematical formula.",
+ );
+
+ self.snippet(
+ "math (block)",
+ "$ ${sum_x^2} $",
+ "Inserts a block-level mathematical formula.",
+ );
+
+ self.snippet(
+ "label",
+ "<${name}>",
+ "Makes the preceding element referencable.",
+ );
+
+ self.snippet(
+ "reference",
+ "@${name}",
+ "Inserts a reference to a label.",
+ );
+
+ self.snippet(
+ "heading",
+ "= ${title}",
+ "Inserts a section heading.",
+ );
+
+ self.snippet(
+ "list item",
+ "- ${item}",
+ "Inserts an item of an unordered list.",
+ );
+
+ self.snippet(
+ "enumeration item",
+ "+ ${item}",
+ "Inserts an item of an ordered list.",
+ );
+
+ self.snippet(
+ "enumeration item (numbered)",
+ "${number}. ${item}",
+ "Inserts an explicitly numbered item of an ordered list.",
+ );
+
+ self.snippet(
+ "description list item",
+ "/ ${term}: ${description}",
+ "Inserts an item of a description list.",
+ );
+
+ self.snippet(
+ "expression",
+ "#${}",
+ "Variables, function calls, and more.",
+ );
+
+ self.snippet(
+ "code block",
+ "{ ${} }",
+ "Switches into code mode.",
+ );
+
+ self.snippet(
+ "content block",
+ "[${content}]",
+ "Inserts a nested content block that isolates styles.",
+ );
+ }
+
+ /// Add completions for math snippets.
+ #[rustfmt::skip]
+ fn math_completions(&mut self, from: usize) {
+ self.symbol_completions(from, false);
+ self.scope_completions(from);
+
+ self.snippet(
+ "subscript",
+ "${x}_${2:2}",
+ "Sets something in subscript.",
+ );
+
+ self.snippet(
+ "superscript",
+ "${x}^${2:2}",
+ "Sets something in superscript.",
+ );
+
+ self.snippet(
+ "fraction",
+ "${x}/${y}",
+ "Inserts a fraction.",
+ );
+ }
+
+ /// Add completions for expression snippets.
+ #[rustfmt::skip]
+ fn expr_completions(&mut self, from: usize, short_form: bool) {
+ self.scope_completions(from);
+
+ self.snippet(
+ "variable",
+ "${variable}",
+ "Accesses a variable.",
+ );
+
+ self.snippet(
+ "function call",
+ "${function}(${arguments})[${body}]",
+ "Evaluates a function.",
+ );
+
+ self.snippet(
+ "set rule",
+ "set ${}",
+ "Sets style properties on an element.",
+ );
+
+ self.snippet(
+ "show rule",
+ "show ${}",
+ "Redefines the look of an element.",
+ );
+
+ self.snippet(
+ "let binding",
+ "let ${name} = ${value}",
+ "Saves a value in a variable.",
+ );
+
+ self.snippet(
+ "let binding (function)",
+ "let ${name}(${params}) = ${output}",
+ "Defines a function.",
+ );
+
+ self.snippet(
+ "if conditional",
+ "if ${1 < 2} {\n\t${}\n}",
+ "Computes or inserts something conditionally.",
+ );
+
+ self.snippet(
+ "if-else conditional",
+ "if ${1 < 2} {\n\t${}\n} else {\n\t${}\n}",
+ "Computes or inserts different things based on a condition.",
+ );
+
+ self.snippet(
+ "while loop",
+ "while ${1 < 2} {\n\t${}\n}",
+ "Computes or inserts somthing while a condition is met.",
+ );
+
+ self.snippet(
+ "for loop",
+ "for ${value} in ${(1, 2, 3)} {\n\t${}\n}",
+ "Computes or inserts somthing for each value in a collection.",
+ );
+
+ self.snippet(
+ "for loop (with key)",
+ "for ${key}, ${value} in ${(a: 1, b: 2)} {\n\t${}\n}",
+ "Computes or inserts somthing for each key and value in a collection.",
+ );
+
+ self.snippet(
+ "break",
+ "break",
+ "Exits early from a loop.",
+ );
+
+ self.snippet(
+ "continue",
+ "continue",
+ "Continues with the next iteration of a loop.",
+ );
+
+ self.snippet(
+ "return",
+ "return ${output}",
+ "Returns early from a function.",
+ );
+
+ self.snippet(
+ "import",
+ "import ${items} from \"${file.typ}\"",
+ "Imports variables from another file.",
+ );
+
+ self.snippet(
+ "include",
+ "include \"${file.typ}\"",
+ "Includes content from another file.",
+ );
+
+ if short_form {
+ return;
+ }
+
+ self.snippet(
+ "code block",
+ "{ ${} }",
+ "Inserts a nested code block.",
+ );
+
+ self.snippet(
+ "content block",
+ "[${content}]",
+ "Switches into markup mode.",
+ );
+
+ self.snippet(
+ "array",
+ "(${1, 2, 3})",
+ "Creates a sequence of values.",
+ );
+
+ self.snippet(
+ "dictionary",
+ "(${a: 1, b: 2})",
+ "Creates a mapping from names to value.",
+ );
+
+ self.snippet(
+ "anonymous function",
+ "(${params}) => ${output}",
+ "Creates an unnamed function.",
+ );
+ }
+
+ /// Add a snippet completion.
+ fn snippet(&mut self, label: &str, snippet: &str, detail: &str) {
+ self.completions.push(Completion {
+ kind: CompletionKind::Syntax,
+ label: label.into(),
+ apply: Some(snippet.into()),
+ detail: Some(detail.into()),
+ });
+ }
+}
diff --git a/src/ide/mod.rs b/src/ide/mod.rs
index a4427983..8068d421 100644
--- a/src/ide/mod.rs
+++ b/src/ide/mod.rs
@@ -1,7 +1,9 @@
//! Capabilities for IDE support.
+mod complete;
mod highlight;
mod tooltip;
+pub use complete::*;
pub use highlight::*;
pub use tooltip::*;