From 526e243a5f8cea4ed52db6a4dc4101a58ac10536 Mon Sep 17 00:00:00 2001 From: Valdemar Lorenzen Date: Mon, 12 Oct 2026 16:17:50 +0200 Subject: [PATCH] fix: unify docked picker leader actions and focus toggle --- docs/editing.md | 2 ++ src/editor.rs | 90 ++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 88 insertions(+), 4 deletions(-) diff --git a/docs/editing.md b/docs/editing.md index ae1b7f1..8bebfe6 100644 --- a/docs/editing.md +++ b/docs/editing.md @@ -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. +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 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. diff --git a/src/editor.rs b/src/editor.rs index c2d5bf4..173c32b 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -4999,6 +4999,24 @@ impl Editor { fn open_file_picker(&mut self, presentation: FilePresentation) { 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 { FilePresentation::Floating => self.file_picker_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) { 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; } if picker_focused @@ -5307,6 +5332,8 @@ impl Editor { | Action::TabNext | Action::TabClose | Action::TabForceClose + | Action::FilePicker + | Action::FileExplorer ) { let action = *action; @@ -9435,6 +9462,58 @@ mod tests { 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] fn focused_text_fields_keep_printable_normal_bindings_as_text() { let directory = Directory::new(); @@ -10812,7 +10891,9 @@ mod tests { } let owner = (editor.tab_id(), editor.pane_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')); assert!(editor.pane_close_picker_origin.is_some()); assert!(editor.palette().is_some()); @@ -10880,7 +10961,9 @@ mod tests { if comparison { 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')); assert!(editor.pane_close_picker_origin.is_some()); assert!(editor.palette().is_some()); @@ -11033,7 +11116,6 @@ mod tests { editor.apply(vec![Effect::Insert("dirty".into())]).unwrap(); let source = editor.buffer_id(); working_diff(&mut editor, &directory.0, &["file.txt"]); - editor.act(Action::FileExplorer); editor.handle(Input::Ctrl('p')); assert!(editor.pane_close_picker_origin.is_some()); assert!(editor.palette().is_some());