feat(workspace): allow alternative actions to open files and symbols in split

Alex Viscreanu and Mikayla Maki created

Co-authored-by: Mikayla Maki <mikayla@zed.dev>

Change summary

assets/keymaps/atom.json                          |  1 
assets/keymaps/default.json                       |  3 
assets/keymaps/jetbrains.json                     |  3 
assets/keymaps/sublime_text.json                  |  1 
assets/keymaps/textmate.json                      |  1 
crates/collab/src/tests/integration_tests.rs      |  6 
crates/collab_ui/src/contact_finder.rs            |  2 
crates/command_palette/src/command_palette.rs     |  2 
crates/editor/src/editor.rs                       | 49 +++++++-
crates/editor/src/element.rs                      |  6 
crates/editor/src/link_go_to_definition.rs        | 15 +-
crates/file_finder/src/file_finder.rs             | 90 +++++++++------
crates/language_selector/src/language_selector.rs |  2 
crates/menu/src/menu.rs                           |  1 
crates/outline/src/outline.rs                     |  2 
crates/picker/src/picker.rs                       | 20 ++-
crates/project_panel/src/project_panel.rs         | 28 ++++
crates/project_symbols/src/project_symbols.rs     |  9 +
crates/recent_projects/src/recent_projects.rs     |  2 
crates/theme_selector/src/theme_selector.rs       |  2 
crates/vcs_menu/src/lib.rs                        |  2 
crates/vector_store/src/modal.rs                  |  2 
crates/welcome/src/base_keymap_picker.rs          |  2 
crates/workspace/src/workspace.rs                 | 98 ++++++++++++++++
crates/zed/src/zed.rs                             |  8 +
25 files changed, 277 insertions(+), 80 deletions(-)

Detailed changes

assets/keymaps/atom.json 🔗

