winx_code_agent/utils/
symbols.rs1use tree_sitter_tags::{TagsConfiguration, TagsContext};
9
10#[derive(Debug, Clone)]
12pub struct Symbol {
13 pub name: String,
15 pub kind: String,
18 pub line: usize,
20 pub is_definition: bool,
22}
23
24fn lang_and_query(ext: &str) -> Option<(tree_sitter::Language, &'static str)> {
27 let pair = match ext {
28 "rs" => (tree_sitter_rust::LANGUAGE.into(), tree_sitter_rust::TAGS_QUERY),
29 "js" | "mjs" | "cjs" | "jsx" => {
30 (tree_sitter_javascript::LANGUAGE.into(), tree_sitter_javascript::TAGS_QUERY)
31 }
32 "ts" => {
33 (tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(), tree_sitter_typescript::TAGS_QUERY)
34 }
35 "tsx" => (tree_sitter_typescript::LANGUAGE_TSX.into(), tree_sitter_typescript::TAGS_QUERY),
36 "go" => (tree_sitter_go::LANGUAGE.into(), tree_sitter_go::TAGS_QUERY),
37 "c" | "h" => (tree_sitter_c::LANGUAGE.into(), tree_sitter_c::TAGS_QUERY),
38 "cpp" | "cc" | "cxx" | "hpp" | "hxx" | "hh" => {
39 (tree_sitter_cpp::LANGUAGE.into(), tree_sitter_cpp::TAGS_QUERY)
40 }
41 "java" => (tree_sitter_java::LANGUAGE.into(), tree_sitter_java::TAGS_QUERY),
42 "rb" => (tree_sitter_ruby::LANGUAGE.into(), tree_sitter_ruby::TAGS_QUERY),
43 "cs" => (tree_sitter_c_sharp::LANGUAGE.into(), tree_sitter_c_sharp::TAGS_QUERY),
44 "php" => (tree_sitter_php::LANGUAGE_PHP.into(), tree_sitter_php::TAGS_QUERY),
45 "lua" => (tree_sitter_lua::LANGUAGE.into(), tree_sitter_lua::TAGS_QUERY),
46 _ => return None,
47 };
48 Some(pair)
49}
50
51pub fn supports(ext: &str) -> bool {
53 lang_and_query(ext).is_some()
54}
55
56pub fn config_for(ext: &str) -> Option<TagsConfiguration> {
60 let (language, query) = lang_and_query(ext)?;
61 TagsConfiguration::new(language, query, "").ok()
62}
63
64pub fn extract(context: &mut TagsContext, config: &TagsConfiguration, text: &str) -> Vec<Symbol> {
67 collect(context, config, text, true)
68}
69
70pub fn extract_all(
72 context: &mut TagsContext,
73 config: &TagsConfiguration,
74 text: &str,
75) -> Vec<Symbol> {
76 collect(context, config, text, false)
77}
78
79fn collect(
83 context: &mut TagsContext,
84 config: &TagsConfiguration,
85 text: &str,
86 definitions_only: bool,
87) -> Vec<Symbol> {
88 let source = text.as_bytes();
89 let mut out = Vec::new();
90 let Ok((tags, _)) = context.generate_tags(config, source, None) else {
91 return out;
92 };
93 for tag in tags.flatten() {
94 if definitions_only && !tag.is_definition {
95 continue;
96 }
97 let name = String::from_utf8_lossy(&source[tag.name_range.clone()]).into_owned();
98 if name.is_empty() {
99 continue;
100 }
101 out.push(Symbol {
102 name,
103 kind: config.syntax_type_name(tag.syntax_type_id).to_string(),
104 line: tag.span.start.row + 1,
105 is_definition: tag.is_definition,
106 });
107 }
108 out
109}
110
111#[cfg(test)]
112mod tests {
113 #![allow(clippy::expect_used)]
114 use super::*;
115
116 #[test]
117 fn extracts_rust_symbols() {
118 let mut ctx = TagsContext::new();
119 let cfg = config_for("rs").expect("rust config");
120 let src = "pub fn alpha() {}\nstruct Beta;\nimpl Beta { fn gamma(&self) {} }\n";
121 let syms = extract(&mut ctx, &cfg, src);
122 let names: Vec<&str> = syms.iter().map(|s| s.name.as_str()).collect();
123 assert!(names.contains(&"alpha"), "got {names:?}");
124 assert!(names.contains(&"Beta"), "got {names:?}");
125 assert!(names.contains(&"gamma"), "got {names:?}");
126 assert_eq!(syms.iter().find(|s| s.name == "alpha").map(|s| s.line), Some(1));
128 }
129
130 #[test]
131 fn unsupported_extension_has_no_config() {
132 assert!(config_for("json").is_none());
133 assert!(!supports("css"));
134 assert!(supports("rs"));
135 assert!(supports("ts"));
136 }
137}