tinymist_query/
references.rs

1use 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/// The [`textDocument/references`] request is sent from the client to the
15/// server to resolve project-wide references for the symbol denoted by the
16/// given text document position.
17///
18/// [`textDocument/references`]: https://microsoft.github.io/language-server-protocol/specification#textDocument_references
19#[derive(Debug, Clone)]
20pub struct ReferencesRequest {
21    /// The path of the document to request for.
22    pub path: PathBuf,
23    /// The source code position to request for.
24    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        // todo: reference of builtin items?
78        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            // todo: this is not necessary a name span
152            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    // todo: references of package
161    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            // sort
214            if let Some(result) = result.as_mut() {
215                result.sort();
216            }
217
218            assert_snapshot!(JsonRepr::new_pure(result));
219        });
220    }
221}