@@ -9,6 +9,7 @@
     "context": "Editor",
     "bindings": {
       "cmd-b": "editor::GoToDefinition",
+      "alt-cmd-b": "editor::GoToDefinitionSplit",
       "cmd-<": "editor::ScrollCursorCenter",
       "cmd-g": [
         "editor::SelectNext",

assets/keymaps/default.json 🔗

@@ -13,6 +13,7 @@
       "cmd-up": "menu::SelectFirst",
       "cmd-down": "menu::SelectLast",
       "enter": "menu::Confirm",
+      "cmd-enter": "menu::SecondaryConfirm",
       "escape": "menu::Cancel",
       "ctrl-c": "menu::Cancel",
       "cmd-{": "pane::ActivatePrevItem",
@@ -298,7 +299,9 @@
       "shift-f8": "editor::GoToPrevDiagnostic",
       "f2": "editor::Rename",
       "f12": "editor::GoToDefinition",
+      "alt-f12": "editor::GoToDefinitionSplit",
       "cmd-f12": "editor::GoToTypeDefinition",
+      "alt-cmd-f12": "editor::GoToTypeDefinitionSplit",
       "alt-shift-f12": "editor::FindAllReferences",
       "ctrl-m": "editor::MoveToEnclosingBracket",
       "alt-cmd-[": "editor::Fold",

assets/keymaps/jetbrains.json 🔗

@@ -46,8 +46,9 @@
       "alt-f7": "editor::FindAllReferences",
       "cmd-alt-f7": "editor::FindAllReferences",
       "cmd-b": "editor::GoToDefinition",
-      "cmd-alt-b": "editor::GoToDefinition",
+      "cmd-alt-b": "editor::GoToDefinitionSplit",
       "cmd-shift-b": "editor::GoToTypeDefinition",
+      "cmd-alt-shift-b": "editor::GoToTypeDefinitionSplit",
       "alt-enter": "editor::ToggleCodeActions",
       "f2": "editor::GoToDiagnostic",
       "cmd-f2": "editor::GoToPrevDiagnostic",

assets/keymaps/sublime_text.json 🔗

@@ -20,6 +20,7 @@
       "cmd-shift-a": "editor::SelectLargerSyntaxNode",
       "shift-f12": "editor::FindAllReferences",
       "alt-cmd-down": "editor::GoToDefinition",
+      "ctrl-alt-cmd-down": "editor::GoToDefinitionSplit",
       "alt-shift-cmd-down": "editor::FindAllReferences",
       "ctrl-.": "editor::GoToHunk",
       "ctrl-,": "editor::GoToPrevHunk",

assets/keymaps/textmate.json 🔗

@@ -12,6 +12,7 @@
       "cmd-l": "go_to_line::Toggle",
       "ctrl-shift-d": "editor::DuplicateLine",
       "cmd-b": "editor::GoToDefinition",
+      "alt-cmd-b": "editor::GoToDefinition",
       "cmd-j": "editor::ScrollCursorCenter",
       "cmd-shift-l": "editor::SelectLine",
       "cmd-shift-t": "outline::Toggle",

crates/collab/src/tests/integration_tests.rs 🔗

@@ -7217,7 +7217,7 @@ async fn test_peers_following_each_other(
 
     // Clients A and B follow each other in split panes
     workspace_a.update(cx_a, |workspace, cx| {
-        workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
+        workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
     });
     workspace_a
         .update(cx_a, |workspace, cx| {
@@ -7228,7 +7228,7 @@ async fn test_peers_following_each_other(
         .await
         .unwrap();
     workspace_b.update(cx_b, |workspace, cx| {
-        workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
+        workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
     });
     workspace_b
         .update(cx_b, |workspace, cx| {
@@ -7455,7 +7455,7 @@ async fn test_auto_unfollowing(
 
     // When client B activates a different pane, it continues following client A in the original pane.
     workspace_b.update(cx_b, |workspace, cx| {
-        workspace.split_pane(pane_b.clone(), SplitDirection::Right, cx)
+        workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx)
     });
     assert_eq!(
         workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),

crates/collab_ui/src/contact_finder.rs 🔗

@@ -67,7 +67,7 @@ impl PickerDelegate for ContactFinderDelegate {
         })
     }
 
-    fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
         if let Some(user) = self.potential_contacts.get(self.selected_index) {
             let user_store = self.user_store.read(cx);
             match user_store.contact_request_status(user) {

crates/command_palette/src/command_palette.rs 🔗

@@ -160,7 +160,7 @@ impl PickerDelegate for CommandPaletteDelegate {
 
     fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
 
-    fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
         if !self.matches.is_empty() {
             let window_id = cx.window_id();
             let focused_view_id = self.focused_view_id;

crates/editor/src/editor.rs 🔗

@@ -271,7 +271,9 @@ actions!(
         SelectLargerSyntaxNode,
         SelectSmallerSyntaxNode,
         GoToDefinition,
+        GoToDefinitionSplit,
         GoToTypeDefinition,
+        GoToTypeDefinitionSplit,
         MoveToEnclosingBracket,
         UndoSelection,
         RedoSelection,
@@ -407,7 +409,9 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(Editor::go_to_hunk);
     cx.add_action(Editor::go_to_prev_hunk);
     cx.add_action(Editor::go_to_definition);
+    cx.add_action(Editor::go_to_definition_split);
     cx.add_action(Editor::go_to_type_definition);
+    cx.add_action(Editor::go_to_type_definition_split);
     cx.add_action(Editor::fold);
     cx.add_action(Editor::fold_at);
     cx.add_action(Editor::unfold_lines);
@@ -6185,14 +6189,31 @@ impl Editor {
     }
 
     pub fn go_to_definition(&mut self, _: &GoToDefinition, cx: &mut ViewContext<Self>) {
-        self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, cx);
+        self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, false, cx);
     }
 
     pub fn go_to_type_definition(&mut self, _: &GoToTypeDefinition, cx: &mut ViewContext<Self>) {
-        self.go_to_definition_of_kind(GotoDefinitionKind::Type, cx);
+        self.go_to_definition_of_kind(GotoDefinitionKind::Type, false, cx);
     }
 
-    fn go_to_definition_of_kind(&mut self, kind: GotoDefinitionKind, cx: &mut ViewContext<Self>) {
+    pub fn go_to_definition_split(&mut self, _: &GoToDefinitionSplit, cx: &mut ViewContext<Self>) {
+        self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, true, cx);
+    }
+
+    pub fn go_to_type_definition_split(
+        &mut self,
+        _: &GoToTypeDefinitionSplit,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.go_to_definition_of_kind(GotoDefinitionKind::Type, true, cx);
+    }
+
+    fn go_to_definition_of_kind(
+        &mut self,
+        kind: GotoDefinitionKind,
+        split: bool,
+        cx: &mut ViewContext<Self>,
+    ) {
         let Some(workspace) = self.workspace(cx) else { return };
         let buffer = self.buffer.read(cx);
         let head = self.selections.newest::<usize>(cx).head();
@@ -6211,7 +6232,7 @@ impl Editor {
         cx.spawn_labeled("Fetching Definition...", |editor, mut cx| async move {
             let definitions = definitions.await?;
             editor.update(&mut cx, |editor, cx| {
-                editor.navigate_to_definitions(definitions, cx);
+                editor.navigate_to_definitions(definitions, split, cx);
             })?;
             Ok::<(), anyhow::Error>(())
         })
@@ -6221,6 +6242,7 @@ impl Editor {
     pub fn navigate_to_definitions(
         &mut self,
         mut definitions: Vec<LocationLink>,
+        split: bool,
         cx: &mut ViewContext<Editor>,
     ) {
         let Some(workspace) = self.workspace(cx) else { return };
@@ -6240,7 +6262,11 @@ impl Editor {
             } else {
                 cx.window_context().defer(move |cx| {
                     let target_editor: ViewHandle<Self> = workspace.update(cx, |workspace, cx| {
-                        workspace.open_project_item(definition.target.buffer.clone(), cx)
+                        if split {
+                            workspace.split_project_item(definition.target.buffer.clone(), cx)
+                        } else {
+                            workspace.open_project_item(definition.target.buffer.clone(), cx)
+                        }
                     });
                     target_editor.update(cx, |target_editor, cx| {
                         // When selecting a definition in a different buffer, disable the nav history
@@ -6276,7 +6302,9 @@ impl Editor {
                     .map(|definition| definition.target)
                     .collect();
                 workspace.update(cx, |workspace, cx| {
-                    Self::open_locations_in_multibuffer(workspace, locations, replica_id, title, cx)
+                    Self::open_locations_in_multibuffer(
+                        workspace, locations, replica_id, title, split, cx,
+                    )
                 });
             });
         }
@@ -6321,7 +6349,7 @@ impl Editor {
                         })
                         .unwrap();
                     Self::open_locations_in_multibuffer(
-                        workspace, locations, replica_id, title, cx,
+                        workspace, locations, replica_id, title, false, cx,
                     );
                 })?;
 
@@ -6336,6 +6364,7 @@ impl Editor {
         mut locations: Vec<Location>,
         replica_id: ReplicaId,
         title: String,
+        split: bool,
         cx: &mut ViewContext<Workspace>,
     ) {
         // If there are multiple definitions, open them in a multibuffer
@@ -6382,7 +6411,11 @@ impl Editor {
                 cx,
             );
         });
-        workspace.add_item(Box::new(editor), cx);
+        if split {
+            workspace.split_item(Box::new(editor), cx);
+        } else {
+            workspace.add_item(Box::new(editor), cx);
+        }
     }
 
     pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {

crates/editor/src/element.rs 🔗

@@ -156,6 +156,7 @@ impl EditorElement {
                         event.position,
                         event.cmd,
                         event.shift,
+                        event.alt,
                         position_map.as_ref(),
                         text_bounds,
                         cx,
@@ -308,6 +309,7 @@ impl EditorElement {
         position: Vector2F,
         cmd: bool,
         shift: bool,
+        alt: bool,
         position_map: &PositionMap,
         text_bounds: RectF,
         cx: &mut EventContext<Editor>,
@@ -324,9 +326,9 @@ impl EditorElement {
 
             if point == target_point {
                 if shift {
-                    go_to_fetched_type_definition(editor, point, cx);
+                    go_to_fetched_type_definition(editor, point, alt, cx);
                 } else {
-                    go_to_fetched_definition(editor, point, cx);
+                    go_to_fetched_definition(editor, point, alt, cx);
                 }
 
                 return true;
@@ -246,23 +246,26 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
 pub fn go_to_fetched_definition(
     editor: &mut Editor,
     point: DisplayPoint,
+    split: bool,
     cx: &mut ViewContext<Editor>,
 ) {
-    go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, editor, point, cx);
+    go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, editor, point, split, cx);
 }
 
 pub fn go_to_fetched_type_definition(
     editor: &mut Editor,
     point: DisplayPoint,
+    split: bool,
     cx: &mut ViewContext<Editor>,
 ) {
-    go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, editor, point, cx);
+    go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, editor, point, split, cx);
 }
 
 fn go_to_fetched_definition_of_kind(
     kind: LinkDefinitionKind,
     editor: &mut Editor,
     point: DisplayPoint,
+    split: bool,
     cx: &mut ViewContext<Editor>,
 ) {
     let cached_definitions = editor.link_go_to_definition_state.definitions.clone();
@@ -275,7 +278,7 @@ fn go_to_fetched_definition_of_kind(
             cx.focus_self();
         }
 
-        editor.navigate_to_definitions(cached_definitions, cx);
+        editor.navigate_to_definitions(cached_definitions, split, cx);
     } else {
         editor.select(
             SelectPhase::Begin {
@@ -403,7 +406,7 @@ mod tests {
             });
 
         cx.update_editor(|editor, cx| {
-            go_to_fetched_type_definition(editor, hover_point, cx);
+            go_to_fetched_type_definition(editor, hover_point, false, cx);
         });
         requests.next().await;
         cx.foreground().run_until_parked();
@@ -614,7 +617,7 @@ mod tests {
 
         // Cmd click with existing definition doesn't re-request and dismisses highlight
         cx.update_editor(|editor, cx| {
-            go_to_fetched_definition(editor, hover_point, cx);
+            go_to_fetched_definition(editor, hover_point, false, cx);
         });
         // Assert selection moved to to definition
         cx.lsp
@@ -655,7 +658,7 @@ mod tests {
             ])))
         });
         cx.update_editor(|editor, cx| {
-            go_to_fetched_definition(editor, hover_point, cx);
+            go_to_fetched_definition(editor, hover_point, false, cx);
         });
         requests.next().await;
         cx.foreground().run_until_parked();

crates/file_finder/src/file_finder.rs 🔗

@@ -442,53 +442,71 @@ impl PickerDelegate for FileFinderDelegate {
         }
     }
 
-    fn confirm(&mut self, cx: &mut ViewContext<FileFinder>) {
+    fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<FileFinder>) {
         if let Some(m) = self.matches.get(self.selected_index()) {
             if let Some(workspace) = self.workspace.upgrade(cx) {
-                let open_task = workspace.update(cx, |workspace, cx| match m {
-                    Match::History(history_match) => {
-                        let worktree_id = history_match.project.worktree_id;
-                        if workspace
-                            .project()
-                            .read(cx)
-                            .worktree_for_id(worktree_id, cx)
-                            .is_some()
-                        {
-                            workspace.open_path(
-                                ProjectPath {
-                                    worktree_id,
-                                    path: Arc::clone(&history_match.project.path),
-                                },
-                                None,
-                                true,
-                                cx,
-                            )
+                let open_task = workspace.update(cx, move |workspace, cx| {
+                    let split_or_open = |workspace: &mut Workspace, project_path, cx| {
+                        if secondary {
+                            workspace.split_path(project_path, cx)
                         } else {
-                            match history_match.absolute.as_ref() {
-                                Some(abs_path) => {
-                                    workspace.open_abs_path(abs_path.to_path_buf(), false, cx)
-                                }
-                                None => workspace.open_path(
+                            workspace.open_path(project_path, None, true, cx)
+                        }
+                    };
+                    match m {
+                        Match::History(history_match) => {
+                            let worktree_id = history_match.project.worktree_id;
+                            if workspace
+                                .project()
+                                .read(cx)
+                                .worktree_for_id(worktree_id, cx)
+                                .is_some()
+                            {
+                                split_or_open(
+                                    workspace,
                                     ProjectPath {
                                         worktree_id,
                                         path: Arc::clone(&history_match.project.path),
                                     },
-                                    None,
-                                    true,
                                     cx,
-                                ),
+                                )
+                            } else {
+                                match history_match.absolute.as_ref() {
+                                    Some(abs_path) => {
+                                        if secondary {
+                                            workspace.split_abs_path(
+                                                abs_path.to_path_buf(),
+                                                false,
+                                                cx,
+                                            )
+                                        } else {
+                                            workspace.open_abs_path(
+                                                abs_path.to_path_buf(),
+                                                false,
+                                                cx,
+                                            )
+                                        }
+                                    }
+                                    None => split_or_open(
+                                        workspace,
+                                        ProjectPath {
+                                            worktree_id,
+                                            path: Arc::clone(&history_match.project.path),
+                                        },
+                                        cx,
+                                    ),
+                                }
                             }
                         }
+                        Match::Search(m) => split_or_open(
+                            workspace,
+                            ProjectPath {
+                                worktree_id: WorktreeId::from_usize(m.worktree_id),
+                                path: m.path.clone(),
+                            },
+                            cx,
+                        ),
                     }
-                    Match::Search(m) => workspace.open_path(
-                        ProjectPath {
-                            worktree_id: WorktreeId::from_usize(m.worktree_id),
-                            path: m.path.clone(),
-                        },
-                        None,
-                        true,
-                        cx,
-                    ),
                 });
 
                 let row = self

crates/language_selector/src/language_selector.rs 🔗

@@ -93,7 +93,7 @@ impl PickerDelegate for LanguageSelectorDelegate {
         self.matches.len()
     }
 
-    fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
         if let Some(mat) = self.matches.get(self.selected_index) {
             let language_name = &self.candidates[mat.candidate_id].string;
             let language = self.language_registry.language_for_name(language_name);

crates/menu/src/menu.rs 🔗

@@ -3,6 +3,7 @@ gpui::actions!(
     [
         Cancel,
         Confirm,
+        SecondaryConfirm,
         SelectPrev,
         SelectNext,
         SelectFirst,

crates/outline/src/outline.rs 🔗

@@ -177,7 +177,7 @@ impl PickerDelegate for OutlineViewDelegate {
         Task::ready(())
     }
 
-    fn confirm(&mut self, cx: &mut ViewContext<OutlineView>) {
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<OutlineView>) {
         self.prev_scroll_position.take();
         self.active_editor.update(cx, |active_editor, cx| {
             if let Some(rows) = active_editor.highlighted_rows() {

crates/picker/src/picker.rs 🔗

@@ -7,7 +7,7 @@ use gpui::{
     AnyElement, AnyViewHandle, AppContext, Axis, Entity, MouseState, Task, View, ViewContext,
     ViewHandle,
 };
-use menu::{Cancel, Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev};
+use menu::{Cancel, Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrev};
 use parking_lot::Mutex;
 use std::{cmp, sync::Arc};
 use util::ResultExt;
@@ -34,7 +34,7 @@ pub trait PickerDelegate: Sized + 'static {
     fn selected_index(&self) -> usize;
     fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>);
     fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()>;
-    fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>);
+    fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>);
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>);
     fn render_match(
         &self,
@@ -118,8 +118,8 @@ impl<D: PickerDelegate> View for Picker<D> {
                                 // Capture mouse events
                                 .on_down(MouseButton::Left, |_, _, _| {})
                                 .on_up(MouseButton::Left, |_, _, _| {})
-                                .on_click(MouseButton::Left, move |_, picker, cx| {
-                                    picker.select_index(ix, cx);
+                                .on_click(MouseButton::Left, move |click, picker, cx| {
+                                    picker.select_index(ix, click.cmd, cx);
                                 })
                                 .with_cursor_style(CursorStyle::PointingHand)
                                 .into_any()
@@ -175,6 +175,7 @@ impl<D: PickerDelegate> Picker<D> {
         cx.add_action(Self::select_next);
         cx.add_action(Self::select_prev);
         cx.add_action(Self::confirm);
+        cx.add_action(Self::secondary_confirm);
         cx.add_action(Self::cancel);
     }
 
@@ -288,11 +289,11 @@ impl<D: PickerDelegate> Picker<D> {
         cx.notify();
     }
 
-    pub fn select_index(&mut self, index: usize, cx: &mut ViewContext<Self>) {
+    pub fn select_index(&mut self, index: usize, cmd: bool, cx: &mut ViewContext<Self>) {
         if self.delegate.match_count() > 0 {
             self.confirmed = true;
             self.delegate.set_selected_index(index, cx);
-            self.delegate.confirm(cx);
+            self.delegate.confirm(cmd, cx);
         }
     }
 
@@ -330,7 +331,12 @@ impl<D: PickerDelegate> Picker<D> {
 
     pub fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
         self.confirmed = true;
-        self.delegate.confirm(cx);
+        self.delegate.confirm(false, cx);
+    }
+
+    pub fn secondary_confirm(&mut self, _: &SecondaryConfirm, cx: &mut ViewContext<Self>) {
+        self.confirmed = true;
+        self.delegate.confirm(true, cx);
     }
 
     fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {

crates/project_panel/src/project_panel.rs 🔗

@@ -159,6 +159,9 @@ pub enum Event {
         entry_id: ProjectEntryId,
         focus_opened_item: bool,
     },
+    SplitEntry {
+        entry_id: ProjectEntryId,
+    },
     DockPositionChanged,
     Focus,
 }
@@ -290,6 +293,21 @@ impl ProjectPanel {
                         }
                     }
                 }
+                &Event::SplitEntry { entry_id } => {
+                    if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
+                        if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
+                            workspace
+                                .split_path(
+                                    ProjectPath {
+                                        worktree_id: worktree.read(cx).id(),
+                                        path: entry.path.clone(),
+                                    },
+                                    cx,
+                                )
+                                .detach_and_log_err(cx);
+                        }
+                    }
+                }
                 _ => {}
             }
         })
@@ -620,6 +638,10 @@ impl ProjectPanel {
         });
     }
 
+    fn split_entry(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext<Self>) {
+        cx.emit(Event::SplitEntry { entry_id });
+    }
+
     fn new_file(&mut self, _: &NewFile, cx: &mut ViewContext<Self>) {
         self.add_entry(false, cx)
     }
@@ -1333,7 +1355,11 @@ impl ProjectPanel {
                 if kind.is_dir() {
                     this.toggle_expanded(entry_id, cx);
                 } else {
-                    this.open_entry(entry_id, event.click_count > 1, cx);
+                    if event.cmd && event.click_count > 1 {
+                        this.split_entry(entry_id, cx);
+                    } else if !event.cmd {
+                        this.open_entry(entry_id, event.click_count > 1, cx);
+                    }
                 }
             }
         })

crates/project_symbols/src/project_symbols.rs 🔗

@@ -104,7 +104,7 @@ impl PickerDelegate for ProjectSymbolsDelegate {
         "Search project symbols...".into()
     }
 
-    fn confirm(&mut self, cx: &mut ViewContext<ProjectSymbols>) {
+    fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<ProjectSymbols>) {
         if let Some(symbol) = self
             .matches
             .get(self.selected_match_index)
@@ -122,7 +122,12 @@ impl PickerDelegate for ProjectSymbolsDelegate {
                         .read(cx)
                         .clip_point_utf16(symbol.range.start, Bias::Left);
 
-                    let editor = workspace.open_project_item::<Editor>(buffer, cx);
+                    let editor = if secondary {
+                        workspace.split_project_item::<Editor>(buffer, cx)
+                    } else {
+                        workspace.open_project_item::<Editor>(buffer, cx)
+                    };
+
                     editor.update(cx, |editor, cx| {
                         editor.change_selections(Some(Autoscroll::center()), cx, |s| {
                             s.select_ranges([position..position])

crates/recent_projects/src/recent_projects.rs 🔗

@@ -161,7 +161,7 @@ impl PickerDelegate for RecentProjectsDelegate {
         Task::ready(())
     }
 
-    fn confirm(&mut self, cx: &mut ViewContext<RecentProjects>) {
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<RecentProjects>) {
         if let Some((selected_match, workspace)) = self
             .matches
             .get(self.selected_index())

crates/theme_selector/src/theme_selector.rs 🔗

@@ -120,7 +120,7 @@ impl PickerDelegate for ThemeSelectorDelegate {
         self.matches.len()
     }
 
-    fn confirm(&mut self, cx: &mut ViewContext<ThemeSelector>) {
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<ThemeSelector>) {
         self.selection_completed = true;
 
         let theme_name = theme::current(cx).meta.name.clone();

crates/vcs_menu/src/lib.rs 🔗

@@ -182,7 +182,7 @@ impl PickerDelegate for BranchListDelegate {
         })
     }
 
-    fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
         let current_pick = self.selected_index();
         let Some(current_pick) = self.matches.get(current_pick).map(|pick| pick.string.clone()) else {
             return;

crates/vector_store/src/modal.rs 🔗

@@ -51,7 +51,7 @@ impl PickerDelegate for SemanticSearchDelegate {
         "Search repository in natural language...".into()
     }
 
-    fn confirm(&mut self, cx: &mut ViewContext<SemanticSearch>) {
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<SemanticSearch>) {
         if let Some(search_result) = self.matches.get(self.selected_match_index) {
             // Open Buffer
             let search_result = search_result.clone();

crates/welcome/src/base_keymap_picker.rs 🔗

@@ -120,7 +120,7 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
         })
     }
 
-    fn confirm(&mut self, cx: &mut ViewContext<BaseKeymapSelector>) {
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<BaseKeymapSelector>) {
         if let Some(selection) = self.matches.get(self.selected_index) {
             let base_keymap = BaseKeymap::from_names(&selection.string);
             update_settings_file::<BaseKeymap>(self.fs.clone(), cx, move |setting| {

crates/workspace/src/workspace.rs 🔗

@@ -1821,6 +1821,13 @@ impl Workspace {
             .update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
     }
 
+    pub fn split_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
+        let new_pane = self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
+        new_pane.update(cx, move |new_pane, cx| {
+            new_pane.add_item(item, true, true, None, cx)
+        })
+    }
+
     pub fn open_abs_path(
         &mut self,
         abs_path: PathBuf,
@@ -1851,6 +1858,21 @@ impl Workspace {
         })
     }
 
+    pub fn split_abs_path(
+        &mut self,
+        abs_path: PathBuf,
+        visible: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
+        let project_path_task =
+            Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
+        cx.spawn(|this, mut cx| async move {
+            let (_, path) = project_path_task.await?;
+            this.update(&mut cx, |this, cx| this.split_path(path, cx))?
+                .await
+        })
+    }
+
     pub fn open_path(
         &mut self,
         path: impl Into<ProjectPath>,
@@ -1876,6 +1898,38 @@ impl Workspace {
         })
     }
 
+    pub fn split_path(
+        &mut self,
+        path: impl Into<ProjectPath>,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
+        let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
+            self.panes
+                .first()
+                .expect("There must be an active pane")
+                .downgrade()
+        });
+
+        if let Member::Pane(center_pane) = &self.center.root {
+            if center_pane.read(cx).items_len() == 0 {
+                return self.open_path(path, Some(pane), true, cx);
+            }
+        }
+
+        let task = self.load_path(path.into(), cx);
+        cx.spawn(|this, mut cx| async move {
+            let (project_entry_id, build_item) = task.await?;
+            this.update(&mut cx, move |this, cx| -> Option<_> {
+                let pane = pane.upgrade(cx)?;
+                let new_pane = this.split_pane(pane, SplitDirection::Right, cx);
+                new_pane.update(cx, |new_pane, cx| {
+                    Some(new_pane.open_item(project_entry_id, true, cx, build_item))
+                })
+            })
+            .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
+        })
+    }
+
     pub(crate) fn load_path(
         &mut self,
         path: ProjectPath,
@@ -1926,6 +1980,30 @@ impl Workspace {
         item
     }
 
+    pub fn split_project_item<T>(
+        &mut self,
+        project_item: ModelHandle<T::Item>,
+        cx: &mut ViewContext<Self>,
+    ) -> ViewHandle<T>
+    where
+        T: ProjectItem,
+    {
+        use project::Item as _;
+
+        let entry_id = project_item.read(cx).entry_id(cx);
+        if let Some(item) = entry_id
+            .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
+            .and_then(|item| item.downcast())
+        {
+            self.activate_item(&item, cx);
+            return item;
+        }
+
+        let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
+        self.split_item(Box::new(item.clone()), cx);
+        item
+    }
+
     pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
         if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
             self.active_pane.update(cx, |pane, cx| {
@@ -1953,7 +2031,7 @@ impl Workspace {
         if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
             cx.focus(&pane);
         } else {
-            self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
+            self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx);
         }
     }
 
@@ -2006,7 +2084,7 @@ impl Workspace {
         match event {
             pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx),
             pane::Event::Split(direction) => {
-                self.split_pane(pane, *direction, cx);
+                self.split_and_clone(pane, *direction, cx);
             }
             pane::Event::Remove => self.remove_pane(pane, cx),
             pane::Event::ActivateItem { local } => {
@@ -2057,6 +2135,20 @@ impl Workspace {
     }
 
     pub fn split_pane(
+        &mut self,
+        pane_to_split: ViewHandle<Pane>,
+        split_direction: SplitDirection,
+        cx: &mut ViewContext<Self>,
+    ) -> ViewHandle<Pane> {
+        let new_pane = self.add_pane(cx);
+        self.center
+            .split(&pane_to_split, &new_pane, split_direction)
+            .unwrap();
+        cx.notify();
+        new_pane
+    }
+
+    pub fn split_and_clone(
         &mut self,
         pane: ViewHandle<Pane>,
         direction: SplitDirection,
@@ -4246,7 +4338,7 @@ mod tests {
             });
 
             workspace
-                .split_pane(left_pane.clone(), SplitDirection::Right, cx)
+                .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
                 .unwrap();
 
             left_pane

crates/zed/src/zed.rs 🔗

@@ -1021,7 +1021,7 @@ mod tests {
         // Split the pane with the first entry, then open the second entry again.
         workspace
             .update(cx, |w, cx| {
-                w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx);
+                w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, cx);
                 w.open_path(file2.clone(), None, true, cx)
             })
             .await
@@ -1344,7 +1344,11 @@ mod tests {
         cx.dispatch_action(window_id, NewFile);
         workspace
             .update(cx, |workspace, cx| {
-                workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
+                workspace.split_and_clone(
+                    workspace.active_pane().clone(),
+                    SplitDirection::Right,
+                    cx,
+                );
                 workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx)
             })
             .await