Add a way to use splits when opening in file finder (#20507)

Kirill Bulatov created

Change summary

assets/keymaps/default-linux.json        |  14 +
assets/keymaps/default-macos.json        |  14 +
crates/file_finder/src/file_finder.rs    | 171 ++++++++++++++++++++++++-
crates/ui/src/components/popover_menu.rs |   2 
crates/workspace/src/workspace.rs        |   6 
5 files changed, 191 insertions(+), 16 deletions(-)

Detailed changes

assets/keymaps/default-linux.json 🔗

@@ -649,7 +649,19 @@
   },
   {
     "context": "FileFinder",
-    "bindings": { "ctrl-shift-p": "file_finder::SelectPrev" }
+    "bindings": {
+      "ctrl-shift-p": "file_finder::SelectPrev",
+      "ctrl-k": "file_finder::OpenMenu"
+    }
+  },
+  {
+    "context": "FileFinder && menu_open",
+    "bindings": {
+      "u": "pane::SplitUp",
+      "d": "pane::SplitDown",
+      "l": "pane::SplitLeft",
+      "r": "pane::SplitRight"
+    }
   },
   {
     "context": "TabSwitcher",

assets/keymaps/default-macos.json 🔗

@@ -649,7 +649,19 @@
   },
   {
     "context": "FileFinder",
-    "bindings": { "cmd-shift-p": "file_finder::SelectPrev" }
+    "bindings": {
+      "cmd-shift-p": "file_finder::SelectPrev",
+      "cmd-k": "file_finder::OpenMenu"
+    }
+  },
+  {
+    "context": "FileFinder && menu_open",
+    "bindings": {
+      "u": "pane::SplitUp",
+      "d": "pane::SplitDown",
+      "l": "pane::SplitLeft",
+      "r": "pane::SplitRight"
+    }
   },
   {
     "context": "TabSwitcher",

crates/file_finder/src/file_finder.rs 🔗

@@ -15,8 +15,8 @@ use file_icons::FileIcons;
 use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
 use gpui::{
     actions, rems, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle,
-    FocusableView, Model, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task,
-    View, ViewContext, VisualContext, WeakView,
+    FocusableView, KeyContext, Model, Modifiers, ModifiersChangedEvent, ParentElement, Render,
+    Styled, Task, View, ViewContext, VisualContext, WeakView,
 };
 use new_path_prompt::NewPathPrompt;
 use open_path_prompt::OpenPathPrompt;
@@ -32,16 +32,30 @@ use std::{
     },
 };
 use text::Point;
-use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
+use ui::{
+    prelude::*, ButtonLike, ContextMenu, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing,
+    PopoverMenu, PopoverMenuHandle, TintColor,
+};
 use util::{paths::PathWithPosition, post_inc, ResultExt};
-use workspace::{item::PreviewTabsSettings, notifications::NotifyResultExt, ModalView, Workspace};
+use workspace::{
+    item::PreviewTabsSettings, notifications::NotifyResultExt, pane, ModalView, SplitDirection,
+    Workspace,
+};
 
-actions!(file_finder, [SelectPrev]);
+actions!(file_finder, [SelectPrev, OpenMenu]);
 
-impl ModalView for FileFinder {}
+impl ModalView for FileFinder {
+    fn on_before_dismiss(&mut self, cx: &mut ViewContext<Self>) -> workspace::DismissDecision {
+        let submenu_focused = self.picker.update(cx, |picker, cx| {
+            picker.delegate.popover_menu_handle.is_focused(cx)
+        });
+        workspace::DismissDecision::Dismiss(!submenu_focused)
+    }
+}
 
 pub struct FileFinder {
     picker: View<Picker<FileFinderDelegate>>,
+    picker_focus_handle: FocusHandle,
     init_modifiers: Option<Modifiers>,
 }
 
@@ -142,8 +156,14 @@ impl FileFinder {
     }
 
     fn new(delegate: FileFinderDelegate, cx: &mut ViewContext<Self>) -> Self {
+        let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
+        let picker_focus_handle = picker.focus_handle(cx);
+        picker.update(cx, |picker, _| {
+            picker.delegate.focus_handle = picker_focus_handle.clone();
+        });
         Self {
-            picker: cx.new_view(|cx| Picker::uniform_list(delegate, cx)),
+            picker,
+            picker_focus_handle,
             init_modifiers: cx.modifiers().modified().then_some(cx.modifiers()),
         }
     }
