Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions src/interactive/app/eventloop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,11 @@ impl AppState {
Glob => {
let glob_pane = window.glob_pane.as_mut().expect("glob pane");
match key.code {
Enter => self.search_glob_pattern(&mut tree_view, &glob_pane.input),
Enter => self.search_glob_pattern(
&mut tree_view,
&glob_pane.input,
glob_pane.case,
),
_ => glob_pane.process_events(key),
}
}
Expand Down Expand Up @@ -476,9 +480,19 @@ impl AppState {
}
}

fn search_glob_pattern(&mut self, tree_view: &mut TreeView<'_>, glob_pattern: &str) {
fn search_glob_pattern(
&mut self,
tree_view: &mut TreeView<'_>,
glob_pattern: &str,
case: gix_glob::pattern::Case,
) {
use FocussedPane::*;
match glob_search(tree_view.tree(), self.navigation.view_root, glob_pattern) {
match glob_search(
tree_view.tree(),
self.navigation.view_root,
glob_pattern,
case,
) {
Ok(matches) if matches.is_empty() => {
self.message = Some("No match found".into());
}
Expand Down
3 changes: 2 additions & 1 deletion src/interactive/app/tests/journeys_with_writes.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use crate::interactive::app::tests::utils::{
initialized_app_and_terminal_from_paths, into_codes, WritableFixture,

Check warning on line 2 in src/interactive/app/tests/journeys_with_writes.rs

View workflow job for this annotation

GitHub Actions / Windows (nightly)

unused imports: `WritableFixture`, `initialized_app_and_terminal_from_paths`, and `into_codes`

Check warning on line 2 in src/interactive/app/tests/journeys_with_writes.rs

View workflow job for this annotation

GitHub Actions / Windows (nightly)

unused imports: `WritableFixture`, `initialized_app_and_terminal_from_paths`, and `into_codes`
};
use anyhow::Result;

Check warning on line 4 in src/interactive/app/tests/journeys_with_writes.rs

View workflow job for this annotation

GitHub Actions / Windows (nightly)

unused import: `anyhow::Result`

Check warning on line 4 in src/interactive/app/tests/journeys_with_writes.rs

View workflow job for this annotation

GitHub Actions / Windows (nightly)

unused import: `anyhow::Result`
use crosstermion::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};

Check warning on line 5 in src/interactive/app/tests/journeys_with_writes.rs

View workflow job for this annotation

GitHub Actions / Windows (nightly)

unused imports: `KeyCode`, `KeyEvent`, and `KeyModifiers`

Check warning on line 5 in src/interactive/app/tests/journeys_with_writes.rs

View workflow job for this annotation

GitHub Actions / Windows (nightly)

unused imports: `KeyCode`, `KeyEvent`, and `KeyModifiers`
use crosstermion::input::Event;

Check warning on line 6 in src/interactive/app/tests/journeys_with_writes.rs

View workflow job for this annotation

GitHub Actions / Windows (nightly)

unused import: `crosstermion::input::Event`

Check warning on line 6 in src/interactive/app/tests/journeys_with_writes.rs

View workflow job for this annotation

GitHub Actions / Windows (nightly)

unused import: `crosstermion::input::Event`
use pretty_assertions::assert_eq;

Check warning on line 7 in src/interactive/app/tests/journeys_with_writes.rs

View workflow job for this annotation

GitHub Actions / Windows (nightly)

unused import: `pretty_assertions::assert_eq`

Check warning on line 7 in src/interactive/app/tests/journeys_with_writes.rs

View workflow job for this annotation

GitHub Actions / Windows (nightly)

unused import: `pretty_assertions::assert_eq`

#[test]
#[cfg(not(target_os = "windows"))] // it stopped working here, don't know if it's truly broken or if it's the test. Let's wait for windows users to report.
Expand All @@ -12,7 +12,8 @@
use crate::interactive::app::tests::utils::into_events;

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

// With a selection of items
app.process_events(&mut terminal, into_codes("doddd"))?;
Expand Down
20 changes: 19 additions & 1 deletion src/interactive/app/tests/unit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::interactive::app::tests::utils::{
use crate::interactive::widgets::glob_search;
use anyhow::Result;
use dua::traverse::TreeIndex;
use gix_glob::pattern::Case;
use pretty_assertions::assert_eq;

#[test]
Expand Down Expand Up @@ -35,7 +36,24 @@ fn it_can_handle_ending_traversal_without_reaching_the_top() -> Result<()> {
#[test]
fn it_can_do_a_glob_search() {
let (tree, root_index) = sample_02_tree(false);
let result = glob_search(&tree, root_index, "tests/fixtures/sample-02").unwrap();
let result = glob_search(&tree, root_index, "tests/fixtures/sample-02", Case::Fold).unwrap();
let expected = vec![TreeIndex::from(1)];
assert_eq!(result, expected);
}

#[test]
fn it_can_do_a_case_sensitive_glob_search() {
let (tree, root_index) = sample_02_tree(false);
let result_insensitive =
glob_search(&tree, root_index, "TESTS/FIXTURES/SAMPLE-02", Case::Fold).unwrap();
assert_eq!(result_insensitive, vec![TreeIndex::from(1)]);

let result_sensitive = glob_search(
&tree,
root_index,
"TESTS/FIXTURES/SAMPLE-02",
Case::Sensitive,
)
.unwrap();
assert!(result_sensitive.is_empty());
}
67 changes: 60 additions & 7 deletions src/interactive/widgets/glob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use bstr::BString;
use crosstermion::crossterm::event::KeyEventKind;
use crosstermion::input::Key;
use dua::traverse::{Tree, TreeIndex};
use gix_glob::pattern::Case;
use petgraph::Direction;
use std::borrow::Borrow;
use tui::{
Expand All @@ -26,23 +27,40 @@ pub struct GlobPaneProps {
pub has_focus: bool,
}

#[derive(Default)]
pub struct GlobPane {
pub input: String,
/// The index of the grapheme the cursor currently points to.
/// This hopefully rightfully assumes that a grapheme will be matching the block size on screen
/// and is treated as 'one character'. If not, it will be off, which isn't the end of the world.
// TODO: use `tui-textarea` for proper cursor handling, needs native crossterm events.
cursor_grapheme_idx: usize,
pub case: Case,
}

impl Default for GlobPane {
fn default() -> Self {
GlobPane {
input: "".to_string(),
cursor_grapheme_idx: 0,
case: Case::Fold,
}
}
}

impl GlobPane {
pub fn process_events(&mut self, key: Key) {
use crosstermion::crossterm::event::KeyCode::*;
use crosstermion::crossterm::event::KeyModifiers;
if key.kind == KeyEventKind::Release {
return;
}
match key.code {
Char('f') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.case = match self.case {
Case::Sensitive => Case::Fold,
Case::Fold => Case::Sensitive,
};
}
Char(to_insert) => {
self.enter_char(to_insert);
}
Expand Down Expand Up @@ -113,7 +131,11 @@ impl GlobPane {
has_focus,
} = props.borrow();

let title = "Git-Glob";
let title = match self.case {
Case::Sensitive => "Git-Glob (case-sensitive)",
Case::Fold => "Git-Glob (case-insensitive)",
};

let block = Block::default()
.title(title)
.border_style(*border_style)
Expand Down Expand Up @@ -146,7 +168,7 @@ impl GlobPane {
}

fn draw_top_right_help(area: Rect, title: &str, buf: &mut Buffer) -> Rect {
let help_text = " search = enter | cancel = esc ";
let help_text = " search = enter | case = ^f | cancel = esc ";
let help_text_block_width = block_width(help_text);
let bound = Rect {
width: area.width.saturating_sub(1),
Expand Down Expand Up @@ -178,6 +200,7 @@ fn glob_search_neighbours(
root_index: TreeIndex,
glob: &gix_glob::Pattern,
path: &mut BString,
case: Case,
) {
for node_index in tree.neighbors_directed(root_index, Direction::Outgoing) {
if let Some(node) = tree.node_weight(node_index) {
Expand All @@ -193,23 +216,53 @@ fn glob_search_neighbours(
path.as_ref(),
basename_start,
Some(node.is_dir),
gix_glob::pattern::Case::Fold,
case,
gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL,
) {
results.push(node_index);
} else {
glob_search_neighbours(results, tree, node_index, glob, path);
glob_search_neighbours(results, tree, node_index, glob, path, case);
}
path.truncate(previous_len);
}
}
}

pub fn glob_search(tree: &Tree, root_index: TreeIndex, glob: &str) -> Result<Vec<TreeIndex>> {
pub fn glob_search(
tree: &Tree,
root_index: TreeIndex,
glob: &str,
case: gix_glob::pattern::Case,
) -> Result<Vec<TreeIndex>> {
let glob = gix_glob::Pattern::from_bytes_without_negation(glob.as_bytes())
.with_context(|| anyhow!("Glob was empty or only whitespace"))?;
let mut results = Vec::new();
let mut path = Default::default();
glob_search_neighbours(&mut results, tree, root_index, &glob, &mut path);
glob_search_neighbours(&mut results, tree, root_index, &glob, &mut path, case);
Ok(results)
}

#[cfg(test)]
mod tests {
use super::*;
use crosstermion::crossterm::event::{KeyCode, KeyEventKind, KeyModifiers};

#[test]
fn ctrl_f_key_types_into_input() {
let mut glob_pane = GlobPane::default();
assert_eq!(glob_pane.input, "");
assert_eq!(glob_pane.case, Case::Fold); // default is case-insensitive

let ctrl_f = Key {
code: KeyCode::Char('f'),
modifiers: KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
state: crosstermion::crossterm::event::KeyEventState::empty(),
};
glob_pane.process_events(ctrl_f);
assert_eq!(glob_pane.case, Case::Sensitive);

glob_pane.process_events(ctrl_f);
assert_eq!(glob_pane.case, Case::Fold);
}
}
2 changes: 1 addition & 1 deletion src/interactive/widgets/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ impl HelpPane {
hotkey("a", "Toggle all entries.", None);
hotkey(
"/",
"Git-style glob search, case-insensitive.",
"Git-style glob search. Toggle case with 'I'.",
Some("Search starts from the current directory."),
);
hotkey("r", "Refresh only the selected entry.", None);
Expand Down
Loading