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#[derive(Debug, Clone)]
13pub enum Definition {
14 Span(Span),
16 File(FileId),
18 Std(Value),
20}
21
22pub 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 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 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 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 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}