Skip to main content

typst_ide/
definition.rs

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