From 03804b4982be18e1822af0ce3c6530566f322f9c Mon Sep 17 00:00:00 2001 From: Yves Ineichen Date: Wed, 1 Oct 2025 16:06:55 +0200 Subject: [PATCH] prototype new split clone/empty/move behavior - adding an enum for SplitBehavior (naming not final) to support bindings like `["pane::SplitLeft", {"behavior": "Empty"}]` - subsuming SplitAndMoveDirection actions but probably have to keep them (?) - empty behavior for terminal panes: unclear what this means. Currently split but does not clone. --- crates/collab/src/tests/integration_tests.rs | 6 +- crates/file_finder/src/file_finder.rs | 15 +- crates/terminal_view/src/terminal_panel.rs | 139 +++++++++------- crates/vim/src/command.rs | 4 +- crates/workspace/src/pane.rs | 158 ++++++++++++------- crates/workspace/src/workspace.rs | 21 +-- crates/zed/src/zed.rs | 2 +- crates/zed/src/zed/app_menus.rs | 8 +- 8 files changed, 213 insertions(+), 140 deletions(-) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index f080b1cc4a56a7..95f2da4a1e2ebe 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -6746,7 +6746,11 @@ 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.split( + workspace::SplitDirection::Right, + &workspace::SplitBehavior::default(), + cx, + ); }); let right_pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 979cfa72fffffd..839d202d20c588 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -46,8 +46,8 @@ use util::{ rel_path::RelPath, }; use workspace::{ - ModalView, OpenOptions, OpenVisible, SplitDirection, Workspace, item::PreviewTabsSettings, - notifications::NotifyResultExt, pane, + ModalView, OpenOptions, OpenVisible, SplitDirection, Workspace, + item::PreviewTabsSettings, notifications::NotifyResultExt, pane, }; actions!( @@ -1732,16 +1732,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(), ) } })) diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index e146a9cdb9d602..584069a064e42a 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -29,9 +29,9 @@ use util::{ResultExt, TryFutureExt}; use workspace::{ ActivateNextPane, ActivatePane, ActivatePaneDown, ActivatePaneLeft, ActivatePaneRight, ActivatePaneUp, ActivatePreviousPane, DraggedSelection, DraggedTab, ItemId, MoveItemToPane, - MoveItemToPaneInDirection, NewTerminal, Pane, PaneGroup, SplitDirection, SplitDown, SplitLeft, - SplitRight, SplitUp, SwapPaneDown, SwapPaneLeft, SwapPaneRight, SwapPaneUp, ToggleZoom, - Workspace, + MoveItemToPaneInDirection, NewTerminal, Pane, PaneGroup, SplitBehavior, SplitDirection, + SplitDown, SplitLeft, SplitRight, SplitUp, SwapPaneDown, SwapPaneLeft, SwapPaneRight, + SwapPaneUp, ToggleZoom, Workspace, dock::{DockPosition, Panel, PanelEvent, PanelHandle}, item::SerializableItem, move_active_item, move_item, pane, @@ -193,10 +193,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() } @@ -384,42 +384,53 @@ impl TerminalPanel { } &pane::Event::Split { direction, - clone_active_item, + ref behavior, } => { - 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 { + match behavior { + // TODO not exactly clear what we expect in terminal panes when splitting with + // Empty? Assuming not cloning for now. + SplitBehavior::Clone | SplitBehavior::Empty => { + let clone = match behavior { + SplitBehavior::Clone => true, + SplitBehavior::Empty => false, + _ => unreachable!(), + }; + let fut = self.new_pane_with_active_terminal(window, cx, clone); + let pane = pane.clone(); + cx.spawn_in(window, async move |panel, cx| { + let Some(new_pane) = fut.await else { + return; + }; + panel + .update_in(cx, |panel, window, cx| { + panel.center.split(&pane, &new_pane, direction).log_err(); + window.focus(&new_pane.focus_handle(cx)); + }) + .ok(); + }) + .detach(); + } + SplitBehavior::Move => { + 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).log_err(); - window.focus(&new_pane.focus_handle(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).log_err(); - window.focus(&new_pane.focus_handle(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).log_err(); + window.focus(&new_pane.focus_handle(cx)); + } + }; } pane::Event::Focus => { self.active_pane = pane.clone(); @@ -432,10 +443,11 @@ impl TerminalPanel { } } - fn new_pane_with_cloned_active_terminal( + fn new_pane_with_active_terminal( &mut self, window: &mut Window, cx: &mut Context, + clone: bool, ) -> Task>> { let Some(workspace) = self.workspace.upgrade() else { return Task::ready(None); @@ -445,21 +457,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::()); - 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::()) + } 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 { @@ -1368,7 +1393,7 @@ impl Render for TerminalPanel { window.focus(&pane.read(cx).focus_handle(cx)); } else { let future = - terminal_panel.new_pane_with_cloned_active_terminal(window, cx); + terminal_panel.new_pane_with_active_terminal(window, cx, true); cx.spawn_in(window, async move |terminal_panel, cx| { if let Some(new_pane) = future.await { _ = terminal_panel.update_in( diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index ef19d41ed88f7f..01c0cedcd5503e 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -1164,7 +1164,7 @@ fn generate_commands(_: &App) -> Vec { save_intent: Some(SaveIntent::Overwrite), }), VimCommand::new(("cq", "uit"), zed_actions::Quit), - VimCommand::new(("sp", "lit"), workspace::SplitHorizontal).args(|_, args| { + VimCommand::new(("sp", "lit"), workspace::SplitHorizontal::default()).args(|_, args| { Some( VimSplit { vertical: false, @@ -1173,7 +1173,7 @@ fn generate_commands(_: &App) -> Vec { .boxed_clone(), ) }), - VimCommand::new(("vs", "plit"), workspace::SplitVertical).args(|_, args| { + VimCommand::new(("vs", "plit"), workspace::SplitVertical::default()).args(|_, args| { Some( VimSplit { vertical: true, diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index ebb55f4a75669d..2e4aa168965843 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -196,6 +196,73 @@ pub struct DeploySearch { pub excluded_files: Option, } +// TODO naming +#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)] +#[serde(deny_unknown_fields)] +pub enum SplitBehavior { + /// clone the current item when splitting + #[default] + Clone, + /// create an empty new item when splitting + Empty, + /// move the current item + Move, +} + +// TODO tuple struct seem to fail for default - maybe issues with deserializing from {}? +// #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] +// #[action(namespace = pane)] +// #[serde(deny_unknown_fields)] +// pub struct SplitLeft(#[serde(default)] SplitAnd); + +/// Splits the pane to the left. +#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] +#[action(namespace = pane)] +#[serde(deny_unknown_fields, default)] +pub struct SplitLeft { + pub behavior: SplitBehavior, +} + +/// Splits the pane to the right. +#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] +#[action(namespace = pane)] +#[serde(deny_unknown_fields, default)] +pub struct SplitRight { + pub behavior: SplitBehavior, +} + +/// Splits the pane upward. +#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] +#[action(namespace = pane)] +#[serde(deny_unknown_fields, default)] +pub struct SplitUp { + pub behavior: SplitBehavior, +} + +/// Splits the pane downward. +#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] +#[action(namespace = pane)] +#[serde(deny_unknown_fields, default)] +pub struct SplitDown { + pub behavior: SplitBehavior, +} + +/// Splits the pane horizontally. +#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] +#[action(namespace = pane)] +#[serde(deny_unknown_fields, default)] +pub struct SplitHorizontal { + pub behavior: SplitBehavior, +} + +/// Splits the pane vertically. +#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] +#[action(namespace = pane)] +#[serde(deny_unknown_fields, default)] +pub struct SplitVertical { + pub behavior: SplitBehavior, +} + actions!( pane, [ @@ -217,26 +284,7 @@ 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. - SplitAndMoveUp, - /// Splits the pane to the right, moving the current item. - SplitAndMoveRight, - /// Splits the pane downward, moving the current item. - SplitAndMoveDown, - /// Splits the pane horizontally. - SplitHorizontal, - /// Splits the pane vertically. - SplitVertical, + // TODO can we remove old bindings SplitAndMoveLeft...? /// Swaps the current item with the one to the left. SwapItemLeft, /// Swaps the current item with the one to the right. @@ -278,7 +326,7 @@ pub enum Event { }, Split { direction: SplitDirection, - clone_active_item: bool, + behavior: SplitBehavior, }, ItemPinned, ItemUnpinned, @@ -312,11 +360,11 @@ impl fmt::Debug for Event { .finish(), Event::Split { direction, - clone_active_item, + behavior, } => f .debug_struct("Split") .field("direction", direction) - .field("clone_active_item", clone_active_item) + .field("behavior", behavior) .finish(), Event::JoinAll => f.write_str("JoinAll"), Event::JoinIntoNext => f.write_str("JoinIntoNext"), @@ -2294,22 +2342,22 @@ impl Pane { } } - pub fn split(&mut self, direction: SplitDirection, cx: &mut Context) { + pub fn split( + &mut self, + direction: SplitDirection, + behavior: &SplitBehavior, + cx: &mut Context, + ) { + if self.items.len() <= 1 && *behavior == SplitBehavior::Move { + return; + } + cx.emit(Event::Split { direction, - clone_active_item: true, + behavior: behavior.to_owned(), }); } - pub fn split_and_move(&mut self, direction: SplitDirection, cx: &mut Context) { - if self.items.len() > 1 { - cx.emit(Event::Split { - direction, - clone_active_item: false, - }); - } - } - pub fn toolbar(&self) -> &Entity { &self.toolbar } @@ -3619,10 +3667,10 @@ fn default_render_tab_bar_buttons( .with_handle(pane.split_item_context_menu_handle.clone()) .menu(move |window, cx| { ContextMenu::build(window, cx, |menu, _, _| { - 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() }), @@ -3677,33 +3725,23 @@ 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, _, cx| { + pane.split(SplitDirection::Left, &split.behavior, cx) })) - .on_action(cx.listener(|pane, _: &SplitVertical, _, cx| { - pane.split(SplitDirection::vertical(cx), cx) + .on_action(cx.listener(|pane, split: &SplitUp, _, cx| { + pane.split(SplitDirection::Up, &split.behavior, 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, _, cx| { + pane.split(SplitDirection::horizontal(cx), &split.behavior, cx) })) - .on_action(cx.listener(|pane, _: &SplitAndMoveDown, _, cx| { - pane.split_and_move(SplitDirection::Down, cx) + .on_action(cx.listener(|pane, split: &SplitVertical, _, cx| { + pane.split(SplitDirection::vertical(cx), &split.behavior, cx) })) - .on_action(cx.listener(|pane, _: &SplitAndMoveLeft, _, cx| { - pane.split_and_move(SplitDirection::Left, cx) + .on_action(cx.listener(|pane, split: &SplitRight, _, cx| { + pane.split(SplitDirection::Right, &split.behavior, cx) })) - .on_action(cx.listener(|pane, _: &SplitAndMoveRight, _, cx| { - pane.split_and_move(SplitDirection::Right, cx) + .on_action(cx.listener(|pane, split: &SplitDown, _, cx| { + pane.split(SplitDirection::Down, &split.behavior, cx) })) .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| { cx.emit(Event::JoinIntoNext); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 81247b89593074..b5389e6d213d73 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3964,15 +3964,18 @@ 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); - } else { - self.split_and_move(pane.clone(), *direction, window, cx); - } + pane::Event::Split { direction, behavior } => { + match behavior { + SplitBehavior::Clone => { + self.split_and_clone(pane.clone(), *direction, window, cx); + } + SplitBehavior::Empty => { + self.split_pane(pane.clone(), *direction, window, cx); + } + SplitBehavior::Move => { + self.split_and_move(pane.clone(), *direction, window, cx); + } + }; } pane::Event::JoinIntoNext => { self.join_pane_into_next(pane.clone(), window, cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 928ffa5bafb7f4..bcb4f58691363e 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -3598,7 +3598,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); diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index cd18503f61be4a..52cbc8e9d204aa 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -28,10 +28,10 @@ pub fn app_menus(cx: &mut App) -> Vec { 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(),