feat: retain explorer filters in navigation mode

This commit is contained in:
Valdemar Lorenzen
2026-10-09 09:28:20 +02:00
parent 86395cf5ce
commit 4fe3accc56
6 changed files with 890 additions and 208 deletions
+311 -13
View File
@@ -1479,7 +1479,7 @@ impl Editor {
match input {
Input::Ctrl('w') => self.file_picker = None,
Input::Escape if picker.view == FileView::Tree && picker.searching => {
picker.cancel_search()
picker.finish_search()
}
Input::Escape => self.blur_file_picker(),
Input::Ctrl('p') => {
@@ -1490,6 +1490,7 @@ impl Editor {
}
Input::Ctrl('r') => picker.refresh(),
Input::Ctrl('t') => picker.toggle_view(),
Input::Ctrl('u') => picker.clear_filter(),
Input::Backspace if picker.view == FileView::List || picker.searching => {
picker.backspace()
}
@@ -3366,10 +3367,22 @@ mod tests {
editor.handle(Input::Escape);
let picker = editor.file_picker().unwrap();
assert!(picker.focused && !picker.searching);
assert_eq!(picker.query, "second.txt");
assert_eq!(
picker.entries().nth(picker.selected).unwrap().path,
Path::new("dir")
Path::new("dir/second.txt")
);
editor.handle(Input::Char('i'));
assert!(editor.file_picker().unwrap().searching);
assert_eq!(editor.file_picker().unwrap().query, "second.txt");
editor.handle(Input::Escape);
editor.handle(Input::Char('h'));
editor.handle(Input::Char('h'));
let picker = editor.file_picker().unwrap();
assert_eq!(picker.entries().len(), 1);
assert_eq!(picker.entries().next().unwrap().path, Path::new("dir"));
assert!(!picker.entries().next().unwrap().expanded);
assert_eq!(picker.query, "second.txt");
editor.handle(Input::Char('l'));
assert!(
editor
@@ -3391,6 +3404,271 @@ mod tests {
assert!(editor.file_picker().is_none());
}
#[test]
fn filter_escape_resume_and_clear_do_not_edit_underlying_buffers_or_history() {
let directory = Directory::new();
fs::create_dir_all(directory.path("src/deep")).unwrap();
fs::write(directory.path("src/deep/needle.txt"), "needle").unwrap();
fs::write(directory.path("src/other.txt"), "other").unwrap();
let selected_path = |editor: &Editor| {
let picker = editor.file_picker().unwrap();
picker
.entries()
.nth(picker.selected)
.unwrap()
.path
.to_owned()
};
for presentation in [FilePresentation::Floating, FilePresentation::Docked] {
for mode in [
Mode::Normal,
Mode::Insert,
Mode::Command,
Mode::Search,
Mode::Visual,
] {
let mut editor = project_editor(&directory);
editor
.apply(vec![Effect::Replace("alpha\nbeta".into())])
.unwrap();
editor
.apply(vec![Effect::Replace("changed".into())])
.unwrap();
editor.act(Action::Undo);
set_carets(&mut editor, &[(0, 1), (1, 2)], 1);
editor.begin_insert();
editor.active.mode = mode;
editor.command_line = "original prompt".into();
editor.open_file_picker(presentation);
if !editor.file_picker().unwrap().searching {
editor.handle(Input::Char('i'));
}
editor.count = Some(7);
let snapshot =
|state: &Snapshot| (state.lines.clone(), state.targets.clone(), state.primary);
let lines = editor.lines().to_owned();
let targets = editor.active.targets.clone();
let primary = editor.active.primary;
let undo: Vec<_> = editor.active.undo.iter().map(&snapshot).collect();
let redo: Vec<_> = editor.active.redo.iter().map(&snapshot).collect();
let insert = editor.active.insert_start.as_ref().map(snapshot);
let buffer = editor.buffer_id();
let buffers = editor.buffer_count();
type_text(&mut editor, "needle");
let expected = PathBuf::from("src/deep/needle.txt");
assert_eq!(selected_path(&editor), expected);
editor.handle(Input::Escape);
assert!(!editor.file_picker().unwrap().searching);
assert_eq!(editor.file_picker().unwrap().query, "needle");
assert_eq!(selected_path(&editor), expected);
editor.handle(Input::Char('i'));
assert!(editor.file_picker().unwrap().searching);
assert_eq!(editor.file_picker().unwrap().query, "needle");
assert_eq!(selected_path(&editor), expected);
editor.handle(Input::Ctrl('U'));
assert!(editor.file_picker().unwrap().searching);
assert!(editor.file_picker().unwrap().query.is_empty());
assert_eq!(selected_path(&editor), expected);
type_text(&mut editor, "needle");
editor.handle(Input::Escape);
editor.handle(Input::Ctrl('u'));
assert!(!editor.file_picker().unwrap().searching);
assert!(editor.file_picker().unwrap().query.is_empty());
assert_eq!(selected_path(&editor), expected);
editor.handle(Input::Ctrl('u'));
assert_eq!(selected_path(&editor), expected);
editor.handle(Input::Char('i'));
type_text(&mut editor, "missing");
editor.handle(Input::Ctrl('j'));
editor.handle(Input::Ctrl('k'));
editor.handle(Input::Escape);
assert_eq!(editor.file_picker().unwrap().entries().len(), 0);
editor.handle(Input::Ctrl('u'));
assert_eq!(selected_path(&editor), expected);
assert!(editor.file_picker().unwrap().focused);
assert!(!editor.file_picker().unwrap().searching);
assert_eq!(editor.mode(), mode);
assert_eq!(editor.lines(), lines);
assert_eq!(editor.active.targets, targets);
assert_eq!(editor.active.primary, primary);
assert_eq!(editor.command_line, "original prompt");
assert_eq!(editor.count(), Some(7));
assert_eq!(editor.buffer_id(), buffer);
assert_eq!(editor.buffer_count(), buffers);
assert_eq!(
editor.active.undo.iter().map(&snapshot).collect::<Vec<_>>(),
undo
);
assert_eq!(
editor.active.redo.iter().map(&snapshot).collect::<Vec<_>>(),
redo
);
assert_eq!(editor.active.insert_start.as_ref().map(snapshot), insert);
editor.handle(Input::Escape);
match presentation {
FilePresentation::Floating => assert!(editor.file_picker().is_none()),
FilePresentation::Docked => {
assert!(!editor.file_picker().unwrap().focused);
editor.open_file_picker(presentation);
assert!(!editor.file_picker().unwrap().searching);
assert_eq!(selected_path(&editor), expected);
}
}
}
}
}
#[test]
fn filtered_folder_keys_and_directory_accept_work_in_both_presentations() {
let directory = Directory::new();
fs::create_dir_all(directory.path("src/deep")).unwrap();
fs::write(directory.path("src/deep/needle.txt"), "needle").unwrap();
fs::write(directory.path("src/other.txt"), "other").unwrap();
for presentation in [FilePresentation::Floating, FilePresentation::Docked] {
let mut editor = project_editor(&directory);
editor.open_file_picker(presentation);
if !editor.file_picker().unwrap().searching {
editor.handle(Input::Char('i'));
}
type_text(&mut editor, "needle");
editor.handle(Input::Ctrl('h'));
let picker = editor.file_picker().unwrap();
assert!(picker.searching);
assert_eq!(picker.query, "needle");
assert_eq!(
picker.entries().nth(picker.selected).unwrap().path,
Path::new("src/deep")
);
assert!(picker.entries().nth(picker.selected).unwrap().expanded);
editor.handle(Input::Ctrl('l'));
let picker = editor.file_picker().unwrap();
assert_eq!(
picker.entries().nth(picker.selected).unwrap().path,
Path::new("src/deep/needle.txt")
);
editor.handle(Input::Escape);
editor.handle(Input::Char('h'));
editor.handle(Input::Char('h'));
let picker = editor.file_picker().unwrap();
assert!(!picker.searching);
assert_eq!(picker.query, "needle");
assert_eq!(picker.entries().len(), 2);
assert_eq!(
picker.entries().nth(picker.selected).unwrap().path,
Path::new("src/deep")
);
assert!(!picker.entries().nth(picker.selected).unwrap().expanded);
editor.handle(Input::Char('l'));
assert!(
editor
.file_picker()
.unwrap()
.entries()
.nth(1)
.unwrap()
.expanded
);
editor.handle(Input::Char('l'));
editor.handle(Input::Char('h'));
editor.handle(Input::Enter);
assert!(
!editor
.file_picker()
.unwrap()
.entries()
.nth(1)
.unwrap()
.expanded
);
editor.handle(Input::Enter);
assert!(
editor
.file_picker()
.unwrap()
.entries()
.nth(1)
.unwrap()
.expanded
);
editor.handle(Input::Char('i'));
editor.handle(Input::Enter);
let picker = editor.file_picker().unwrap();
assert!(!picker.searching && picker.focused);
assert_eq!(picker.query, "needle");
assert_eq!(
picker.entries().nth(picker.selected).unwrap().path,
Path::new("src/deep")
);
assert!(!picker.entries().nth(picker.selected).unwrap().expanded);
assert_eq!(editor.context().text, "");
editor.handle(Input::Ctrl('r'));
editor.handle(Input::Ctrl('t'));
editor.handle(Input::Ctrl('t'));
let picker = editor.file_picker().unwrap();
assert!(!picker.searching && picker.focused);
assert_eq!(picker.query, "needle");
assert!(!picker.entries().nth(picker.selected).unwrap().expanded);
if presentation == FilePresentation::Docked {
editor.handle(Input::Escape);
editor.open_file_picker(presentation);
assert!(!editor.file_picker().unwrap().searching);
assert_eq!(editor.file_picker().unwrap().query, "needle");
}
editor.open_file_picker(FilePresentation::Floating);
assert!(editor.file_picker().unwrap().searching);
assert_eq!(editor.file_picker().unwrap().query, "needle");
}
}
#[test]
fn filter_clear_respects_navigation_bindings_and_does_not_clear_palette_or_idle_dock() {
let directory = Directory::new();
for name in ["a.txt", "b.txt"] {
fs::write(directory.path(name), "text").unwrap();
}
for view in [FileView::List, FileView::Tree] {
let mut editor = project_editor(&directory);
editor.open_file_picker(FilePresentation::Docked);
editor.file_picker.as_mut().unwrap().set_view(view);
if view == FileView::Tree {
editor.handle(Input::Char('i'));
}
type_text(&mut editor, "txt");
editor
.bind("selection", "ctrl+u", Some("core.down".into()))
.unwrap();
editor.handle(Input::Ctrl('u'));
assert_eq!(editor.file_picker().unwrap().query, "txt");
assert_eq!(editor.file_picker().unwrap().selected, 1);
editor.bind("selection", "ctrl+u", None).unwrap();
editor.handle(Input::Ctrl('u'));
assert_eq!(editor.file_picker().unwrap().query, "txt");
editor.selection_bindings.remove(&Input::Ctrl('u'));
if view == FileView::Tree {
editor.bind("tree", "ctrl+u", None).unwrap();
editor.handle(Input::Ctrl('u'));
assert_eq!(editor.file_picker().unwrap().query, "txt");
editor.tree_bindings.remove(&Input::Ctrl('u'));
}
editor.handle(Input::Ctrl('u'));
assert!(editor.file_picker().unwrap().query.is_empty());
assert_eq!(editor.file_picker().unwrap().selected, 1);
assert_eq!(
editor.file_picker().unwrap().searching,
view == FileView::Tree
);
type_text(&mut editor, "txt");
editor.handle(Input::Char('\t'));
editor.handle(Input::Ctrl('u'));
assert_eq!(editor.file_picker().unwrap().query, "txt");
editor.handle(Input::Ctrl('p'));
type_text(&mut editor, "core");
editor.handle(Input::Ctrl('u'));
assert_eq!(editor.palette().unwrap().query, "core");
assert_eq!(editor.file_picker().unwrap().query, "txt");
}
}
#[test]
fn selection_navigation_wraps_without_touching_any_underlying_mode() {
let directory = Directory::new();
@@ -4185,7 +4463,12 @@ mod tests {
assert_eq!(editor.file_picker().unwrap().query, "hjkl");
editor.handle(Input::Escape);
assert!(editor.file_picker().unwrap().focused);
assert_eq!(selected(&editor), Path::new("dir"));
assert_eq!(selected(&editor), Path::new("dir/hjkl-a.txt"));
assert_eq!(editor.file_picker().unwrap().query, "hjkl");
editor.handle(Input::Ctrl('u'));
assert_eq!(selected(&editor), Path::new("dir/hjkl-a.txt"));
editor.handle(Input::Char('h'));
editor.handle(Input::Char('h'));
assert!(
!editor
.file_picker()
@@ -4265,12 +4548,6 @@ mod tests {
assert_eq!(editor.file_picker().unwrap().selected, 1);
editor.handle(Input::Ctrl('h'));
assert_eq!(editor.file_picker().unwrap().selected, 0);
let before = editor
.file_picker()
.unwrap()
.entries()
.map(|row| row.path.to_owned())
.collect::<Vec<_>>();
type_text(&mut editor, "ideep/");
assert!(editor.file_picker().unwrap().searching);
assert_eq!(editor.file_picker().unwrap().query, "deep/");
@@ -4281,12 +4558,19 @@ mod tests {
type_text(&mut editor, "👩‍💻");
editor.handle(Input::Backspace);
assert_eq!(editor.file_picker().unwrap().query, "deep");
let before = editor
.file_picker()
.unwrap()
.entries()
.map(|row| row.path.to_owned())
.collect::<Vec<_>>();
let selected_before = editor.file_picker().unwrap().selected;
editor.handle(Input::Escape);
let picker = editor.file_picker().unwrap();
assert!(picker.focused);
assert!(!picker.searching);
assert!(picker.query.is_empty());
assert_eq!(picker.selected, 0);
assert_eq!(picker.query, "deep");
assert_eq!(picker.selected, selected_before);
assert_eq!(
picker
.entries()
@@ -4489,10 +4773,24 @@ mod tests {
let picker = editor.file_picker().unwrap();
assert_eq!(editor.buffer_id(), id);
assert!(!picker.searching);
assert!(picker.query.is_empty());
assert_eq!(picker.query, "module");
let selected = picker.entries().nth(picker.selected).unwrap();
assert_eq!(selected.path, Path::new("src/module"));
assert!(selected.expanded);
assert!(!selected.expanded);
assert!(
!picker
.entries()
.any(|row| row.path == Path::new("src/module/needle.txt"))
);
editor.handle(Input::Enter);
let picker = editor.file_picker().unwrap();
assert_eq!(picker.query, "module");
assert!(picker.entries().nth(picker.selected).unwrap().expanded);
assert!(
picker
.entries()
.any(|row| row.path == Path::new("src/module/needle.txt"))
);
assert_eq!(editor.buffer_count(), 2);
}
}