typst_ide/
definition.rs

1use typst::foundations::{Label, Selector, Value};
2use typst::layout::PagedDocument;
3use typst::syntax::{LinkedNode, Side, Source, Span, ast};
4use typst::utils::PicoStr;
5
6use crate::utils::globals;
7use crate::{
8    DerefTarget, IdeWorld, NamedItem, analyze_expr, analyze_import, deref_target,
9    named_items,
10};
11
12/// A definition of some item.
13#[derive(Debug, Clone)]
14pub enum Definition {
15    /// The item is defined at the given span.
16    Span(Span),
17    /// The item is defined in the standard library.
18    Std(Value),
19}
20
21/// Find the definition of the item under the cursor.
22///
23/// Passing a `document` (from a previous compilation) is optional, but enhances
24/// the definition search. Label definitions, for instance, are only generated
25/// when the document is available.
26pub fn definition(
27    world: &dyn IdeWorld,
28    document: Option<&PagedDocument>,
29    source: &Source,
30    cursor: usize,
31    side: Side,
32) -> Option<Definition> {
33    let root = LinkedNode::new(source.root());
34    let leaf = root.leaf_at(cursor, side)?;
35
36    match deref_target(leaf.clone())? {
37        // Try to find a named item (defined in this file or an imported file)
38        // or fall back to a standard library item.
39        DerefTarget::VarAccess(node) | DerefTarget::Callee(node) => {
40            let name = node.cast::<ast::Ident>()?.get().clone();
41            if let Some(src) = named_items(world, node.clone(), |item: NamedItem| {
42                (*item.name() == name).then(|| Definition::Span(item.span()))
43            }) {
44                return Some(src);
45            };
46
47            if let Some((value, _)) = analyze_expr(world, &node).first() {
48                let span = match value {
49                    Value::Content(content) => content.span(),
50                    Value::Func(func) => func.span(),
51                    _ => Span::detached(),
52                };
53                if !span.is_detached() && span != node.span() {
54                    return Some(Definition::Span(span));
55                }
56            }
57
58            if let Some(binding) = globals(world, &leaf).get(&name) {
59                return Some(Definition::Std(binding.read().clone()));
60            }
61        }
62
63        // Try to jump to the an imported file or package.
64        DerefTarget::ImportPath(node) | DerefTarget::IncludePath(node) => {
65            let Some(Value::Module(module)) = analyze_import(world, &node) else {
66                return None;
67            };
68            let id = module.file_id()?;
69            let span = Span::from_range(id, 0..0);
70            return Some(Definition::Span(span));
71        }
72
73        // Try to jump to the referenced content.
74        DerefTarget::Ref(node) => {
75            let label = Label::new(PicoStr::intern(node.cast::<ast::Ref>()?.target()))
76                .expect("unexpected empty reference");
77            let selector = Selector::Label(label);
78            let elem = document?.introspector.query_first(&selector)?;
79            return Some(Definition::Span(elem.span()));
80        }
81
82        _ => {}
83    }
84
85    None
86}
87
88#[cfg(test)]
89mod tests {
90    use std::borrow::Borrow;
91    use std::ops::Range;
92
93    use typst::WorldExt;
94    use typst::foundations::{IntoValue, NativeElement};
95    use typst::syntax::Side;
96
97    use super::{Definition, definition};
98    use crate::tests::{FilePos, TestWorld, WorldLike};
99
100    type Response = (TestWorld, Option<Definition>);
101
102    trait ResponseExt {
103        fn must_be_at(&self, path: &str, range: Range<usize>) -> &Self;
104        fn must_be_value(&self, value: impl IntoValue) -> &Self;
105    }
106
107    impl ResponseExt for Response {
108        #[track_caller]
109        fn must_be_at(&self, path: &str, expected: Range<usize>) -> &Self {
110            match self.1 {
111                Some(Definition::Span(span)) => {
112                    let range = self.0.range(span);
113                    assert_eq!(
114                        span.id().unwrap().vpath().as_rootless_path().to_string_lossy(),
115                        path
116                    );
117                    assert_eq!(range, Some(expected));
118                }
119                _ => panic!("expected span definition"),
120            }
121            self
122        }
123
124        #[track_caller]
125        fn must_be_value(&self, expected: impl IntoValue) -> &Self {
126            match &self.1 {
127                Some(Definition::Std(value)) => {
128                    assert_eq!(*value, expected.into_value())
129                }
130                _ => panic!("expected std definition"),
131            }
132            self
133        }
134    }
135
136    #[track_caller]
137    fn test(world: impl WorldLike, pos: impl FilePos, side: Side) -> Response {
138        let world = world.acquire();
139        let world = world.borrow();
140        let doc = typst::compile(world).output.ok();
141        let (source, cursor) = pos.resolve(world);
142        let def = definition(world, doc.as_ref(), &source, cursor, side);
143        (world.clone(), def)
144    }
145
146    #[test]
147    fn test_definition_let() {
148        test("#let x; #x", -2, Side::After).must_be_at("main.typ", 5..6);
149        test("#let x() = {}; #x", -2, Side::After).must_be_at("main.typ", 5..6);
150    }
151
152    #[test]
153    fn test_definition_field_access_function() {
154        let world = TestWorld::new("#import \"other.typ\"; #other.foo")
155            .with_source("other.typ", "#let foo(x) = x + 1");
156
157        // The span is at the args here because that's what the function value's
158        // span is. Not ideal, but also not too big of a big deal.
159        test(&world, -2, Side::Before).must_be_at("other.typ", 8..11);
160    }
161
162    #[test]
163    fn test_definition_cross_file() {
164        let world = TestWorld::new("#import \"other.typ\": x; #x")
165            .with_source("other.typ", "#let x = 1");
166        test(&world, -2, Side::After).must_be_at("other.typ", 5..6);
167    }
168
169    #[test]
170    fn test_definition_import() {
171        let world = TestWorld::new("#import \"other.typ\" as o: x")
172            .with_source("other.typ", "#let x = 1");
173        test(&world, 14, Side::Before).must_be_at("other.typ", 0..0);
174    }
175
176    #[test]
177    fn test_definition_include() {
178        let world = TestWorld::new("#include \"other.typ\"")
179            .with_source("other.typ", "Hello there");
180        test(&world, 14, Side::Before).must_be_at("other.typ", 0..0);
181    }
182
183    #[test]
184    fn test_definition_ref() {
185        test("#figure[] <hi> See @hi", -2, Side::After).must_be_at("main.typ", 1..9);
186    }
187
188    #[test]
189    fn test_definition_std() {
190        test("#table", 1, Side::After).must_be_value(typst::model::TableElem::ELEM);
191    }
192}