1#![doc = include_str!("../README.md")]
2#![deny(clippy::unwrap_used, clippy::expect_used)]
3#![warn(clippy::pedantic, clippy::nursery, clippy::cargo)]
4#![deny(missing_debug_implementations, clippy::missing_panics_doc)]
5#![warn(clippy::pedantic, clippy::nursery, clippy::cargo)]
6#![deny(clippy::use_self, rust_2018_idioms)]
7use core::fmt;
8
9use supported_languages::SupportedLanguage;
10use tree_sitter::{Language, LanguageError, Node, QueryError, Range, Tree};
11
12pub mod supported_languages;
14
15fn run_query<'a>(
16 query_str: &'a str,
17 lang: Language,
18 node: Node<'a>,
19 code: &'a [u8],
20) -> Result<Box<[Range]>, QueryError> {
21 let query = tree_sitter::Query::new(lang, query_str)?;
22 let mut query_cursor = tree_sitter::QueryCursor::new();
23 let matches = query_cursor.matches(&query, node, code);
24 let ranges = matches.map(|m| m.captures[0].node.range());
25 Ok(ranges.collect())
26}
27
28#[derive(Debug)]
30pub enum Error {
31 FileTypeUnkown(String),
34 ParseError(String),
36 GrammarLoad(&'static str, LanguageError),
38 InvalidQuery(&'static str, QueryError),
40 NoSuchResultsForFilter,
42 NoResultsForSearch,
44}
45
46pub fn get_file_type_from_file_ext<'a>(
52 ext: &str,
53 langs: &'a [&'a dyn SupportedLanguage],
54) -> Result<&'a dyn SupportedLanguage, Error> {
55 langs
56 .iter()
57 .find(|lang| lang.file_exts().contains(&ext))
58 .copied()
59 .ok_or_else(|| Error::FileTypeUnkown(ext.to_string()))
60}
61
62pub fn get_file_type_from_file<'a>(
70 file_name: &str,
71 langs: &'a [&'a dyn SupportedLanguage],
72) -> Result<&'a dyn SupportedLanguage, Error> {
73 file_name
74 .rsplit_once('.')
75 .ok_or_else(|| Error::FileTypeUnkown(file_name.to_string()))
76 .map(|(_, ext)| ext)
77 .and_then(|ext| get_file_type_from_file_ext(ext, langs))
78}
79
80#[derive(Debug, Clone)]
81pub struct ParsedFile<'a> {
84 file: &'a str,
88 file_name: Option<&'a str>,
89 function_name: &'a str,
90 language_type: &'a str,
93 tree: Tree,
94 results: Box<[Range]>,
95}
96
97impl<'a> ParsedFile<'a> {
98 #[must_use]
99 pub fn new(
100 file: &'a str,
101 function_name: &'a str,
102 language_type: &'a str,
103 tree: Tree,
104 results: Box<[Range]>,
105 ) -> Self {
106 Self {
107 file,
108 function_name,
109 language_type,
110 tree,
111 results,
112 file_name: None,
113 }
114 }
115
116 pub fn filter(&self, f: fn(&Node<'_>) -> bool) -> Result<Self, Error> {
126 let root = self.tree.root_node();
127 let ranges: Box<[Range]> = self
128 .ranges()
129 .filter_map(|range| root.descendant_for_point_range(range.start_point, range.end_point))
130 .filter(f)
131 .map(|n| n.range())
132 .collect();
133 if ranges.is_empty() {
134 return Err(Error::NoSuchResultsForFilter);
135 }
136 let clone = Self {
137 results: ranges,
138 ..self.clone()
139 };
140 Ok(clone)
141 }
142
143 #[must_use]
144 pub const fn language(&self) -> &str {
146 self.language_type
147 }
148
149 #[must_use]
150 pub const fn search_name(&self) -> &str {
152 self.function_name
153 }
154
155 fn ranges(&self) -> impl Iterator<Item = &Range> {
156 self.results.iter()
157 }
158 pub fn search_file(
170 name: &'a str,
171 code: &'a str,
172 language: &'a dyn SupportedLanguage,
173 ) -> Result<Self, Error> {
174 let code_bytes = code.as_bytes();
175 let mut parser = tree_sitter::Parser::new();
176 let ts_lang = language.language();
177 parser
178 .set_language(ts_lang)
179 .map_err(|lang_err| Error::GrammarLoad(language.name(), lang_err))?;
180 let parsed = parser
181 .parse(code, None)
182 .ok_or_else(|| Error::ParseError(code.to_string()))?;
183
184 let query_str = language.query(name);
185 let node = parsed.root_node();
186 let command_ranges = run_query(&query_str, ts_lang, node, code_bytes)
187 .map_err(|query_err| Error::InvalidQuery(language.name(), query_err))?;
188
189 if command_ranges.is_empty() {
190 return Err(Error::NoResultsForSearch);
191 }
192 Ok(ParsedFile::new(
193 code,
194 name,
195 language.name(),
196 parsed,
197 command_ranges,
198 ))
199 }
200
201 pub fn search_file_with_name(
210 name: &'a str,
211 code: &'a str,
212 file_name: &'a str,
213 langs: &'a [&'a dyn SupportedLanguage],
214 ) -> Result<Self, Error> {
215 get_file_type_from_file(file_name, langs)
216 .and_then(|language| Self::search_file(name, code, language))
217 .map(|file| file.set_file_name(file_name))
218 }
219
220 fn set_file_name(mut self, file_name: &'a str) -> Self {
221 self.file_name.replace(file_name);
222 self
223 }
224
225 #[must_use]
226 pub const fn file_name(&self) -> Option<&str> {
228 self.file_name
229 }
230
231 #[must_use]
232 pub fn results(&self) -> &[Range] {
234 &self.results
235 }
236}
237
238impl IntoIterator for ParsedFile<'_> {
239 type Item = (Range, String);
240
241 type IntoIter = Box<dyn Iterator<Item = Self::Item>>;
242
243 fn into_iter(self) -> Self::IntoIter {
244 Box::new(
245 self.ranges()
246 .map(|range| {
247 (
248 *range,
249 self.file[range.start_byte..range.end_byte].to_string(),
250 )
251 })
252 .collect::<Vec<_>>()
253 .into_iter(),
254 )
255 }
256}
257
258impl fmt::Display for ParsedFile<'_> {
259 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260 let lines = self.file.lines().enumerate();
261 let texts = self
262 .ranges()
263 .map(move |range| {
264 lines
265 .clone()
266 .filter_map(move |(line, str)| {
267 if line >= range.start_point.row && line <= range.end_point.row {
268 Some(format!("{}: {str}", line + 1))
269 } else {
270 None
271 }
272 })
273 .collect::<Vec<_>>()
274 .join("\n")
275 })
276 .collect::<Vec<_>>()
277 .join("\n...\n");
278 write!(f, "{texts}")
279 }
280}