use typst::foundations::{Label, Selector, Value}; use typst::model::Document; use typst::syntax::{ast, LinkedNode, Side, Source, Span}; use crate::utils::globals; use crate::{ 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 /// the definition search. Label definitions, for instance, are only generated /// when the document is available. pub fn definition( world: &dyn IdeWorld, document: Option<&Document>, source: &Source, cursor: usize, side: Side, ) -> Option { let root = LinkedNode::new(source.root()); let leaf = root.leaf_at(cursor, side)?; 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); }; 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)); } } if let Some(value) = globals(world, &leaf).get(&name) { return Some(Definition::Std(value.clone())); } } // 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)); } // 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())); } _ => {} } None } #[cfg(test)] mod tests { use std::ops::Range; use typst::foundations::{IntoValue, NativeElement}; use typst::syntax::Side; use typst::WorldExt; 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: 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 source = &world.main; let def = definition(&world, doc.as_ref(), source, source.cursor(cursor), side); (world, def) } #[test] 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()); } }