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