diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/app.rs | 259 | ||||
-rw-r--r-- | src/config.rs | 26 | ||||
-rw-r--r-- | src/main.rs | 102 |
3 files changed, 387 insertions, 0 deletions
diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..285034b --- /dev/null +++ b/src/app.rs | |||
@@ -0,0 +1,259 @@ | |||
1 | use crate::config::Config; | ||
2 | |||
3 | use std::{ | ||
4 | collections::HashMap, | ||
5 | fmt::Write, | ||
6 | path::{Path, PathBuf}, | ||
7 | }; | ||
8 | |||
9 | use console::{style, Style, Term}; | ||
10 | use tree_sitter::{Node, Parser, Query, QueryCursor, Range, Tree}; | ||
11 | |||
12 | pub struct App { | ||
13 | config: Config, | ||
14 | language: tree_sitter::Language, | ||
15 | path: PathBuf, | ||
16 | query: Option<Query>, | ||
17 | query_path: Option<PathBuf>, | ||
18 | src: Vec<u8>, | ||
19 | tree: Tree, | ||
20 | } | ||
21 | |||
22 | impl App { | ||
23 | pub fn new<'a, P: AsRef<Path>>( | ||
24 | src: &'a [u8], | ||
25 | path: P, | ||
26 | query_path: Option<P>, | ||
27 | language: tree_sitter::Language, | ||
28 | ) -> Self { | ||
29 | let path = path.as_ref().to_owned(); | ||
30 | |||
31 | let mut parser = Parser::new(); | ||
32 | parser.set_language(&language).unwrap(); | ||
33 | |||
34 | let tree = parser.parse(&src, None).unwrap(); | ||
35 | let query_path = query_path.map(|q| q.as_ref().to_owned()); | ||
36 | let query = query_path.as_ref().map(|p| { | ||
37 | let query_src = std::fs::read_to_string(&p).expect("unable to read query"); | ||
38 | Query::new(&language, &query_src).expect("query parse error") | ||
39 | }); | ||
40 | |||
41 | Self { | ||
42 | config: Default::default(), | ||
43 | path, | ||
44 | query, | ||
45 | query_path, | ||
46 | src: src.to_owned(), | ||
47 | tree, | ||
48 | language, | ||
49 | } | ||
50 | } | ||
51 | |||
52 | pub fn draw(&self) { | ||
53 | let term = Term::stdout(); | ||
54 | term.clear_screen().unwrap(); | ||
55 | let mut done = false; | ||
56 | let mut depth = 0; | ||
57 | let mut in_capture: Option<Range> = None; | ||
58 | let mut cursor = self.tree.walk(); | ||
59 | |||
60 | let capture_names = self | ||
61 | .query | ||
62 | .as_ref() | ||
63 | .map(|q| q.capture_names()) | ||
64 | .unwrap_or_default(); | ||
65 | let capture_map = self | ||
66 | .query | ||
67 | .as_ref() | ||
68 | .map(|query| { | ||
69 | QueryCursor::new() | ||
70 | .matches(&query, self.tree.root_node(), self.src.as_slice()) | ||
71 | .flat_map(|match_| match_.captures) | ||
72 | .fold( | ||
73 | HashMap::new(), | ||
74 | |mut map: HashMap<Node, Vec<u32>>, capture| { | ||
75 | map.entry(capture.node) | ||
76 | .and_modify(|idxs| idxs.push(capture.index)) | ||
77 | .or_insert_with(|| vec![capture.index]); | ||
78 | map | ||
79 | }, | ||
80 | ) | ||
81 | }) | ||
82 | .unwrap_or_default(); | ||
83 | |||
84 | while !done { | ||
85 | let node = cursor.node(); | ||
86 | let mut tree_string = String::new(); | ||
87 | in_capture = match in_capture { | ||
88 | Some(range) | ||
89 | if !contains(&range, &node.range()) && capture_map.contains_key(&node) => | ||
90 | { | ||
91 | Some(node.range()) | ||
92 | } | ||
93 | Some(range) if !contains(&range, &node.range()) => None, | ||
94 | None if capture_map.contains_key(&node) => Some(node.range()), | ||
95 | i => i, | ||
96 | }; | ||
97 | |||
98 | write!( | ||
99 | tree_string, | ||
100 | "{}", | ||
101 | (if in_capture.is_some() { | ||
102 | Style::new().on_yellow().on_bright() | ||
103 | } else { | ||
104 | Style::new() | ||
105 | }) | ||
106 | .bright() | ||
107 | .black() | ||
108 | .apply_to( | ||
109 | format!("{}{}", "|", " ".repeat(self.config.indent_level)) | ||
110 | .repeat(depth as usize) | ||
111 | ) | ||
112 | ) | ||
113 | .unwrap(); | ||
114 | |||
115 | if self.config.show_field_name { | ||
116 | if let Some(f) = cursor.field_name() { | ||
117 | write!( | ||
118 | tree_string, | ||
119 | "{} ", | ||
120 | if in_capture.is_some() { | ||
121 | Style::new().on_yellow().on_bright() | ||
122 | } else { | ||
123 | Style::new() | ||
124 | } | ||
125 | .yellow() | ||
126 | .apply_to(f) | ||
127 | ) | ||
128 | .unwrap() | ||
129 | } | ||
130 | } | ||
131 | |||
132 | write!( | ||
133 | tree_string, | ||
134 | "{} ", | ||
135 | if node.is_error() { | ||
136 | Style::new().red() | ||
137 | } else if in_capture.is_some() { | ||
138 | Style::new().on_yellow().on_bright() | ||
139 | } else { | ||
140 | Style::new() | ||
141 | } | ||
142 | .apply_to(node.kind()), | ||
143 | ) | ||
144 | .unwrap(); | ||
145 | |||
146 | if let Some(idxs) = capture_map.get(&node) { | ||
147 | for index in idxs { | ||
148 | write!( | ||
149 | tree_string, | ||
150 | "@{} ", | ||
151 | style(capture_names[*index as usize]).magenta() | ||
152 | ) | ||
153 | .unwrap(); | ||
154 | } | ||
155 | } | ||
156 | |||
157 | if self.config.show_ranges { | ||
158 | let range = node.range(); | ||
159 | write!( | ||
160 | tree_string, | ||
161 | " {}", | ||
162 | style(format!("{:?}..{:?}", range.start_byte, range.end_byte,)) | ||
163 | .bright() | ||
164 | .black() | ||
165 | ) | ||
166 | .unwrap(); | ||
167 | } | ||
168 | |||
169 | if self.config.show_src { | ||
170 | write!( | ||
171 | tree_string, | ||
172 | " {:.?}", | ||
173 | style(node.utf8_text(&self.src).unwrap()).cyan() | ||
174 | ) | ||
175 | .unwrap(); | ||
176 | } | ||
177 | |||
178 | term.write_line(&tree_string).unwrap(); | ||
179 | term.clear_to_end_of_screen().unwrap(); | ||
180 | |||
181 | if cursor.goto_first_child() { | ||
182 | depth += 1; | ||
183 | continue; | ||
184 | } | ||
185 | if cursor.goto_next_sibling() { | ||
186 | continue; | ||
187 | } | ||
188 | |||
189 | loop { | ||
190 | if !cursor.goto_parent() { | ||
191 | done = true; | ||
192 | break; | ||
193 | } else { | ||
194 | depth -= 1; | ||
195 | } | ||
196 | |||
197 | if cursor.goto_next_sibling() { | ||
198 | break; | ||
199 | } | ||
200 | } | ||
201 | } | ||
202 | |||
203 | // see https://github.com/console-rs/console/issues/36#issuecomment-624731432 | ||
204 | // for the reasoning behing this hackjob | ||
205 | |||
206 | term.write_line("\n(>) increase indent").unwrap(); | ||
207 | term.clear_to_end_of_screen().unwrap(); | ||
208 | |||
209 | term.write_line("(<) decrease indent ").unwrap(); | ||
210 | term.clear_to_end_of_screen().unwrap(); | ||
211 | |||
212 | term.write_line("(n) toggle ranges").unwrap(); | ||
213 | term.clear_to_end_of_screen().unwrap(); | ||
214 | |||
215 | term.write_line("(s) toggle source text").unwrap(); | ||
216 | term.clear_to_end_of_screen().unwrap(); | ||
217 | |||
218 | term.write_line("(r) reload from disk").unwrap(); | ||
219 | term.clear_to_end_of_screen().unwrap(); | ||
220 | |||
221 | term.write_line("(C-c) quit").unwrap(); | ||
222 | term.clear_to_end_of_screen().unwrap(); | ||
223 | } | ||
224 | |||
225 | pub fn increase_indent(&mut self) { | ||
226 | self.config.indent_level = self.config.indent_level.saturating_add(1); | ||
227 | } | ||
228 | |||
229 | pub fn decrease_indent(&mut self) { | ||
230 | self.config.indent_level = self.config.indent_level.saturating_sub(1); | ||
231 | } | ||
232 | |||
233 | pub fn toggle_ranges(&mut self) { | ||
234 | self.config.show_ranges = !self.config.show_ranges; | ||
235 | } | ||
236 | |||
237 | pub fn toggle_source(&mut self) { | ||
238 | self.config.show_src = !self.config.show_src; | ||
239 | } | ||
240 | |||
241 | pub fn reload(&mut self) { | ||
242 | let src = std::fs::read_to_string(&self.path).unwrap(); | ||
243 | let new = Self::new( | ||
244 | src.as_bytes(), | ||
245 | &self.path, | ||
246 | self.query_path.as_ref(), | ||
247 | self.language.clone(), | ||
248 | ); | ||
249 | *self = Self { | ||
250 | config: self.config, | ||
251 | ..new | ||
252 | }; | ||
253 | } | ||
254 | } | ||
255 | |||
256 | // does a encompass b | ||
257 | fn contains(a: &Range, b: &Range) -> bool { | ||
258 | a.start_byte <= b.start_byte && a.end_byte >= b.end_byte | ||
259 | } | ||
diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..18ed5cc --- /dev/null +++ b/src/config.rs | |||
@@ -0,0 +1,26 @@ | |||
1 | use std::default::Default; | ||
2 | |||
3 | #[derive(Clone, Copy)] | ||
4 | pub struct Config { | ||
5 | pub indent_level: usize, | ||
6 | pub show_ranges: bool, | ||
7 | pub show_src: bool, | ||
8 | pub show_field_name: bool, | ||
9 | } | ||
10 | |||
11 | impl Default for Config { | ||
12 | fn default() -> Self { | ||
13 | Config::new() | ||
14 | } | ||
15 | } | ||
16 | |||
17 | impl Config { | ||
18 | fn new() -> Self { | ||
19 | Self { | ||
20 | indent_level: 2, | ||
21 | show_ranges: true, | ||
22 | show_src: true, | ||
23 | show_field_name: true, | ||
24 | } | ||
25 | } | ||
26 | } | ||
diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ce7c84c --- /dev/null +++ b/src/main.rs | |||
@@ -0,0 +1,102 @@ | |||
1 | mod app; | ||
2 | mod config; | ||
3 | |||
4 | use std::{ | ||
5 | env, fs, | ||
6 | path::Path, | ||
7 | sync::{mpsc, Arc, RwLock}, | ||
8 | thread, | ||
9 | time::Duration, | ||
10 | }; | ||
11 | |||
12 | use app::App; | ||
13 | use console::{Key, Term}; | ||
14 | use notify::{Event as WatchEvent, EventKind as WatchEventKind, RecursiveMode, Watcher}; | ||
15 | |||
16 | fn main() { | ||
17 | let mut args = env::args(); | ||
18 | let _ = args.next(); | ||
19 | |||
20 | let language = match args.next().as_ref().map(|s| s.as_str()) { | ||
21 | Some("rust") => tree_sitter_rust::language(), | ||
22 | Some("tsx") | Some("typescript") => tree_sitter_typescript::language_tsx(), | ||
23 | Some("javascript") => tree_sitter_javascript::language(), | ||
24 | Some("python") => tree_sitter_python::language(), | ||
25 | Some("ruby") => tree_sitter_ruby::language(), | ||
26 | Some("markdown") => tree_sitter_md::language(), | ||
27 | Some(s) => panic!("invalid language passed: {s}"), | ||
28 | None => panic!("no language passed"), | ||
29 | }; | ||
30 | let path = args.next().expect("no arg passed"); | ||
31 | let query_path = args.next(); | ||
32 | let src = fs::read_to_string(&path).expect("unable to read file"); | ||
33 | |||
34 | let app = Arc::new(RwLock::new(App::new( | ||
35 | src.as_bytes(), | ||
36 | &path, | ||
37 | query_path.as_ref(), | ||
38 | language, | ||
39 | ))); | ||
40 | |||
41 | let watch_fn = |watcher_app: Arc<RwLock<App>>| { | ||
42 | move |ev| { | ||
43 | if let Ok(WatchEvent { | ||
44 | kind: WatchEventKind::Modify(..), | ||
45 | .. | ||
46 | }) = ev | ||
47 | { | ||
48 | if let Ok(mut locked) = watcher_app.try_write() { | ||
49 | locked.reload(); | ||
50 | locked.draw(); | ||
51 | }; | ||
52 | } | ||
53 | } | ||
54 | }; | ||
55 | |||
56 | let mut watcher1 = notify::recommended_watcher(watch_fn(Arc::clone(&app))).unwrap(); | ||
57 | watcher1 | ||
58 | .watch(Path::new(&path), RecursiveMode::NonRecursive) | ||
59 | .unwrap(); | ||
60 | |||
61 | let mut watcher2 = notify::recommended_watcher(watch_fn(Arc::clone(&app))).unwrap(); | ||
62 | if let Some(query_path) = query_path { | ||
63 | watcher2 | ||
64 | .watch(Path::new(&query_path), RecursiveMode::NonRecursive) | ||
65 | .unwrap(); | ||
66 | } | ||
67 | |||
68 | let (tx, rx) = mpsc::channel(); | ||
69 | let tx0 = tx.clone(); | ||
70 | thread::spawn(move || { | ||
71 | let term = Term::stdout(); | ||
72 | loop { | ||
73 | if let Ok(Key::Char(ev)) = term.read_key() { | ||
74 | tx0.send(ev).unwrap(); | ||
75 | } | ||
76 | } | ||
77 | }); | ||
78 | |||
79 | if let Ok(locked) = app.try_read() { | ||
80 | locked.draw(); | ||
81 | } | ||
82 | |||
83 | loop { | ||
84 | match rx.try_recv() { | ||
85 | Ok(ev) => { | ||
86 | if let Ok(mut locked) = app.try_write() { | ||
87 | match ev { | ||
88 | '>' => locked.increase_indent(), | ||
89 | '<' => locked.decrease_indent(), | ||
90 | 'n' => locked.toggle_ranges(), | ||
91 | 's' => locked.toggle_source(), | ||
92 | 'r' => locked.reload(), | ||
93 | _ => (), | ||
94 | } | ||
95 | locked.draw(); | ||
96 | } | ||
97 | } | ||
98 | _ => (), | ||
99 | } | ||
100 | thread::sleep(Duration::from_millis(10)); | ||
101 | } | ||
102 | } | ||