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::Normal(..) => {
58            return None;
59        }
60    };
61
62    let def = ctx.def_of_syntax(source, syntax)?;
63
64    let worker = ReferencesWorker {
65        ctx: ctx.fork_for_search(),
66        references: vec![],
67        def,
68        module_path: OnceLock::new(),
69    };
70
71    if finding_label {
72        worker.label_root()
73    } else {
74        // todo: reference of builtin items?
75        worker.ident_root()
76    }
77}
78
79struct ReferencesWorker<'a> {
80    ctx: SearchCtx<'a>,
81    references: Vec<LspLocation>,
82    def: Definition,
83    module_path: OnceLock<StrRef>,
84}
85
86impl ReferencesWorker<'_> {
87    fn label_root(mut self) -> Option<Vec<LspLocation>> {
88        for ref_fid in self.ctx.ctx.depended_files() {
89            self.file(ref_fid)?;
90        }
91
92        Some(self.references)
93    }
94
95    fn ident_root(mut self) -> Option<Vec<LspLocation>> {
96        self.file(self.def.decl.file_id()?);
97        while let Some(ref_fid) = self.ctx.worklist.pop() {
98            self.file(ref_fid);
99        }
100
101        Some(self.references)
102    }
103
104    fn file(&mut self, ref_fid: TypstFileId) -> Option<()> {
105        log::debug!("references: file: {ref_fid:?}");
106        let src = self.ctx.ctx.source_by_id(ref_fid).ok()?;
107        let index = get_index_info(&src);
108        match self.def.decl.kind() {
109            DefKind::Constant | DefKind::Function | DefKind::Struct | DefKind::Variable => {
110                if !index.identifiers.contains(self.def.decl.name()) {
111                    return Some(());
112                }
113            }
114            DefKind::Module => {
115                let ref_by_ident = index.identifiers.contains(self.def.decl.name());
116                let ref_by_path = index.paths.contains(self.module_path());
117                if !(ref_by_ident || ref_by_path) {
118                    return Some(());
119                }
120            }
121            DefKind::Reference => {}
122        }
123
124        let ei = self.ctx.ctx.expr_stage(&src);
125        let uri = self.ctx.ctx.uri_for_id(ref_fid).ok()?;
126
127        let t = ei.get_refs(self.def.decl.clone());
128        self.push_idents(&ei.source, &uri, t);
129
130        if ei.is_exported(&self.def.decl) {
131            self.ctx.push_dependents(ref_fid);
132        }
133
134        Some(())
135    }
136
137    fn push_idents<'b>(
138        &mut self,
139        src: &Source,
140        url: &Url,
141        idents: impl Iterator<Item = (&'b Span, &'b Interned<RefExpr>)>,
142    ) {
143        self.push_ranges(src, url, idents.map(|(span, _)| span));
144    }
145
146    fn push_ranges<'b>(&mut self, src: &Source, url: &Url, spans: impl Iterator<Item = &'b Span>) {
147        self.references.extend(spans.filter_map(|span| {
148            // todo: this is not necessary a name span
149            let range = self.ctx.ctx.to_lsp_range(src.range(*span)?, src);
150            Some(LspLocation {
151                uri: url.clone(),
152                range,
153            })
154        }));
155    }
156
157    // todo: references of package
158    fn module_path(&self) -> &StrRef {
159        self.module_path.get_or_init(|| {
160            self.def
161                .decl
162                .file_id()
163                .and_then(|fid| {
164                    fid.vpath()
165                        .as_rooted_path()
166                        .file_name()?
167                        .to_str()
168                        .map(From::from)
169                })
170                .unwrap_or_default()
171        })
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use crate::tests::*;
179
180    #[test]
181    fn test() {
182        snapshot_testing("references", &|ctx, path| {
183            let source = ctx.source_by_path(&path).unwrap();
184
185            let request = ReferencesRequest {
186                path: path.clone(),
187                position: find_test_position(&source),
188            };
189
190            let result = request.request(ctx);
191            let mut result = result.map(|v| {
192                v.into_iter()
193                    .map(|loc| {
194                        let fp = file_uri(loc.uri.as_str());
195                        format!(
196                            "{fp}@{}:{}:{}:{}",
197                            loc.range.start.line,
198                            loc.range.start.character,
199                            loc.range.end.line,
200                            loc.range.end.character
201                        )
202                    })
203                    .collect::<Vec<_>>()
204            });
205            // sort
206            if let Some(result) = result.as_mut() {
207                result.sort();
208            }
209
210            assert_snapshot!(JsonRepr::new_pure(result));
211        });
212    }
213}