summaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/typst-ide/src/complete.rs66
-rw-r--r--crates/typst-ide/src/definition.rs264
-rw-r--r--crates/typst-ide/src/lib.rs6
-rw-r--r--crates/typst-ide/src/matchers.rs266
-rw-r--r--crates/typst-ide/src/tooltip.rs7
-rw-r--r--crates/typst/src/eval/mod.rs2
-rw-r--r--crates/typst/src/foundations/module.rs18
7 files changed, 565 insertions, 64 deletions
diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs
index 90c8b4a1..c4f86d04 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::<ast::LetBinding>() {
- for ident in v.kind().bindings() {
- defined.insert(ident.get().clone());
- }
- }
-
- if let Some(v) = node.cast::<ast::ModuleImport>() {
- let imports = v.imports();
- match imports {
- None | Some(ast::Imports::Wildcard) => {
- if let Some(value) = node
- .children()
- .find(|child| child.is::<ast::Expr>())
- .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::<ast::ForLoop>() {
- 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 00000000..45262781
--- /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<Definition> {
+ 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::<Module>().ok())?;
+ return Some(Definition::module(&import_item, path.span(), Span::detached()));
+ }
+ DerefTarget::Ref(r) => {
+ let label = Label::new(r.cast::<ast::Ref>()?.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::<ast::FieldAccess>() {
+ has_path = true;
+ use_site = use_site.find(node.target().span())?;
+ }
+
+ let name = use_site.cast::<ast::Ident>()?.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<Value>,
+ /// 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<Value>) -> 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
+ /// <foo>
+ /// ^^^^^ 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<T>(
+ text: &str,
+ cursor: usize,
+ name: &str,
+ kind: Kind,
+ value: Option<T>,
+ range: Option<Range<usize>>,
+ ) 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::<Value>, Some(1..6));
+ test("#let x() = {}; #x", 16, "x", Kind::Function, None::<Value>, Some(1..13));
+ test(
+ "#table",
+ 1,
+ "table",
+ Kind::Function,
+ Some(typst::model::TableElem::elem()),
+ None,
+ );
+ test(
+ "#figure[] <hi> 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 1f8562fd..c4a88085 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 00000000..757e5ab6
--- /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<T>(
+ world: &dyn World,
+ position: LinkedNode,
+ mut recv: impl FnMut(NamedItem) -> Option<T>,
+) -> Option<T> {
+ 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::<ast::LetBinding>() {
+ 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::<ast::ModuleImport>() {
+ let imports = v.imports();
+ let source = node
+ .children()
+ .find(|child| child.is::<ast::Expr>())
+ .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::<Module>().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::<ast::ForLoop>() {
+ 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<Value> {
+ 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<DerefTarget<'_>> {
+ // Move to the first ancestor that is an expression.
+ let mut ancestor = node;
+ while !ancestor.is::<ast::Expr>() {
+ ancestor = ancestor.parent()?.clone();
+ }
+
+ // Identify convenient expression kinds.
+ let expr_node = ancestor;
+ let expr = expr_node.cast::<ast::Expr>()?;
+ 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 3bf8bb14..c78c02d8 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 e4221df7..dc8a1802 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 580d09ef..91b50855 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<FileId>,
}
impl Module {
@@ -50,7 +53,7 @@ impl Module {
pub fn new(name: impl Into<EcoString>, 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<FileId> {
+ 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