1use std::ops::Range;
4use std::path::PathBuf;
5use std::process::{Command, Stdio};
6use std::time::{Duration, Instant};
7
8use crate::code::{Document, DocumentId, DocumentVersion, EditorLanguage};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
22pub struct SymbolId(pub u64);
23
24#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
37pub struct CodeSymbol {
38 pub id: SymbolId,
40 pub name: String,
42 pub kind: SymbolKind,
44 pub language: EditorLanguage,
46 pub range: Range<usize>,
48 pub selection_range: Range<usize>,
50 pub parent: Option<SymbolId>,
52 pub signature: Option<String>,
54 pub docs: Option<String>,
56 pub source: SymbolSource,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
70pub enum SymbolKind {
71 Module,
73 Namespace,
75 Type,
77 Impl,
79 Struct,
81 Enum,
83 EnumVariant,
85 Trait,
87 Interface,
89 Function,
91 Method,
93 Constructor,
95 Field,
97 Variable,
99 Constant,
101 TypeAlias,
103 Macro,
105 Import,
107 Unknown,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
122pub enum SymbolSource {
123 TreeSitter,
125 Lexical,
127 Ctags,
129 Lsp,
131 Scip,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
145pub struct CodeFileSummary {
146 pub document_id: DocumentId,
148 pub path: Option<PathBuf>,
150 pub language: EditorLanguage,
152 pub version: DocumentVersion,
154 pub symbols: Vec<CodeSymbol>,
156 pub edges: Vec<SymbolEdge>,
158}
159
160impl CodeFileSummary {
162 pub fn to_json_pretty(&self) -> serde_json::Result<String> {
178 serde_json::to_string_pretty(self)
179 }
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
195pub struct SymbolEdge {
196 pub from: SymbolId,
198 pub to: SymbolId,
200 pub kind: SymbolEdgeKind,
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
213pub enum SymbolEdgeKind {
214 Contains,
216 Calls,
218 Imports,
220 References,
222 Extends,
224 Implements,
226}
227
228pub trait SymbolIndexer {
242 fn index_document(&mut self, document: &Document) -> CodeFileSummary;
254}
255
256#[derive(Debug, Default)]
270pub struct LexicalRustSymbolIndexer;
271
272impl SymbolIndexer for LexicalRustSymbolIndexer {
274 fn index_document(&mut self, document: &Document) -> CodeFileSummary {
276 summarize_rust_lexical(document)
277 }
278}
279
280#[derive(Debug, Clone)]
296pub struct CtagsSymbolIndexer {
297 source: CtagsSymbolSource,
299}
300
301#[derive(Debug, Clone)]
303enum CtagsSymbolSource {
304 JsonLines(String),
306 Runner(CtagsRunnerConfig),
308}
309
310impl Default for CtagsSymbolIndexer {
312 fn default() -> Self {
314 Self {
315 source: CtagsSymbolSource::Runner(CtagsRunnerConfig::default()),
316 }
317 }
318}
319
320impl CtagsSymbolIndexer {
322 pub fn from_json_lines(json_lines: impl Into<String>) -> Self {
337 Self {
338 source: CtagsSymbolSource::JsonLines(json_lines.into()),
339 }
340 }
341
342 pub fn from_runner_config(config: CtagsRunnerConfig) -> Self {
352 Self {
353 source: CtagsSymbolSource::Runner(config),
354 }
355 }
356
357 pub fn try_index_document(
381 &mut self,
382 document: &Document,
383 ) -> Result<CodeFileSummary, CtagsError> {
384 let json_lines = match &self.source {
385 CtagsSymbolSource::JsonLines(json_lines) => json_lines.clone(),
386 CtagsSymbolSource::Runner(config) => run_ctags(document, config)?,
387 };
388 Ok(summary_from_ctags_json_lines(document, &json_lines))
389 }
390}
391
392impl SymbolIndexer for CtagsSymbolIndexer {
394 fn index_document(&mut self, document: &Document) -> CodeFileSummary {
396 self.try_index_document(document)
397 .unwrap_or_else(|_| empty_summary(document))
398 }
399}
400
401pub fn index_symbols_with_fallback(
418 document: &Document,
419 ctags: &mut CtagsSymbolIndexer,
420) -> CodeFileSummary {
421 #[cfg(feature = "tree_sitter")]
422 if document.language == EditorLanguage::Rust {
423 let mut tree_sitter = TreeSitterRustSymbolIndexer;
424 let summary = tree_sitter.index_document(document);
425 if !summary.symbols.is_empty() {
426 return summary;
427 }
428 }
429 ctags.index_document(document)
430}
431
432fn run_ctags(document: &Document, config: &CtagsRunnerConfig) -> Result<String, CtagsError> {
443 let temp_path = ctags_temp_path(document);
444 std::fs::write(&temp_path, document.buffer.as_str())
445 .map_err(|err| CtagsError::Io(err.to_string()))?;
446 let mut child = Command::new(&config.binary)
447 .arg("--output-format=json")
448 .arg("--fields=+nksSzt")
449 .arg("--extras=+q")
450 .arg("-f")
451 .arg("-")
452 .arg(&temp_path)
453 .stdin(Stdio::null())
454 .stdout(Stdio::piped())
455 .stderr(Stdio::piped())
456 .spawn()
457 .map_err(|err| {
458 if err.kind() == std::io::ErrorKind::NotFound {
459 CtagsError::MissingBinary(config.binary.display().to_string())
460 } else {
461 CtagsError::Io(err.to_string())
462 }
463 })?;
464
465 let started = Instant::now();
466 loop {
467 match child.try_wait() {
468 Ok(Some(_)) => break,
469 Ok(None) if started.elapsed() >= config.timeout => {
470 let _ = child.kill();
471 let _ = child.wait();
472 let _ = std::fs::remove_file(&temp_path);
473 return Err(CtagsError::Timeout {
474 timeout: config.timeout,
475 });
476 }
477 Ok(None) => std::thread::sleep(Duration::from_millis(5)),
478 Err(err) => {
479 let _ = child.kill();
480 let _ = std::fs::remove_file(&temp_path);
481 return Err(CtagsError::Io(err.to_string()));
482 }
483 }
484 }
485
486 let output = child
487 .wait_with_output()
488 .map_err(|err| CtagsError::Io(err.to_string()))?;
489 let _ = std::fs::remove_file(&temp_path);
490 if output.stdout.len() > config.max_stdout_bytes {
491 return Err(CtagsError::OutputTooLarge {
492 len: output.stdout.len(),
493 max: config.max_stdout_bytes,
494 });
495 }
496 if !output.status.success() {
497 return Err(CtagsError::NonZeroStatus {
498 code: output.status.code(),
499 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
500 });
501 }
502 String::from_utf8(output.stdout).map_err(|err| CtagsError::Utf8(err.to_string()))
503}
504
505fn ctags_temp_path(document: &Document) -> PathBuf {
507 let ext = document
508 .path
509 .as_deref()
510 .and_then(|path| path.extension())
511 .and_then(|ext| ext.to_str())
512 .unwrap_or("txt");
513 std::env::temp_dir().join(format!(
514 "ailloli_ui_ctags_{}_{}.{}",
515 document.id.0, document.version.0, ext
516 ))
517}
518
519#[derive(Debug, Clone, PartialEq, Eq)]
536pub struct CtagsRunnerConfig {
537 pub binary: PathBuf,
539 pub timeout: Duration,
541 pub max_stdout_bytes: usize,
543}
544
545impl Default for CtagsRunnerConfig {
547 fn default() -> Self {
549 Self {
550 binary: PathBuf::from("ctags"),
551 timeout: Duration::from_secs(2),
552 max_stdout_bytes: 512 * 1024,
553 }
554 }
555}
556
557#[derive(Debug, Clone, PartialEq, Eq)]
567pub enum CtagsError {
568 MissingBinary(String),
570 Timeout {
572 timeout: Duration,
574 },
575 OutputTooLarge {
577 len: usize,
579 max: usize,
581 },
582 NonZeroStatus {
584 code: Option<i32>,
586 stderr: String,
588 },
589 Io(String),
591 Utf8(String),
593}
594
595#[cfg(feature = "tree_sitter")]
596#[derive(Debug, Default)]
614pub struct TreeSitterRustSymbolIndexer;
615
616#[cfg(feature = "tree_sitter")]
617impl SymbolIndexer for TreeSitterRustSymbolIndexer {
619 fn index_document(&mut self, document: &Document) -> CodeFileSummary {
621 let text = document.buffer.as_str();
622 let mut parser = tree_sitter::Parser::new();
623 let language = tree_sitter_rust::LANGUAGE.into();
624 if parser.set_language(&language).is_err() {
625 return summarize_rust_lexical(document);
626 }
627 let Some(tree) = parser.parse(&text, None) else {
628 return summarize_rust_lexical(document);
629 };
630
631 let symbols = collect_tree_sitter_rust_symbols(tree.root_node(), &text, document.language);
632 let mut edges = containment_edges(&symbols);
633 edges.extend(collect_tree_sitter_rust_call_edges(
634 tree.root_node(),
635 &text,
636 &symbols,
637 ));
638 normalize_edges(&mut edges);
639
640 CodeFileSummary {
641 symbols,
642 edges,
643 ..empty_summary(document)
644 }
645 }
646}
647
648pub fn summarize_rust_lexical(document: &Document) -> CodeFileSummary {
668 let text = document.buffer.as_str();
669 let mut symbols = Vec::new();
670 for (line_start, line) in lines_with_offsets(&text) {
671 let trimmed = line.trim_start();
672 let leading = line.len() - trimmed.len();
673 let start = line_start + leading;
674 if let Some(rest) = trimmed.strip_prefix("use ") {
675 let rest = rest.trim();
676 let name = rest
677 .trim_end_matches(';')
678 .rsplit("::")
679 .next()
680 .unwrap_or(rest)
681 .trim()
682 .to_string();
683 push_symbol(
684 &mut symbols,
685 document.language,
686 SymbolKind::Import,
687 name,
688 start,
689 line,
690 );
691 } else if let Some(rest) = trimmed.strip_prefix("fn ") {
692 let name = take_ident(rest);
693 push_symbol(
694 &mut symbols,
695 document.language,
696 SymbolKind::Function,
697 name,
698 start,
699 line,
700 );
701 } else if let Some(rest) = trimmed.strip_prefix("struct ") {
702 let name = take_ident(rest);
703 push_symbol(
704 &mut symbols,
705 document.language,
706 SymbolKind::Struct,
707 name,
708 start,
709 line,
710 );
711 } else if let Some(rest) = trimmed.strip_prefix("enum ") {
712 let name = take_ident(rest);
713 push_symbol(
714 &mut symbols,
715 document.language,
716 SymbolKind::Enum,
717 name,
718 start,
719 line,
720 );
721 } else if let Some(rest) = trimmed.strip_prefix("trait ") {
722 let name = take_ident(rest);
723 push_symbol(
724 &mut symbols,
725 document.language,
726 SymbolKind::Trait,
727 name,
728 start,
729 line,
730 );
731 }
732 }
733
734 let mut edges = containment_edges(&symbols);
735 edges.extend(call_edges(&text, &symbols));
736 normalize_edges(&mut edges);
737
738 CodeFileSummary {
739 symbols,
740 edges,
741 ..empty_summary(document)
742 }
743}
744
745#[cfg(feature = "tree_sitter")]
746fn collect_tree_sitter_rust_symbols(
748 root: tree_sitter::Node<'_>,
749 text: &str,
750 language: EditorLanguage,
751) -> Vec<CodeSymbol> {
752 let mut collector = TreeSitterSymbolCollector {
753 text,
754 language,
755 symbols: Vec::new(),
756 };
757 collector.collect(root, None);
758 collector.symbols
759}
760
761#[cfg(feature = "tree_sitter")]
762struct TreeSitterSymbolCollector<'a> {
764 text: &'a str,
766 language: EditorLanguage,
768 symbols: Vec<CodeSymbol>,
770}
771
772#[cfg(feature = "tree_sitter")]
773impl<'a> TreeSitterSymbolCollector<'a> {
775 fn collect(&mut self, node: tree_sitter::Node<'_>, parent: Option<SymbolId>) {
777 let mut next_parent = parent;
778 if let Some(symbol) = self.symbol_for_node(node, parent) {
779 next_parent = symbol_container_parent(symbol.kind).then_some(symbol.id);
780 self.symbols.push(symbol);
781 }
782
783 let mut cursor = node.walk();
784 for child in node.children(&mut cursor) {
785 self.collect(child, next_parent.or(parent));
786 }
787 }
788
789 fn symbol_for_node(
791 &self,
792 node: tree_sitter::Node<'_>,
793 parent: Option<SymbolId>,
794 ) -> Option<CodeSymbol> {
795 let spec = tree_sitter_rust_symbol_spec(
796 self.text,
797 node,
798 parent.and_then(|id| self.kind_for_id(id)),
799 )?;
800 let name_node = spec.name_node?;
801 let name = spec
802 .name_override
803 .unwrap_or_else(|| node_text(self.text, name_node).trim().to_string());
804 if name.is_empty() {
805 return None;
806 }
807 let id = SymbolId(self.symbols.len() as u64 + 1);
808 Some(CodeSymbol {
809 id,
810 name,
811 kind: spec.kind,
812 language: self.language,
813 range: valid_range(self.text, node.start_byte()..node.end_byte()),
814 selection_range: valid_range(self.text, name_node.start_byte()..name_node.end_byte()),
815 parent,
816 signature: signature_for_symbol(self.text, node, spec.kind),
817 docs: docs_for_symbol(self.text, node),
818 source: SymbolSource::TreeSitter,
819 })
820 }
821
822 fn kind_for_id(&self, id: SymbolId) -> Option<SymbolKind> {
824 self.symbols
825 .iter()
826 .find(|symbol| symbol.id == id)
827 .map(|symbol| symbol.kind)
828 }
829}
830
831#[cfg(feature = "tree_sitter")]
832struct RustSymbolSpec<'tree> {
834 kind: SymbolKind,
836 name_node: Option<tree_sitter::Node<'tree>>,
838 name_override: Option<String>,
840}
841
842#[cfg(feature = "tree_sitter")]
843fn tree_sitter_rust_symbol_spec<'tree>(
845 text: &str,
846 node: tree_sitter::Node<'tree>,
847 parent_kind: Option<SymbolKind>,
848) -> Option<RustSymbolSpec<'tree>> {
849 let name = || node.child_by_field_name("name");
850 match node.kind() {
851 "mod_item" => Some(RustSymbolSpec {
852 kind: SymbolKind::Module,
853 name_node: name(),
854 name_override: None,
855 }),
856 "use_declaration" => Some(RustSymbolSpec {
857 kind: SymbolKind::Import,
858 name_node: Some(node),
859 name_override: Some(import_name(node_text(text, node))),
860 }),
861 "struct_item" => Some(RustSymbolSpec {
862 kind: SymbolKind::Struct,
863 name_node: name(),
864 name_override: None,
865 }),
866 "enum_item" => Some(RustSymbolSpec {
867 kind: SymbolKind::Enum,
868 name_node: name(),
869 name_override: None,
870 }),
871 "trait_item" => Some(RustSymbolSpec {
872 kind: SymbolKind::Trait,
873 name_node: name(),
874 name_override: None,
875 }),
876 "impl_item" => {
877 let type_node = impl_type_node(node).or_else(name);
878 let type_text = type_node
879 .map(|type_node| compact_node_text(node_text(text, type_node)))
880 .unwrap_or_else(|| "impl".to_string());
881 Some(RustSymbolSpec {
882 kind: SymbolKind::Impl,
883 name_node: type_node,
884 name_override: Some(format!("impl {type_text}")),
885 })
886 }
887 "function_item" | "function_signature_item" => Some(RustSymbolSpec {
888 kind: if matches!(parent_kind, Some(SymbolKind::Impl | SymbolKind::Trait)) {
889 SymbolKind::Method
890 } else {
891 SymbolKind::Function
892 },
893 name_node: name(),
894 name_override: None,
895 }),
896 "const_item" | "static_item" => Some(RustSymbolSpec {
897 kind: SymbolKind::Constant,
898 name_node: name(),
899 name_override: None,
900 }),
901 "type_item" => Some(RustSymbolSpec {
902 kind: SymbolKind::TypeAlias,
903 name_node: name(),
904 name_override: None,
905 }),
906 "macro_definition" => Some(RustSymbolSpec {
907 kind: SymbolKind::Macro,
908 name_node: name(),
909 name_override: None,
910 }),
911 "field_declaration" if parent_kind == Some(SymbolKind::Struct) => Some(RustSymbolSpec {
912 kind: SymbolKind::Field,
913 name_node: name(),
914 name_override: None,
915 }),
916 "enum_variant" if parent_kind == Some(SymbolKind::Enum) => Some(RustSymbolSpec {
917 kind: SymbolKind::EnumVariant,
918 name_node: name(),
919 name_override: None,
920 }),
921 _ => None,
922 }
923}
924
925#[cfg(feature = "tree_sitter")]
926fn symbol_container_parent(kind: SymbolKind) -> bool {
928 matches!(
929 kind,
930 SymbolKind::Module
931 | SymbolKind::Impl
932 | SymbolKind::Struct
933 | SymbolKind::Enum
934 | SymbolKind::Trait
935 )
936}
937
938#[cfg(feature = "tree_sitter")]
939fn impl_type_node(node: tree_sitter::Node<'_>) -> Option<tree_sitter::Node<'_>> {
941 node.child_by_field_name("type").or_else(|| {
942 let mut cursor = node.walk();
943 let found = node.children(&mut cursor).find(|child| {
944 matches!(
945 child.kind(),
946 "type_identifier"
947 | "generic_type"
948 | "scoped_type_identifier"
949 | "reference_type"
950 | "primitive_type"
951 )
952 });
953 found
954 })
955}
956
957#[cfg(feature = "tree_sitter")]
958fn node_text<'a>(text: &'a str, node: tree_sitter::Node<'_>) -> &'a str {
960 text.get(node.start_byte()..node.end_byte())
961 .unwrap_or_default()
962}
963
964#[cfg(feature = "tree_sitter")]
965fn compact_node_text(text: &str) -> String {
967 text.split_whitespace().collect::<Vec<_>>().join(" ")
968}
969
970#[cfg(feature = "tree_sitter")]
971fn import_name(text: &str) -> String {
973 text.trim()
974 .trim_start_matches("pub")
975 .trim_start()
976 .trim_start_matches("use")
977 .trim()
978 .trim_end_matches(';')
979 .rsplit("::")
980 .next()
981 .unwrap_or(text)
982 .trim_matches(|ch: char| matches!(ch, '{' | '}' | '*' | ' ' | ';'))
983 .to_string()
984}
985
986#[cfg(feature = "tree_sitter")]
987fn valid_range(text: &str, range: Range<usize>) -> Range<usize> {
989 let mut start = range.start.min(text.len());
990 while start > 0 && !text.is_char_boundary(start) {
991 start -= 1;
992 }
993 let mut end = range.end.min(text.len());
994 while end > start && !text.is_char_boundary(end) {
995 end -= 1;
996 }
997 start..end
998}
999
1000#[cfg(feature = "tree_sitter")]
1001fn signature_for_symbol(
1003 text: &str,
1004 node: tree_sitter::Node<'_>,
1005 kind: SymbolKind,
1006) -> Option<String> {
1007 if matches!(
1008 kind,
1009 SymbolKind::Field | SymbolKind::EnumVariant | SymbolKind::Import
1010 ) {
1011 return Some(compact_node_text(
1012 node_text(text, node)
1013 .trim()
1014 .trim_end_matches(',')
1015 .trim_end_matches(';'),
1016 ));
1017 }
1018 let raw = node_text(text, node).trim();
1019 if raw.is_empty() {
1020 return None;
1021 }
1022 let end = raw
1023 .find('{')
1024 .or_else(|| raw.find(';').map(|idx| idx + 1))
1025 .unwrap_or_else(|| raw.lines().next().map_or(raw.len(), str::len));
1026 let signature = compact_node_text(raw[..end].trim().trim_end_matches('{').trim());
1027 (!signature.is_empty()).then_some(signature)
1028}
1029
1030#[cfg(feature = "tree_sitter")]
1031fn docs_for_symbol(text: &str, node: tree_sitter::Node<'_>) -> Option<String> {
1033 let prefix = &text[..node.start_byte().min(text.len())];
1034 let mut docs = Vec::new();
1035 for line in prefix.lines().rev() {
1036 let trimmed = line.trim();
1037 if trimmed.is_empty() {
1038 if docs.is_empty() {
1039 continue;
1040 }
1041 break;
1042 }
1043 if let Some(doc) = trimmed.strip_prefix("///") {
1044 docs.push(doc.trim().to_string());
1045 } else if let Some(doc) = trimmed.strip_prefix("//!") {
1046 docs.push(doc.trim().to_string());
1047 } else if let Some(doc) = trimmed
1048 .strip_prefix("#[doc = \"")
1049 .and_then(|rest| rest.strip_suffix("\"]"))
1050 {
1051 docs.push(doc.trim().to_string());
1052 } else if trimmed.starts_with("#[") {
1053 continue;
1054 } else {
1055 break;
1056 }
1057 }
1058 docs.reverse();
1059 (!docs.is_empty()).then_some(docs.join("\n"))
1060}
1061
1062pub fn parse_ctags_json_lines(document: &Document, json_lines: &str) -> Vec<CodeSymbol> {
1082 parse_ctags_entries(document, json_lines)
1083 .into_iter()
1084 .map(|entry| entry.symbol)
1085 .collect()
1086}
1087
1088fn summary_from_ctags_json_lines(document: &Document, json_lines: &str) -> CodeFileSummary {
1090 let symbols = parse_ctags_json_lines(document, json_lines);
1091 let mut edges = containment_edges(&symbols);
1092 normalize_edges(&mut edges);
1093 CodeFileSummary {
1094 symbols,
1095 edges,
1096 ..empty_summary(document)
1097 }
1098}
1099
1100fn parse_ctags_entries(document: &Document, json_lines: &str) -> Vec<CtagsParsedSymbol> {
1102 let entries: Vec<CtagsParsedSymbol> = json_lines
1103 .lines()
1104 .filter_map(|line| serde_json::from_str::<CtagsEntry>(line).ok())
1105 .filter(|entry| entry.name.as_deref().is_some_and(|name| !name.is_empty()))
1106 .enumerate()
1107 .map(|(idx, entry)| ctags_entry_to_symbol(document, idx, entry))
1108 .collect();
1109 attach_ctags_parents(entries)
1110}
1111
1112#[derive(Debug, Clone)]
1114struct CtagsParsedSymbol {
1115 symbol: CodeSymbol,
1117 scope: Option<String>,
1119 scope_kind: Option<String>,
1121}
1122
1123#[allow(dead_code)]
1124fn parse_ctags_json_lines_legacy(document: &Document, json_lines: &str) -> Vec<CodeSymbol> {
1126 json_lines
1127 .lines()
1128 .filter_map(|line| serde_json::from_str::<CtagsEntry>(line).ok())
1129 .filter(|entry| entry.name.as_deref().is_some_and(|name| !name.is_empty()))
1130 .enumerate()
1131 .map(|(idx, entry)| ctags_entry_to_symbol(document, idx, entry).symbol)
1132 .collect()
1133}
1134
1135fn empty_summary(document: &Document) -> CodeFileSummary {
1137 CodeFileSummary {
1138 document_id: document.id,
1139 path: document.path.clone(),
1140 language: document.language,
1141 version: document.version,
1142 symbols: Vec::new(),
1143 edges: Vec::new(),
1144 }
1145}
1146
1147#[derive(Debug, serde::Deserialize)]
1149struct CtagsEntry {
1150 name: Option<String>,
1152 kind: Option<String>,
1154 line: Option<usize>,
1156 end: Option<usize>,
1158 pattern: Option<String>,
1160 signature: Option<String>,
1162 typeref: Option<String>,
1164 scope: Option<String>,
1166 #[serde(rename = "scopeKind")]
1168 scope_kind: Option<String>,
1169 language: Option<String>,
1171 roles: Option<String>,
1173}
1174
1175fn ctags_entry_to_symbol(document: &Document, idx: usize, entry: CtagsEntry) -> CtagsParsedSymbol {
1177 let name = entry.name.unwrap_or_default();
1178 let text = document.buffer.as_str();
1179 let range = ctags_full_range(&text, entry.line, entry.end);
1180 let selection_range = ctags_selection_range(&text, &name, range.clone());
1181 let signature = entry
1182 .signature
1183 .or(entry.pattern)
1184 .or(entry.typeref)
1185 .map(|value| value.trim().to_string())
1186 .filter(|value| !value.is_empty());
1187 let _ctags_language = entry.language;
1188 let _ctags_roles = entry.roles;
1189 CtagsParsedSymbol {
1190 symbol: CodeSymbol {
1191 id: SymbolId(idx as u64 + 1),
1192 name,
1193 kind: ctags_kind_to_symbol_kind(entry.kind.as_deref()),
1194 language: document.language,
1195 selection_range,
1196 range,
1197 parent: None,
1198 signature,
1199 docs: None,
1200 source: SymbolSource::Ctags,
1201 },
1202 scope: entry.scope,
1203 scope_kind: entry.scope_kind,
1204 }
1205}
1206
1207fn attach_ctags_parents(mut entries: Vec<CtagsParsedSymbol>) -> Vec<CtagsParsedSymbol> {
1209 for idx in 0..entries.len() {
1210 let Some(scope) = entries[idx].scope.clone() else {
1211 continue;
1212 };
1213 let scope_name = scope.rsplit("::").next().unwrap_or(scope.as_str());
1214 let scope_kind = entries[idx]
1215 .scope_kind
1216 .as_deref()
1217 .map(|kind| ctags_kind_to_symbol_kind(Some(kind)));
1218 let parent = entries
1219 .iter()
1220 .take(idx)
1221 .rev()
1222 .find(|entry| {
1223 entry.symbol.name == scope_name
1224 && scope_kind
1225 .is_none_or(|kind| kind == SymbolKind::Unknown || kind == entry.symbol.kind)
1226 })
1227 .map(|entry| entry.symbol.id);
1228 entries[idx].symbol.parent = parent;
1229 }
1230 entries
1231}
1232
1233fn ctags_kind_to_symbol_kind(kind: Option<&str>) -> SymbolKind {
1235 match kind.unwrap_or_default() {
1236 "function" | "f" => SymbolKind::Function,
1237 "method" | "m" => SymbolKind::Method,
1238 "struct" | "s" => SymbolKind::Struct,
1239 "enum" | "e" => SymbolKind::Enum,
1240 "trait" | "t" => SymbolKind::Trait,
1241 "impl" => SymbolKind::Impl,
1242 "class" | "c" => SymbolKind::Type,
1243 "type" | "typedef" => SymbolKind::TypeAlias,
1244 "interface" | "i" => SymbolKind::Interface,
1245 "field" | "member" | "F" => SymbolKind::Field,
1246 "enumerator" | "enumConstant" => SymbolKind::EnumVariant,
1247 "variable" | "v" => SymbolKind::Variable,
1248 "constant" | "C" => SymbolKind::Constant,
1249 "macro" | "macrodef" => SymbolKind::Macro,
1250 "namespace" | "module" | "n" => SymbolKind::Namespace,
1251 "import" | "use" => SymbolKind::Import,
1252 _ => SymbolKind::Unknown,
1253 }
1254}
1255
1256fn ctags_full_range(text: &str, line: Option<usize>, end: Option<usize>) -> Range<usize> {
1258 let start = line
1259 .and_then(|line| line_start_offset(text, line.saturating_sub(1)))
1260 .unwrap_or(0)
1261 .min(text.len());
1262 let end = end
1263 .and_then(|line| line_start_offset(text, line))
1264 .unwrap_or_else(|| line_end_offset(text, start))
1265 .min(text.len());
1266 start..end.max(start)
1267}
1268
1269fn ctags_selection_range(text: &str, name: &str, range: Range<usize>) -> Range<usize> {
1271 if let Some(name_start) = text[range.clone()].find(name) {
1272 let absolute = range.start + name_start;
1273 absolute..absolute + name.len()
1274 } else {
1275 range
1276 }
1277}
1278
1279#[allow(dead_code)]
1280fn ctags_range(text: &str, name: &str, line: Option<usize>, end: Option<usize>) -> Range<usize> {
1282 ctags_selection_range(text, name, ctags_full_range(text, line, end))
1283}
1284
1285fn line_start_offset(text: &str, zero_based_line: usize) -> Option<usize> {
1287 if zero_based_line == 0 {
1288 return Some(0);
1289 }
1290 let mut current = 0;
1291 for (offset, ch) in text.char_indices() {
1292 if ch == '\n' {
1293 current += 1;
1294 if current == zero_based_line {
1295 return Some(offset + ch.len_utf8());
1296 }
1297 }
1298 }
1299 None
1300}
1301
1302fn line_end_offset(text: &str, start: usize) -> usize {
1304 text[start..]
1305 .find('\n')
1306 .map(|offset| start + offset)
1307 .unwrap_or(text.len())
1308}
1309
1310fn containment_edges(symbols: &[CodeSymbol]) -> Vec<SymbolEdge> {
1312 symbols
1313 .iter()
1314 .map(|symbol| SymbolEdge {
1315 from: symbol.parent.unwrap_or(SymbolId(0)),
1316 to: symbol.id,
1317 kind: if symbol.kind == SymbolKind::Import {
1318 SymbolEdgeKind::Imports
1319 } else {
1320 SymbolEdgeKind::Contains
1321 },
1322 })
1323 .collect()
1324}
1325
1326fn normalize_edges(edges: &mut Vec<SymbolEdge>) {
1328 edges.sort_by_key(|edge| (symbol_edge_rank(edge.kind), edge.from.0, edge.to.0));
1329 edges.dedup_by_key(|edge| (edge.from, edge.to, edge.kind));
1330}
1331
1332fn symbol_edge_rank(kind: SymbolEdgeKind) -> u8 {
1334 match kind {
1335 SymbolEdgeKind::Contains => 0,
1336 SymbolEdgeKind::Imports => 1,
1337 SymbolEdgeKind::Calls => 2,
1338 SymbolEdgeKind::References => 3,
1339 SymbolEdgeKind::Extends => 4,
1340 SymbolEdgeKind::Implements => 5,
1341 }
1342}
1343
1344fn call_edges(text: &str, symbols: &[CodeSymbol]) -> Vec<SymbolEdge> {
1346 let functions: Vec<_> = symbols
1347 .iter()
1348 .filter(|symbol| symbol.kind == SymbolKind::Function)
1349 .collect();
1350 let mut edges = Vec::new();
1351 for function in &functions {
1352 let needle = format!("{}(", function.name);
1353 let mut start = 0;
1354 while let Some(pos) = text[start..].find(&needle) {
1355 let absolute = start + pos;
1356 if !function.selection_range.contains(&absolute) {
1357 edges.push(SymbolEdge {
1358 from: SymbolId(0),
1359 to: function.id,
1360 kind: SymbolEdgeKind::Calls,
1361 });
1362 break;
1363 }
1364 start = absolute + needle.len();
1365 }
1366 }
1367 edges
1368}
1369
1370#[cfg(feature = "tree_sitter")]
1371fn collect_tree_sitter_rust_call_edges(
1373 root: tree_sitter::Node<'_>,
1374 text: &str,
1375 symbols: &[CodeSymbol],
1376) -> Vec<SymbolEdge> {
1377 let mut edges = Vec::new();
1378 collect_tree_sitter_rust_call_edges_from_node(root, text, symbols, &mut edges);
1379 normalize_edges(&mut edges);
1380 edges
1381}
1382
1383#[cfg(feature = "tree_sitter")]
1384fn collect_tree_sitter_rust_call_edges_from_node(
1386 node: tree_sitter::Node<'_>,
1387 text: &str,
1388 symbols: &[CodeSymbol],
1389 edges: &mut Vec<SymbolEdge>,
1390) {
1391 if node.kind() == "call_expression" {
1392 if let Some(edge) = call_edge_for_node(node, text, symbols) {
1393 edges.push(edge);
1394 }
1395 }
1396
1397 let mut cursor = node.walk();
1398 for child in node.children(&mut cursor) {
1399 collect_tree_sitter_rust_call_edges_from_node(child, text, symbols, edges);
1400 }
1401}
1402
1403#[cfg(feature = "tree_sitter")]
1404fn call_edge_for_node(
1406 node: tree_sitter::Node<'_>,
1407 text: &str,
1408 symbols: &[CodeSymbol],
1409) -> Option<SymbolEdge> {
1410 let caller = caller_for_byte(symbols, node.start_byte())?;
1411 let callee = resolve_callee_symbol(symbols, caller, &call_expression_target(node, text)?)?;
1412 if caller.id == callee.id {
1413 return None;
1414 }
1415 Some(SymbolEdge {
1416 from: caller.id,
1417 to: callee.id,
1418 kind: SymbolEdgeKind::Calls,
1419 })
1420}
1421
1422#[cfg(feature = "tree_sitter")]
1423fn caller_for_byte(symbols: &[CodeSymbol], byte: usize) -> Option<&CodeSymbol> {
1425 symbols
1426 .iter()
1427 .filter(|symbol| {
1428 matches!(symbol.kind, SymbolKind::Function | SymbolKind::Method)
1429 && symbol.range.start <= byte
1430 && byte <= symbol.range.end
1431 })
1432 .min_by_key(|symbol| (symbol.range.end - symbol.range.start, symbol.id.0))
1433}
1434
1435#[cfg(feature = "tree_sitter")]
1436fn resolve_callee_symbol<'a>(
1438 symbols: &'a [CodeSymbol],
1439 caller: &CodeSymbol,
1440 target: &CallTarget,
1441) -> Option<&'a CodeSymbol> {
1442 symbols
1443 .iter()
1444 .filter(|symbol| {
1445 symbol.name == target.name
1446 && matches!(
1447 symbol.kind,
1448 SymbolKind::Function | SymbolKind::Method | SymbolKind::Macro
1449 )
1450 && target
1451 .scope
1452 .as_deref()
1453 .is_none_or(|scope| symbol_parent_name(symbols, symbol) == Some(scope))
1454 })
1455 .min_by_key(|symbol| {
1456 (
1457 callee_scope_distance(symbols, caller, symbol),
1458 symbol.range.start.abs_diff(caller.range.start),
1459 symbol.id.0,
1460 )
1461 })
1462}
1463
1464#[cfg(feature = "tree_sitter")]
1465fn callee_scope_distance(
1467 symbols: &[CodeSymbol],
1468 caller: &CodeSymbol,
1469 callee: &CodeSymbol,
1470) -> usize {
1471 if caller.parent == callee.parent {
1472 return 0;
1473 }
1474 if caller.parent.is_some_and(|parent| parent == callee.id) {
1475 return 1;
1476 }
1477 let caller_ancestors = symbol_ancestors(symbols, caller);
1478 if callee
1479 .parent
1480 .is_some_and(|parent| caller_ancestors.contains(&parent))
1481 {
1482 return 2;
1483 }
1484 if callee.parent.is_none() {
1485 return 3;
1486 }
1487 4
1488}
1489
1490#[cfg(feature = "tree_sitter")]
1491fn symbol_ancestors(symbols: &[CodeSymbol], symbol: &CodeSymbol) -> Vec<SymbolId> {
1493 let mut ancestors = Vec::new();
1494 let mut current = symbol.parent;
1495 while let Some(parent) = current {
1496 ancestors.push(parent);
1497 current = symbols
1498 .iter()
1499 .find(|candidate| candidate.id == parent)
1500 .and_then(|candidate| candidate.parent);
1501 }
1502 ancestors
1503}
1504
1505#[cfg(feature = "tree_sitter")]
1506struct CallTarget {
1508 name: String,
1510 scope: Option<String>,
1512}
1513
1514#[cfg(feature = "tree_sitter")]
1515fn call_expression_target(node: tree_sitter::Node<'_>, text: &str) -> Option<CallTarget> {
1517 let function = node.child_by_field_name("function")?;
1518 final_call_target(function, text)
1519}
1520
1521#[cfg(feature = "tree_sitter")]
1522fn final_call_target(node: tree_sitter::Node<'_>, text: &str) -> Option<CallTarget> {
1524 let name = match node.kind() {
1525 "identifier" | "field_identifier" => node_text(text, node).to_string(),
1526 "scoped_identifier" | "field_expression" => node
1527 .child_by_field_name("name")
1528 .map(|name| node_text(text, name).to_string())
1529 .or_else(|| last_identifier_child(node, text))?,
1530 _ => last_identifier_child(node, text)?,
1531 }
1532 .trim()
1533 .to_string();
1534 if name.is_empty() {
1535 return None;
1536 }
1537 Some(CallTarget {
1538 name,
1539 scope: call_scope_name(node, text),
1540 })
1541}
1542
1543#[cfg(feature = "tree_sitter")]
1544fn last_identifier_child(node: tree_sitter::Node<'_>, text: &str) -> Option<String> {
1546 let mut cursor = node.walk();
1547 node.children(&mut cursor)
1548 .filter_map(|child| final_call_target(child, text).map(|target| target.name))
1549 .last()
1550}
1551
1552#[cfg(feature = "tree_sitter")]
1553fn call_scope_name(node: tree_sitter::Node<'_>, text: &str) -> Option<String> {
1555 if node.kind() != "scoped_identifier" {
1556 return None;
1557 }
1558 node.child_by_field_name("path")
1559 .or_else(|| node.child_by_field_name("scope"))
1560 .map(|scope| node_text(text, scope).trim().to_string())
1561 .filter(|scope| !scope.is_empty())
1562}
1563
1564#[cfg(feature = "tree_sitter")]
1565fn symbol_parent_name<'a>(symbols: &'a [CodeSymbol], symbol: &CodeSymbol) -> Option<&'a str> {
1567 let parent = symbol.parent?;
1568 symbols
1569 .iter()
1570 .find(|candidate| candidate.id == parent)
1571 .map(|candidate| candidate.name.as_str())
1572}
1573
1574fn push_symbol(
1576 symbols: &mut Vec<CodeSymbol>,
1577 language: EditorLanguage,
1578 kind: SymbolKind,
1579 name: String,
1580 start: usize,
1581 line: &str,
1582) {
1583 if name.is_empty() {
1584 return;
1585 }
1586 let id = SymbolId(symbols.len() as u64 + 1);
1587 let range = start..start + line.trim_end_matches('\n').len();
1588 let name_start = range.start
1589 + line[range.start.saturating_sub(start)..]
1590 .find(&name)
1591 .unwrap_or(0);
1592 symbols.push(CodeSymbol {
1593 id,
1594 name,
1595 kind,
1596 language,
1597 range,
1598 selection_range: name_start..name_start + symbols.last().map_or(0, |_| 0),
1599 parent: None,
1600 signature: None,
1601 docs: None,
1602 source: SymbolSource::Lexical,
1603 });
1604 let last = symbols.last_mut().expect("symbol just pushed");
1605 last.selection_range = name_start..name_start + last.name.len();
1606}
1607
1608fn take_ident(rest: &str) -> String {
1610 rest.chars()
1611 .take_while(|ch| *ch == '_' || ch.is_ascii_alphanumeric())
1612 .collect()
1613}
1614
1615fn lines_with_offsets(text: &str) -> impl Iterator<Item = (usize, &str)> {
1617 let mut offset = 0;
1618 text.split_inclusive('\n').map(move |line| {
1619 let current = offset;
1620 offset += line.len();
1621 (current, line)
1622 })
1623}