diff --git a/crates/typst-eval/src/import.rs b/crates/typst-eval/src/import.rs index aee632bfa..5b67c0608 100644 --- a/crates/typst-eval/src/import.rs +++ b/crates/typst-eval/src/import.rs @@ -163,52 +163,15 @@ pub fn import(engine: &mut Engine, from: &str, span: Span) -> SourceResult().at(span)?; import_package(engine, spec, span) } else { - import_file(engine, from, span) + let id = span.resolve_path(from).at(span)?; + import_file(engine, id, span) } } -/// Import an external package. -fn import_package( - engine: &mut Engine, - spec: PackageSpec, - span: Span, -) -> SourceResult { - // Evaluate the manifest. - let manifest_id = FileId::new(Some(spec.clone()), VirtualPath::new("typst.toml")); - let bytes = engine.world.file(manifest_id).at(span)?; - let string = std::str::from_utf8(&bytes).map_err(FileError::from).at(span)?; - let manifest: PackageManifest = toml::from_str(string) - .map_err(|err| eco_format!("package manifest is malformed ({})", err.message())) - .at(span)?; - manifest.validate(&spec).at(span)?; - - // Evaluate the entry point. - let entrypoint_id = manifest_id.join(&manifest.package.entrypoint); - let source = engine.world.source(entrypoint_id).at(span)?; - - // Prevent cyclic importing. - if engine.route.contains(source.id()) { - bail!(span, "cyclic import"); - } - - let point = || Tracepoint::Import; - Ok(eval( - engine.routines, - engine.world, - engine.traced, - TrackedMut::reborrow_mut(&mut engine.sink), - engine.route.track(), - &source, - ) - .trace(engine.world, point, span)? - .with_name(manifest.package.name)) -} - /// Import a file from a path. The path is resolved relative to the given /// `span`. -fn import_file(engine: &mut Engine, path: &str, span: Span) -> SourceResult { +fn import_file(engine: &mut Engine, id: FileId, span: Span) -> SourceResult { // Load the source file. - let id = span.resolve_path(path).at(span)?; let source = engine.world.source(id).at(span)?; // Prevent cyclic importing. @@ -228,3 +191,32 @@ fn import_file(engine: &mut Engine, path: &str, span: Span) -> SourceResult SourceResult { + let (name, id) = resolve_package(engine, spec, span)?; + import_file(engine, id, span).map(|module| module.with_name(name)) +} + +/// Resolve the name and entrypoint of a package. +fn resolve_package( + engine: &mut Engine, + spec: PackageSpec, + span: Span, +) -> SourceResult<(EcoString, FileId)> { + // Evaluate the manifest. + let manifest_id = FileId::new(Some(spec.clone()), VirtualPath::new("typst.toml")); + let bytes = engine.world.file(manifest_id).at(span)?; + let string = std::str::from_utf8(&bytes).map_err(FileError::from).at(span)?; + let manifest: PackageManifest = toml::from_str(string) + .map_err(|err| eco_format!("package manifest is malformed ({})", err.message())) + .at(span)?; + manifest.validate(&spec).at(span)?; + + // Evaluate the entry point. + Ok((manifest.package.name, manifest_id.join(&manifest.package.entrypoint))) +} diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index fba1177f3..f25e40c6b 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -6,7 +6,7 @@ use ecow::{eco_format, EcoString}; use if_chain::if_chain; use serde::{Deserialize, Serialize}; use typst::foundations::{ - fields_on, repr, AutoValue, CastInfo, Func, Label, NoneValue, ParamInfo, Repr, Scope, + fields_on, repr, AutoValue, CastInfo, Func, Label, NoneValue, ParamInfo, Repr, StyleChain, Styles, Type, Value, }; use typst::model::Document; @@ -19,7 +19,7 @@ use typst::text::RawElem; use typst::visualize::Color; use unscanny::Scanner; -use crate::utils::{plain_docs_sentence, summarize_font_family}; +use crate::utils::{globals, plain_docs_sentence, summarize_font_family}; use crate::{analyze_expr, analyze_import, analyze_labels, named_items, IdeWorld}; /// Autocomplete a cursor position in a source file. @@ -839,10 +839,11 @@ fn resolve_global_callee<'a>( ctx: &CompletionContext<'a>, callee: ast::Expr<'a>, ) -> Option<&'a Func> { + let globals = globals(ctx.world, ctx.leaf); let value = match callee { - ast::Expr::Ident(ident) => ctx.global.get(&ident)?, + ast::Expr::Ident(ident) => globals.get(&ident)?, ast::Expr::FieldAccess(access) => match access.target() { - ast::Expr::Ident(target) => match ctx.global.get(&target)? { + ast::Expr::Ident(target) => match globals.get(&target)? { Value::Module(module) => module.field(&access.field()).ok()?, Value::Func(func) => func.field(&access.field()).ok()?, _ => return None, @@ -1051,8 +1052,6 @@ fn code_completions(ctx: &mut CompletionContext, hash: bool) { struct CompletionContext<'a> { world: &'a (dyn IdeWorld + 'a), document: Option<&'a Document>, - global: &'a Scope, - math: &'a Scope, text: &'a str, before: &'a str, after: &'a str, @@ -1075,12 +1074,9 @@ impl<'a> CompletionContext<'a> { explicit: bool, ) -> Option { let text = source.text(); - let library = world.library(); Some(Self { world, document, - global: library.global.scope(), - math: library.math.scope(), text, before: &text[..cursor], after: &text[cursor..], @@ -1433,16 +1429,7 @@ impl<'a> CompletionContext<'a> { None::<()> }); - 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() { + for (name, value, _) in globals(self.world, self.leaf).iter() { if filter(value) && !defined.contains(name) { self.value_completion_full(Some(name.clone()), value, parens, None, None); } diff --git a/crates/typst-ide/src/definition.rs b/crates/typst-ide/src/definition.rs index a8286554b..94def1c18 100644 --- a/crates/typst-ide/src/definition.rs +++ b/crates/typst-ide/src/definition.rs @@ -1,13 +1,22 @@ -use ecow::EcoString; -use typst::foundations::{Label, Module, Selector, Value}; +use typst::foundations::{Label, Selector, Value}; use typst::model::Document; -use typst::syntax::ast::AstNode; -use typst::syntax::{ast, LinkedNode, Side, Source, Span, SyntaxKind}; +use typst::syntax::{ast, LinkedNode, Side, Source, Span}; +use crate::utils::globals; use crate::{ - analyze_import, deref_target, named_items, DerefTarget, IdeWorld, NamedItem, + analyze_expr, analyze_import, deref_target, named_items, DerefTarget, IdeWorld, + NamedItem, }; +/// A definition of some item. +#[derive(Debug, Clone)] +pub enum Definition { + /// The item is defined at the given span. + Span(Span), + /// The item is defined in the standard library. + Std(Value), +} + /// Find the definition of the item under the cursor. /// /// Passing a `document` (from a previous compilation) is optional, but enhances @@ -23,241 +32,162 @@ pub fn definition( let root = LinkedNode::new(source.root()); let leaf = root.leaf_at(cursor, side)?; - let mut use_site = match deref_target(leaf.clone())? { - 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; - } - }; + match deref_target(leaf.clone())? { + // Try to find a named item (defined in this file or an imported file) + // or fall back to a standard library item. + DerefTarget::VarAccess(node) | DerefTarget::Callee(node) => { + let name = node.cast::()?.get().clone(); + if let Some(src) = named_items(world, node.clone(), |item: NamedItem| { + (*item.name() == name).then(|| Definition::Span(item.span())) + }) { + return Some(src); + }; - 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)) + if let Some((value, _)) = analyze_expr(world, &node).first() { + let span = match value { + Value::Content(content) => content.span(), + Value::Func(func) => func.span(), + _ => Span::detached(), + }; + if !span.is_detached() && span != node.span() { + return Some(Definition::Span(span)); + } } - 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, name_span, value) => Some(Definition::item( - name.clone(), - Span::detached(), - name_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, span) in scope.iter() { - if *item_name == name { - return Some(Definition::item( - name, - span, - Span::detached(), - Some(value.clone()), - )); + if let Some(value) = globals(world, &leaf).get(&name) { + return Some(Definition::Std(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, + // Try to jump to the an imported file or package. + DerefTarget::ImportPath(node) | DerefTarget::IncludePath(node) => { + let Some(Value::Module(module)) = analyze_import(world, &node) else { + return None; + }; + let id = module.file_id()?; + let span = Span::from_range(id, 0..0); + return Some(Definition::Span(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, + // Try to jump to the referenced content. + DerefTarget::Ref(node) => { + let label = Label::new(node.cast::()?.target()); + let selector = Selector::Label(label); + let elem = document?.introspector.query_first(&selector)?; + return Some(Definition::Span(elem.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) + None } #[cfg(test)] mod tests { use std::ops::Range; - use typst::foundations::{IntoValue, Label, NativeElement, Value}; + use typst::foundations::{IntoValue, NativeElement}; use typst::syntax::Side; use typst::WorldExt; - use super::{definition, DefinitionKind as Kind}; - use crate::tests::TestWorld; + use super::{definition, Definition}; + use crate::tests::{SourceExt, TestWorld}; + + type Response = (TestWorld, Option); + + trait ResponseExt { + fn must_be_at(&self, path: &str, range: Range) -> &Self; + fn must_be_value(&self, value: impl IntoValue) -> &Self; + } + + impl ResponseExt for Response { + #[track_caller] + fn must_be_at(&self, path: &str, expected: Range) -> &Self { + match self.1 { + Some(Definition::Span(span)) => { + let range = self.0.range(span); + assert_eq!( + span.id().unwrap().vpath().as_rootless_path().to_string_lossy(), + path + ); + assert_eq!(range, Some(expected)); + } + _ => panic!("expected span definition"), + } + self + } + + #[track_caller] + fn must_be_value(&self, expected: impl IntoValue) -> &Self { + match &self.1 { + Some(Definition::Std(value)) => { + assert_eq!(*value, expected.into_value()) + } + _ => panic!("expected std definition"), + } + self + } + } #[track_caller] - fn test( - text: &str, - cursor: usize, - name: &str, - kind: Kind, - value: Option, - range: Option>, - ) where - T: IntoValue, - { + fn test(text: &str, cursor: isize, side: Side) -> Response { let world = TestWorld::new(text); + test_with_world(world, cursor, side) + } + + #[track_caller] + fn test_with_world(world: TestWorld, cursor: isize, side: Side) -> Response { 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))) - ); + let source = &world.main; + let def = definition(&world, doc.as_ref(), source, source.cursor(cursor), side); + (world, def) } #[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), - ); + fn test_definition_let() { + test("#let x; #x", 9, Side::After).must_be_at("main.typ", 5..6); + test("#let x() = {}; #x", 16, Side::After).must_be_at("main.typ", 5..6); + } + + #[test] + fn test_definition_field_access_function() { + let world = TestWorld::new("#import \"other.typ\"; #other.foo") + .with_source("other.typ", "#let foo(x) = x + 1"); + + // The span is at the args here because that's what the function value's + // span is. Not ideal, but also not too big of a big deal. + test_with_world(world, -1, Side::Before).must_be_at("other.typ", 8..11); + } + + #[test] + fn test_definition_cross_file() { + let world = TestWorld::new("#import \"other.typ\": x; #x") + .with_source("other.typ", "#let x = 1"); + test_with_world(world, -1, Side::After).must_be_at("other.typ", 5..6); + } + + #[test] + fn test_definition_import() { + let world = TestWorld::new("#import \"other.typ\" as o: x") + .with_source("other.typ", "#let x = 1"); + test_with_world(world, 14, Side::Before).must_be_at("other.typ", 0..0); + } + + #[test] + fn test_definition_include() { + let world = TestWorld::new("#include \"other.typ\"") + .with_source("other.typ", "Hello there"); + test_with_world(world, 14, Side::Before).must_be_at("other.typ", 0..0); + } + + #[test] + fn test_definition_ref() { + test("#figure[] See @hi", 21, Side::After).must_be_at("main.typ", 1..9); + } + + #[test] + fn test_definition_std() { + test("#table", 1, Side::After).must_be_value(typst::model::TableElem::elem()); } } diff --git a/crates/typst-ide/src/lib.rs b/crates/typst-ide/src/lib.rs index 038589c0d..c0edcce9f 100644 --- a/crates/typst-ide/src/lib.rs +++ b/crates/typst-ide/src/lib.rs @@ -10,7 +10,7 @@ mod utils; 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::definition::{definition, Definition}; 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}; diff --git a/crates/typst-ide/src/matchers.rs b/crates/typst-ide/src/matchers.rs index dd7dfd1ff..4aeba29be 100644 --- a/crates/typst-ide/src/matchers.rs +++ b/crates/typst-ide/src/matchers.rs @@ -162,6 +162,14 @@ impl<'a> NamedItem<'a> { NamedItem::Import(_, _, value) => value.cloned(), } } + + pub(crate) fn span(&self) -> Span { + match *self { + NamedItem::Var(name) | NamedItem::Fn(name) => name.span(), + NamedItem::Module(_, site) => site.span(), + NamedItem::Import(_, span, _) => span, + } + } } /// Categorize an expression into common classes IDE functionality can operate @@ -177,29 +185,29 @@ pub fn deref_target(node: LinkedNode) -> Option> { 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::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(..) => { + ast::Expr::Ident(_) | ast::Expr::MathIdent(_) | ast::Expr::FieldAccess(_) => { DerefTarget::VarAccess(expr_node) } - ast::Expr::Str(..) => { + 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) + DerefTarget::Code(expr_node) } } _ if expr.hash() || matches!(expr_node.kind(), SyntaxKind::MathIdent | SyntaxKind::Error) => { - DerefTarget::Code(expr_node.kind(), expr_node) + DerefTarget::Code(expr_node) } _ => return None, }) @@ -208,10 +216,6 @@ pub fn deref_target(node: LinkedNode) -> Option> { /// 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. @@ -223,7 +227,11 @@ pub enum DerefTarget<'a> { /// An include path expression. IncludePath(LinkedNode<'a>), /// Any code expression. - Code(SyntaxKind, LinkedNode<'a>), + Code(LinkedNode<'a>), + /// A label expression. + Label(LinkedNode<'a>), + /// A reference expression. + Ref(LinkedNode<'a>), } #[cfg(test)] diff --git a/crates/typst-ide/src/utils.rs b/crates/typst-ide/src/utils.rs index 903fb2f3d..ad8ed6b50 100644 --- a/crates/typst-ide/src/utils.rs +++ b/crates/typst-ide/src/utils.rs @@ -3,7 +3,9 @@ use std::fmt::Write; use comemo::Track; use ecow::{eco_format, EcoString}; use typst::engine::{Engine, Route, Sink, Traced}; +use typst::foundations::Scope; use typst::introspection::Introspector; +use typst::syntax::{LinkedNode, SyntaxKind}; use typst::text::{FontInfo, FontStyle}; use crate::IdeWorld; @@ -105,3 +107,21 @@ pub fn summarize_font_family<'a>( detail } + +/// The global definitions at the given node. +pub fn globals<'a>(world: &'a dyn IdeWorld, leaf: &LinkedNode) -> &'a Scope { + let in_math = matches!( + leaf.parent_kind(), + Some(SyntaxKind::Equation) + | Some(SyntaxKind::Math) + | Some(SyntaxKind::MathFrac) + | Some(SyntaxKind::MathAttach) + ); + + let library = world.library(); + if in_math { + library.math.scope() + } else { + library.global.scope() + } +}