tinymist_query/index/
scip_query.rs

1//! Offline query SCIP indexes.
2
3use serde::Serialize;
4use tinymist_std::error::{WithContextUntyped, prelude::*};
5use tinymist_std::hash::{FxHashMap, FxHashSet};
6
7use crate::{
8    CompilerQueryRequest, CompilerQueryResponse, GotoDefinitionRequest, HoverRequest, path_to_url,
9    url_to_path,
10};
11
12use lsp_types::{
13    GotoDefinitionResponse, Hover, HoverContents, LocationLink, MarkedString, Position, Range, Url,
14};
15use protobuf::{Enum, Message};
16
17use super::scip_utils::{ScipParamGroup, merge_symbol_information};
18
19/// The context for querying a SCIP index.
20#[derive(Default)]
21pub struct ScipQueryCtx {
22    documents_by_uri: FxHashMap<Url, ScipDocumentQuery>,
23    documents_by_path: FxHashMap<String, Url>,
24    definitions: FxHashMap<String, Vec<ScipDefinition>>,
25    public_symbols_by_path: FxHashMap<String, Vec<ScipPublicSymbol>>,
26    symbol_hovers: FxHashMap<String, Hover>,
27}
28
29/// A public package symbol exported by a module.
30#[derive(Debug, Clone, Serialize)]
31pub struct ScipPublicSymbol {
32    /// SCIP symbol.
33    pub symbol: String,
34    /// Display name.
35    pub name: String,
36    /// Package docs definition kind.
37    pub kind: String,
38}
39
40/// A token on a source page that has index-backed interactions.
41#[derive(Debug, Clone, Serialize)]
42#[serde(rename_all = "camelCase")]
43pub struct ScipSourceToken {
44    /// Token range in LSP UTF-16 positions.
45    pub range: Range,
46    /// Hover information for the token.
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub hover: Option<Hover>,
49    /// First definition target for the token.
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub definition: Option<LocationLink>,
52}
53
54#[derive(Debug, Clone)]
55struct ScipDocumentQuery {
56    uri: Url,
57    occurrences: Vec<ScipOccurrence>,
58}
59
60#[derive(Debug, Clone)]
61struct ScipOccurrence {
62    range: Range,
63    symbol: String,
64    is_definition: bool,
65    hover: Option<Hover>,
66}
67
68#[derive(Debug, Clone)]
69struct ScipDefinition {
70    uri: Url,
71    range: Range,
72}
73
74impl ScipQueryCtx {
75    /// Reads the index from SCIP protobuf bytes.
76    pub fn read(db: &[u8]) -> Result<Self> {
77        let index =
78            scip::types::Index::parse_from_bytes(db).context_ut("failed to parse SCIP index")?;
79        Ok(Self::build_query_tables(index))
80    }
81
82    fn build_query_tables(index: scip::types::Index) -> Self {
83        let project_root = index
84            .metadata
85            .as_ref()
86            .and_then(|metadata| Url::parse(&metadata.project_root).ok());
87        let symbol_infos = collect_symbol_infos(&index);
88        let mut this = Self {
89            symbol_hovers: collect_symbol_hovers(&symbol_infos),
90            ..Default::default()
91        };
92
93        for document in index.documents {
94            let relative_path = document.relative_path.clone();
95            let Some(uri) = scip_document_uri(project_root.as_ref(), &document.relative_path)
96            else {
97                continue;
98            };
99            let public_symbols = scip_public_symbols(&document, &symbol_infos);
100            let mut occurrences = Vec::with_capacity(document.occurrences.len());
101            for occurrence in document.occurrences {
102                let Some(range) = scip_range_to_lsp(&occurrence.range) else {
103                    continue;
104                };
105                let is_definition =
106                    occurrence.symbol_roles & scip::types::SymbolRole::Definition.value() != 0;
107                let hover = hover_from_scip_docs(&occurrence.override_documentation);
108                let symbol = occurrence.symbol;
109
110                if !symbol.is_empty() && is_definition {
111                    this.definitions
112                        .entry(symbol.clone())
113                        .or_default()
114                        .push(ScipDefinition {
115                            uri: uri.clone(),
116                            range,
117                        });
118                }
119
120                occurrences.push(ScipOccurrence {
121                    range,
122                    symbol,
123                    is_definition,
124                    hover,
125                });
126            }
127
128            if !public_symbols.is_empty() {
129                this.public_symbols_by_path
130                    .insert(relative_path.clone(), public_symbols);
131            }
132            this.documents_by_path.insert(relative_path, uri.clone());
133            this.documents_by_uri
134                .insert(uri.clone(), ScipDocumentQuery { uri, occurrences });
135        }
136
137        this
138    }
139
140    /// Requests the index for a compiler query.
141    pub fn request(&mut self, request: CompilerQueryRequest) -> Option<CompilerQueryResponse> {
142        match request {
143            CompilerQueryRequest::Hover(request) => {
144                Some(CompilerQueryResponse::Hover(self.hover(request)))
145            }
146            CompilerQueryRequest::HoverSymbol(symbol) => {
147                Some(CompilerQueryResponse::Hover(self.hover_symbol(&symbol)))
148            }
149            CompilerQueryRequest::GotoDefinition(request) => Some(
150                CompilerQueryResponse::GotoDefinition(self.goto_definition(request)),
151            ),
152            CompilerQueryRequest::GotoDefinitionSymbol(symbol) => Some(
153                CompilerQueryResponse::GotoDefinition(self.goto_definition_symbol(&symbol)),
154            ),
155            _ => None,
156        }
157    }
158
159    fn hover(&self, request: HoverRequest) -> Option<Hover> {
160        let uri = path_to_url(&request.path).ok()?;
161        let document = self.documents_by_uri.get(&uri)?;
162        let occurrence = find_scip_occurrence(document, request.position)?;
163        let mut hover = occurrence
164            .hover
165            .clone()
166            .or_else(|| self.symbol_hovers.get(&occurrence.symbol).cloned())?;
167        hover.range.get_or_insert(occurrence.range);
168        Some(hover)
169    }
170
171    /// Requests the hover information for a SCIP symbol.
172    pub fn hover_symbol(&self, symbol: &str) -> Option<Hover> {
173        self.symbol_hovers.get(symbol).cloned()
174    }
175
176    /// Requests the public symbols exported by a SCIP document path.
177    pub fn public_symbols(&self, path: &str) -> Vec<ScipPublicSymbol> {
178        self.public_symbols_by_path
179            .get(path)
180            .cloned()
181            .unwrap_or_default()
182    }
183
184    /// Requests source-page tokens that have hover or definition information.
185    pub fn source_tokens(&self, path: &str) -> Vec<ScipSourceToken> {
186        let Some(uri) = self.documents_by_path.get(path) else {
187            return Vec::new();
188        };
189        let Some(document) = self.documents_by_uri.get(uri) else {
190            return Vec::new();
191        };
192
193        document
194            .occurrences
195            .iter()
196            .filter(|occurrence| !occurrence.symbol.is_empty())
197            .filter_map(|occurrence| {
198                let mut hover = occurrence
199                    .hover
200                    .clone()
201                    .or_else(|| self.symbol_hovers.get(&occurrence.symbol).cloned());
202                if let Some(hover) = &mut hover {
203                    hover.range.get_or_insert(occurrence.range);
204                }
205
206                let definition = self
207                    .definition_links_for_occurrence(document, occurrence)
208                    .into_iter()
209                    .next();
210
211                if hover.is_none() && definition.is_none() {
212                    return None;
213                }
214
215                Some(ScipSourceToken {
216                    range: occurrence.range,
217                    hover,
218                    definition,
219                })
220            })
221            .collect()
222    }
223
224    fn goto_definition(&self, request: GotoDefinitionRequest) -> Option<GotoDefinitionResponse> {
225        let uri = path_to_url(&request.path).ok()?;
226        let document = self.documents_by_uri.get(&uri)?;
227        let occurrence = find_scip_occurrence(document, request.position)?;
228        let links = self.definition_links_for_occurrence(document, occurrence);
229
230        if links.is_empty() {
231            None
232        } else {
233            Some(GotoDefinitionResponse::Link(links))
234        }
235    }
236
237    fn definition_links_for_occurrence(
238        &self,
239        document: &ScipDocumentQuery,
240        occurrence: &ScipOccurrence,
241    ) -> Vec<LocationLink> {
242        let origin_selection_range = occurrence.range;
243        if occurrence.is_definition {
244            return vec![LocationLink {
245                origin_selection_range: Some(origin_selection_range),
246                target_uri: document.uri.clone(),
247                target_range: occurrence.range,
248                target_selection_range: occurrence.range,
249            }];
250        }
251
252        self.definitions
253            .get(&occurrence.symbol)
254            .into_iter()
255            .flatten()
256            .map(|definition| LocationLink {
257                origin_selection_range: Some(origin_selection_range),
258                target_uri: definition.uri.clone(),
259                target_range: definition.range,
260                target_selection_range: definition.range,
261            })
262            .collect()
263    }
264
265    /// Requests the definition for a SCIP symbol.
266    pub fn goto_definition_symbol(&self, symbol: &str) -> Option<GotoDefinitionResponse> {
267        let links = self
268            .definitions
269            .get(symbol)?
270            .iter()
271            .map(|definition| LocationLink {
272                origin_selection_range: None,
273                target_uri: definition.uri.clone(),
274                target_range: definition.range,
275                target_selection_range: definition.range,
276            })
277            .collect::<Vec<_>>();
278
279        if links.is_empty() {
280            None
281        } else {
282            Some(GotoDefinitionResponse::Link(links))
283        }
284    }
285}
286
287fn collect_symbol_infos(
288    index: &scip::types::Index,
289) -> FxHashMap<String, scip::types::SymbolInformation> {
290    let mut symbol_infos = FxHashMap::default();
291    for symbol in index.external_symbols.iter().chain(
292        index
293            .documents
294            .iter()
295            .flat_map(|document| document.symbols.iter()),
296    ) {
297        if let Some(current) = symbol_infos.get_mut(&symbol.symbol) {
298            merge_symbol_information(current, symbol.clone());
299        } else {
300            symbol_infos.insert(symbol.symbol.clone(), symbol.clone());
301        }
302    }
303    symbol_infos
304}
305
306fn collect_symbol_hovers(
307    symbol_infos: &FxHashMap<String, scip::types::SymbolInformation>,
308) -> FxHashMap<String, Hover> {
309    symbol_infos
310        .values()
311        .filter_map(|symbol| {
312            hover_from_scip_symbol(symbol, symbol_infos).map(|hover| (symbol.symbol.clone(), hover))
313        })
314        .collect()
315}
316
317fn scip_public_symbols(
318    document: &scip::types::Document,
319    symbol_infos: &FxHashMap<String, scip::types::SymbolInformation>,
320) -> Vec<ScipPublicSymbol> {
321    let mut symbols = Vec::new();
322    let mut seen = FxHashSet::default();
323
324    for symbol in &document.symbols {
325        if scip_package_docs_kind(symbol) != "module" {
326            continue;
327        }
328
329        for relationship in &symbol.relationships {
330            if !relationship.is_definition || !seen.insert(relationship.symbol.clone()) {
331                continue;
332            }
333
334            let Some(info) = symbol_infos.get(&relationship.symbol) else {
335                continue;
336            };
337            symbols.push(ScipPublicSymbol {
338                symbol: relationship.symbol.clone(),
339                name: scip_symbol_display_name(info, &relationship.symbol),
340                kind: scip_package_docs_kind(info).to_owned(),
341            });
342        }
343    }
344
345    symbols
346}
347
348fn scip_symbol_display_name(info: &scip::types::SymbolInformation, symbol: &str) -> String {
349    if !info.display_name.is_empty() {
350        return info.display_name.clone();
351    }
352
353    scip::symbol::parse_symbol(symbol)
354        .ok()
355        .and_then(|symbol| {
356            symbol
357                .descriptors
358                .last()
359                .map(|descriptor| descriptor.name.clone())
360        })
361        .unwrap_or_default()
362}
363
364fn scip_package_docs_kind(info: &scip::types::SymbolInformation) -> &'static str {
365    match info.kind.enum_value().ok() {
366        Some(scip::types::symbol_information::Kind::Module) => "module",
367        Some(scip::types::symbol_information::Kind::Function) => "function",
368        Some(scip::types::symbol_information::Kind::Constant) => "constant",
369        Some(scip::types::symbol_information::Kind::Variable) => "variable",
370        Some(scip::types::symbol_information::Kind::Parameter) => "parameter",
371        _ => "unknown",
372    }
373}
374
375fn find_scip_occurrence(
376    document: &ScipDocumentQuery,
377    position: Position,
378) -> Option<&ScipOccurrence> {
379    document
380        .occurrences
381        .iter()
382        .filter(|occurrence| contains_position(&occurrence.range, position))
383        .min_by_key(|occurrence| range_len_key(&occurrence.range))
384}
385
386fn scip_document_uri(project_root: Option<&Url>, relative_path: &str) -> Option<Url> {
387    if let Some(project_root) = project_root
388        && project_root.scheme() == "file"
389    {
390        let mut path = url_to_path(project_root);
391        path.push(relative_path);
392        if let Ok(uri) = path_to_url(&path) {
393            return Some(uri);
394        }
395    }
396
397    if let Some(project_root) = project_root
398        && let Ok(uri) = project_root.join(relative_path)
399    {
400        return Some(uri);
401    }
402
403    let path = std::path::Path::new(relative_path);
404    if path.is_absolute() {
405        path_to_url(path).ok()
406    } else {
407        None
408    }
409}
410
411fn scip_range_to_lsp(range: &[i32]) -> Option<Range> {
412    let line = as_u32(*range.first()?)?;
413    let start_character = as_u32(*range.get(1)?)?;
414    let (end_line, end_character) = match range.len() {
415        3 => (line, as_u32(range[2])?),
416        4 => (as_u32(range[2])?, as_u32(range[3])?),
417        _ => return None,
418    };
419
420    Some(Range {
421        start: Position {
422            line,
423            character: start_character,
424        },
425        end: Position {
426            line: end_line,
427            character: end_character,
428        },
429    })
430}
431
432fn as_u32(value: i32) -> Option<u32> {
433    u32::try_from(value).ok()
434}
435
436fn hover_from_scip_symbol(
437    symbol: &scip::types::SymbolInformation,
438    symbol_infos: &FxHashMap<String, scip::types::SymbolInformation>,
439) -> Option<Hover> {
440    if let Some(signature) = symbol.signature_documentation.as_ref()
441        && !signature.text.trim().is_empty()
442    {
443        return hover_from_scip_signature_symbol(symbol, signature, symbol_infos);
444    }
445
446    hover_from_scip_docs(&symbol.documentation)
447}
448
449fn hover_from_scip_signature_symbol(
450    symbol: &scip::types::SymbolInformation,
451    signature: &scip::types::Signature,
452    symbol_infos: &FxHashMap<String, scip::types::SymbolInformation>,
453) -> Option<Hover> {
454    let mut docs = Vec::new();
455    let language = if signature.language.trim().is_empty() {
456        "typc"
457    } else {
458        signature.language.as_str()
459    };
460    docs.push(format!("```{language}\n{}\n```", signature.text));
461    docs.extend(symbol.documentation.iter().cloned());
462
463    let params = scip_signature_params(signature, symbol_infos);
464    extend_scip_param_section(
465        &mut docs,
466        "# Positional Parameters",
467        "positional",
468        &params.positional,
469    );
470    extend_scip_param_section(&mut docs, "# Rest Parameters", "rest", &params.rest);
471    extend_scip_param_section(&mut docs, "# Named Parameters", "named", &params.named);
472
473    let markdown = docs
474        .into_iter()
475        .filter(|doc| !doc.trim().is_empty())
476        .collect::<Vec<_>>()
477        .join("\n\n");
478    if markdown.trim().is_empty() {
479        return None;
480    }
481
482    Some(Hover {
483        contents: HoverContents::Scalar(MarkedString::String(markdown)),
484        range: None,
485    })
486}
487
488#[derive(Default)]
489struct ScipSignatureParams {
490    positional: Vec<ScipSignatureParam>,
491    rest: Vec<ScipSignatureParam>,
492    named: Vec<ScipSignatureParam>,
493}
494
495struct ScipSignatureParam {
496    name: String,
497    documentation: Vec<String>,
498}
499
500fn scip_signature_params(
501    signature: &scip::types::Signature,
502    symbol_infos: &FxHashMap<String, scip::types::SymbolInformation>,
503) -> ScipSignatureParams {
504    let mut occurrences = signature.occurrences.iter().collect::<Vec<_>>();
505    occurrences.sort_by(|left, right| {
506        left.range
507            .cmp(&right.range)
508            .then_with(|| left.symbol.cmp(&right.symbol))
509    });
510
511    let mut seen = FxHashSet::default();
512    let mut params = ScipSignatureParams::default();
513    for occurrence in occurrences {
514        if !seen.insert(occurrence.symbol.clone()) {
515            continue;
516        }
517        let Some(group) = scip_parameter_group(&occurrence.symbol) else {
518            continue;
519        };
520        let Some(info) = symbol_infos.get(&occurrence.symbol) else {
521            continue;
522        };
523        let param = ScipSignatureParam {
524            name: if info.display_name.is_empty() {
525                scip_parameter_name(&occurrence.symbol).unwrap_or_default()
526            } else {
527                info.display_name.clone()
528            },
529            documentation: info.documentation.clone(),
530        };
531
532        match group {
533            ScipParamGroup::Positional => params.positional.push(param),
534            ScipParamGroup::Rest => params.rest.push(param),
535            ScipParamGroup::Named => params.named.push(param),
536        }
537    }
538
539    params
540}
541
542fn scip_parameter_group(symbol: &str) -> Option<ScipParamGroup> {
543    let symbol = scip::symbol::parse_symbol(symbol).ok()?;
544    let mut saw_parameter = false;
545    for descriptor in symbol.descriptors.iter().rev() {
546        match descriptor.suffix.enum_value().ok()? {
547            scip::types::descriptor::Suffix::Parameter => saw_parameter = true,
548            scip::types::descriptor::Suffix::Meta if saw_parameter => {
549                return ScipParamGroup::from_descriptor(&descriptor.name);
550            }
551            _ if saw_parameter => return None,
552            _ => {}
553        }
554    }
555
556    None
557}
558
559fn scip_parameter_name(symbol: &str) -> Option<String> {
560    let symbol = scip::symbol::parse_symbol(symbol).ok()?;
561    symbol.descriptors.iter().rev().find_map(|descriptor| {
562        matches!(
563            descriptor.suffix.enum_value().ok()?,
564            scip::types::descriptor::Suffix::Parameter
565        )
566        .then(|| descriptor.name.clone())
567    })
568}
569
570fn extend_scip_param_section(
571    docs: &mut Vec<String>,
572    title: &str,
573    kind: &str,
574    params: &[ScipSignatureParam],
575) {
576    if params.is_empty() {
577        return;
578    }
579
580    let mut section = title.to_owned();
581    for (idx, param) in params.iter().enumerate() {
582        let heading = if idx == 0 {
583            param.name.clone()
584        } else {
585            format!("{} ({kind})", param.name)
586        };
587        section.push_str("\n\n## ");
588        section.push_str(&heading);
589
590        let body = param
591            .documentation
592            .iter()
593            .filter(|doc| !doc.trim().is_empty())
594            .cloned()
595            .collect::<Vec<_>>()
596            .join("\n\n");
597        if !body.trim().is_empty() {
598            section.push_str("\n\n");
599            section.push_str(&body);
600        }
601    }
602    docs.push(section);
603}
604
605fn hover_from_scip_docs(docs: &[String]) -> Option<Hover> {
606    let docs = docs
607        .iter()
608        .filter(|doc| !doc.trim().is_empty())
609        .cloned()
610        .collect::<Vec<_>>();
611    if docs.is_empty() {
612        return None;
613    }
614
615    Some(Hover {
616        contents: HoverContents::Scalar(MarkedString::String(docs.join("\n\n---\n\n"))),
617        range: None,
618    })
619}
620
621fn contains_position(range: &Range, position: Position) -> bool {
622    position_after_or_eq(position, range.start) && position_before(position, range.end)
623}
624
625fn position_after_or_eq(a: Position, b: Position) -> bool {
626    (a.line, a.character) >= (b.line, b.character)
627}
628
629fn position_before(a: Position, b: Position) -> bool {
630    (a.line, a.character) < (b.line, b.character)
631}
632
633fn range_len_key(range: &Range) -> (u32, u32) {
634    (
635        range.end.line.saturating_sub(range.start.line),
636        range.end.character.saturating_sub(range.start.character),
637    )
638}
639
640#[cfg(test)]
641mod tests {
642    use std::path::PathBuf;
643
644    use lsp_types::{HoverContents, MarkedString};
645    use protobuf::{Enum, EnumOrUnknown, Message, MessageField};
646    use scip::types::{
647        Document as ScipDocument, Index as ScipIndex, Metadata as ScipMetadata,
648        Occurrence as ScipOccurrence, PositionEncoding as ScipPositionEncoding,
649        Relationship as ScipRelationship, Signature as ScipSignature,
650        SymbolInformation as ScipSymbolInformation, SymbolRole as ScipSymbolRole,
651        TextEncoding as ScipTextEncoding, ToolInfo as ScipToolInfo, symbol_information,
652    };
653
654    use super::*;
655
656    #[test]
657    fn scip_hover_and_definition() {
658        let path = fixture_path();
659        let symbol = "local value";
660        let bytes = test_index(vec![main_document(
661            vec![
662                definition_occurrence(vec![0, 0, 4], symbol),
663                occurrence(vec![1, 0, 4], symbol),
664            ],
665            vec![symbol_info(symbol, "value", &["hello"])],
666        )]);
667        let mut index = ScipQueryCtx::read(&bytes).unwrap();
668
669        let hover = index.request(CompilerQueryRequest::Hover(HoverRequest {
670            path: path.clone(),
671            position: Position {
672                line: 1,
673                character: 1,
674            },
675        }));
676        let Some(CompilerQueryResponse::Hover(Some(hover))) = hover else {
677            panic!("expected SCIP hover response");
678        };
679        assert_eq!(
680            hover.contents,
681            HoverContents::Scalar(MarkedString::String("hello".to_owned()))
682        );
683
684        let definition = index.request(CompilerQueryRequest::GotoDefinition(
685            GotoDefinitionRequest {
686                path,
687                position: Position {
688                    line: 1,
689                    character: 1,
690                },
691            },
692        ));
693        let Some(CompilerQueryResponse::GotoDefinition(Some(GotoDefinitionResponse::Link(links)))) =
694            definition
695        else {
696            panic!("expected SCIP definition response");
697        };
698        assert_eq!(links.len(), 1);
699        assert_eq!(
700            links[0].target_selection_range,
701            Range {
702                start: Position {
703                    line: 0,
704                    character: 0,
705                },
706                end: Position {
707                    line: 0,
708                    character: 4,
709                },
710            }
711        );
712    }
713
714    #[test]
715    fn scip_signature_hover_uses_parameter_symbols() {
716        let path = fixture_path();
717        let symbol = "typst tinymist workspace . main.typ/L0_C0:foo().";
718        let param_symbol = "typst tinymist workspace . main.typ/L0_C0:foo().positional:(arg)";
719        let mut function = symbol_info(symbol, "foo", &["Function docs."]);
720        function.signature_documentation = MessageField::some(ScipSignature {
721            language: "typc".to_owned(),
722            text: "let foo(\n  arg: int,\n) = none;".to_owned(),
723            occurrences: vec![definition_occurrence(vec![1, 2, 5], param_symbol)],
724            ..Default::default()
725        });
726        let mut parameter = symbol_info(
727            param_symbol,
728            "arg",
729            &["```typc\ntype: int\n```", "Arg docs."],
730        );
731        parameter.enclosing_symbol = symbol.to_owned();
732        let bytes = test_index(vec![main_document(
733            vec![occurrence(vec![1, 0, 3], symbol)],
734            vec![function, parameter],
735        )]);
736        let mut index = ScipQueryCtx::read(&bytes).unwrap();
737
738        let hover = index.request(CompilerQueryRequest::Hover(HoverRequest {
739            path,
740            position: Position {
741                line: 1,
742                character: 1,
743            },
744        }));
745        let Some(CompilerQueryResponse::Hover(Some(hover))) = hover else {
746            panic!("expected SCIP hover response");
747        };
748        assert_eq!(
749            hover.contents,
750            HoverContents::Scalar(MarkedString::String(
751                "```typc\nlet foo(\n  arg: int,\n) = none;\n```\n\nFunction docs.\n\n# Positional Parameters\n\n## arg\n\n```typc\ntype: int\n```\n\nArg docs.".to_owned()
752            ))
753        );
754    }
755
756    #[test]
757    fn scip_public_symbols_from_module_document() {
758        let module_symbol = "typst tinymist workspace . main.typ/1:main/";
759        let function_symbol = "typst tinymist workspace . main.typ/L0_C0:foo().";
760        let mut module =
761            typed_symbol_info(module_symbol, "main", symbol_information::Kind::Module, &[]);
762        module.relationships = vec![public_relationship(function_symbol)];
763        let function = typed_symbol_info(
764            function_symbol,
765            "foo",
766            symbol_information::Kind::Function,
767            &["Function docs."],
768        );
769        let bytes = test_index(vec![main_document(vec![], vec![module, function])]);
770        let index = ScipQueryCtx::read(&bytes).unwrap();
771
772        let symbols = index.public_symbols("main.typ");
773        assert_eq!(symbols.len(), 1);
774        assert_eq!(symbols[0].symbol, function_symbol.to_owned());
775        assert_eq!(symbols[0].name, "foo");
776        assert_eq!(symbols[0].kind, "function");
777    }
778
779    #[test]
780    fn scip_source_tokens_include_hover_and_definition() {
781        let symbol = "local value";
782        let bytes = test_index(vec![main_document(
783            vec![
784                definition_occurrence(vec![0, 0, 4], symbol),
785                occurrence(vec![1, 0, 4], symbol),
786            ],
787            vec![symbol_info(symbol, "value", &["hello"])],
788        )]);
789        let index = ScipQueryCtx::read(&bytes).unwrap();
790
791        let tokens = index.source_tokens("main.typ");
792        assert_eq!(tokens.len(), 2);
793        assert_eq!(
794            tokens[0]
795                .definition
796                .as_ref()
797                .unwrap()
798                .target_selection_range,
799            Range {
800                start: Position {
801                    line: 0,
802                    character: 0,
803                },
804                end: Position {
805                    line: 0,
806                    character: 4,
807                },
808            }
809        );
810        assert_eq!(
811            tokens[1].hover.as_ref().unwrap().contents,
812            HoverContents::Scalar(MarkedString::String("hello".to_owned()))
813        );
814        assert_eq!(
815            tokens[1]
816                .definition
817                .as_ref()
818                .unwrap()
819                .target_selection_range,
820            Range {
821                start: Position {
822                    line: 0,
823                    character: 0,
824                },
825                end: Position {
826                    line: 0,
827                    character: 4,
828                },
829            }
830        );
831    }
832
833    fn test_index(documents: Vec<ScipDocument>) -> Vec<u8> {
834        let path = fixture_path();
835        let root = path.parent().unwrap();
836        ScipIndex {
837            metadata: MessageField::some(ScipMetadata {
838                tool_info: MessageField::some(ScipToolInfo {
839                    name: "tinymist".to_owned(),
840                    version: "test".to_owned(),
841                    ..Default::default()
842                }),
843                project_root: path_to_url(root).unwrap().to_string(),
844                text_document_encoding: EnumOrUnknown::new(ScipTextEncoding::UTF8),
845                ..Default::default()
846            }),
847            documents,
848            ..Default::default()
849        }
850        .write_to_bytes()
851        .unwrap()
852    }
853
854    fn main_document(
855        occurrences: Vec<ScipOccurrence>,
856        symbols: Vec<ScipSymbolInformation>,
857    ) -> ScipDocument {
858        ScipDocument {
859            language: "typst".to_owned(),
860            relative_path: "main.typ".to_owned(),
861            occurrences,
862            symbols,
863            position_encoding: EnumOrUnknown::new(
864                ScipPositionEncoding::UTF16CodeUnitOffsetFromLineStart,
865            ),
866            ..Default::default()
867        }
868    }
869
870    fn occurrence(range: Vec<i32>, symbol: &str) -> ScipOccurrence {
871        ScipOccurrence {
872            range,
873            symbol: symbol.to_owned(),
874            ..Default::default()
875        }
876    }
877
878    fn definition_occurrence(range: Vec<i32>, symbol: &str) -> ScipOccurrence {
879        ScipOccurrence {
880            symbol_roles: ScipSymbolRole::Definition.value(),
881            ..occurrence(range, symbol)
882        }
883    }
884
885    fn symbol_info(
886        symbol: &str,
887        display_name: &str,
888        documentation: &[&str],
889    ) -> ScipSymbolInformation {
890        typed_symbol_info(
891            symbol,
892            display_name,
893            symbol_information::Kind::UnspecifiedKind,
894            documentation,
895        )
896    }
897
898    fn typed_symbol_info(
899        symbol: &str,
900        display_name: &str,
901        kind: symbol_information::Kind,
902        documentation: &[&str],
903    ) -> ScipSymbolInformation {
904        ScipSymbolInformation {
905            symbol: symbol.to_owned(),
906            documentation: documentation.iter().map(|doc| (*doc).to_owned()).collect(),
907            kind: EnumOrUnknown::new(kind),
908            display_name: display_name.to_owned(),
909            ..Default::default()
910        }
911    }
912
913    fn public_relationship(symbol: &str) -> ScipRelationship {
914        ScipRelationship {
915            symbol: symbol.to_owned(),
916            is_reference: true,
917            is_definition: true,
918            ..Default::default()
919        }
920    }
921
922    fn fixture_path() -> PathBuf {
923        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("main.typ")
924    }
925}