@@ -168,23 +188,85 @@ impl FileFinder {
         self.init_modifiers = Some(cx.modifiers());
         cx.dispatch_action(Box::new(menu::SelectPrev));
     }
+
+    fn handle_open_menu(&mut self, _: &OpenMenu, cx: &mut ViewContext<Self>) {
+        self.picker.update(cx, |picker, cx| {
+            let menu_handle = &picker.delegate.popover_menu_handle;
+            if !menu_handle.is_deployed() {
+                menu_handle.show(cx);
+            }
+        });
+    }
+
+    fn go_to_file_split_left(&mut self, _: &pane::SplitLeft, cx: &mut ViewContext<Self>) {
+        self.go_to_file_split_inner(SplitDirection::Left, cx)
+    }
+
+    fn go_to_file_split_right(&mut self, _: &pane::SplitRight, cx: &mut ViewContext<Self>) {
+        self.go_to_file_split_inner(SplitDirection::Right, cx)
+    }
+
+    fn go_to_file_split_up(&mut self, _: &pane::SplitUp, cx: &mut ViewContext<Self>) {
+        self.go_to_file_split_inner(SplitDirection::Up, cx)
+    }
+
+    fn go_to_file_split_down(&mut self, _: &pane::SplitDown, cx: &mut ViewContext<Self>) {
+        self.go_to_file_split_inner(SplitDirection::Down, cx)
+    }
+
+    fn go_to_file_split_inner(
+        &mut self,
+        split_direction: SplitDirection,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.picker.update(cx, |picker, cx| {
+            let delegate = &mut picker.delegate;
+            if let Some(workspace) = delegate.workspace.upgrade() {
+                if let Some(m) = delegate.matches.get(delegate.selected_index()) {
+                    let path = match &m {
+                        Match::History { path, .. } => {
+                            let worktree_id = path.project.worktree_id;
+                            ProjectPath {
+                                worktree_id,
+                                path: Arc::clone(&path.project.path),
+                            }
+                        }
+                        Match::Search(m) => ProjectPath {
+                            worktree_id: WorktreeId::from_usize(m.0.worktree_id),
+                            path: m.0.path.clone(),
+                        },
+                    };
+                    let open_task = workspace.update(cx, move |workspace, cx| {
+                        workspace.split_path_preview(path, false, Some(split_direction), cx)
+                    });
+                    open_task.detach_and_log_err(cx);
+                }
+            }
+        })
+    }
 }
 
 impl EventEmitter<DismissEvent> for FileFinder {}
 
 impl FocusableView for FileFinder {
-    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
-        self.picker.focus_handle(cx)
+    fn focus_handle(&self, _: &AppContext) -> FocusHandle {
+        self.picker_focus_handle.clone()
     }
 }
 
 impl Render for FileFinder {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        let key_context = self.picker.read(cx).delegate.key_context(cx);
         v_flex()
-            .key_context("FileFinder")
+            .key_context(key_context)
             .w(rems(34.))
             .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
             .on_action(cx.listener(Self::handle_select_prev))
+            .on_action(cx.listener(Self::handle_open_menu))
+            .on_action(cx.listener(Self::go_to_file_split_left))
+            .on_action(cx.listener(Self::go_to_file_split_right))
+            .on_action(cx.listener(Self::go_to_file_split_up))
+            .on_action(cx.listener(Self::go_to_file_split_down))
             .child(self.picker.clone())
     }
 }
@@ -205,6 +287,8 @@ pub struct FileFinderDelegate {
     history_items: Vec<FoundPath>,
     separate_history: bool,
     first_update: bool,
+    popover_menu_handle: PopoverMenuHandle<ContextMenu>,
+    focus_handle: FocusHandle,
 }
 
 /// Use a custom ordering for file finder: the regular one
@@ -533,6 +617,8 @@ impl FileFinderDelegate {
             history_items,
             separate_history,
             first_update: true,
+            popover_menu_handle: PopoverMenuHandle::default(),
+            focus_handle: cx.focus_handle(),
         }
     }
 
@@ -845,6 +931,15 @@ impl FileFinderDelegate {
 
         0
     }
