From e78adfd9ab2de01c87ec8a755847f67f9c0d64f0 Mon Sep 17 00:00:00 2001 From: vinod akshat Date: Wed, 22 Jul 2026 08:42:34 +0000 Subject: [PATCH] Picker: Support child and parent navigation ( menu::SelectChild and menu::SelectParent in PickerDelegate ) fixes (#61053) (#61314) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective - Provides directory navigation (forward and backward) inside the built-in system file finder / path prompt using keyboard shortcuts. - Fixes #61053 ## Solution 1. Extended `PickerDelegate` trait with `select_child` and `select_parent` methods following conventions from other methods such as `confirm_completion` (defaulting to `None`) 2. Implemented the new delegate methods inside `OpenPathDelegate`: - `select_child`: Finds the selected directory candidate, appends it to the current path query, and appends the path separator. - `select_parent`: Moves upwards by trimming the current directory and getting the parent directory path. 3. Added listeners for `Self::select_child` and `Self::select_parent` in the `/crates/picker/src/render.rs` ## Testing - Did you test these changes? If so, how? I actually went ahead and changed the keybindings in `/assets/keymaps/default-macos.json` as shown : ```json { "context": "Picker > Editor", "use_key_equivalents": true, "bindings": { "escape": "menu::Cancel", "up": "menu::SelectPrevious", "down": "menu::SelectNext", // I binded to shift arrow because I wanted to keep the cursor movement still relevant "shift-left": "menu::SelectParent",// shift left "shift-right": "menu::SelectChild",// shift right "tab": "picker::ConfirmCompletion", "alt-enter": ["picker::ConfirmInput", { "secondary": false }], "cmd-alt-enter": ["picker::ConfirmInput", { "secondary": true }] // Picker bindings (TogglePreview, SetPreviewRight/Below/Hidden, // ToggleActionsMenu) live in keymaps/specific-overrides-macos.json, which // is loaded after the base keymap so they win over conflicting base-keymap // Editor bindings. } }, ``` Then I did manual testing as shown in the Showcase Section. I did not push this change as I was not sure if this was required as the bug was about adding support for child and parent navigation. - Are there any parts that need more testing? - How can other people (reviewers) test your changes? Is there anything specific they need to know? This is my first Pull Request in a large Open Source Repository, so any feedback on testing conventions or additional edge cases would be especially helpful. - If relevant, what platforms did you test these changes on, and are there any important ones you can't test? ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [ ] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [ ] Tests cover the new/changed behavior - [ ] Performance impact has been considered and is acceptable ## Showcase Screen Recording 2026-07-20 at 12
52 06 AM --- Release Notes: - Fixed child and parent directory navigation in the built-in folder finder using the left and right arrow keys. --------- Co-authored-by: Christopher Biscardi --- .../open_path_prompt/src/open_path_prompt.rs | 38 +++++++++++++++++ crates/picker/src/picker.rs | 41 +++++++++++++++++++ crates/picker/src/render.rs | 2 + 3 files changed, 81 insertions(+) diff --git a/crates/open_path_prompt/src/open_path_prompt.rs b/crates/open_path_prompt/src/open_path_prompt.rs index 5fa29d453a..cbbebeb6a7 100644 --- a/crates/open_path_prompt/src/open_path_prompt.rs +++ b/crates/open_path_prompt/src/open_path_prompt.rs @@ -577,7 +577,45 @@ impl PickerDelegate for OpenPathDelegate { .ok(); }) } + fn select_child( + &mut self, + _window: &mut Window, + _cx: &mut Context>, + ) -> Option { + let candidate = self.get_entry(self.selected_index)?; + if candidate.path.string.is_empty() || !candidate.is_dir { + return None; + } + let path_style = self.path_style; + match &self.directory_state { + DirectoryState::List { parent_path, .. } + | DirectoryState::Create { parent_path, .. } => Some(format!( + "{}{}{}", + parent_path, + candidate.path.string, + path_style.primary_separator() + )), + DirectoryState::None { .. } => None, + } + } + fn select_parent( + &mut self, + _window: &mut Window, + _cx: &mut Context>, + ) -> Option { + let parent_path = match &self.directory_state { + DirectoryState::List { parent_path, .. } + | DirectoryState::Create { parent_path, .. } => parent_path, + DirectoryState::None { .. } => return None, + }; + if parent_path == &self.prompt_root { + return None; + } + let trimmed = parent_path.trim_end_matches(['/', '\\']); + let (dir, _) = get_dir_and_suffix(trimmed.to_string(), self.path_style); + Some(dir) + } fn confirm_completion( &mut self, query: String, diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 9f8baac0cb..ad20dfc28c 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -301,6 +301,27 @@ pub trait PickerDelegate: Sized + 'static { ) -> Option { None } + /// Called when `SelectChild` fires (e.g. shift-right-arrow). Return `Some(query)` + /// to step into the currently selected item (e.g. a directory); the picker + /// will set the query and refresh matches. + fn select_child( + &mut self, + _window: &mut Window, + _cx: &mut Context>, + ) -> Option { + None + } + + /// Called when `SelectParent` fires (e.g. shift-left-arrow). Return `Some(query)` + /// to step back to the parent; the picker will set the query and refresh + /// matches. + fn select_parent( + &mut self, + _window: &mut Window, + _cx: &mut Context>, + ) -> Option { + None + } fn editor_position(&self) -> PickerEditorPosition { PickerEditorPosition::default() @@ -1002,6 +1023,26 @@ impl Picker { cx.propagate() } } + fn select_child(&mut self, _: &menu::SelectChild, window: &mut Window, cx: &mut Context) { + if let Some(new_query) = self.delegate.select_child(window, cx) { + self.set_query(&new_query, window, cx); + } else { + cx.propagate() + } + } + + fn select_parent( + &mut self, + _: &menu::SelectParent, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(new_query) = self.delegate.select_parent(window, cx) { + self.set_query(&new_query, window, cx); + } else { + cx.propagate() + } + } fn set_preview_right( &mut self, diff --git a/crates/picker/src/render.rs b/crates/picker/src/render.rs index 6abdf57113..d9bf085707 100644 --- a/crates/picker/src/render.rs +++ b/crates/picker/src/render.rs @@ -192,6 +192,8 @@ impl Picker { .on_action(cx.listener(Self::editor_move_up)) .on_action(cx.listener(Self::select_first)) .on_action(cx.listener(Self::select_last)) + .on_action(cx.listener(Self::select_child)) + .on_action(cx.listener(Self::select_parent)) .on_action(cx.listener(Self::cancel)) .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::secondary_confirm))