tinymist_query/
references.rs

1use 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/// The [`textDocument/references`] request is sent from the client to the
14/// server to resolve project-wide references for the symbol denoted by the
15/// given text document position.
16///
17/// [`textDocument/references`]: https://microsoft.github.io/language-server-protocol/specification#textDocument_references
18#[derive(Debug, Clone)]
19pub struct ReferencesRequest {
20    /// The path of the document to request for.
21    pub path: PathBuf,
22    /// The source code position to request for.
23    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        // todo: reference of builtin items?
76        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        // todo: find references in data files
109        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            // todo: this is not necessary a name span
178            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    // todo: references of package
192    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            // sort
240            if let Some(result) = result.as_mut() {
241                result.sort();
242            }
243
244            assert_snapshot!(JsonRepr::new_pure(result));
245        });
246    }
247}