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 let src = self.ctx.ctx.source_by_id(ref_fid).ok()?;
108 let index = get_index_info(&src);
109 match self.def.decl.kind() {
110 DefKind::Constant | DefKind::Function | DefKind::Struct | DefKind::Variable => {
111 if !index.identifiers.contains(self.def.decl.name()) {
112 return Some(());
113 }
114 }
115 DefKind::Module => {
116 let ref_by_ident = index.identifiers.contains(self.def.decl.name());
117 let ref_by_path = index.paths.contains(self.module_path());
118 if !(ref_by_ident || ref_by_path) {
119 return Some(());
120 }
121 }
122 DefKind::Reference => {}
123 }
124
125 let ei = self.ctx.ctx.expr_stage(&src);
126 let uri = self.ctx.ctx.uri_for_id(ref_fid).ok()?;
127
128 let t = ei.get_refs(self.def.decl.clone());
129 self.push_idents(&ei.source, &uri, t);
130
131 if ei.is_exported(&self.def.decl) {
132 self.ctx.push_dependents(ref_fid);
133 }
134
135 Some(())
136 }
137
138 fn push_idents<'b>(
139 &mut self,
140 src: &Source,
141 url: &Url,
142 idents: impl Iterator<Item = (&'b Span, &'b Interned<RefExpr>)>,
143 ) {
144 self.push_ranges(src, url, idents.map(|(span, _)| span));
145 }
146
147 fn push_ranges<'b>(&mut self, src: &Source, url: &Url, spans: impl Iterator<Item = &'b Span>) {
148 self.references.extend(spans.filter_map(|span| {
149 let range = self.ctx.ctx.to_lsp_range(src.range(*span)?, src);
151 Some(LspLocation {
152 uri: url.clone(),
153 range,
154 })
155 }));
156 }
157
158 fn module_path(&self) -> &StrRef {
160 self.module_path.get_or_init(|| {
161 self.def
162 .decl
163 .file_id()
164 .and_then(|fid| {
165 fid.vpath()
166 .as_rooted_path()
167 .file_name()?
168 .to_str()
169 .map(From::from)
170 })
171 .unwrap_or_default()
172 })
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use crate::tests::*;
180
181 #[test]
182 fn test() {
183 snapshot_testing("references", &|ctx, path| {
184 let source = ctx.source_by_path(&path).unwrap();
185
186 let request = ReferencesRequest {
187 path: path.clone(),
188 position: find_test_position(&source),
189 };
190
191 let result = request.request(ctx);
192 let mut result = result.map(|v| {
193 v.into_iter()
194 .map(|loc| {
195 let fp = file_uri(loc.uri.as_str());
196 format!(
197 "{fp}@{}:{}:{}:{}",
198 loc.range.start.line,
199 loc.range.start.character,
200 loc.range.end.line,
201 loc.range.end.character
202 )
203 })
204 .collect::<Vec<_>>()
205 });
206 if let Some(result) = result.as_mut() {
208 result.sort();
209 }
210
211 assert_snapshot!(JsonRepr::new_pure(result));
212 });
213 }
214}