reference_query/lang/
mod.rs1use tree_sitter::{Language, Node, Parser};
8
9use crate::core::{Kind, Symbol};
10
11pub(crate) mod go;
12pub(crate) mod python;
13pub(crate) mod ruby;
14pub(crate) mod rust;
15pub(crate) mod typescript;
16
17pub(crate) struct Ctx<'a> {
20 src: &'a [u8],
21 file: &'a str,
22 language: &'static str,
23}
24
25impl Ctx<'_> {
26 pub(crate) fn field_text(&self, node: Node, field: &str) -> Option<String> {
28 node.child_by_field_name(field)
29 .and_then(|n| n.utf8_text(self.src).ok())
30 .map(str::to_string)
31 }
32
33 pub(crate) fn node_text(&self, node: Node) -> Option<String> {
35 node.utf8_text(self.src).ok().map(str::to_string)
36 }
37
38 pub(crate) fn symbol(
40 &self,
41 name: &str,
42 kind: Kind,
43 node: Node,
44 parent: Option<&str>,
45 ) -> Symbol {
46 Symbol {
47 name: name.to_string(),
48 kind,
49 language: self.language.to_string(),
50 file: self.file.to_string(),
51 line: node.start_position().row as u32 + 1,
52 end_line: node.end_position().row as u32 + 1,
53 parent: parent.map(str::to_string),
54 visibility: None, }
56 }
57}
58
59pub(crate) fn qualify(parent: Option<&str>, name: &str, sep: &str) -> String {
61 match parent {
62 Some(p) => format!("{p}{sep}{name}"),
63 None => name.to_string(),
64 }
65}
66
67thread_local! {
68 static PARSERS: std::cell::RefCell<std::collections::HashMap<&'static str, Parser>> =
72 std::cell::RefCell::new(std::collections::HashMap::new());
73}
74
75pub(crate) fn extract_with(
80 language: &'static str,
81 grammar: Language,
82 file: &str,
83 source: &str,
84 walk: impl FnOnce(&Ctx, Node, &mut Vec<Symbol>),
85) -> Vec<Symbol> {
86 extract_with_key(language, language, grammar, file, source, walk)
87}
88
89pub(crate) fn extract_with_key(
94 key: &'static str,
95 language: &'static str,
96 grammar: Language,
97 file: &str,
98 source: &str,
99 walk: impl FnOnce(&Ctx, Node, &mut Vec<Symbol>),
100) -> Vec<Symbol> {
101 PARSERS.with(|cell| {
102 let mut parsers = cell.borrow_mut();
103 let parser = match parsers.entry(key) {
104 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
105 std::collections::hash_map::Entry::Vacant(v) => {
106 let mut p = Parser::new();
107 if p.set_language(&grammar).is_err() {
108 return Vec::new();
109 }
110 v.insert(p)
111 }
112 };
113 let Some(tree) = parser.parse(source, None) else {
114 return Vec::new();
115 };
116 let mut out = Vec::new();
117 let ctx = Ctx {
118 src: source.as_bytes(),
119 file,
120 language,
121 };
122 walk(&ctx, tree.root_node(), &mut out);
123 out
124 })
125}
126
127pub(crate) trait LanguagePlugin {
129 fn language(&self) -> &'static str;
132
133 fn extensions(&self) -> &[&str];
135
136 fn extract(&self, file: &str, source: &str) -> Vec<Symbol>;
139}
140
141static REGISTRY: [&(dyn LanguagePlugin + Sync); 6] = [
143 &ruby::Ruby,
144 &rust::Rust,
145 &go::Go,
146 &python::Python,
147 &typescript::TypeScript,
148 &typescript::JavaScript,
149];
150
151pub(crate) fn languages() -> Vec<&'static str> {
154 registry().iter().map(|p| p.language()).collect()
155}
156
157pub(crate) fn registry() -> &'static [&'static (dyn LanguagePlugin + Sync)] {
159 ®ISTRY
160}
161
162pub(crate) fn plugin_for_extension(ext: &str) -> Option<&'static (dyn LanguagePlugin + Sync)> {
164 REGISTRY
165 .iter()
166 .copied()
167 .find(|p| p.extensions().contains(&ext))
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn languages_are_registered_by_extension() {
176 for ext in ["rb", "rs", "go", "py", "ts", "tsx", "js", "jsx"] {
177 assert!(plugin_for_extension(ext).is_some(), "{ext} should resolve");
178 }
179 assert!(plugin_for_extension("java").is_none());
180 }
181}