1use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
13use nucleo_matcher::pattern::{CaseMatching, Normalization, Pattern};
14use nucleo_matcher::{Config, Matcher, Utf32Str};
15use tui_input::Input;
16use tui_input::backend::crossterm::EventHandler;
17
18use crate::keys::Key;
19use crate::tree::{NodeId, Tree};
20
21struct Candidate {
24 path: String,
25 active: bool,
26}
27
28#[derive(Clone, Debug, PartialEq, Eq)]
31pub struct Match {
32 pub id: NodeId,
33 pub score: u32,
34 pub indices: Vec<u32>,
35}
36
37#[derive(Clone, Debug, PartialEq, Eq)]
39pub enum JumpOutcome {
40 Stay,
42 Accept(NodeId),
44 Cancel,
46}
47
48pub struct Jump {
52 input: Input,
53 candidates: Vec<Candidate>,
55 candidate_count: usize,
56 results: Vec<Match>,
57 selected: usize,
59 scroll: usize,
61 page_height: usize,
63 matcher: Matcher,
64}
65
66impl Jump {
67 pub fn open(tree: &Tree) -> Self {
70 let candidates: Vec<_> = (0..tree.len())
71 .map(|id| Candidate {
72 path: tree.jump_key(id),
73 active: tree.is_in_view(id),
74 })
75 .collect();
76 let candidate_count = candidates
77 .iter()
78 .filter(|candidate| candidate.active)
79 .count();
80 let mut jump = Self {
81 input: Input::default(),
82 candidates,
83 candidate_count,
84 results: Vec::new(),
85 selected: 0,
86 scroll: 0,
87 page_height: 10,
88 matcher: Matcher::new(Config::DEFAULT.match_paths()),
89 };
90 jump.rank();
91 jump
92 }
93
94 pub fn handle_key(&mut self, key: Key) -> JumpOutcome {
99 let ctrl = key.mods == KeyModifiers::CONTROL;
100 match key.code {
101 KeyCode::Esc => return JumpOutcome::Cancel,
102 KeyCode::Char('c') if ctrl => return JumpOutcome::Cancel,
103 KeyCode::Enter => {
104 return match self.results.get(self.selected) {
105 Some(m) => JumpOutcome::Accept(m.id),
106 None => JumpOutcome::Stay,
107 };
108 }
109 KeyCode::Down => self.move_selection(1),
110 KeyCode::Up => self.move_selection(-1),
111 KeyCode::Char('j' | 'n') if ctrl => self.move_selection(1),
112 KeyCode::Char('k' | 'p') if ctrl => self.move_selection(-1),
113 _ => {
118 let event = Event::Key(KeyEvent::new(key.code, key.mods));
119 if self
120 .input
121 .handle_event(&event)
122 .is_some_and(|change| change.value)
123 {
124 self.rank();
125 }
126 }
127 }
128 JumpOutcome::Stay
129 }
130
131 fn rank(&mut self) {
139 self.selected = 0;
140 self.scroll = 0;
141 self.results.clear();
142 if self.input.value().is_empty() {
143 self.results = self
145 .candidates
146 .iter()
147 .enumerate()
148 .filter(|(_, candidate)| candidate.active)
149 .map(|(id, _)| Match {
150 id,
151 score: 0,
152 indices: Vec::new(),
153 })
154 .collect();
155 return;
156 }
157 let pattern = Pattern::parse(
158 self.input.value(),
159 CaseMatching::Smart,
160 Normalization::Smart,
161 );
162 let mut buf = Vec::new();
163 for (id, candidate) in self
164 .candidates
165 .iter()
166 .enumerate()
167 .filter(|(_, candidate)| candidate.active)
168 {
169 let mut indices = Vec::new();
170 let haystack = Utf32Str::new(&candidate.path, &mut buf);
171 if let Some(score) = pattern.indices(haystack, &mut self.matcher, &mut indices) {
172 indices.sort_unstable();
175 indices.dedup();
176 self.results.push(Match { id, score, indices });
177 }
178 }
179 let candidates = &self.candidates;
181 self.results.sort_by(|a, b| {
182 b.score
183 .cmp(&a.score)
184 .then_with(|| {
185 candidates[a.id]
186 .path
187 .len()
188 .cmp(&candidates[b.id].path.len())
189 })
190 .then_with(|| a.id.cmp(&b.id))
191 });
192 }
193
194 fn move_selection(&mut self, delta: isize) {
195 if self.results.is_empty() {
196 return;
197 }
198 let last = self.results.len() as isize - 1;
199 self.selected = (self.selected as isize + delta).clamp(0, last) as usize;
200 self.follow_selection();
201 }
202
203 fn follow_selection(&mut self) {
205 let height = self.page_height.max(1);
206 if self.selected < self.scroll {
207 self.scroll = self.selected;
208 } else if self.selected >= self.scroll + height {
209 self.scroll = self.selected + 1 - height;
210 }
211 }
212
213 pub fn query(&self) -> &str {
216 self.input.value()
217 }
218
219 pub fn visual_cursor(&self) -> usize {
222 self.input.visual_cursor()
223 }
224
225 pub fn visual_scroll(&self, width: usize) -> usize {
228 self.input.visual_scroll(width)
229 }
230
231 pub fn results(&self) -> &[Match] {
232 &self.results
233 }
234
235 pub fn selected(&self) -> usize {
236 self.selected
237 }
238
239 pub fn scroll(&self) -> usize {
240 self.scroll
241 }
242
243 pub fn matched(&self) -> usize {
245 self.results.len()
246 }
247
248 pub fn total(&self) -> usize {
250 self.candidate_count
251 }
252
253 pub fn path(&self, id: NodeId) -> &str {
255 &self.candidates[id].path
256 }
257
258 pub fn set_viewport(&mut self, rows: usize) {
261 self.page_height = rows.max(1);
262 self.follow_selection();
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269 use crate::tree::ActionValues;
270
271 fn jump_over(paths: &[&str]) -> Jump {
274 let mut tree = Tree::new();
275 for p in paths {
276 tree.push(None, *p, false, ActionValues::new(*p, *p, *p));
277 }
278 Jump::open(&tree)
279 }
280
281 fn ch(c: char) -> Key {
282 Key::new(KeyCode::Char(c), KeyModifiers::NONE)
283 }
284
285 fn ctrl(c: char) -> Key {
286 Key::new(KeyCode::Char(c), KeyModifiers::CONTROL)
287 }
288
289 fn key(code: KeyCode) -> Key {
290 Key::new(code, KeyModifiers::NONE)
291 }
292
293 fn type_str(j: &mut Jump, s: &str) {
294 for c in s.chars() {
295 j.handle_key(ch(c));
296 }
297 }
298
299 fn result_ids(j: &Jump) -> Vec<NodeId> {
300 j.results().iter().map(|m| m.id).collect()
301 }
302
303 #[test]
304 fn empty_query_lists_all_candidates_in_tree_order() {
305 let j = jump_over(&["a", "b", "c"]);
306 assert_eq!(result_ids(&j), vec![0, 1, 2]);
307 assert_eq!(j.total(), 3);
308 assert_eq!(j.matched(), 3);
309 }
310
311 #[test]
312 fn narrowed_tree_only_offers_nodes_under_the_view_root() {
313 let mut tree = Tree::new();
314 tree.push(None, "outside", false, ActionValues::new("", "", "outside"));
315 let root = tree.push(None, "scope", true, ActionValues::new("", "", "scope"));
316 let child = tree.push(
317 Some(root),
318 "child",
319 false,
320 ActionValues::new("", "", "scope/child"),
321 );
322 tree.set_view_root(Some(root));
323
324 let jump = Jump::open(&tree);
325
326 assert_eq!(result_ids(&jump), vec![root, child]);
327 assert_eq!(jump.total(), 2);
328 assert_eq!(jump.path(child), "scope/child");
329 }
330
331 #[test]
332 fn typing_filters_to_fuzzy_matches() {
333 let mut j = jump_over(&["src/app.rs", "apple/pie", "README"]);
334 type_str(&mut j, "apprs");
335 assert_eq!(result_ids(&j), vec![0]);
337 }
338
339 #[test]
340 fn ranking_puts_the_stronger_match_first() {
341 let mut j = jump_over(&["app.rs", "zz/app.rs"]);
342 type_str(&mut j, "app");
343 assert_eq!(result_ids(&j), vec![0, 1]);
345 }
346
347 #[test]
348 fn extended_syntax_prefix_anchor() {
349 let mut j = jump_over(&["src/a", "x/src"]);
350 type_str(&mut j, "^src");
351 assert_eq!(result_ids(&j), vec![0]);
352 }
353
354 #[test]
355 fn extended_syntax_negation_excludes() {
356 let mut j = jump_over(&["app.rs", "app_test.rs"]);
357 type_str(&mut j, "app !test");
358 assert_eq!(result_ids(&j), vec![0]);
359 }
360
361 #[test]
362 fn space_is_a_second_and_atom() {
363 let mut j = jump_over(&["src/app.rs", "src/xx"]);
364 type_str(&mut j, "src rs");
365 assert_eq!(result_ids(&j), vec![0]);
367 }
368
369 #[test]
370 fn match_indices_are_recorded_for_highlighting() {
371 let mut j = jump_over(&["abcd"]);
372 type_str(&mut j, "abc");
373 assert_eq!(j.results()[0].indices, vec![0, 1, 2]);
374 }
375
376 #[test]
377 fn navigation_keys_move_and_clamp_selection() {
378 let mut j = jump_over(&["a1", "a2", "a3"]);
379 type_str(&mut j, "a");
380 assert_eq!(j.selected(), 0);
381 j.handle_key(ctrl('n'));
382 assert_eq!(j.selected(), 1);
383 j.handle_key(key(KeyCode::Down));
384 assert_eq!(j.selected(), 2);
385 j.handle_key(ctrl('n')); assert_eq!(j.selected(), 2);
387 j.handle_key(ctrl('p'));
388 assert_eq!(j.selected(), 1);
389 j.handle_key(ctrl('k'));
390 assert_eq!(j.selected(), 0);
391 j.handle_key(key(KeyCode::Up)); assert_eq!(j.selected(), 0);
393 }
394
395 #[test]
396 fn enter_accepts_the_selected_result() {
397 let mut j = jump_over(&["a1", "a2", "a3"]);
398 type_str(&mut j, "a");
399 j.handle_key(ctrl('n')); assert_eq!(j.handle_key(key(KeyCode::Enter)), JumpOutcome::Accept(1));
401 }
402
403 #[test]
404 fn enter_with_no_matches_is_a_noop() {
405 let mut j = jump_over(&["a", "b"]);
406 type_str(&mut j, "zzzz");
407 assert_eq!(j.matched(), 0);
408 assert_eq!(j.handle_key(key(KeyCode::Enter)), JumpOutcome::Stay);
409 }
410
411 #[test]
412 fn esc_and_ctrl_c_cancel() {
413 let mut j = jump_over(&["a"]);
414 assert_eq!(j.handle_key(key(KeyCode::Esc)), JumpOutcome::Cancel);
415 assert_eq!(j.handle_key(ctrl('c')), JumpOutcome::Cancel);
416 }
417
418 #[test]
419 fn backspace_edits_the_query_and_reranks() {
420 let mut j = jump_over(&["app.rs", "api.rs"]);
421 type_str(&mut j, "app");
422 assert_eq!(result_ids(&j), vec![0]);
423 j.handle_key(key(KeyCode::Backspace)); assert_eq!(j.query(), "ap");
425 assert_eq!(result_ids(&j), vec![0, 1]);
426 }
427
428 #[test]
429 fn a_new_query_char_resets_the_selection() {
430 let mut j = jump_over(&["a1", "a2", "a3"]);
431 type_str(&mut j, "a");
432 j.handle_key(ctrl('n'));
433 j.handle_key(ctrl('n'));
434 assert_eq!(j.selected(), 2);
435 type_str(&mut j, "3"); assert_eq!(j.selected(), 0);
437 }
438
439 #[test]
440 fn scroll_follows_selection_within_a_small_viewport() {
441 let mut j = jump_over(&["a1", "a2", "a3", "a4", "a5"]);
442 type_str(&mut j, "a");
443 j.set_viewport(2); for _ in 0..4 {
445 j.handle_key(ctrl('n'));
446 }
447 assert_eq!(j.selected(), 4);
448 assert!(
450 j.scroll() <= 4 && 4 < j.scroll() + 2,
451 "scroll={}",
452 j.scroll()
453 );
454 }
455
456 #[test]
457 fn cursor_can_move_and_edit_mid_query() {
458 let mut j = jump_over(&["abc"]);
459 type_str(&mut j, "ac");
460 j.handle_key(key(KeyCode::Left)); assert_eq!(j.visual_cursor(), 1);
462 type_str(&mut j, "b"); assert_eq!(j.query(), "abc");
464 assert_eq!(result_ids(&j), vec![0]);
465 }
466
467 #[test]
468 fn home_end_move_the_caret_to_the_bounds() {
469 let mut j = jump_over(&["x"]);
470 type_str(&mut j, "abc");
471 j.handle_key(key(KeyCode::Home));
472 assert_eq!(j.visual_cursor(), 0);
473 j.handle_key(key(KeyCode::End));
474 assert_eq!(j.visual_cursor(), 3);
475 }
476
477 #[test]
478 fn ctrl_w_deletes_the_previous_word() {
479 let mut j = jump_over(&["foo", "bar"]);
480 type_str(&mut j, "foo bar");
481 j.handle_key(ctrl('w')); assert!(j.query().starts_with("foo"));
483 assert!(!j.query().contains("bar"), "query: {:?}", j.query());
484 }
485
486 #[test]
487 fn moving_the_caret_does_not_rerank_or_reset_selection() {
488 let mut j = jump_over(&["a1", "a2", "a3"]);
489 type_str(&mut j, "a");
490 j.handle_key(ctrl('n'));
491 assert_eq!(j.selected(), 1);
492 j.handle_key(key(KeyCode::Left)); assert_eq!(j.selected(), 1);
494 assert_eq!(result_ids(&j), vec![0, 1, 2]);
495 }
496}