fix: unify docked picker leader actions and focus toggle

This commit is contained in:
Valdemar Lorenzen
2026-10-12 16:17:50 +02:00
parent 1fd5f98256
commit 526e243a5f
2 changed files with 88 additions and 4 deletions
+2
View File
@@ -256,6 +256,8 @@ Switching from Insert commits its pending undo group and parks that view in Norm
Space o opens a floating **Tree** explorer directly in **INSERT/filter** mode, ready for typing; Space e opens or focuses the docked **Tree**, starting in **NORMAL/navigation** mode when new. Both use one catalog and instance. Invoking the other action applies that presentation's configured default while retaining query, selection identities, and surviving folder expansion. Floating Tree invocation enters filtering if not already filtering. These actions are available as `config.core.file_picker` and `config.core.file_explorer` in Normal/Visual and in the command palette. Space o opens a floating **Tree** explorer directly in **INSERT/filter** mode, ready for typing; Space e opens or focuses the docked **Tree**, starting in **NORMAL/navigation** mode when new. Both use one catalog and instance. Invoking the other action applies that presentation's configured default while retaining query, selection identities, and surviving folder expansion. Floating Tree invocation enters filtering if not already filtering. These actions are available as `config.core.file_picker` and `config.core.file_explorer` in Normal/Visual and in the command palette.
While a docked Tree has focus in **NORMAL/navigation** mode, leader sequences work in ordinary and diff views: Space o replaces the dock with the floating picker, and Space e returns focus to the document like Escape while leaving the dock visible. Invoking Space e again from the document refocuses it. Tree/selection navigation overrides retain priority. In **INSERT/filter** mode and List query fields, printable leader keys remain literal query text rather than triggering editor actions; configured control-key action bindings remain available.
The floating explorer is centered, capped at 100 columns by 24 rows, and shrinks to fit smaller terminals. The tree mode appears in the title; the help row does not repeat it. The floating explorer is centered, capped at 100 columns by 24 rows, and shrinks to fit smaller terminals. The tree mode appears in the title; the help row does not repeat it.
The dock defaults to the right of the editor workspace. See [configuration preferences](configuration.md#preferences) to choose another side. Status and command/search prompts remain full-width; editor text, gutter, highlights, and cursors use the remaining rectangle. Focused panes prioritize their query on tiny terminals; unfocused docks shrink or hide to preserve editing space. The dock defaults to the right of the editor workspace. See [configuration preferences](configuration.md#preferences) to choose another side. Status and command/search prompts remain full-width; editor text, gutter, highlights, and cursors use the remaining rectangle. Focused panes prioritize their query on tiny terminals; unfocused docks shrink or hide to preserve editing space.
+86 -4
View File
@@ -4999,6 +4999,24 @@ impl Editor {
fn open_file_picker(&mut self, presentation: FilePresentation) { fn open_file_picker(&mut self, presentation: FilePresentation) {
self.pane_close_picker_origin = None; self.pane_close_picker_origin = None;
if presentation == FilePresentation::Docked
&& self.file_picker().is_some_and(|picker| {
picker.focused
&& picker.presentation == FilePresentation::Docked
&& picker.view == FileView::Tree
&& !picker.searching
})
{
let return_focus = self.diff_bound.map(|bound| bound.2);
self.blur_file_picker();
if let Some(diff) = &mut self.diff
&& let Some(focus) = return_focus
{
diff.focus = focus;
diff.normalize_focus();
}
return;
}
let view = match presentation { let view = match presentation {
FilePresentation::Floating => self.file_picker_view, FilePresentation::Floating => self.file_picker_view,
FilePresentation::Docked => self.file_explorer_view, FilePresentation::Docked => self.file_explorer_view,
@@ -5288,7 +5306,14 @@ impl Editor {
if (picker_focused || self.palette.is_some()) && self.handle_selection_binding(&input) { if (picker_focused || self.palette.is_some()) && self.handle_selection_binding(&input) {
return None; return None;
} }
if picker_focused && !matches!(input, Input::Char(_)) && self.start_sequence(mode, &input) { let picker_browsing = self
.file_picker
.as_ref()
.is_some_and(|picker| picker.view == FileView::Tree && !picker.searching);
if picker_focused
&& (!matches!(input, Input::Char(_)) || picker_browsing)
&& self.start_sequence(mode, &input)
{
return None; return None;
} }
if picker_focused if picker_focused
@@ -5307,6 +5332,8 @@ impl Editor {
| Action::TabNext | Action::TabNext
| Action::TabClose | Action::TabClose
| Action::TabForceClose | Action::TabForceClose
| Action::FilePicker
| Action::FileExplorer
) )
{ {
let action = *action; let action = *action;
@@ -9435,6 +9462,58 @@ mod tests {
assert_eq!(editor.active.view.targets, targets); assert_eq!(editor.active.view.targets, targets);
} }
#[test]
fn docked_picker_leaders_replace_or_blur_consistently_without_stealing_filter_text() {
let directory = Directory::new();
fs::write(directory.path("file.txt"), "source").unwrap();
for comparison in [false, true] {
let mut editor = editor();
editor.project_root = directory.0.clone();
if comparison {
historical_diff(
&mut editor,
&directory.0,
vec!["before".into()],
vec!["after".into()],
);
}
type_text(&mut editor, " e");
assert!(editor.file_picker().unwrap().focused);
assert!(!editor.file_picker().unwrap().searching);
type_text(&mut editor, " o");
assert_eq!(
editor.file_picker().unwrap().presentation,
FilePresentation::Floating
);
assert!(editor.file_picker().unwrap().searching);
editor.handle(Input::Escape);
editor.handle(Input::Escape);
type_text(&mut editor, " e");
let selected = editor.file_picker().unwrap().selected;
editor.handle(Input::Escape);
let escape_focus = editor.diff().map(|diff| diff.focus);
type_text(&mut editor, " e");
assert!(editor.file_picker().unwrap().focused);
type_text(&mut editor, " e");
assert!(!editor.file_picker().unwrap().focused);
assert_eq!(
editor.file_picker().unwrap().presentation,
FilePresentation::Docked
);
assert_eq!(editor.file_picker().unwrap().selected, selected);
assert_eq!(editor.diff().map(|diff| diff.focus), escape_focus);
type_text(&mut editor, " e");
editor.handle(Input::Char('i'));
type_text(&mut editor, " o e");
assert_eq!(editor.file_picker().unwrap().query, " o e");
assert!(editor.file_picker().unwrap().searching);
assert_eq!(
editor.file_picker().unwrap().presentation,
FilePresentation::Docked
);
}
}
#[test] #[test]
fn focused_text_fields_keep_printable_normal_bindings_as_text() { fn focused_text_fields_keep_printable_normal_bindings_as_text() {
let directory = Directory::new(); let directory = Directory::new();
@@ -10812,7 +10891,9 @@ mod tests {
} }
let owner = (editor.tab_id(), editor.pane_id()); let owner = (editor.tab_id(), editor.pane_id());
let comparison_id = editor.diff().and_then(|diff| diff.comparison_id); let comparison_id = editor.diff().and_then(|diff| diff.comparison_id);
editor.act(Action::FileExplorer); if !editor.file_picker().is_some_and(|picker| picker.focused) {
editor.act(Action::FileExplorer);
}
editor.handle(Input::Ctrl('p')); editor.handle(Input::Ctrl('p'));
assert!(editor.pane_close_picker_origin.is_some()); assert!(editor.pane_close_picker_origin.is_some());
assert!(editor.palette().is_some()); assert!(editor.palette().is_some());
@@ -10880,7 +10961,9 @@ mod tests {
if comparison { if comparison {
working_diff(&mut editor, &directory.0, &["file.txt"]); working_diff(&mut editor, &directory.0, &["file.txt"]);
} }
editor.act(Action::FileExplorer); if !editor.file_picker().is_some_and(|picker| picker.focused) {
editor.act(Action::FileExplorer);
}
editor.handle(Input::Ctrl('p')); editor.handle(Input::Ctrl('p'));
assert!(editor.pane_close_picker_origin.is_some()); assert!(editor.pane_close_picker_origin.is_some());
assert!(editor.palette().is_some()); assert!(editor.palette().is_some());
@@ -11033,7 +11116,6 @@ mod tests {
editor.apply(vec![Effect::Insert("dirty".into())]).unwrap(); editor.apply(vec![Effect::Insert("dirty".into())]).unwrap();
let source = editor.buffer_id(); let source = editor.buffer_id();
working_diff(&mut editor, &directory.0, &["file.txt"]); working_diff(&mut editor, &directory.0, &["file.txt"]);
editor.act(Action::FileExplorer);
editor.handle(Input::Ctrl('p')); editor.handle(Input::Ctrl('p'));
assert!(editor.pane_close_picker_origin.is_some()); assert!(editor.pane_close_picker_origin.is_some());
assert!(editor.palette().is_some()); assert!(editor.palette().is_some());