Allow empty splits on panes (#40245)

Draft as a base for continuing the discussion in #8008 : adds a
`SplitOperation` enum to support bindings like `["pane::SplitLeft",
{"operation": "Clear"}]`

To be discussed @MrSubidubi and others:

- Naming: Generally not happy with names yet and specifically `Empty` is
unclear, e.g., what does this mean for terminal panes? Added placeholder
code to split without cloning, but unsure what users would expect in
this case.
- ~~I removed `SplitAndMoveXyz` actions but I guess we should keep them
for backwards compatibility?~~
- May have missed details in the move implementation. Will check the
code again for opportunities to refactor more code after we agree on the
approach.
- ~~Tests should go to `crates/collab/src/tests/integration_tests.rs`?~~

Closes #8008

Release Notes:

- Add `pane::Split` mode (`{ClonePane,EmptyPane,MovePane}`) to allow
creating an empty buffer.

---------

Co-authored-by: Finn Evers <finn.evers@outlook.de>
Co-authored-by: MrSubidubi <finn@zed.dev>
This commit is contained in:
Yves Ineichen
2025-12-22 00:50:02 +01:00
committed by GitHub
parent 3dc0614dba
commit 3b626c8ac1
8 changed files with 417 additions and 167 deletions

View File

@@ -6745,8 +6745,13 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
});
// Split pane to the right
pane.update(cx, |pane, cx| {
pane.split(workspace::SplitDirection::Right, cx);
pane.update_in(cx, |pane, window, cx| {
pane.split(
workspace::SplitDirection::Right,
workspace::SplitMode::default(),
window,
cx,
);
});
cx.run_until_parked();
let right_pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());

View File

@@ -1760,16 +1760,19 @@ impl PickerDelegate for FileFinderDelegate {
menu.context(focus_handle)
.action(
"Split Left",
pane::SplitLeft.boxed_clone(),
pane::SplitLeft::default().boxed_clone(),
)
.action(
"Split Right",
pane::SplitRight.boxed_clone(),
pane::SplitRight::default().boxed_clone(),
)
.action(
"Split Up",
pane::SplitUp::default().boxed_clone(),
)
.action("Split Up", pane::SplitUp.boxed_clone())
.action(
"Split Down",
pane::SplitDown.boxed_clone(),
pane::SplitDown::default().boxed_clone(),
)
}
}))

View File

