summaryrefslogtreecommitdiff
path: root/src/ide
diff options
context:
space:
mode:
Diffstat (limited to 'src/ide')
-rw-r--r--src/ide/analyze.rs111
-rw-r--r--src/ide/complete.rs1201
-rw-r--r--src/ide/highlight.rs430
-rw-r--r--src/ide/jump.rs173
-rw-r--r--src/ide/mod.rs97
-rw-r--r--src/ide/tooltip.rs222
6 files changed, 0 insertions, 2234 deletions
diff --git a/src/ide/analyze.rs b/src/ide/analyze.rs
deleted file mode 100644
index dad466c1..00000000
--- a/src/ide/analyze.rs
+++ /dev/null
@@ -1,111 +0,0 @@
-use comemo::Track;
-use ecow::EcoString;
-
-use crate::doc::Frame;
-use crate::eval::{eval, Module, Route, Tracer, Value};
-use crate::model::{Introspector, Label};
-use crate::syntax::{ast, LinkedNode, Source, SyntaxKind};
-use crate::World;
-
-/// Try to determine a set of possible values for an expression.
-pub fn analyze_expr(world: &(dyn World + 'static), node: &LinkedNode) -> Vec<Value> {
- match node.cast::<ast::Expr>() {
- Some(ast::Expr::None(_)) => vec![Value::None],
- Some(ast::Expr::Auto(_)) => vec![Value::Auto],
- Some(ast::Expr::Bool(v)) => vec![Value::Bool(v.get())],
- Some(ast::Expr::Int(v)) => vec![Value::Int(v.get())],
- Some(ast::Expr::Float(v)) => vec![Value::Float(v.get())],
- Some(ast::Expr::Numeric(v)) => vec![Value::numeric(v.get())],
- Some(ast::Expr::Str(v)) => vec![Value::Str(v.get().into())],
-
- Some(ast::Expr::FieldAccess(access)) => {
- let Some(child) = node.children().next() else { return vec![] };
- analyze_expr(world, &child)
- .into_iter()
- .filter_map(|target| target.field(&access.field()).ok())
- .collect()
- }
-
- Some(_) => {
- if let Some(parent) = node.parent() {
- if parent.kind() == SyntaxKind::FieldAccess && node.index() > 0 {
- return analyze_expr(world, parent);
- }
- }
-
- let route = Route::default();
- let mut tracer = Tracer::new(Some(node.span()));
- typst::eval::eval(
- world.track(),
- route.track(),
- tracer.track_mut(),
- &world.main(),
- )
- .and_then(|module| {
- typst::model::typeset(
- world.track(),
- tracer.track_mut(),
- &module.content(),
- )
- })
- .ok();
-
- tracer.finish()
- }
-
- _ => vec![],
- }
-}
-
-/// Try to load a module from the current source file.
-pub fn analyze_import(
- world: &(dyn World + 'static),
- source: &Source,
- path: &str,
-) -> Option<Module> {
- let route = Route::default();
- let mut tracer = Tracer::default();
- let id = source.id().join(path).ok()?;
- let source = world.source(id).ok()?;
- eval(world.track(), route.track(), tracer.track_mut(), &source).ok()
-}
-
-/// Find all labels and details for them.
-///
-/// Returns:
-/// - All labels and descriptions for them, if available
-/// - A split offset: All labels before this offset belong to nodes, all after
-/// belong to a bibliography.
-pub fn analyze_labels(
- world: &(dyn World + 'static),
- frames: &[Frame],
-) -> (Vec<(Label, Option<EcoString>)>, usize) {
- let mut output = vec![];
- let introspector = Introspector::new(frames);
- let items = &world.library().items;
-
- // Labels in the document.
- for elem in introspector.all() {
- let Some(label) = elem.label().cloned() else { continue };
- let details = elem
- .field("caption")
- .or_else(|| elem.field("body"))
- .and_then(|field| match field {
- Value::Content(content) => Some(content),
- _ => None,
- })
- .as_ref()
- .unwrap_or(elem)
- .plain_text();
- output.push((label, Some(details)));
- }
-
- let split = output.len();
-
- // Bibliography keys.
- for (key, detail) in (items.bibliography_keys)(introspector.track()) {
- output.push((Label(key), detail));
- }
-
- (output, split)
-}
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, &param);
- 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(&param.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(&param.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,
- });
- }
- }
- }
-}
diff --git a/src/ide/highlight.rs b/src/ide/highlight.rs
deleted file mode 100644
index 2db636e3..00000000
--- a/src/ide/highlight.rs
+++ /dev/null
@@ -1,430 +0,0 @@
-use crate::syntax::{ast, LinkedNode, SyntaxKind, SyntaxNode};
-
-/// A syntax highlighting tag.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum Tag {
- /// A line or block comment.
- Comment,
- /// Punctuation in code.
- Punctuation,
- /// An escape sequence or shorthand.
- Escape,
- /// Strong markup.
- Strong,
- /// Emphasized markup.
- Emph,
- /// A hyperlink.
- Link,
- /// Raw text.
- Raw,
- /// A label.
- Label,
- /// A reference to a label.
- Ref,
- /// A section heading.
- Heading,
- /// A marker of a list, enumeration, or term list.
- ListMarker,
- /// A term in a term list.
- ListTerm,
- /// The delimiters of an equation.
- MathDelimiter,
- /// An operator with special meaning in an equation.
- MathOperator,
- /// A keyword.
- Keyword,
- /// An operator in code.
- Operator,
- /// A numeric literal.
- Number,
- /// A string literal.
- String,
- /// A function or method name.
- Function,
- /// An interpolated variable in markup or math.
- Interpolated,
- /// A syntax error.
- Error,
-}
-
-impl Tag {
- /// Return the recommended TextMate grammar scope for the given highlighting
- /// tag.
- pub fn tm_scope(&self) -> &'static str {
- match self {
- Self::Comment => "comment.typst",
- Self::Punctuation => "punctuation.typst",
- Self::Escape => "constant.character.escape.typst",
- Self::Strong => "markup.bold.typst",
- Self::Emph => "markup.italic.typst",
- Self::Link => "markup.underline.link.typst",
- Self::Raw => "markup.raw.typst",
- Self::MathDelimiter => "punctuation.definition.math.typst",
- Self::MathOperator => "keyword.operator.math.typst",
- Self::Heading => "markup.heading.typst",
- Self::ListMarker => "punctuation.definition.list.typst",
- Self::ListTerm => "markup.list.term.typst",
- Self::Label => "entity.name.label.typst",
- Self::Ref => "markup.other.reference.typst",
- Self::Keyword => "keyword.typst",
- Self::Operator => "keyword.operator.typst",
- Self::Number => "constant.numeric.typst",
- Self::String => "string.quoted.double.typst",
- Self::Function => "entity.name.function.typst",
- Self::Interpolated => "meta.interpolation.typst",
- Self::Error => "invalid.typst",
- }
- }
-
- /// The recommended CSS class for the highlighting tag.
- pub fn css_class(self) -> &'static str {
- match self {
- Self::Comment => "typ-comment",
- Self::Punctuation => "typ-punct",
- Self::Escape => "typ-escape",
- Self::Strong => "typ-strong",
- Self::Emph => "typ-emph",
- Self::Link => "typ-link",
- Self::Raw => "typ-raw",
- Self::Label => "typ-label",
- Self::Ref => "typ-ref",
- Self::Heading => "typ-heading",
- Self::ListMarker => "typ-marker",
- Self::ListTerm => "typ-term",
- Self::MathDelimiter => "typ-math-delim",
- Self::MathOperator => "typ-math-op",
- Self::Keyword => "typ-key",
- Self::Operator => "typ-op",
- Self::Number => "typ-num",
- Self::String => "typ-str",
- Self::Function => "typ-func",
- Self::Interpolated => "typ-pol",
- Self::Error => "typ-error",
- }
- }
-}
-
-/// Determine the highlight tag of a linked syntax node.
-///
-/// Returns `None` if the node should not be highlighted.
-pub fn highlight(node: &LinkedNode) -> Option<Tag> {
- match node.kind() {
- SyntaxKind::Markup
- if node.parent_kind() == Some(SyntaxKind::TermItem)
- && node.next_sibling_kind() == Some(SyntaxKind::Colon) =>
- {
- Some(Tag::ListTerm)
- }
- SyntaxKind::Markup => None,
- SyntaxKind::Text => None,
- SyntaxKind::Space => None,
- SyntaxKind::Linebreak => Some(Tag::Escape),
- SyntaxKind::Parbreak => None,
- SyntaxKind::Escape => Some(Tag::Escape),
- SyntaxKind::Shorthand => Some(Tag::Escape),
- SyntaxKind::SmartQuote => None,
- SyntaxKind::Strong => Some(Tag::Strong),
- SyntaxKind::Emph => Some(Tag::Emph),
- SyntaxKind::Raw => Some(Tag::Raw),
- SyntaxKind::Link => Some(Tag::Link),
- SyntaxKind::Label => Some(Tag::Label),
- SyntaxKind::Ref => Some(Tag::Ref),
- SyntaxKind::RefMarker => None,
- SyntaxKind::Heading => Some(Tag::Heading),
- SyntaxKind::HeadingMarker => None,
- SyntaxKind::ListItem => None,
- SyntaxKind::ListMarker => Some(Tag::ListMarker),
- SyntaxKind::EnumItem => None,
- SyntaxKind::EnumMarker => Some(Tag::ListMarker),
- SyntaxKind::TermItem => None,
- SyntaxKind::TermMarker => Some(Tag::ListMarker),
- SyntaxKind::Equation => None,
-
- SyntaxKind::Math => None,
- SyntaxKind::MathIdent => highlight_ident(node),
- SyntaxKind::MathAlignPoint => Some(Tag::MathOperator),
- SyntaxKind::MathDelimited => None,
- SyntaxKind::MathAttach => None,
- SyntaxKind::MathFrac => None,
- SyntaxKind::MathRoot => None,
-
- SyntaxKind::Hashtag => highlight_hashtag(node),
- SyntaxKind::LeftBrace => Some(Tag::Punctuation),
- SyntaxKind::RightBrace => Some(Tag::Punctuation),
- SyntaxKind::LeftBracket => Some(Tag::Punctuation),
- SyntaxKind::RightBracket => Some(Tag::Punctuation),
- SyntaxKind::LeftParen => Some(Tag::Punctuation),
- SyntaxKind::RightParen => Some(Tag::Punctuation),
- SyntaxKind::Comma => Some(Tag::Punctuation),
- SyntaxKind::Semicolon => Some(Tag::Punctuation),
- SyntaxKind::Colon => Some(Tag::Punctuation),
- SyntaxKind::Star => match node.parent_kind() {
- Some(SyntaxKind::Strong) => None,
- _ => Some(Tag::Operator),
- },
- SyntaxKind::Underscore => match node.parent_kind() {
- Some(SyntaxKind::MathAttach) => Some(Tag::MathOperator),
- _ => None,
- },
- SyntaxKind::Dollar => Some(Tag::MathDelimiter),
- SyntaxKind::Plus => Some(Tag::Operator),
- SyntaxKind::Minus => Some(Tag::Operator),
- SyntaxKind::Slash => Some(match node.parent_kind() {
- Some(SyntaxKind::MathFrac) => Tag::MathOperator,
- _ => Tag::Operator,
- }),
- SyntaxKind::Hat => Some(Tag::MathOperator),
- SyntaxKind::Dot => Some(Tag::Punctuation),
- SyntaxKind::Eq => match node.parent_kind() {
- Some(SyntaxKind::Heading) => None,
- _ => Some(Tag::Operator),
- },
- SyntaxKind::EqEq => Some(Tag::Operator),
- SyntaxKind::ExclEq => Some(Tag::Operator),
- SyntaxKind::Lt => Some(Tag::Operator),
- SyntaxKind::LtEq => Some(Tag::Operator),
- SyntaxKind::Gt => Some(Tag::Operator),
- SyntaxKind::GtEq => Some(Tag::Operator),
- SyntaxKind::PlusEq => Some(Tag::Operator),
- SyntaxKind::HyphEq => Some(Tag::Operator),
- SyntaxKind::StarEq => Some(Tag::Operator),
- SyntaxKind::SlashEq => Some(Tag::Operator),
- SyntaxKind::Dots => Some(Tag::Operator),
- SyntaxKind::Arrow => Some(Tag::Operator),
- SyntaxKind::Root => Some(Tag::MathOperator),
-
- SyntaxKind::Not => Some(Tag::Keyword),
- SyntaxKind::And => Some(Tag::Keyword),
- SyntaxKind::Or => Some(Tag::Keyword),
- SyntaxKind::None => Some(Tag::Keyword),
- SyntaxKind::Auto => Some(Tag::Keyword),
- SyntaxKind::Let => Some(Tag::Keyword),
- SyntaxKind::Set => Some(Tag::Keyword),
- SyntaxKind::Show => Some(Tag::Keyword),
- SyntaxKind::If => Some(Tag::Keyword),
- SyntaxKind::Else => Some(Tag::Keyword),
- SyntaxKind::For => Some(Tag::Keyword),
- SyntaxKind::In => Some(Tag::Keyword),
- SyntaxKind::While => Some(Tag::Keyword),
- SyntaxKind::Break => Some(Tag::Keyword),
- SyntaxKind::Continue => Some(Tag::Keyword),
- SyntaxKind::Return => Some(Tag::Keyword),
- SyntaxKind::Import => Some(Tag::Keyword),
- SyntaxKind::Include => Some(Tag::Keyword),
- SyntaxKind::As => Some(Tag::Keyword),
-
- SyntaxKind::Code => None,
- SyntaxKind::Ident => highlight_ident(node),
- SyntaxKind::Bool => Some(Tag::Keyword),
- SyntaxKind::Int => Some(Tag::Number),
- SyntaxKind::Float => Some(Tag::Number),
- SyntaxKind::Numeric => Some(Tag::Number),
- SyntaxKind::Str => Some(Tag::String),
- SyntaxKind::CodeBlock => None,
- SyntaxKind::ContentBlock => None,
- SyntaxKind::Parenthesized => None,
- SyntaxKind::Array => None,
- SyntaxKind::Dict => None,
- SyntaxKind::Named => None,
- SyntaxKind::Keyed => None,
- SyntaxKind::Unary => None,
- SyntaxKind::Binary => None,
- SyntaxKind::FieldAccess => None,
- SyntaxKind::FuncCall => None,
- SyntaxKind::Args => None,
- SyntaxKind::Spread => None,
- SyntaxKind::Closure => None,
- SyntaxKind::Params => None,
- SyntaxKind::LetBinding => None,
- SyntaxKind::SetRule => None,
- SyntaxKind::ShowRule => None,
- SyntaxKind::Conditional => None,
- SyntaxKind::WhileLoop => None,
- SyntaxKind::ForLoop => None,
- SyntaxKind::ModuleImport => None,
- SyntaxKind::ImportItems => None,
- SyntaxKind::ModuleInclude => None,
- SyntaxKind::LoopBreak => None,
- SyntaxKind::LoopContinue => None,
- SyntaxKind::FuncReturn => None,
- SyntaxKind::Destructuring => None,
- SyntaxKind::DestructAssignment => None,
-
- SyntaxKind::LineComment => Some(Tag::Comment),
- SyntaxKind::BlockComment => Some(Tag::Comment),
- SyntaxKind::Error => Some(Tag::Error),
- SyntaxKind::Eof => None,
- }
-}
-
-/// Highlight an identifier based on context.
-fn highlight_ident(node: &LinkedNode) -> Option<Tag> {
- // Are we directly before an argument list?
- let next_leaf = node.next_leaf();
- if let Some(next) = &next_leaf {
- if node.range().end == next.offset()
- && ((next.kind() == SyntaxKind::LeftParen
- && matches!(
- next.parent_kind(),
- Some(SyntaxKind::Args | SyntaxKind::Params)
- ))
- || (next.kind() == SyntaxKind::LeftBracket
- && next.parent_kind() == Some(SyntaxKind::ContentBlock)))
- {
- return Some(Tag::Function);
- }
- }
-
- // Are we in math?
- if node.kind() == SyntaxKind::MathIdent {
- return Some(Tag::Interpolated);
- }
-
- // Find the first non-field access ancestor.
- let mut ancestor = node;
- while ancestor.parent_kind() == Some(SyntaxKind::FieldAccess) {
- ancestor = ancestor.parent()?;
- }
-
- // Are we directly before or behind a show rule colon?
- if ancestor.parent_kind() == Some(SyntaxKind::ShowRule)
- && (next_leaf.map(|leaf| leaf.kind()) == Some(SyntaxKind::Colon)
- || node.prev_leaf().map(|leaf| leaf.kind()) == Some(SyntaxKind::Colon))
- {
- return Some(Tag::Function);
- }
-
- // Are we (or an ancestor field access) directly after a hashtag.
- if ancestor.prev_leaf().map(|leaf| leaf.kind()) == Some(SyntaxKind::Hashtag) {
- return Some(Tag::Interpolated);
- }
-
- // Are we behind a dot, that is behind another identifier?
- let prev = node.prev_leaf()?;
- if prev.kind() == SyntaxKind::Dot {
- let prev_prev = prev.prev_leaf()?;
- if is_ident(&prev_prev) {
- return highlight_ident(&prev_prev);
- }
- }
-
- None
-}
-
-/// Highlight a hashtag based on context.
-fn highlight_hashtag(node: &LinkedNode) -> Option<Tag> {
- let next = node.next_sibling()?;
- let expr = next.cast::<ast::Expr>()?;
- if !expr.hashtag() {
- return None;
- }
- highlight(&next.leftmost_leaf()?)
-}
-
-/// Whether the node is one of the two identifier nodes.
-fn is_ident(node: &LinkedNode) -> bool {
- matches!(node.kind(), SyntaxKind::Ident | SyntaxKind::MathIdent)
-}
-
-/// Highlight a node to an HTML `code` element.
-///
-/// This uses these [CSS classes for categories](Tag::css_class).
-pub fn highlight_html(root: &SyntaxNode) -> String {
- let mut buf = String::from("<code>");
- let node = LinkedNode::new(root);
- highlight_html_impl(&mut buf, &node);
- buf.push_str("</code>");
- buf
-}
-
-/// Highlight one source node, emitting HTML.
-fn highlight_html_impl(html: &mut String, node: &LinkedNode) {
- let mut span = false;
- if let Some(tag) = highlight(node) {
- if tag != Tag::Error {
- span = true;
- html.push_str("<span class=\"");
- html.push_str(tag.css_class());
- html.push_str("\">");
- }
- }
-
- let text = node.text();
- if !text.is_empty() {
- for c in text.chars() {
- match c {
- '<' => html.push_str("&lt;"),
- '>' => html.push_str("&gt;"),
- '&' => html.push_str("&amp;"),
- '\'' => html.push_str("&#39;"),
- '"' => html.push_str("&quot;"),
- _ => html.push(c),
- }
- }
- } else {
- for child in node.children() {
- highlight_html_impl(html, &child);
- }
- }
-
- if span {
- html.push_str("</span>");
- }
-}
-
-#[cfg(test)]
-mod tests {
- use std::ops::Range;
-
- use super::*;
- use crate::syntax::Source;
-
- #[test]
- fn test_highlighting() {
- use Tag::*;
-
- #[track_caller]
- fn test(text: &str, goal: &[(Range<usize>, Tag)]) {
- let mut vec = vec![];
- let source = Source::detached(text);
- highlight_tree(&mut vec, &LinkedNode::new(source.root()));
- assert_eq!(vec, goal);
- }
-
- fn highlight_tree(tags: &mut Vec<(Range<usize>, Tag)>, node: &LinkedNode) {
- if let Some(tag) = highlight(node) {
- tags.push((node.range(), tag));
- }
-
- for child in node.children() {
- highlight_tree(tags, &child);
- }
- }
-
- test("= *AB*", &[(0..6, Heading), (2..6, Strong)]);
-
- test(
- "#f(x + 1)",
- &[
- (0..1, Function),
- (1..2, Function),
- (2..3, Punctuation),
- (5..6, Operator),
- (7..8, Number),
- (8..9, Punctuation),
- ],
- );
-
- test(
- "#let f(x) = x",
- &[
- (0..1, Keyword),
- (1..4, Keyword),
- (5..6, Function),
- (6..7, Punctuation),
- (8..9, Punctuation),
- (10..11, Operator),
- ],
- );
- }
-}
diff --git a/src/ide/jump.rs b/src/ide/jump.rs
deleted file mode 100644
index 14a82e26..00000000
--- a/src/ide/jump.rs
+++ /dev/null
@@ -1,173 +0,0 @@
-use std::num::NonZeroUsize;
-
-use ecow::EcoString;
-
-use crate::doc::{Destination, Frame, FrameItem, Meta, Position};
-use crate::file::FileId;
-use crate::geom::{Geometry, Point, Size};
-use crate::model::Introspector;
-use crate::syntax::{LinkedNode, Source, Span, SyntaxKind};
-use crate::World;
-
-/// Where to [jump](jump_from_click) to.
-#[derive(Debug, Clone, Eq, PartialEq)]
-pub enum Jump {
- /// Jump to a position in a source file.
- Source(FileId, usize),
- /// Jump to an external URL.
- Url(EcoString),
- /// Jump to a point on a page.
- Position(Position),
-}
-
-impl Jump {
- fn from_span(world: &dyn World, span: Span) -> Option<Self> {
- let source = world.source(span.id()).ok()?;
- let node = source.find(span)?;
- Some(Self::Source(span.id(), node.offset()))
- }
-}
-
-/// Determine where to jump to based on a click in a frame.
-pub fn jump_from_click(
- world: &dyn World,
- frames: &[Frame],
- frame: &Frame,
- click: Point,
-) -> Option<Jump> {
- let mut introspector = None;
-
- // Try to find a link first.
- for (pos, item) in frame.items() {
- if let FrameItem::Meta(Meta::Link(dest), size) = item {
- if is_in_rect(*pos, *size, click) {
- return Some(match dest {
- Destination::Url(url) => Jump::Url(url.clone()),
- Destination::Position(pos) => Jump::Position(*pos),
- Destination::Location(loc) => Jump::Position(
- introspector
- .get_or_insert_with(|| Introspector::new(frames))
- .position(*loc),
- ),
- });
- }
- }
- }
-
- // If there's no link, search for a jump target.
- for (mut pos, item) in frame.items().rev() {
- match item {
- FrameItem::Group(group) => {
- // TODO: Handle transformation.
- if let Some(span) =
- jump_from_click(world, frames, &group.frame, click - pos)
- {
- return Some(span);
- }
- }
-
- FrameItem::Text(text) => {
- for glyph in &text.glyphs {
- let (span, span_offset) = glyph.span;
- if span.is_detached() {
- continue;
- }
-
- let width = glyph.x_advance.at(text.size);
- if is_in_rect(
- Point::new(pos.x, pos.y - text.size),
- Size::new(width, text.size),
- click,
- ) {
- let source = world.source(span.id()).ok()?;
- let node = source.find(span)?;
- let pos = if node.kind() == SyntaxKind::Text {
- let range = node.range();
- let mut offset = range.start + usize::from(span_offset);
- if (click.x - pos.x) > width / 2.0 {
- offset += glyph.range().len();
- }
- offset.min(range.end)
- } else {
- node.offset()
- };
- return Some(Jump::Source(source.id(), pos));
- }
-
- pos.x += width;
- }
- }
-
- FrameItem::Shape(shape, span) => {
- let Geometry::Rect(size) = shape.geometry else { continue };
- if is_in_rect(pos, size, click) {
- return Jump::from_span(world, *span);
- }
- }
-
- FrameItem::Image(_, size, span) if is_in_rect(pos, *size, click) => {
- return Jump::from_span(world, *span);
- }
-
- _ => {}
- }
- }
-
- None
-}
-
-/// Find the output location in the document for a cursor position.
-pub fn jump_from_cursor(
- frames: &[Frame],
- source: &Source,
- cursor: usize,
-) -> Option<Position> {
- let node = LinkedNode::new(source.root()).leaf_at(cursor)?;
- if node.kind() != SyntaxKind::Text {
- return None;
- }
-
- let span = node.span();
- for (i, frame) in frames.iter().enumerate() {
- if let Some(pos) = find_in_frame(frame, span) {
- return Some(Position {
- page: NonZeroUsize::new(i + 1).unwrap(),
- point: pos,
- });
- }
- }
-
- None
-}
-
-/// Find the position of a span in a frame.
-fn find_in_frame(frame: &Frame, span: Span) -> Option<Point> {
- for (mut pos, item) in frame.items() {
- if let FrameItem::Group(group) = item {
- // TODO: Handle transformation.
- if let Some(point) = find_in_frame(&group.frame, span) {
- return Some(point + pos);
- }
- }
-
- if let FrameItem::Text(text) = item {
- for glyph in &text.glyphs {
- if glyph.span.0 == span {
- return Some(pos);
- }
- pos.x += glyph.x_advance.at(text.size);
- }
- }
- }
-
- None
-}
-
-/// Whether a rectangle with the given size at the given position contains the
-/// click position.
-fn is_in_rect(pos: Point, size: Size, click: Point) -> bool {
- pos.x <= click.x
- && pos.x + size.x >= click.x
- && pos.y <= click.y
- && pos.y + size.y >= click.y
-}
diff --git a/src/ide/mod.rs b/src/ide/mod.rs
deleted file mode 100644
index 4b08b66b..00000000
--- a/src/ide/mod.rs
+++ /dev/null
@@ -1,97 +0,0 @@
-//! Capabilities for IDE support.
-
-mod analyze;
-mod complete;
-mod highlight;
-mod jump;
-mod tooltip;
-
-pub use self::analyze::analyze_labels;
-pub use self::complete::{autocomplete, Completion, CompletionKind};
-pub use self::highlight::{highlight, highlight_html, Tag};
-pub use self::jump::{jump_from_click, jump_from_cursor, Jump};
-pub use self::tooltip::{tooltip, Tooltip};
-
-use std::fmt::Write;
-
-use ecow::{eco_format, EcoString};
-
-use self::analyze::*;
-use crate::font::{FontInfo, FontStyle};
-
-/// Extract the first sentence of plain text of a piece of documentation.
-///
-/// Removes Markdown formatting.
-fn plain_docs_sentence(docs: &str) -> EcoString {
- let mut s = unscanny::Scanner::new(docs);
- let mut output = EcoString::new();
- let mut link = false;
- while let Some(c) = s.eat() {
- match c {
- '`' => {
- let mut raw = s.eat_until('`');
- if (raw.starts_with('{') && raw.ends_with('}'))
- || (raw.starts_with('[') && raw.ends_with(']'))
- {
- raw = &raw[1..raw.len() - 1];
- }
-
- s.eat();
- output.push('`');
- output.push_str(raw);
- output.push('`');
- }
- '[' => link = true,
- ']' if link => {
- if s.eat_if('(') {
- s.eat_until(')');
- s.eat();
- } else if s.eat_if('[') {
- s.eat_until(']');
- s.eat();
- }
- link = false
- }
- '*' | '_' => {}
- '.' => {
- output.push('.');
- break;
- }
- _ => output.push(c),
- }
- }
-
- output
-}
-
-/// Create a short description of a font family.
-fn summarize_font_family<'a>(variants: impl Iterator<Item = &'a FontInfo>) -> EcoString {
- let mut infos: Vec<_> = variants.collect();
- infos.sort_by_key(|info| info.variant);
-
- let mut has_italic = false;
- let mut min_weight = u16::MAX;
- let mut max_weight = 0;
- for info in &infos {
- let weight = info.variant.weight.to_number();
- has_italic |= info.variant.style == FontStyle::Italic;
- min_weight = min_weight.min(weight);
- max_weight = min_weight.max(weight);
- }
-
- let count = infos.len();
- let s = if count == 1 { "" } else { "s" };
- let mut detail = eco_format!("{count} variant{s}.");
-
- if min_weight == max_weight {
- write!(detail, " Weight {min_weight}.").unwrap();
- } else {
- write!(detail, " Weights {min_weight}–{max_weight}.").unwrap();
- }
-
- if has_italic {
- detail.push_str(" Has italics.");
- }
-
- detail
-}
diff --git a/src/ide/tooltip.rs b/src/ide/tooltip.rs
deleted file mode 100644
index 35125e92..00000000
--- a/src/ide/tooltip.rs
+++ /dev/null
@@ -1,222 +0,0 @@
-use std::fmt::Write;
-
-use ecow::{eco_format, EcoString};
-
-use if_chain::if_chain;
-
-use super::analyze::analyze_labels;
-use super::{analyze_expr, plain_docs_sentence, summarize_font_family};
-use crate::doc::Frame;
-use crate::eval::{CastInfo, Tracer, Value};
-use crate::geom::{round_2, Length, Numeric};
-use crate::syntax::{ast, LinkedNode, Source, SyntaxKind};
-use crate::util::pretty_comma_list;
-use crate::World;
-
-/// Describe the item under the cursor.
-pub fn tooltip(
- world: &(dyn World + 'static),
- frames: &[Frame],
- source: &Source,
- cursor: usize,
-) -> Option<Tooltip> {
- let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?;
- if leaf.kind().is_trivia() {
- return None;
- }
-
- named_param_tooltip(world, &leaf)
- .or_else(|| font_tooltip(world, &leaf))
- .or_else(|| ref_tooltip(world, frames, &leaf))
- .or_else(|| expr_tooltip(world, &leaf))
-}
-
-/// A hover tooltip.
-#[derive(Debug, Clone)]
-pub enum Tooltip {
- /// A string of text.
- Text(EcoString),
- /// A string of Typst code.
- Code(EcoString),
-}
-
-/// Tooltip for a hovered expression.
-fn expr_tooltip(world: &(dyn World + 'static), leaf: &LinkedNode) -> Option<Tooltip> {
- let mut ancestor = leaf;
- while !ancestor.is::<ast::Expr>() {
- ancestor = ancestor.parent()?;
- }
-
- let expr = ancestor.cast::<ast::Expr>()?;
- if !expr.hashtag() && !matches!(expr, ast::Expr::MathIdent(_)) {
- return None;
- }
-
- let values = analyze_expr(world, ancestor);
-
- if let [value] = values.as_slice() {
- if let Some(docs) = value.docs() {
- return Some(Tooltip::Text(plain_docs_sentence(docs)));
- }
-
- if let &Value::Length(length) = value {
- if let Some(tooltip) = length_tooltip(length) {
- return Some(tooltip);
- }
- }
- }
-
- if expr.is_literal() {
- return None;
- }
-
- let mut last = None;
- let mut pieces: Vec<EcoString> = vec![];
- let mut iter = values.iter();
- for value in (&mut iter).take(Tracer::MAX - 1) {
- if let Some((prev, count)) = &mut last {
- if *prev == value {
- *count += 1;
- continue;
- } else if *count > 1 {
- write!(pieces.last_mut().unwrap(), " (x{count})").unwrap();
- }
- }
- pieces.push(value.repr().into());
- last = Some((value, 1));
- }
-
- if let Some((_, count)) = last {
- if count > 1 {
- write!(pieces.last_mut().unwrap(), " (x{count})").unwrap();
- }
- }
-
- if iter.next().is_some() {
- pieces.push("...".into());
- }
-
- let tooltip = pretty_comma_list(&pieces, false);
- (!tooltip.is_empty()).then(|| Tooltip::Code(tooltip.into()))
-}
-
-/// Tooltip text for a hovered length.
-fn length_tooltip(length: Length) -> Option<Tooltip> {
- length.em.is_zero().then(|| {
- Tooltip::Code(eco_format!(
- "{}pt = {}mm = {}cm = {}in",
- round_2(length.abs.to_pt()),
- round_2(length.abs.to_mm()),
- round_2(length.abs.to_cm()),
- round_2(length.abs.to_inches())
- ))
- })
-}
-
-/// Tooltip for a hovered reference.
-fn ref_tooltip(
- world: &(dyn World + 'static),
- frames: &[Frame],
- leaf: &LinkedNode,
-) -> Option<Tooltip> {
- if leaf.kind() != SyntaxKind::RefMarker {
- return None;
- }
-
- let target = leaf.text().trim_start_matches('@');
- for (label, detail) in analyze_labels(world, frames).0 {
- if label.0 == target {
- return Some(Tooltip::Text(detail?));
- }
- }
-
- None
-}
-
-/// Tooltips for components of a named parameter.
-fn named_param_tooltip(
- world: &(dyn World + 'static),
- leaf: &LinkedNode,
-) -> Option<Tooltip> {
- let (info, named) = if_chain! {
- // Ensure that we are in a named pair in the arguments to a function
- // call or set rule.
- if let Some(parent) = leaf.parent();
- if let Some(named) = parent.cast::<ast::Named>();
- if let Some(grand) = parent.parent();
- if matches!(grand.kind(), SyntaxKind::Args);
- if let Some(grand_grand) = grand.parent();
- if let Some(expr) = grand_grand.cast::<ast::Expr>();
- if let Some(ast::Expr::Ident(callee)) = match expr {
- ast::Expr::FuncCall(call) => Some(call.callee()),
- ast::Expr::Set(set) => Some(set.target()),
- _ => None,
- };
-
- // Find metadata about the function.
- if let Some(Value::Func(func)) = world.library().global.scope().get(&callee);
- if let Some(info) = func.info();
- then { (info, named) }
- else { return None; }
- };
-
- // Hovering over the parameter name.
- if_chain! {
- if leaf.index() == 0;
- if let Some(ident) = leaf.cast::<ast::Ident>();
- if let Some(param) = info.param(&ident);
- then {
- return Some(Tooltip::Text(plain_docs_sentence(param.docs)));
- }
- }
-
- // Hovering over a string parameter value.
- if_chain! {
- if let Some(string) = leaf.cast::<ast::Str>();
- if let Some(param) = info.param(&named.name());
- if let Some(docs) = find_string_doc(&param.cast, &string.get());
- then {
- return Some(Tooltip::Text(docs.into()));
- }
- }
-
- None
-}
-
-/// Find documentation for a castable string.
-fn find_string_doc(info: &CastInfo, string: &str) -> Option<&'static str> {
- match info {
- CastInfo::Value(Value::Str(s), docs) if s.as_str() == string => Some(docs),
- CastInfo::Union(options) => {
- options.iter().find_map(|option| find_string_doc(option, string))
- }
- _ => None,
- }
-}
-
-/// Tooltip for font.
-fn font_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option<Tooltip> {
- if_chain! {
- // Ensure that we are on top of a string.
- if let Some(string) = leaf.cast::<ast::Str>();
- let lower = string.get().to_lowercase();
-
- // Ensure that we are in the arguments to the text function.
- if let Some(parent) = leaf.parent();
- if let Some(named) = parent.cast::<ast::Named>();
- if named.name().as_str() == "font";
-
- // Find the font family.
- if let Some((_, iter)) = world
- .book()
- .families()
- .find(|&(family, _)| family.to_lowercase().as_str() == lower.as_str());
-
- then {
- let detail = summarize_font_family(iter);
- return Some(Tooltip::Text(detail));
- }
- };
-
- None
-}