Skip to content

Commit bbe368f

Browse files
committed
feat: Ctrl+f in the glob prompt now toggles the mode from case-insensitive to sensitive.
1 parent 32ab50f commit bbe368f

File tree

4 files changed

+72
-61
lines changed

4 files changed

+72
-61
lines changed

src/interactive/app/eventloop.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,11 @@ impl AppState {
294294
Glob => {
295295
let glob_pane = window.glob_pane.as_mut().expect("glob pane");
296296
match key.code {
297-
Enter => self.search_glob_pattern(&mut tree_view, &glob_pane.input, glob_pane.case_sensitive),
297+
Enter => self.search_glob_pattern(
298+
&mut tree_view,
299+
&glob_pane.input,
300+
glob_pane.case,
301+
),
298302
_ => glob_pane.process_events(key),
299303
}
300304
}
@@ -476,9 +480,19 @@ impl AppState {
476480
}
477481
}
478482

479-
fn search_glob_pattern(&mut self, tree_view: &mut TreeView<'_>, glob_pattern: &str, case_sensitive: bool) {
483+
fn search_glob_pattern(
484+
&mut self,
485+
tree_view: &mut TreeView<'_>,
486+
glob_pattern: &str,
487+
case: gix_glob::pattern::Case,
488+
) {
480489
use FocussedPane::*;
481-
match glob_search(tree_view.tree(), self.navigation.view_root, glob_pattern, case_sensitive) {
490+
match glob_search(
491+
tree_view.tree(),
492+
self.navigation.view_root,
493+
glob_pattern,
494+
case,
495+
) {
482496
Ok(matches) if matches.is_empty() => {
483497
self.message = Some("No match found".into());
484498
}

src/interactive/app/tests/journeys_with_writes.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ fn basic_user_journey_with_deletion() -> Result<()> {
1212
use crate::interactive::app::tests::utils::into_events;
1313

1414
let fixture = WritableFixture::from("sample-02");
15-
let (mut terminal, mut app) = initialized_app_and_terminal_from_paths(&[fixture.root.clone()])?;
15+
let (mut terminal, mut app) =
16+
initialized_app_and_terminal_from_paths(std::slice::from_ref(&fixture.root))?;
1617

1718
// With a selection of items
1819
app.process_events(&mut terminal, into_codes("doddd"))?;

src/interactive/app/tests/unit.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::interactive::app::tests::utils::{
44
use crate::interactive::widgets::glob_search;
55
use anyhow::Result;
66
use dua::traverse::TreeIndex;
7+
use gix_glob::pattern::Case;
78
use pretty_assertions::assert_eq;
89

910
#[test]
@@ -35,19 +36,24 @@ fn it_can_handle_ending_traversal_without_reaching_the_top() -> Result<()> {
3536
#[test]
3637
fn it_can_do_a_glob_search() {
3738
let (tree, root_index) = sample_02_tree(false);
38-
let result = glob_search(&tree, root_index, "tests/fixtures/sample-02", false).unwrap();
39+
let result = glob_search(&tree, root_index, "tests/fixtures/sample-02", Case::Fold).unwrap();
3940
let expected = vec![TreeIndex::from(1)];
4041
assert_eq!(result, expected);
4142
}
4243

4344
#[test]
4445
fn it_can_do_a_case_sensitive_glob_search() {
4546
let (tree, root_index) = sample_02_tree(false);
46-
// This should match case-insensitively
47-
let result_insensitive = glob_search(&tree, root_index, "TESTS/FIXTURES/SAMPLE-02", false).unwrap();
47+
let result_insensitive =
48+
glob_search(&tree, root_index, "TESTS/FIXTURES/SAMPLE-02", Case::Fold).unwrap();
4849
assert_eq!(result_insensitive, vec![TreeIndex::from(1)]);
49-
50-
// This should not match case-sensitively
51-
let result_sensitive = glob_search(&tree, root_index, "TESTS/FIXTURES/SAMPLE-02", true).unwrap();
50+
51+
let result_sensitive = glob_search(
52+
&tree,
53+
root_index,
54+
"TESTS/FIXTURES/SAMPLE-02",
55+
Case::Sensitive,
56+
)
57+
.unwrap();
5258
assert!(result_sensitive.is_empty());
5359
}

src/interactive/widgets/glob.rs

Lines changed: 41 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use bstr::BString;
33
use crosstermion::crossterm::event::KeyEventKind;
44
use crosstermion::input::Key;
55
use dua::traverse::{Tree, TreeIndex};
6+
use gix_glob::pattern::Case;
67
use petgraph::Direction;
78
use std::borrow::Borrow;
89
use tui::{
@@ -26,16 +27,24 @@ pub struct GlobPaneProps {
2627
pub has_focus: bool,
2728
}
2829

29-
#[derive(Default)]
3030
pub struct GlobPane {
3131
pub input: String,
3232
/// The index of the grapheme the cursor currently points to.
3333
/// This hopefully rightfully assumes that a grapheme will be matching the block size on screen
3434
/// and is treated as 'one character'. If not, it will be off, which isn't the end of the world.
3535
// TODO: use `tui-textarea` for proper cursor handling, needs native crossterm events.
3636
cursor_grapheme_idx: usize,
37-
/// Whether the glob search should be case-sensitive
38-
pub case_sensitive: bool,
37+
pub case: Case,
38+
}
39+
40+
impl Default for GlobPane {
41+
fn default() -> Self {
42+
GlobPane {
43+
input: "".to_string(),
44+
cursor_grapheme_idx: 0,
45+
case: Case::Fold,
46+
}
47+
}
3948
}
4049

4150
impl GlobPane {
@@ -46,8 +55,11 @@ impl GlobPane {
4655
return;
4756
}
4857
match key.code {
49-
Char('i') if key.modifiers.contains(KeyModifiers::CONTROL) => {
50-
self.case_sensitive = !self.case_sensitive;
58+
Char('f') if key.modifiers.contains(KeyModifiers::CONTROL) => {
59+
self.case = match self.case {
60+
Case::Sensitive => Case::Fold,
61+
Case::Fold => Case::Sensitive,
62+
};
5163
}
5264
Char(to_insert) => {
5365
self.enter_char(to_insert);
@@ -119,11 +131,11 @@ impl GlobPane {
119131
has_focus,
120132
} = props.borrow();
121133

122-
let title = if self.case_sensitive {
123-
"Git-Glob (case-sensitive)"
124-
} else {
125-
"Git-Glob (case-insensitive)"
134+
let title = match self.case {
135+
Case::Sensitive => "Git-Glob (case-sensitive)",
136+
Case::Fold => "Git-Glob (case-insensitive)",
126137
};
138+
127139
let block = Block::default()
128140
.title(title)
129141
.border_style(*border_style)
@@ -156,7 +168,7 @@ impl GlobPane {
156168
}
157169

158170
fn draw_top_right_help(area: Rect, title: &str, buf: &mut Buffer) -> Rect {
159-
let help_text = " search = enter | case = ^I | cancel = esc ";
171+
let help_text = " search = enter | case = ^f | cancel = esc ";
160172
let help_text_block_width = block_width(help_text);
161173
let bound = Rect {
162174
width: area.width.saturating_sub(1),
@@ -188,7 +200,7 @@ fn glob_search_neighbours(
188200
root_index: TreeIndex,
189201
glob: &gix_glob::Pattern,
190202
path: &mut BString,
191-
case_sensitive: bool,
203+
case: Case,
192204
) {
193205
for node_index in tree.neighbors_directed(root_index, Direction::Outgoing) {
194206
if let Some(node) = tree.node_weight(node_index) {
@@ -200,33 +212,33 @@ fn glob_search_neighbours(
200212
Some(previous_len + 1)
201213
};
202214
path.extend_from_slice(gix_path::into_bstr(&node.name).as_ref());
203-
let case_mode = if case_sensitive {
204-
gix_glob::pattern::Case::Sensitive
205-
} else {
206-
gix_glob::pattern::Case::Fold
207-
};
208215
if glob.matches_repo_relative_path(
209216
path.as_ref(),
210217
basename_start,
211218
Some(node.is_dir),
212-
case_mode,
219+
case,
213220
gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL,
214221
) {
215222
results.push(node_index);
216223
} else {
217-
glob_search_neighbours(results, tree, node_index, glob, path, case_sensitive);
224+
glob_search_neighbours(results, tree, node_index, glob, path, case);
218225
}
219226
path.truncate(previous_len);
220227
}
221228
}
222229
}
223230

224-
pub fn glob_search(tree: &Tree, root_index: TreeIndex, glob: &str, case_sensitive: bool) -> Result<Vec<TreeIndex>> {
231+
pub fn glob_search(
232+
tree: &Tree,
233+
root_index: TreeIndex,
234+
glob: &str,
235+
case: gix_glob::pattern::Case,
236+
) -> Result<Vec<TreeIndex>> {
225237
let glob = gix_glob::Pattern::from_bytes_without_negation(glob.as_bytes())
226238
.with_context(|| anyhow!("Glob was empty or only whitespace"))?;
227239
let mut results = Vec::new();
228240
let mut path = Default::default();
229-
glob_search_neighbours(&mut results, tree, root_index, &glob, &mut path, case_sensitive);
241+
glob_search_neighbours(&mut results, tree, root_index, &glob, &mut path, case);
230242
Ok(results)
231243
}
232244

@@ -236,43 +248,21 @@ mod tests {
236248
use crosstermion::crossterm::event::{KeyCode, KeyEventKind, KeyModifiers};
237249

238250
#[test]
239-
fn test_i_key_types_into_input() {
251+
fn ctrl_f_key_types_into_input() {
240252
let mut glob_pane = GlobPane::default();
241253
assert_eq!(glob_pane.input, "");
242-
assert!(!glob_pane.case_sensitive); // default is case-insensitive
254+
assert_eq!(glob_pane.case, Case::Fold); // default is case-insensitive
243255

244-
// Test that typing 'I' adds it to the input
245-
let key_i = Key {
246-
code: KeyCode::Char('I'),
247-
modifiers: KeyModifiers::empty(),
248-
kind: KeyEventKind::Press,
249-
state: crosstermion::crossterm::event::KeyEventState::empty(),
250-
};
251-
glob_pane.process_events(key_i);
252-
253-
assert_eq!(glob_pane.input, "I");
254-
assert!(!glob_pane.case_sensitive); // should remain unchanged
255-
}
256-
257-
#[test]
258-
fn test_ctrl_i_toggles_case_sensitivity() {
259-
let mut glob_pane = GlobPane::default();
260-
assert!(!glob_pane.case_sensitive); // default is case-insensitive
261-
262-
// Test that Ctrl+I toggles case sensitivity
263-
let key_ctrl_i = Key {
264-
code: KeyCode::Char('i'),
256+
let ctrl_f = Key {
257+
code: KeyCode::Char('f'),
265258
modifiers: KeyModifiers::CONTROL,
266259
kind: KeyEventKind::Press,
267260
state: crosstermion::crossterm::event::KeyEventState::empty(),
268261
};
269-
glob_pane.process_events(key_ctrl_i);
270-
271-
assert_eq!(glob_pane.input, ""); // input should remain empty
272-
assert!(glob_pane.case_sensitive); // should toggle to case-sensitive
273-
274-
// Test toggling back
275-
glob_pane.process_events(key_ctrl_i);
276-
assert!(! glob_pane.case_sensitive); // should toggle back to case-insensitive
262+
glob_pane.process_events(ctrl_f);
263+
assert_eq!(glob_pane.case, Case::Sensitive);
264+
265+
glob_pane.process_events(ctrl_f);
266+
assert_eq!(glob_pane.case, Case::Fold);
277267
}
278268
}

0 commit comments

Comments
 (0)