tinymist_query/
references.rs1use std::sync::OnceLock;
2
3use tinymist_analysis::adt::interner::Interned;
4use typst::syntax::Span;
5
6use crate::{
7 StrRef,
8 analysis::{Definition, SearchCtx},
9 prelude::*,
10 syntax::{RefExpr, SyntaxClass, get_index_info},
11};
12
13#[derive(Debug, Clone)]
19pub struct ReferencesRequest {
20 pub path: PathBuf,
22 pub position: LspPosition,
24}
25
26impl SemanticRequest for ReferencesRequest {
27 type Response = Vec<LspLocation>;
28
29 fn request(self, ctx: &mut LocalContext) -> Option<Self::Response> {
30 let source = ctx.source_by_path(&self.path).ok()?;
31 let syntax = ctx.classify_for_decl(&source, self.position)?;
32
33 let locations = find_references(ctx, &source, syntax)?;
34
35 crate::log_debug_ct!("references: {locations:?}");
36 Some(locations)
37 }
38}
39
40pub(crate) fn find_references(
41 ctx: &mut LocalContext,
42 source: &Source,
43 syntax: SyntaxClass<'_>,
44) -> Option<Vec<LspLocation>> {
45 let finding_label = match syntax {
46 SyntaxClass::VarAccess(..) | SyntaxClass::Callee(..) => false,
47 SyntaxClass::Label { .. }
48 | SyntaxClass::Ref {
49 suffix_colon: false,
50 ..
51 } => true,
52 SyntaxClass::ImportPath(..)
53 | SyntaxClass::IncludePath(..)
54 | SyntaxClass::Ref {
55 suffix_colon: true, ..
56 }
57 | SyntaxClass::At { node: _ }
58 | SyntaxClass::Normal(..) => {
59 return None;
60 }
61 };
62
63 let def = ctx.def_of_syntax(source, syntax)?;
64
65 let worker = ReferencesWorker {
66 ctx: ctx.fork_for_search(),
67 references: vec![],
68 def,
69 module_path: OnceLock::new(),
70 };
71
72 if finding_label {
73 worker.label_root()
74 } else {
75 worker.ident_root()
77 }
78}
79
80struct ReferencesWorker<'a> {
81 ctx: SearchCtx<'a>,
82 references: Vec<LspLocation>,
83 def: Definition,
84 module_path: OnceLock<StrRef>,
85}
86
87impl ReferencesWorker<'_> {
88 fn label_root(mut self) -> Option<Vec<LspLocation>> {
89 for ref_fid in self.ctx.ctx.depended_files() {
90 self.file(ref_fid)?;
91 }
92
93 Some(self.references)
94 }
95
96 fn ident_root(mut self) -> Option<Vec<LspLocation>> {
97 self.file(self.def.decl.file_id()?);
98 while let Some(ref_fid) = self.ctx.worklist.pop() {
99 self.file(ref_fid);
100 }
101
102 Some(self.references)
103 }
104
105 fn file(&mut self, ref_fid: TypstFileId) -> Option<()> {
106 log::debug!("references: file: {ref_fid:?}");
107
108 if ref_fid
110 .vpath()
111 .as_rooted_path()
112 .extension()
113 .is_none_or(|e| e != "typ")
114 {
115 return Some(());
116 }
117
118 let src = self.ctx.ctx.source_by_id(ref_fid).ok()?;
119 let index = get_index_info(&src);
120 match self.def.decl.kind() {
121 DefKind::Constant | DefKind::Function | DefKind::Struct | DefKind::Variable => {
122 if !index.identifiers.contains(self.def.decl.name()) {
123 return Some(());
124 }
125 }
126 DefKind::Module => {
127 let ref_by_ident = index.identifiers.contains(self.def.decl.name());
128 let ref_by_path = index.paths.contains(self.module_path());
129 if !(ref_by_ident || ref_by_path) {
130 return Some(());
131 }
132 }
133 DefKind::Reference => {}
134 }
135
136 let ei = self.ctx.ctx.expr_stage(&src);
137 let uri = self.ctx.ctx.uri_for_id(ref_fid).ok()?;
138
139 let t = ei.get_refs(self.def.decl.clone());
140 self.push_idents(&ei.source, &uri, t);
141
142 if ei.is_exported(&self.def.decl) {
143 self.ctx.push_dependents(ref_fid);
144 }
145
146 Some(())
147 }
148
149 fn push_idents<'b>(
150 &mut self,
151 src: &Source,
152 url: &Url,
153 idents: impl Iterator<Item = (&'b Span, &'b Interned<RefExpr>)>,
154 ) {
155 self.push_ranges(
156 src,
157 url,
158 idents.map(|(span, expr)| {
159 let adjust = match expr.decl.as_ref() {
160 Decl::Label(..) => Some((1, -1)),
161 Decl::ContentRef(..) => Some((1, 0)),
162 _ => None,
163 };
164
165 (*span, adjust)
166 }),
167 );
168 }
169
170 fn push_ranges(
171 &mut self,
172 src: &Source,
173 url: &Url,
174 spans: impl Iterator<Item = (Span, Option<(isize, isize)>)>,
175 ) {
176 self.references.extend(spans.filter_map(|(span, adjust)| {
177 let mut range = src.range(span)?;
179 if let Some((start, end)) = adjust {
180 range.start = (range.start as isize + start) as usize;
181 range.end = (range.end as isize + end) as usize;
182 }
183 let range = self.ctx.ctx.to_lsp_range(range, src);
184 Some(LspLocation {
185 uri: url.clone(),
186 range,
187 })
188 }));
189 }
190
191 fn module_path(&self) -> &StrRef {
193 self.module_path.get_or_init(|| {
194 self.def
195 .decl
196 .file_id()
197 .and_then(|fid| {
198 fid.vpath()
199 .as_rooted_path()
200 .file_name()?
201 .to_str()
202 .map(From::from)
203 })
204 .unwrap_or_default()
205 })
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use crate::tests::*;
213
214 #[test]
215 fn test() {
216 snapshot_testing("references", &|ctx, path| {
217 let source = ctx.source_by_path(&path).unwrap();
218
219 let request = ReferencesRequest {
220 path: path.clone(),
221 position: find_test_position(&source),
222 };
223
224 let result = request.request(ctx);
225 let mut result = result.map(|v| {
226 v.into_iter()
227 .map(|loc| {
228 let fp = file_uri(loc.uri.as_str());
229 format!(
230 "{fp}@{}:{}:{}:{}",
231 loc.range.start.line,
232 loc.range.start.character,
233 loc.range.end.line,
234 loc.range.end.character
235 )
236 })
237 .collect::<Vec<_>>()
238 });
239 if let Some(result) = result.as_mut() {
241 result.sort();
242 }
243
244 assert_snapshot!(JsonRepr::new_pure(result));
245 });
246 }
247}