1use 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#[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#[derive(Debug, Clone, Serialize)]
31pub struct ScipPublicSymbol {
32 pub symbol: String,
34 pub name: String,
36 pub kind: String,
38}
39
40#[derive(Debug, Clone, Serialize)]
42#[serde(rename_all = "camelCase")]
43pub struct ScipSourceToken {
44 pub range: Range,
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub hover: Option<Hover>,
49 #[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 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 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 pub fn hover_symbol(&self, symbol: &str) -> Option<Hover> {
173 self.symbol_hovers.get(symbol).cloned()
174 }
175
176 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 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 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 ¶ms.positional,
469 );
470 extend_scip_param_section(&mut docs, "# Rest Parameters", "rest", ¶ms.rest);
471 extend_scip_param_section(&mut docs, "# Named Parameters", "named", ¶ms.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}