@@ -30,8 +30,8 @@ use workspace::{
ActivateNextPane, ActivatePane, ActivatePaneDown, ActivatePaneLeft, ActivatePaneRight,
ActivatePaneUp, ActivatePreviousPane, DraggedSelection, DraggedTab, ItemId, MoveItemToPane,
MoveItemToPaneInDirection, MovePaneDown, MovePaneLeft, MovePaneRight, MovePaneUp, NewTerminal,
Pane, PaneGroup, SplitDirection, SplitDown, SplitLeft, SplitRight, SplitUp, SwapPaneDown,
SwapPaneLeft, SwapPaneRight, SwapPaneUp, ToggleZoom, Workspace,
Pane, PaneGroup, SplitDirection, SplitDown, SplitLeft, SplitMode, SplitRight, SplitUp,
SwapPaneDown, SwapPaneLeft, SwapPaneRight, SwapPaneUp, ToggleZoom, Workspace,
dock::{DockPosition, Panel, PanelEvent, PanelHandle},
item::SerializableItem,
move_active_item, move_item, pane,
@@ -192,10 +192,10 @@ impl TerminalPanel {
split_context.clone(),
|menu, split_context| menu.context(split_context),
)
.action("Split Right", SplitRight.boxed_clone())
.action("Split Left", SplitLeft.boxed_clone())
.action("Split Up", SplitUp.boxed_clone())
.action("Split Down", SplitDown.boxed_clone())
.action("Split Right", SplitRight::default().boxed_clone())
.action("Split Left", SplitLeft::default().boxed_clone())
.action("Split Up", SplitUp::default().boxed_clone())
.action("Split Down", SplitDown::default().boxed_clone())
})
.into()
}
@@ -380,47 +380,49 @@ impl TerminalPanel {
}
self.serialize(cx);
}
&pane::Event::Split {
direction,
clone_active_item,
} => {
if clone_active_item {
let fut = self.new_pane_with_cloned_active_terminal(window, cx);
let pane = pane.clone();
cx.spawn_in(window, async move |panel, cx| {
let Some(new_pane) = fut.await else {
&pane::Event::Split { direction, mode } => {
match mode {
SplitMode::ClonePane | SplitMode::EmptyPane => {
let clone = matches!(mode, SplitMode::ClonePane);
let new_pane = self.new_pane_with_active_terminal(clone, window, cx);
let pane = pane.clone();
cx.spawn_in(window, async move |panel, cx| {
let Some(new_pane) = new_pane.await else {
return;
};
panel
.update_in(cx, |panel, window, cx| {
panel
.center
.split(&pane, &new_pane, direction, cx)
.log_err();
window.focus(&new_pane.focus_handle(cx), cx);
})
.ok();
})
.detach();
}
SplitMode::MovePane => {
let Some(item) =
pane.update(cx, |pane, cx| pane.take_active_item(window, cx))
else {
return;
};
panel
.update_in(cx, |panel, window, cx| {
panel
.center
.split(&pane, &new_pane, direction, cx)
.log_err();
window.focus(&new_pane.focus_handle(cx), cx);
})
.ok();
})
.detach();
} else {
let Some(item) = pane.update(cx, |pane, cx| pane.take_active_item(window, cx))
else {
return;
};
let Ok(project) = self
.workspace
.update(cx, |workspace, _| workspace.project().clone())
else {
return;
};
let new_pane =
new_terminal_pane(self.workspace.clone(), project, false, window, cx);
new_pane.update(cx, |pane, cx| {
pane.add_item(item, true, true, None, window, cx);
});
self.center.split(&pane, &new_pane, direction, cx).log_err();
window.focus(&new_pane.focus_handle(cx), cx);
}
let Ok(project) = self
.workspace
.update(cx, |workspace, _| workspace.project().clone())
else {
return;
};
let new_pane =
new_terminal_pane(self.workspace.clone(), project, false, window, cx);
new_pane.update(cx, |pane, cx| {
pane.add_item(item, true, true, None, window, cx);
});
self.center.split(&pane, &new_pane, direction, cx).log_err();
window.focus(&new_pane.focus_handle(cx), cx);
}
};
}
pane::Event::Focus => {
self.active_pane = pane.clone();
@@ -433,8 +435,9 @@ impl TerminalPanel {
}
}
fn new_pane_with_cloned_active_terminal(
fn new_pane_with_active_terminal(
&mut self,
clone: bool,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Option<Entity<Pane>>> {
@@ -446,21 +449,34 @@ impl TerminalPanel {
let weak_workspace = self.workspace.clone();
let project = workspace.project().clone();
let active_pane = &self.active_pane;
let terminal_view = active_pane
.read(cx)
.active_item()
.and_then(|item| item.downcast::<TerminalView>());
let working_directory = terminal_view
.as_ref()
.and_then(|terminal_view| {
terminal_view
.read(cx)
.terminal()
.read(cx)
.working_directory()
})
.or_else(|| default_working_directory(workspace, cx));
let is_zoomed = active_pane.read(cx).is_zoomed();
let terminal_view = if clone {
active_pane
.read(cx)
.active_item()
.and_then(|item| item.downcast::<TerminalView>())
} else {
None
};
let working_directory = if clone {
terminal_view
.as_ref()
.and_then(|terminal_view| {
terminal_view
.read(cx)
.terminal()
.read(cx)
.working_directory()
})
.or_else(|| default_working_directory(workspace, cx))
} else {
default_working_directory(workspace, cx)
};
let is_zoomed = if clone {
active_pane.read(cx).is_zoomed()
} else {
false
};
cx.spawn_in(window, async move |panel, cx| {
let terminal = project
.update(cx, |project, cx| match terminal_view {
@@ -1482,7 +1498,7 @@ impl Render for TerminalPanel {
window.focus(&pane.read(cx).focus_handle(cx), cx);
} else {
let future =
terminal_panel.new_pane_with_cloned_active_terminal(window, cx);
terminal_panel.new_pane_with_active_terminal(true, window, cx);
cx.spawn_in(window, async move |terminal_panel, cx| {
if let Some(new_pane) = future.await {
_ = terminal_panel.update_in(

View File

@@ -1468,24 +1468,28 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
action.range.replace(range.clone());
Some(Box::new(action))
}),
VimCommand::new(("sp", "lit"), workspace::SplitHorizontal).filename(|_, filename| {
Some(
VimSplit {
vertical: false,
filename,
}
.boxed_clone(),
)
}),
VimCommand::new(("vs", "plit"), workspace::SplitVertical).filename(|_, filename| {
Some(
VimSplit {
vertical: true,
filename,
}
.boxed_clone(),
)
}),
VimCommand::new(("sp", "lit"), workspace::SplitHorizontal::default()).filename(
|_, filename| {
Some(
VimSplit {
vertical: false,
filename,
}
.boxed_clone(),
)
},
),
VimCommand::new(("vs", "plit"), workspace::SplitVertical::default()).filename(
|_, filename| {
Some(
VimSplit {
vertical: true,
filename,
}
.boxed_clone(),
)
},
),
VimCommand::new(("tabe", "dit"), workspace::NewFile)
.filename(|_action, filename| Some(VimEdit { filename }.boxed_clone())),
VimCommand::new(("tabnew", ""), workspace::NewFile)

View File

@@ -197,6 +197,41 @@ pub struct DeploySearch {
pub excluded_files: Option<String>,
}
#[derive(Clone, Copy, PartialEq, Debug, Deserialize, JsonSchema, Default)]
#[serde(deny_unknown_fields)]
pub enum SplitMode {
/// Clone the current pane.
#[default]
ClonePane,
/// Create an empty new pane.
EmptyPane,
/// Move the item into a new pane. This will map to nop if only one pane exists.
MovePane,
}
macro_rules! split_structs {
($($name:ident => $doc:literal),* $(,)?) => {
$(
#[doc = $doc]
#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)]
#[action(namespace = pane)]
#[serde(deny_unknown_fields, default)]
pub struct $name {
pub mode: SplitMode,
}
)*
};
}
split_structs!(
SplitLeft => "Splits the pane to the left.",
SplitRight => "Splits the pane to the right.",
SplitUp => "Splits the pane upward.",
SplitDown => "Splits the pane downward.",
SplitHorizontal => "Splits the pane horizontally.",
SplitVertical => "Splits the pane vertically."
);
actions!(
pane,
[
@@ -218,14 +253,6 @@ actions!(
JoinAll,
/// Reopens the most recently closed item.
ReopenClosedItem,
/// Splits the pane to the left, cloning the current item.
SplitLeft,
/// Splits the pane upward, cloning the current item.
SplitUp,
/// Splits the pane to the right, cloning the current item.
SplitRight,
/// Splits the pane downward, cloning the current item.
SplitDown,
/// Splits the pane to the left, moving the current item.
SplitAndMoveLeft,
/// Splits the pane upward, moving the current item.
@@ -234,10 +261,6 @@ actions!(
SplitAndMoveRight,
/// Splits the pane downward, moving the current item.
SplitAndMoveDown,
/// Splits the pane horizontally.
SplitHorizontal,
/// Splits the pane vertically.
SplitVertical,
/// Swaps the current item with the one to the left.
SwapItemLeft,
/// Swaps the current item with the one to the right.
@@ -279,7 +302,7 @@ pub enum Event {
},
Split {
direction: SplitDirection,
clone_active_item: bool,
mode: SplitMode,
},
ItemPinned,
ItemUnpinned,
@@ -311,13 +334,10 @@ impl fmt::Debug for Event {
.debug_struct("RemovedItem")
.field("item", &item.item_id())
.finish(),
Event::Split {
direction,
clone_active_item,
} => f
Event::Split { direction, mode } => f
.debug_struct("Split")
.field("direction", direction)
.field("clone_active_item", clone_active_item)
.field("mode", mode)
.finish(),
Event::JoinAll => f.write_str("JoinAll"),
Event::JoinIntoNext => f.write_str("JoinIntoNext"),
@@ -2295,10 +2315,7 @@ impl Pane {
let save_task = if let Some(project_path) = project_path {
let (worktree, path) = project_path.await?;
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?;
let new_path = ProjectPath {
worktree_id,
path: path,
};
let new_path = ProjectPath { worktree_id, path };
pane.update_in(cx, |pane, window, cx| {
if let Some(item) = pane.item_for_path(new_path.clone(), cx) {
@@ -2357,19 +2374,30 @@ impl Pane {
}
}
pub fn split(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
cx.emit(Event::Split {
direction,
clone_active_item: true,
});
}
pub fn split_and_move(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
if self.items.len() > 1 {
pub fn split(
&mut self,
direction: SplitDirection,
mode: SplitMode,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.items.len() <= 1 && mode == SplitMode::MovePane {
// MovePane with only one pane present behaves like a SplitEmpty in the opposite direction
let active_item = self.active_item();
cx.emit(Event::Split {
direction,
clone_active_item: false,
direction: direction.opposite(),
mode: SplitMode::EmptyPane,
});
// ensure that we focus the moved pane
// in this case we know that the window is the same as the active_item
if let Some(active_item) = active_item {
cx.defer_in(window, move |_, window, cx| {
let focus_handle = active_item.item_focus_handle(cx);
window.focus(&focus_handle, cx);
});
}
} else {
cx.emit(Event::Split { direction, mode });
}
}
@@ -3824,16 +3852,17 @@ fn default_render_tab_bar_buttons(
.with_handle(pane.split_item_context_menu_handle.clone())
.menu(move |window, cx| {
ContextMenu::build(window, cx, |menu, _, _| {
let mode = SplitMode::MovePane;
if can_split_move {
menu.action("Split Right", SplitAndMoveRight.boxed_clone())
.action("Split Left", SplitAndMoveLeft.boxed_clone())
.action("Split Up", SplitAndMoveUp.boxed_clone())
.action("Split Down", SplitAndMoveDown.boxed_clone())
menu.action("Split Right", SplitRight { mode }.boxed_clone())
.action("Split Left", SplitLeft { mode }.boxed_clone())
.action("Split Up", SplitUp { mode }.boxed_clone())
.action("Split Down", SplitDown { mode }.boxed_clone())
} else {
menu.action("Split Right", SplitRight.boxed_clone())
.action("Split Left", SplitLeft.boxed_clone())
.action("Split Up", SplitUp.boxed_clone())
.action("Split Down", SplitDown.boxed_clone())
menu.action("Split Right", SplitRight::default().boxed_clone())
.action("Split Left", SplitLeft::default().boxed_clone())
.action("Split Up", SplitUp::default().boxed_clone())
.action("Split Down", SplitDown::default().boxed_clone())
}
})
.into()
@@ -3892,33 +3921,35 @@ impl Render for Pane {
.size_full()
.flex_none()
.overflow_hidden()
.on_action(
cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)),
)
.on_action(cx.listener(|pane, _: &SplitUp, _, cx| pane.split(SplitDirection::Up, cx)))
.on_action(cx.listener(|pane, _: &SplitHorizontal, _, cx| {
pane.split(SplitDirection::horizontal(cx), cx)
.on_action(cx.listener(|pane, split: &SplitLeft, window, cx| {
pane.split(SplitDirection::Left, split.mode, window, cx)
}))
.on_action(cx.listener(|pane, _: &SplitVertical, _, cx| {
pane.split(SplitDirection::vertical(cx), cx)
.on_action(cx.listener(|pane, split: &SplitUp, window, cx| {
pane.split(SplitDirection::Up, split.mode, window, cx)
}))
.on_action(
cx.listener(|pane, _: &SplitRight, _, cx| pane.split(SplitDirection::Right, cx)),
)
.on_action(
cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)),
)
.on_action(cx.listener(|pane, _: &SplitAndMoveUp, _, cx| {
pane.split_and_move(SplitDirection::Up, cx)
.on_action(cx.listener(|pane, split: &SplitHorizontal, window, cx| {
pane.split(SplitDirection::horizontal(cx), split.mode, window, cx)
}))
.on_action(cx.listener(|pane, _: &SplitAndMoveDown, _, cx| {
pane.split_and_move(SplitDirection::Down, cx)
.on_action(cx.listener(|pane, split: &SplitVertical, window, cx| {
pane.split(SplitDirection::vertical(cx), split.mode, window, cx)
}))
.on_action(cx.listener(|pane, _: &SplitAndMoveLeft, _, cx| {
pane.split_and_move(SplitDirection::Left, cx)
.on_action(cx.listener(|pane, split: &SplitRight, window, cx| {
pane.split(SplitDirection::Right, split.mode, window, cx)
}))
.on_action(cx.listener(|pane, _: &SplitAndMoveRight, _, cx| {
pane.split_and_move(SplitDirection::Right, cx)
.on_action(cx.listener(|pane, split: &SplitDown, window, cx| {
pane.split(SplitDirection::Down, split.mode, window, cx)
}))
.on_action(cx.listener(|pane, _: &SplitAndMoveUp, window, cx| {
pane.split(SplitDirection::Up, SplitMode::MovePane, window, cx)
}))
.on_action(cx.listener(|pane, _: &SplitAndMoveDown, window, cx| {
pane.split(SplitDirection::Down, SplitMode::MovePane, window, cx)
}))
.on_action(cx.listener(|pane, _: &SplitAndMoveLeft, window, cx| {
pane.split(SplitDirection::Left, SplitMode::MovePane, window, cx)
}))
.on_action(cx.listener(|pane, _: &SplitAndMoveRight, window, cx| {
pane.split(SplitDirection::Right, SplitMode::MovePane, window, cx)
}))
.on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| {
cx.emit(Event::JoinIntoNext);
@@ -4443,11 +4474,14 @@ impl Render for DraggedTab {
#[cfg(test)]
mod tests {
use std::num::NonZero;
use std::{iter::zip, num::NonZero};
use super::*;
use crate::item::test::{TestItem, TestProjectItem};
use gpui::{TestAppContext, VisualTestContext, size};
use crate::{
Member,
item::test::{TestItem, TestProjectItem},
};
use gpui::{AppContext, Axis, TestAppContext, VisualTestContext, size};
use project::FakeFs;
use settings::SettingsStore;
use theme::LoadThemes;
@@ -7125,6 +7159,32 @@ mod tests {
assert_item_labels(&pane, ["A", "C*", "B"], cx);
}
#[gpui::test]
async fn test_split_empty(cx: &mut TestAppContext) {
for split_direction in SplitDirection::all() {
test_single_pane_split(["A"], split_direction, SplitMode::EmptyPane, cx).await;
}
}
#[gpui::test]
async fn test_split_clone(cx: &mut TestAppContext) {
for split_direction in SplitDirection::all() {
test_single_pane_split(["A"], split_direction, SplitMode::ClonePane, cx).await;
}
}
#[gpui::test]
async fn test_split_move_right_on_single_pane(cx: &mut TestAppContext) {
test_single_pane_split(["A"], SplitDirection::Right, SplitMode::MovePane, cx).await;
}
#[gpui::test]
async fn test_split_move(cx: &mut TestAppContext) {
for split_direction in SplitDirection::all() {
test_single_pane_split(["A", "B"], split_direction, SplitMode::MovePane, cx).await;
}
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
@@ -7220,4 +7280,163 @@ mod tests {
"pane items do not match expectation"
);
}
// Assert the item label, with the active item label expected active index
#[track_caller]
fn assert_item_labels_active_index(
pane: &Entity<Pane>,
expected_states: &[&str],
expected_active_idx: usize,
cx: &mut VisualTestContext,
) {
let actual_states = pane.update(cx, |pane, cx| {
pane.items
.iter()
.enumerate()
.map(|(ix, item)| {
let mut state = item
.to_any_view()
.downcast::<TestItem>()
.unwrap()
.read(cx)
.label
.clone();
if ix == pane.active_item_index {
assert_eq!(ix, expected_active_idx);
}
if item.is_dirty(cx) {
state.push('^');
}
if pane.is_tab_pinned(ix) {
state.push('!');
}
state
})
.collect::<Vec<_>>()
});
assert_eq!(
actual_states, expected_states,
"pane items do not match expectation"
);
}
#[track_caller]
fn assert_pane_ids_on_axis<const COUNT: usize>(
workspace: &Entity<Workspace>,
expected_ids: [&EntityId; COUNT],
expected_axis: Axis,
cx: &mut VisualTestContext,
) {
workspace.read_with(cx, |workspace, _| match &workspace.center.root {
Member::Axis(axis) => {
assert_eq!(axis.axis, expected_axis);
assert_eq!(axis.members.len(), expected_ids.len());
assert!(
zip(expected_ids, &axis.members).all(|(e, a)| {
if let Member::Pane(p) = a {
p.entity_id() == *e
} else {
false
}
}),
"pane ids do not match expectation: {expected_ids:?} != {actual_ids:?}",
actual_ids = axis.members
);
}
Member::Pane(_) => panic!("expected axis"),
});
}
async fn test_single_pane_split<const COUNT: usize>(
pane_labels: [&str; COUNT],
direction: SplitDirection,
operation: SplitMode,
cx: &mut TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, None, cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
let mut pane_before =
workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
for label in pane_labels {
add_labeled_item(&pane_before, label, false, cx);
}
pane_before.update_in(cx, |pane, window, cx| {
pane.split(direction, operation, window, cx)
});
cx.executor().run_until_parked();
let pane_after = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
let num_labels = pane_labels.len();
let last_as_active = format!("{}*", String::from(pane_labels[num_labels - 1]));
// check labels for all split operations
match operation {
SplitMode::EmptyPane => {
assert_item_labels_active_index(&pane_before, &pane_labels, num_labels - 1, cx);
assert_item_labels(&pane_after, [], cx);
}
SplitMode::ClonePane => {
assert_item_labels_active_index(&pane_before, &pane_labels, num_labels - 1, cx);
assert_item_labels(&pane_after, [&last_as_active], cx);
}
SplitMode::MovePane => {
let head = &pane_labels[..(num_labels - 1)];
if num_labels == 1 {
// We special-case this behavior and actually execute an empty pane command
// followed by a refocus of the old pane for this case.
pane_before = workspace.read_with(cx, |workspace, _cx| {
workspace
.panes()
.into_iter()
.find(|pane| *pane != &pane_after)
.unwrap()
.clone()
});
};
assert_item_labels_active_index(
&pane_before,
&head,
head.len().saturating_sub(1),
cx,
);
assert_item_labels(&pane_after, [&last_as_active], cx);
pane_after.update_in(cx, |pane, window, cx| {
window.focused(cx).is_some_and(|focus_handle| {
focus_handle == pane.active_item().unwrap().item_focus_handle(cx)
})
});
}
}
// expected axis depends on split direction
let expected_axis = match direction {
SplitDirection::Right | SplitDirection::Left => Axis::Horizontal,
SplitDirection::Up | SplitDirection::Down => Axis::Vertical,
};
// expected ids depends on split direction
let expected_ids = match direction {
SplitDirection::Right | SplitDirection::Down => {
[&pane_before.entity_id(), &pane_after.entity_id()]
}
SplitDirection::Left | SplitDirection::Up => {
[&pane_after.entity_id(), &pane_before.entity_id()]
}
};
// check pane axes for all operations
match operation {
SplitMode::EmptyPane | SplitMode::ClonePane => {
assert_pane_ids_on_axis(&workspace, expected_ids, expected_axis, cx);
}
SplitMode::MovePane => {
assert_pane_ids_on_axis(&workspace, expected_ids, expected_axis, cx);
}
}
}
}

View File

@@ -4262,16 +4262,19 @@ impl Workspace {
item: item.boxed_clone(),
});
}
pane::Event::Split {
direction,
clone_active_item,
} => {
if *clone_active_item {
self.split_and_clone(pane.clone(), *direction, window, cx)
.detach();
} else {
self.split_and_move(pane.clone(), *direction, window, cx);
}
pane::Event::Split { direction, mode } => {
match mode {
SplitMode::ClonePane => {
self.split_and_clone(pane.clone(), *direction, window, cx)
.detach();
}
SplitMode::EmptyPane => {
self.split_pane(pane.clone(), *direction, window, cx);
}
SplitMode::MovePane => {
self.split_and_move(pane.clone(), *direction, window, cx);
}
};
}
pane::Event::JoinIntoNext => {
self.join_pane_into_next(pane.clone(), window, cx);

View File

@@ -3817,7 +3817,7 @@ mod tests {
})
.unwrap();
cx.dispatch_action(window.into(), pane::SplitRight);
cx.dispatch_action(window.into(), pane::SplitRight::default());
let editor_2 = cx.update(|cx| {
let pane_2 = workspace.read(cx).active_pane().clone();
assert_ne!(pane_1, pane_2);

View File

@@ -32,10 +32,10 @@ pub fn app_menus(cx: &mut App) -> Vec<Menu> {
MenuItem::submenu(Menu {
name: "Editor Layout".into(),
items: vec![
MenuItem::action("Split Up", workspace::SplitUp),
MenuItem::action("Split Down", workspace::SplitDown),
MenuItem::action("Split Left", workspace::SplitLeft),
MenuItem::action("Split Right", workspace::SplitRight),
MenuItem::action("Split Up", workspace::SplitUp::default()),
MenuItem::action("Split Down", workspace::SplitDown::default()),
MenuItem::action("Split Left", workspace::SplitLeft::default()),
MenuItem::action("Split Right", workspace::SplitRight::default()),
],
}),
MenuItem::separator(),