diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index 90c8b4a1f..c4f86d04a 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -17,8 +17,10 @@ use typst::visualize::Color; use typst::World; use unscanny::Scanner; -use crate::analyze::{analyze_expr, analyze_import, analyze_labels}; -use crate::{plain_docs_sentence, summarize_font_family}; +use crate::{ + analyze_expr, analyze_import, analyze_labels, named_items, plain_docs_sentence, + summarize_font_family, +}; /// Autocomplete a cursor position in a source file. /// @@ -1327,62 +1329,12 @@ impl<'a> CompletionContext<'a> { /// 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::() { - for ident in v.kind().bindings() { - defined.insert(ident.get().clone()); - } - } - - if let Some(v) = node.cast::() { - let imports = v.imports(); - match imports { - None | Some(ast::Imports::Wildcard) => { - if let Some(value) = node - .children() - .find(|child| child.is::()) - .and_then(|source| analyze_import(self.world, &source)) - { - if imports.is_none() { - defined.extend(value.name().map(Into::into)); - } else if let Some(scope) = value.scope() { - for (name, _) in scope.iter() { - defined.insert(name.clone()); - } - } - } - } - Some(ast::Imports::Items(items)) => { - for item in items.iter() { - defined.insert(item.bound_name().get().clone()); - } - } - } - } - - sibling = node.prev_sibling(); + named_items(self.world, self.leaf.clone(), |name| { + if name.value().as_ref().map_or(true, &filter) { + defined.insert(name.name().clone()); } - - if let Some(parent) = node.parent() { - if let Some(v) = parent.cast::() { - if node.prev_sibling_kind() != Some(SyntaxKind::In) { - let pattern = v.pattern(); - for ident in pattern.bindings() { - defined.insert(ident.get().clone()); - } - } - } - - ancestor = Some(parent.clone()); - continue; - } - - break; - } + None::<()> + }); let in_math = matches!( self.leaf.parent_kind(), diff --git a/crates/typst-ide/src/definition.rs b/crates/typst-ide/src/definition.rs new file mode 100644 index 000000000..452627816 --- /dev/null +++ b/crates/typst-ide/src/definition.rs @@ -0,0 +1,264 @@ +use ecow::EcoString; +use typst::foundations::{Label, Module, Selector, Value}; +use typst::model::Document; +use typst::syntax::ast::AstNode; +use typst::syntax::{ast, LinkedNode, Side, Source, Span, SyntaxKind}; +use typst::World; + +use crate::{analyze_import, deref_target, named_items, DerefTarget, NamedItem}; + +/// Find the definition of the item under the cursor. +/// +/// Passing a `document` (from a previous compilation) is optional, but enhances +/// the definition search. Label definitions, for instance, are only generated +/// when the document is available. +pub fn definition( + world: &dyn World, + document: Option<&Document>, + source: &Source, + cursor: usize, + side: Side, +) -> Option { + let root = LinkedNode::new(source.root()); + let leaf = root.leaf_at(cursor, side)?; + + let target = deref_target(leaf.clone())?; + + let mut use_site = match target { + DerefTarget::VarAccess(node) | DerefTarget::Callee(node) => node, + DerefTarget::IncludePath(path) | DerefTarget::ImportPath(path) => { + let import_item = + analyze_import(world, &path).and_then(|v| v.cast::().ok())?; + return Some(Definition::module(&import_item, path.span(), Span::detached())); + } + DerefTarget::Ref(r) => { + let label = Label::new(r.cast::()?.target()); + let sel = Selector::Label(label); + let elem = document?.introspector.query_first(&sel)?; + let span = elem.span(); + return Some(Definition { + kind: DefinitionKind::Label, + name: label.as_str().into(), + value: Some(Value::Label(label)), + span, + name_span: Span::detached(), + }); + } + DerefTarget::Label(..) | DerefTarget::Code(..) => { + return None; + } + }; + + let mut has_path = false; + while let Some(node) = use_site.cast::() { + has_path = true; + use_site = use_site.find(node.target().span())?; + } + + let name = use_site.cast::()?.get().clone(); + let src = named_items(world, use_site, |item: NamedItem| { + if *item.name() != name { + return None; + } + + match item { + NamedItem::Var(name) => { + let name_span = name.span(); + let span = find_let_binding(source, name_span); + Some(Definition::item(name.get().clone(), span, name_span, None)) + } + NamedItem::Fn(name) => { + let name_span = name.span(); + let span = find_let_binding(source, name_span); + Some( + Definition::item(name.get().clone(), span, name_span, None) + .with_kind(DefinitionKind::Function), + ) + } + NamedItem::Module(item, site) => Some(Definition::module( + item, + site.span(), + matches!(site.kind(), SyntaxKind::Ident) + .then_some(site.span()) + .unwrap_or_else(Span::detached), + )), + NamedItem::Import(name, span, value) => Some(Definition::item( + name.clone(), + Span::detached(), + span, + value.cloned(), + )), + } + }); + + let src = src.or_else(|| { + let in_math = matches!( + leaf.parent_kind(), + Some(SyntaxKind::Equation) + | Some(SyntaxKind::Math) + | Some(SyntaxKind::MathFrac) + | Some(SyntaxKind::MathAttach) + ); + let library = world.library(); + + let scope = if in_math { library.math.scope() } else { library.global.scope() }; + for (item_name, value) in scope.iter() { + if *item_name == name { + return Some(Definition::item( + name, + Span::detached(), + Span::detached(), + Some(value.clone()), + )); + } + } + + None + })?; + + (!has_path).then_some(src) +} + +/// A definition of some item. +#[derive(Debug, Clone)] +pub struct Definition { + /// The name of the definition. + pub name: EcoString, + /// The kind of the definition. + pub kind: DefinitionKind, + /// An instance of the definition, if available. + pub value: Option, + /// The source span of the entire definition. May be detached if unknown. + pub span: Span, + /// The span of the definition's name. May be detached if unknown. + pub name_span: Span, +} + +impl Definition { + fn item(name: EcoString, span: Span, name_span: Span, value: Option) -> Self { + Self { + name, + kind: match value { + Some(Value::Func(_)) => DefinitionKind::Function, + _ => DefinitionKind::Variable, + }, + value, + span, + name_span, + } + } + + fn module(module: &Module, span: Span, name_span: Span) -> Self { + Definition { + name: module.name().clone(), + kind: DefinitionKind::Module, + value: Some(Value::Module(module.clone())), + span, + name_span, + } + } + + fn with_kind(self, kind: DefinitionKind) -> Self { + Self { kind, ..self } + } +} + +/// A kind of item that is definition. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum DefinitionKind { + /// ```plain + /// let foo; + /// ^^^^^^^^ span + /// ^^^ name_span + /// ``` + Variable, + /// ```plain + /// let foo(it) = it; + /// ^^^^^^^^^^^^^^^^^ span + /// ^^^ name_span + /// ``` + Function, + /// Case 1 + /// ```plain + /// import "foo.typ": * + /// ^^^^^^^^^ span + /// name_span is detached + /// ``` + /// + /// Case 2 + /// ```plain + /// import "foo.typ" as bar: * + /// span ^^^ + /// name_span ^^^ + /// ``` + Module, + /// ```plain + /// + /// ^^^^^ span + /// name_span is detached + /// ``` + Label, +} + +fn find_let_binding(source: &Source, name_span: Span) -> Span { + let node = LinkedNode::new(source.root()); + std::iter::successors(node.find(name_span).as_ref(), |n| n.parent()) + .find(|n| matches!(n.kind(), SyntaxKind::LetBinding)) + .map(|s| s.span()) + .unwrap_or_else(Span::detached) +} + +#[cfg(test)] +mod tests { + use std::ops::Range; + + use typst::foundations::{IntoValue, Label, NativeElement, Value}; + use typst::syntax::Side; + use typst::WorldExt; + + use super::{definition, DefinitionKind as Kind}; + use crate::tests::TestWorld; + + #[track_caller] + fn test( + text: &str, + cursor: usize, + name: &str, + kind: Kind, + value: Option, + range: Option>, + ) where + T: IntoValue, + { + let world = TestWorld::new(text); + let doc = typst::compile(&world).output.ok(); + let actual = definition(&world, doc.as_ref(), &world.main, cursor, Side::After) + .map(|d| (d.kind, d.name, world.range(d.span), d.value)); + assert_eq!( + actual, + Some((kind, name.into(), range, value.map(IntoValue::into_value))) + ); + } + + #[test] + fn test_definition() { + test("#let x; #x", 9, "x", Kind::Variable, None::, Some(1..6)); + test("#let x() = {}; #x", 16, "x", Kind::Function, None::, Some(1..13)); + test( + "#table", + 1, + "table", + Kind::Function, + Some(typst::model::TableElem::elem()), + None, + ); + test( + "#figure[] See @hi", + 21, + "hi", + Kind::Label, + Some(Label::new("hi")), + Some(1..9), + ); + } +} diff --git a/crates/typst-ide/src/lib.rs b/crates/typst-ide/src/lib.rs index 1f8562fd2..c4a88085d 100644 --- a/crates/typst-ide/src/lib.rs +++ b/crates/typst-ide/src/lib.rs @@ -2,12 +2,16 @@ mod analyze; mod complete; +mod definition; mod jump; +mod matchers; mod tooltip; -pub use self::analyze::analyze_labels; +pub use self::analyze::{analyze_expr, analyze_import, analyze_labels}; pub use self::complete::{autocomplete, Completion, CompletionKind}; +pub use self::definition::{definition, Definition, DefinitionKind}; pub use self::jump::{jump_from_click, jump_from_cursor, Jump}; +pub use self::matchers::{deref_target, named_items, DerefTarget, NamedItem}; pub use self::tooltip::{tooltip, Tooltip}; use std::fmt::Write; diff --git a/crates/typst-ide/src/matchers.rs b/crates/typst-ide/src/matchers.rs new file mode 100644 index 000000000..757e5ab65 --- /dev/null +++ b/crates/typst-ide/src/matchers.rs @@ -0,0 +1,266 @@ +use ecow::EcoString; +use typst::foundations::{Module, Value}; +use typst::syntax::ast::AstNode; +use typst::syntax::{ast, LinkedNode, Span, SyntaxKind, SyntaxNode}; +use typst::World; + +use crate::analyze_import; + +/// Find the named items starting from the given position. +pub fn named_items( + world: &dyn World, + position: LinkedNode, + mut recv: impl FnMut(NamedItem) -> Option, +) -> Option { + let mut ancestor = Some(position); + while let Some(node) = &ancestor { + let mut sibling = Some(node.clone()); + while let Some(node) = &sibling { + if let Some(v) = node.cast::() { + let kind = if matches!(v.kind(), ast::LetBindingKind::Closure(..)) { + NamedItem::Fn + } else { + NamedItem::Var + }; + for ident in v.kind().bindings() { + if let Some(res) = recv(kind(ident)) { + return Some(res); + } + } + } + + if let Some(v) = node.cast::() { + let imports = v.imports(); + let source = node + .children() + .find(|child| child.is::()) + .and_then(|source: LinkedNode| { + Some((analyze_import(world, &source)?, source)) + }); + let source = source.as_ref(); + + // Seeing the module itself. + if let Some((value, source)) = source { + let site = match (imports, v.new_name()) { + // ```plain + // import "foo" as name; + // import "foo" as name: ..; + // ``` + (_, Some(name)) => Some(name.to_untyped()), + // ```plain + // import "foo"; + // ``` + (None, None) => Some(source.get()), + // ```plain + // import "foo": ..; + // ``` + (Some(..), None) => None, + }; + + if let Some((site, value)) = + site.zip(value.clone().cast::().ok()) + { + if let Some(res) = recv(NamedItem::Module(&value, site)) { + return Some(res); + } + } + } + + // Seeing the imported items. + match imports { + // ```plain + // import "foo"; + // ``` + None => {} + // ```plain + // import "foo": *; + // ``` + Some(ast::Imports::Wildcard) => { + if let Some(scope) = source.and_then(|(value, _)| value.scope()) { + for (name, value) in scope.iter() { + let item = NamedItem::Import( + name, + Span::detached(), + Some(value), + ); + if let Some(res) = recv(item) { + return Some(res); + } + } + } + } + // ```plain + // import "foo": items; + // ``` + Some(ast::Imports::Items(items)) => { + for item in items.iter() { + let name = item.bound_name(); + if let Some(res) = + recv(NamedItem::Import(name.get(), name.span(), None)) + { + return Some(res); + } + } + } + } + } + + sibling = node.prev_sibling(); + } + + if let Some(parent) = node.parent() { + if let Some(v) = parent.cast::() { + if node.prev_sibling_kind() != Some(SyntaxKind::In) { + let pattern = v.pattern(); + for ident in pattern.bindings() { + if let Some(res) = recv(NamedItem::Var(ident)) { + return Some(res); + } + } + } + } + + ancestor = Some(parent.clone()); + continue; + } + + break; + } + + None +} + +/// An item that is named. +pub enum NamedItem<'a> { + /// A variable item. + Var(ast::Ident<'a>), + /// A function item. + Fn(ast::Ident<'a>), + /// A (imported) module item. + Module(&'a Module, &'a SyntaxNode), + /// An imported item. + Import(&'a EcoString, Span, Option<&'a Value>), +} + +impl<'a> NamedItem<'a> { + pub(crate) fn name(&self) -> &'a EcoString { + match self { + NamedItem::Var(ident) => ident.get(), + NamedItem::Fn(ident) => ident.get(), + NamedItem::Module(value, _) => value.name(), + NamedItem::Import(name, _, _) => name, + } + } + + pub(crate) fn value(&self) -> Option { + match self { + NamedItem::Var(..) | NamedItem::Fn(..) => None, + NamedItem::Module(value, _) => Some(Value::Module((*value).clone())), + NamedItem::Import(_, _, value) => value.cloned(), + } + } +} + +/// Categorize an expression into common classes IDE functionality can operate +/// on. +pub fn deref_target(node: LinkedNode) -> Option> { + // Move to the first ancestor that is an expression. + let mut ancestor = node; + while !ancestor.is::() { + ancestor = ancestor.parent()?.clone(); + } + + // Identify convenient expression kinds. + let expr_node = ancestor; + let expr = expr_node.cast::()?; + Some(match expr { + ast::Expr::Label(..) => DerefTarget::Label(expr_node), + ast::Expr::Ref(..) => DerefTarget::Ref(expr_node), + ast::Expr::FuncCall(call) => { + DerefTarget::Callee(expr_node.find(call.callee().span())?) + } + ast::Expr::Set(set) => DerefTarget::Callee(expr_node.find(set.target().span())?), + ast::Expr::Ident(..) | ast::Expr::MathIdent(..) | ast::Expr::FieldAccess(..) => { + DerefTarget::VarAccess(expr_node) + } + ast::Expr::Str(..) => { + let parent = expr_node.parent()?; + if parent.kind() == SyntaxKind::ModuleImport { + DerefTarget::ImportPath(expr_node) + } else if parent.kind() == SyntaxKind::ModuleInclude { + DerefTarget::IncludePath(expr_node) + } else { + DerefTarget::Code(expr_node.kind(), expr_node) + } + } + _ if expr.hash() + || matches!(expr_node.kind(), SyntaxKind::MathIdent | SyntaxKind::Error) => + { + DerefTarget::Code(expr_node.kind(), expr_node) + } + _ => return None, + }) +} + +/// Classes of expressions that can be operated on by IDE functionality. +#[derive(Debug, Clone)] +pub enum DerefTarget<'a> { + /// A label expression. + Label(LinkedNode<'a>), + /// A reference expression. + Ref(LinkedNode<'a>), + /// A variable access expression. + /// + /// It can be either an identifier or a field access. + VarAccess(LinkedNode<'a>), + /// A function call expression. + Callee(LinkedNode<'a>), + /// An import path expression. + ImportPath(LinkedNode<'a>), + /// An include path expression. + IncludePath(LinkedNode<'a>), + /// Any code expression. + Code(SyntaxKind, LinkedNode<'a>), +} + +#[cfg(test)] +mod tests { + use typst::syntax::{LinkedNode, Side}; + + use crate::{named_items, tests::TestWorld}; + + #[track_caller] + fn has_named_items(text: &str, cursor: usize, containing: &str) -> bool { + let world = TestWorld::new(text); + + let src = world.main.clone(); + let node = LinkedNode::new(src.root()); + let leaf = node.leaf_at(cursor, Side::After).unwrap(); + + let res = named_items(&world, leaf, |s| { + if containing == s.name() { + return Some(true); + } + + None + }); + + res.unwrap_or_default() + } + + #[test] + fn test_simple_named_items() { + // Has named items + assert!(has_named_items(r#"#let a = 1;#let b = 2;"#, 8, "a")); + assert!(has_named_items(r#"#let a = 1;#let b = 2;"#, 15, "a")); + + // Doesn't have named items + assert!(!has_named_items(r#"#let a = 1;#let b = 2;"#, 8, "b")); + } + + #[test] + fn test_import_named_items() { + // Cannot test much. + assert!(has_named_items(r#"#import "foo.typ": a; #(a);"#, 24, "a")); + } +} diff --git a/crates/typst-ide/src/tooltip.rs b/crates/typst-ide/src/tooltip.rs index 3bf8bb14a..c78c02d82 100644 --- a/crates/typst-ide/src/tooltip.rs +++ b/crates/typst-ide/src/tooltip.rs @@ -11,14 +11,13 @@ use typst::syntax::{ast, LinkedNode, Side, Source, SyntaxKind}; use typst::utils::{round_2, Numeric}; use typst::World; -use crate::analyze::{analyze_expr, analyze_labels}; -use crate::{plain_docs_sentence, summarize_font_family}; +use crate::{analyze_expr, analyze_labels, plain_docs_sentence, summarize_font_family}; /// Describe the item under the cursor. /// /// Passing a `document` (from a previous compilation) is optional, but enhances -/// the autocompletions. Label completions, for instance, are only generated -/// when the document is available. +/// the tooltips. Label tooltips, for instance, are only generated when the +/// document is available. pub fn tooltip( world: &dyn World, document: Option<&Document>, diff --git a/crates/typst/src/eval/mod.rs b/crates/typst/src/eval/mod.rs index e4221df77..dc8a18020 100644 --- a/crates/typst/src/eval/mod.rs +++ b/crates/typst/src/eval/mod.rs @@ -86,7 +86,7 @@ pub fn eval( .unwrap_or_default() .to_string_lossy(); - Ok(Module::new(name, vm.scopes.top).with_content(output)) + Ok(Module::new(name, vm.scopes.top).with_content(output).with_file_id(id)) } /// Evaluate a string as code and return the resulting value. diff --git a/crates/typst/src/foundations/module.rs b/crates/typst/src/foundations/module.rs index 580d09ef1..91b508554 100644 --- a/crates/typst/src/foundations/module.rs +++ b/crates/typst/src/foundations/module.rs @@ -5,6 +5,7 @@ use ecow::{eco_format, EcoString}; use crate::diag::StrResult; use crate::foundations::{repr, ty, Content, Scope, Value}; +use crate::syntax::FileId; /// An evaluated module, either built-in or resulting from a file. /// @@ -43,6 +44,8 @@ struct Repr { scope: Scope, /// The module's layoutable contents. content: Content, + /// The id of the file which defines the module, if any. + file_id: Option, } impl Module { @@ -50,7 +53,7 @@ impl Module { pub fn new(name: impl Into, scope: Scope) -> Self { Self { name: name.into(), - inner: Arc::new(Repr { scope, content: Content::empty() }), + inner: Arc::new(Repr { scope, content: Content::empty(), file_id: None }), } } @@ -72,6 +75,12 @@ impl Module { self } + /// Update the module's file id. + pub fn with_file_id(mut self, file_id: FileId) -> Self { + Arc::make_mut(&mut self.inner).file_id = Some(file_id); + self + } + /// Get the module's name. pub fn name(&self) -> &EcoString { &self.name @@ -82,6 +91,13 @@ impl Module { &self.inner.scope } + /// Access the module's file id. + /// + /// Some modules are not associated with a file, like the built-in modules. + pub fn file_id(&self) -> Option { + self.inner.file_id + } + /// Access the module's scope, mutably. pub fn scope_mut(&mut self) -> &mut Scope { &mut Arc::make_mut(&mut self.inner).scope