tinymist_query/
symbol.rs

1use crate::{
2    SemanticRequest,
3    prelude::*,
4    syntax::{LexicalHierarchy, LexicalScopeKind, get_lexical_hierarchy},
5};
6
7/// The [`workspace/symbol`] request is sent from the client to the server to
8/// list project-wide symbols matching the given query string.
9///
10/// [`workspace/symbol`]: https://microsoft.github.io/language-server-protocol/specification#workspace_symbol
11///
12/// # Compatibility
13///
14/// Since 3.17.0, servers can also provider a handler for
15/// [`workspaceSymbol/resolve`] requests. This allows servers to return
16/// workspace symbols without a range for a `workspace/symbol` request. Clients
17/// then need to resolve the range when necessary using the `workspaceSymbol/
18/// resolve` request.
19///
20/// // [`workspaceSymbol/resolve`]: Self::symbol_resolve
21///
22/// Servers can only use this new model if clients advertise support for it via
23/// the `workspace.symbol.resolve_support` capability.
24#[derive(Debug, Clone)]
25pub struct SymbolRequest {
26    /// The query string to filter symbols by. It is usually the exact content
27    /// of the user's input box in the UI.
28    pub pattern: Option<String>,
29}
30
31impl SemanticRequest for SymbolRequest {
32    type Response = Vec<SymbolInformation>;
33
34    fn request(self, ctx: &mut LocalContext) -> Option<Self::Response> {
35        let mut symbols = vec![];
36
37        for id in ctx.depended_files() {
38            let Ok(source) = ctx.source_by_id(id) else {
39                continue;
40            };
41            let uri = ctx.uri_for_id(id).unwrap();
42            let res = get_lexical_hierarchy(&source, LexicalScopeKind::Symbol).map(|symbols| {
43                filter_document_symbols(
44                    &symbols,
45                    self.pattern.as_deref(),
46                    &source,
47                    &uri,
48                    ctx.position_encoding(),
49                )
50            });
51
52            if let Some(mut res) = res {
53                symbols.append(&mut res)
54            }
55        }
56
57        Some(symbols)
58    }
59}
60
61#[allow(deprecated)]
62fn filter_document_symbols(
63    hierarchy: &[LexicalHierarchy],
64    query_string: Option<&str>,
65    source: &Source,
66    uri: &Url,
67    position_encoding: PositionEncoding,
68) -> Vec<SymbolInformation> {
69    hierarchy
70        .iter()
71        .flat_map(|hierarchy| {
72            [hierarchy]
73                .into_iter()
74                .chain(hierarchy.children.as_deref().into_iter().flatten())
75        })
76        .filter(|hierarchy| hierarchy.info.kind.is_valid_lsp_symbol())
77        .flat_map(|hierarchy| {
78            if query_string.is_some_and(|s| !hierarchy.info.name.contains(s)) {
79                return None;
80            }
81
82            let rng = to_lsp_range(hierarchy.info.range.clone(), source, position_encoding);
83
84            Some(SymbolInformation {
85                name: hierarchy.info.name.to_string(),
86                kind: hierarchy.info.kind.clone().into(),
87                tags: None,
88                deprecated: None,
89                location: LspLocation {
90                    uri: uri.clone(),
91                    range: rng,
92                },
93                container_name: None,
94            })
95        })
96        .collect()
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use crate::syntax::find_module_level_docs;
103    use crate::tests::*;
104
105    #[test]
106    fn test() {
107        snapshot_testing("symbols", &|ctx, path| {
108            let source = ctx.source_by_path(&path).unwrap();
109
110            let docs = find_module_level_docs(&source).unwrap_or_default();
111            let mut properties = get_test_properties(&docs);
112            // need to compile the doc to get the dependencies
113            properties.insert("compile", "true");
114            let _doc = compile_doc_for_test(ctx, &properties);
115
116            let request = SymbolRequest {
117                pattern: properties.get("pattern").copied().map(str::to_owned),
118            };
119
120            let mut result = request.request(ctx);
121            if let Some(result) = &mut result {
122                // Sort the symbols by name for consistent output
123                result.sort_by(|x, y| {
124                    x.name
125                        .cmp(&y.name)
126                        .then_with(|| x.location.uri.cmp(&y.location.uri))
127                });
128            }
129            assert_snapshot!(JsonRepr::new_redacted(result, &REDACT_LOC));
130        });
131    }
132}