+
+    fn key_context(&self, cx: &WindowContext) -> KeyContext {
+        let mut key_context = KeyContext::new_with_defaults();
+        key_context.add("FileFinder");
+        if self.popover_menu_handle.is_focused(cx) {
+            key_context.add("menu_open");
+        }
+        key_context
+    }
 }
 
 impl PickerDelegate for FileFinderDelegate {
@@ -958,7 +1053,7 @@ impl PickerDelegate for FileFinderDelegate {
                             let allow_preview =
                                 PreviewTabsSettings::get_global(cx).enable_preview_from_file_finder;
                             if secondary {
-                                workspace.split_path_preview(project_path, allow_preview, cx)
+                                workspace.split_path_preview(project_path, allow_preview, None, cx)
                             } else {
                                 workspace.open_path_preview(
                                     project_path,
@@ -1125,6 +1220,60 @@ impl PickerDelegate for FileFinderDelegate {
                 ),
         )
     }
+
+    fn render_footer(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
+        let menu_open = self.popover_menu_handle.is_focused(cx);
+        Some(
+            h_flex()
+                .w_full()
+                .border_t_1()
+                .py_2()
+                .pr_2()
+                .border_color(cx.theme().colors().border)
+                .justify_end()
+                .child(
+                    ButtonLike::new("open-selection")
+                        .when_some(KeyBinding::for_action(&menu::Confirm, cx), |button, key| {
+                            button.child(key)
+                        })
+                        .child(Label::new("Open"))
+                        .on_click(|_, cx| cx.dispatch_action(menu::Confirm.boxed_clone())),
+                )
+                .child(
+                    div().pl_2().child(
+                        PopoverMenu::new("menu-popover")
+                            .with_handle(self.popover_menu_handle.clone())
+                            .attach(gpui::AnchorCorner::TopRight)
+                            .anchor(gpui::AnchorCorner::BottomRight)
+                            .trigger(
+                                ButtonLike::new("menu-trigger")
+                                    .selected(menu_open)
+                                    .selected_style(ButtonStyle::Tinted(TintColor::Accent))
+                                    .when_some(
+                                        KeyBinding::for_action_in(
+                                            &OpenMenu,
+                                            &self.focus_handle,
+                                            cx,
+                                        ),
+                                        |button, key| button.child(key),
+                                    )
+                                    .child(Label::new("More actions")),
+                            )
+                            .menu({
+                                move |cx| {
+                                    Some(ContextMenu::build(cx, move |menu, _| {
+                                        menu.action("Split left", pane::SplitLeft.boxed_clone())
+                                            .action("Split right", pane::SplitRight.boxed_clone())
+                                            .action("Split up", pane::SplitUp.boxed_clone())
+                                            .action("Split down", pane::SplitDown.boxed_clone())
+                                    }))
+                                }
+                            }),
+                    ),
+                )
+                .into_any(),
+        )
+    }
 }
 
 #[cfg(test)]

crates/ui/src/components/popover_menu.rs 🔗

@@ -66,7 +66,7 @@ impl<M: ManagedView> PopoverMenuHandle<M> {
             .map_or(false, |state| state.menu.borrow().as_ref().is_some())
     }
 
-    pub fn is_focused(&self, cx: &mut WindowContext) -> bool {
+    pub fn is_focused(&self, cx: &WindowContext) -> bool {
         self.0.borrow().as_ref().map_or(false, |state| {
             state
                 .menu

crates/workspace/src/workspace.rs 🔗

@@ -2631,13 +2631,14 @@ impl Workspace {
         path: impl Into<ProjectPath>,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
-        self.split_path_preview(path, false, cx)
+        self.split_path_preview(path, false, None, cx)
     }
 
     pub fn split_path_preview(
         &mut self,
         path: impl Into<ProjectPath>,
         allow_preview: bool,
+        split_direction: Option<SplitDirection>,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
         let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
@@ -2658,7 +2659,8 @@ impl Workspace {
             let (project_entry_id, build_item) = task.await?;
             this.update(&mut cx, move |this, cx| -> Option<_> {
                 let pane = pane.upgrade()?;
-                let new_pane = this.split_pane(pane, SplitDirection::Right, cx);
+                let new_pane =
+                    this.split_pane(pane, split_direction.unwrap_or(SplitDirection::Right), cx);
                 new_pane.update(cx, |new_pane, cx| {
                     Some(new_pane.open_item(project_entry_id, true, allow_preview, cx, build_item))
                 })