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#[derive(Debug, Clone)]
14pub enum Definition {
15 Span(Span),
17 Std(Value),
19}
20
21pub 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 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 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 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 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}