From fd3ca0303ff1dfc552119878724eb151bc94b49c Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Sun, 26 Oct 2025 14:24:26 +0100 Subject: [PATCH] workspace: Handle non-cloneable items better (#41215) When trying to split and clone a non clone-able workspace item we now attempt split and move instead of doing nothing. Additionally we disable the split menu buttons if we can't split the active item at all. Release Notes: - Improved handling of unsplittable panes --- crates/agent_ui/src/agent_diff.rs | 4 + crates/collab_ui/src/channel_view.rs | 4 + crates/diagnostics/src/buffer_diagnostics.rs | 4 + crates/diagnostics/src/diagnostics.rs | 4 + crates/editor/src/items.rs | 4 + crates/extension_host/src/wasm_host.rs | 24 +++-- crates/git_ui/src/commit_view.rs | 4 + crates/git_ui/src/project_diff.rs | 4 + crates/gpui_tokio/src/gpui_tokio.rs | 3 +- crates/image_viewer/src/image_viewer.rs | 4 + crates/language_tools/src/key_context_view.rs | 4 + crates/language_tools/src/lsp_log_view.rs | 4 + crates/language_tools/src/syntax_tree_view.rs | 4 + crates/onboarding/src/onboarding.rs | 4 + crates/repl/src/notebook/notebook_ui.rs | 4 + crates/search/src/project_search.rs | 4 + crates/terminal_view/src/terminal_view.rs | 4 + crates/workspace/src/item.rs | 22 ++++- crates/workspace/src/pane.rs | 52 +++++++---- crates/workspace/src/shared_screen.rs | 4 + crates/workspace/src/theme_preview.rs | 4 + crates/workspace/src/workspace.rs | 88 ++++++++++++------- crates/zed/src/zed/component_preview.rs | 4 + 23 files changed, 196 insertions(+), 65 deletions(-) diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index dd11a3f2ccb88e38138d5c5f0e77805833a9a358..a0f117b0bf30abee9d2182cf8c3fadd10099b1f0 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -576,6 +576,10 @@ impl Item for AgentDiffPane { }); } + fn can_split(&self) -> bool { + true + } + fn clone_on_split( &self, _workspace_id: Option, diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 18847363bf1b012acb7916bb7b6a9c0adde4de28..5db588fdb3aad3f523864b5f90600e49eca9d8b6 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -493,6 +493,10 @@ impl Item for ChannelView { None } + fn can_split(&self) -> bool { + true + } + fn clone_on_split( &self, _: Option, diff --git a/crates/diagnostics/src/buffer_diagnostics.rs b/crates/diagnostics/src/buffer_diagnostics.rs index 1a7a97c68691d7bdf941322660eddeb70fa15037..1205cef385fdd91af8e3f986b432b9fff4ad3ac6 100644 --- a/crates/diagnostics/src/buffer_diagnostics.rs +++ b/crates/diagnostics/src/buffer_diagnostics.rs @@ -688,6 +688,10 @@ impl Item for BufferDiagnosticsEditor { true } + fn can_split(&self) -> bool { + true + } + fn clone_on_split( &self, _workspace_id: Option, diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index b96e8f891fda3cc470c7091eba8e92847b59562b..5a43fd135391a5e3d97d5c65e6d3be826210f102 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -727,6 +727,10 @@ impl Item for ProjectDiagnosticsEditor { }); } + fn can_split(&self) -> bool { + true + } + fn clone_on_split( &self, _workspace_id: Option, diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 6dab57db52700bc499376abb0ab80e9cdb45e5e9..346574eba440622a40139a52be6977e55e909980 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -757,6 +757,10 @@ impl Item for Editor { self.buffer.read(cx).is_singleton() } + fn can_split(&self) -> bool { + true + } + fn clone_on_split( &self, _workspace_id: Option, diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index 00e6321fdb5450a08aa331380a5d410652b66582..bf3732b7e8497a09d5067e11ab78e7165fb54a46 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -69,6 +69,7 @@ pub struct WasmExtension { pub work_dir: Arc, #[allow(unused)] pub zed_api_version: SemanticVersion, + _task: Arc>>, } impl Drop for WasmExtension { @@ -649,21 +650,26 @@ impl WasmHost { anyhow::Ok(( extension_task, - WasmExtension { - manifest: manifest.clone(), - work_dir: this.work_dir.join(manifest.id.as_ref()).into(), - tx, - zed_api_version, - }, + manifest.clone(), + this.work_dir.join(manifest.id.as_ref()).into(), + tx, + zed_api_version, )) }; cx.spawn(async move |cx| { - let (extension_task, extension) = load_extension_task.await?; + let (extension_task, manifest, work_dir, tx, zed_api_version) = + load_extension_task.await?; // we need to run run the task in an extension context as wasmtime_wasi may // call into tokio, accessing its runtime handle - gpui_tokio::Tokio::spawn(cx, extension_task)?.detach(); + let task = Arc::new(gpui_tokio::Tokio::spawn(cx, extension_task)?); - Ok(extension) + Ok(WasmExtension { + manifest, + work_dir, + tx, + zed_api_version, + _task: task, + }) }) } diff --git a/crates/git_ui/src/commit_view.rs b/crates/git_ui/src/commit_view.rs index 2430796c89f68e9b5032c3d05f7106b6f6de0bec..0a0c4c18e1f528a9ebaad9a8d9862982632dd04f 100644 --- a/crates/git_ui/src/commit_view.rs +++ b/crates/git_ui/src/commit_view.rs @@ -556,6 +556,10 @@ impl Item for CommitView { }); } + fn can_split(&self) -> bool { + true + } + fn clone_on_split( &self, _workspace_id: Option, diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index e6f8099c732826d3546680fab9ef94c8e1d3db32..5c49ca286eb901a9e97281f27dcaef5c993d73b1 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -709,6 +709,10 @@ impl Item for ProjectDiff { }); } + fn can_split(&self) -> bool { + true + } + fn clone_on_split( &self, _workspace_id: Option, diff --git a/crates/gpui_tokio/src/gpui_tokio.rs b/crates/gpui_tokio/src/gpui_tokio.rs index 8384f2a88ec82b96c0490913019b701cdf01239c..61dcfc48efb1dfecc04c4a131ddc32691e01e255 100644 --- a/crates/gpui_tokio/src/gpui_tokio.rs +++ b/crates/gpui_tokio/src/gpui_tokio.rs @@ -1,9 +1,10 @@ use std::future::Future; use gpui::{App, AppContext, Global, ReadGlobal, Task}; -use tokio::task::JoinError; use util::defer; +pub use tokio::task::JoinError; + pub fn init(cx: &mut App) { cx.set_global(GlobalTokio::new()); } diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index 17259d15f1d81ac2f46e027fcf7889cdbbe9d011..f9a2cc9e045ae67ac7d993250f87cf7ee23789c0 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -174,6 +174,10 @@ impl Item for ImageView { }]) } + fn can_split(&self) -> bool { + true + } + fn clone_on_split( &self, _workspace_id: Option, diff --git a/crates/language_tools/src/key_context_view.rs b/crates/language_tools/src/key_context_view.rs index e704d6bbf03eea18ae717f7aa11b25466dd68e9e..cc34838010bfaf8cacd3773a18fde90fbefc105b 100644 --- a/crates/language_tools/src/key_context_view.rs +++ b/crates/language_tools/src/key_context_view.rs @@ -153,6 +153,10 @@ impl Item for KeyContextView { None } + fn can_split(&self) -> bool { + true + } + fn clone_on_split( &self, _workspace_id: Option, diff --git a/crates/language_tools/src/lsp_log_view.rs b/crates/language_tools/src/lsp_log_view.rs index b1e24303c47e722460d20023d4f7444a8b006406..d480eadc73b9546e5a59b204b036a3ff88a018c7 100644 --- a/crates/language_tools/src/lsp_log_view.rs +++ b/crates/language_tools/src/lsp_log_view.rs @@ -758,6 +758,10 @@ impl Item for LspLogView { } } + fn can_split(&self) -> bool { + true + } + fn clone_on_split( &self, _workspace_id: Option, diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 9e5a0374f54f97fc3d75ebc68e2247bf4b904f0c..e2a0cd4c33a93b7806710e68abca6404290808ce 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -568,6 +568,10 @@ impl Item for SyntaxTreeView { None } + fn can_split(&self) -> bool { + true + } + fn clone_on_split( &self, _: Option, diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 913d92d48c4018759f5ba91bb61d514160ba1b3f..562dea8748eaddad415d7098f6c34f0bea7b5169 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -378,6 +378,10 @@ impl Item for Onboarding { false } + fn can_split(&self) -> bool { + true + } + fn clone_on_split( &self, _workspace_id: Option, diff --git a/crates/repl/src/notebook/notebook_ui.rs b/crates/repl/src/notebook/notebook_ui.rs index 209948685ce263361101e508ce6ab65839b132cb..eaeff234bc1a8e21471cee74f98636dfdd995ca4 100644 --- a/crates/repl/src/notebook/notebook_ui.rs +++ b/crates/repl/src/notebook/notebook_ui.rs @@ -694,6 +694,10 @@ impl EventEmitter<()> for NotebookEditor {} impl Item for NotebookEditor { type Event = (); + fn can_split(&self) -> bool { + true + } + fn clone_on_split( &self, _workspace_id: Option, diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 3a9367db724257d4ba32c343c578ba27bea412d7..f407a0a4dbfd00b6515a392f18572c373499d2cc 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -567,6 +567,10 @@ impl Item for ProjectSearchView { .update(cx, |editor, cx| editor.reload(project, window, cx)) } + fn can_split(&self) -> bool { + true + } + fn clone_on_split( &self, _workspace_id: Option, diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 5111f3a99b49d35a7f3a7ba141e20d4dc7cc4828..ff169e48e53b01f29ca1ab1682927ea116f320fc 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1213,6 +1213,10 @@ impl Item for TerminalView { workspace::item::ItemBufferKind::Singleton } + fn can_split(&self) -> bool { + true + } + fn clone_on_split( &self, workspace_id: Option, diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index a328bb67924f9be7bcbdc457153699a672fea08b..b77075f92bf69dc292cf69ac7eac147043d7d8b7 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -213,16 +213,21 @@ pub trait Item: Focusable + EventEmitter + Render + Sized { ItemBufferKind::None } fn set_nav_history(&mut self, _: ItemNavHistory, _window: &mut Window, _: &mut Context) {} + + fn can_split(&self) -> bool { + false + } fn clone_on_split( &self, - _workspace_id: Option, - _window: &mut Window, - _: &mut Context, + workspace_id: Option, + window: &mut Window, + cx: &mut Context, ) -> Task>> where Self: Sized, { - Task::ready(None) + _ = (workspace_id, window, cx); + unimplemented!("clone_on_split() must be implemented if can_split() returns true") } fn is_dirty(&self, _: &App) -> bool { false @@ -418,6 +423,7 @@ pub trait ItemHandle: 'static + Send { ); fn buffer_kind(&self, cx: &App) -> ItemBufferKind; fn boxed_clone(&self) -> Box; + fn can_split(&self, cx: &App) -> bool; fn clone_on_split( &self, workspace_id: Option, @@ -631,6 +637,10 @@ impl ItemHandle for Entity { Box::new(self.clone()) } + fn can_split(&self, cx: &App) -> bool { + self.read(cx).can_split() + } + fn clone_on_split( &self, workspace_id: Option, @@ -1503,6 +1513,10 @@ pub mod test { self.push_to_nav_history(cx); } + fn can_split(&self) -> bool { + true + } + fn clone_on_split( &self, _workspace_id: Option, diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 178fbdff9f7a9ef8cf4ee293450e0a5b9ad549b3..9b6767086adffde00a0486b6a9cae62aaa8d41df 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -3292,18 +3292,22 @@ impl Pane { else { return; }; - let task = item.clone_on_split(database_id, window, cx); - let to_pane = to_pane.downgrade(); - cx.spawn_in(window, async move |_, cx| { - if let Some(item) = task.await { - to_pane - .update_in(cx, |pane, window, cx| { - pane.add_item(item, true, true, None, window, cx) - }) - .ok(); - } - }) - .detach(); + if item.can_split(cx) { + let task = item.clone_on_split(database_id, window, cx); + let to_pane = to_pane.downgrade(); + cx.spawn_in(window, async move |_, cx| { + if let Some(item) = task.await { + to_pane + .update_in(cx, |pane, window, cx| { + pane.add_item(item, true, true, None, window, cx) + }) + .ok(); + } + }) + .detach(); + } else { + move_item(&from_pane, &to_pane, item_id, ix, true, window, cx); + } } else { move_item(&from_pane, &to_pane, item_id, ix, true, window, cx); } @@ -3597,6 +3601,11 @@ fn default_render_tab_bar_buttons( if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) { return (None, None); } + let (can_clone, can_split_move) = match pane.active_item() { + Some(active_item) if active_item.can_split(cx) => (true, false), + Some(_) => (false, pane.items_len() > 1), + None => (false, false), + }; // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s // `end_slot`, but due to needing a view here that isn't possible. let right_children = h_flex() @@ -3633,17 +3642,26 @@ fn default_render_tab_bar_buttons( .child( PopoverMenu::new("pane-tab-bar-split") .trigger_with_tooltip( - IconButton::new("split", IconName::Split).icon_size(IconSize::Small), + IconButton::new("split", IconName::Split) + .icon_size(IconSize::Small) + .disabled(!can_clone && !can_split_move), Tooltip::text("Split Pane"), ) .anchor(Corner::TopRight) .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()) + 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()) + } 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()) + } }) .into() }), diff --git a/crates/workspace/src/shared_screen.rs b/crates/workspace/src/shared_screen.rs index 34c7d27df73b8b832e9b5b23b832a15161644e3a..3c009f613ea52906649b73bb9fd657bab6906c3b 100644 --- a/crates/workspace/src/shared_screen.rs +++ b/crates/workspace/src/shared_screen.rs @@ -109,6 +109,10 @@ impl Item for SharedScreen { self.nav_history = Some(history); } + fn can_split(&self) -> bool { + true + } + fn clone_on_split( &self, _workspace_id: Option, diff --git a/crates/workspace/src/theme_preview.rs b/crates/workspace/src/theme_preview.rs index 29067400bd72fe56a62af118a0bea6b52d9356df..94a280b4da1283178201898bd3e8c2c71e5f0b1f 100644 --- a/crates/workspace/src/theme_preview.rs +++ b/crates/workspace/src/theme_preview.rs @@ -97,6 +97,10 @@ impl Item for ThemePreview { None } + fn can_split(&self) -> bool { + true + } + fn clone_on_split( &self, _workspace_id: Option, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 74c98d6818aff717f215ec92ba848dbba63a9a88..6933a6bcda8baffee618c219c3b05263f11738f5 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3663,24 +3663,31 @@ impl Workspace { }; if action.clone { - clone_active_item( - self.database_id(), - &self.active_pane, - &destination, - action.focus, - window, - cx, - ) - } else { - move_active_item( - &self.active_pane, - &destination, - action.focus, - true, - window, - cx, - ) + if self + .active_pane + .read(cx) + .active_item() + .is_some_and(|item| item.can_split(cx)) + { + clone_active_item( + self.database_id(), + &self.active_pane, + &destination, + action.focus, + window, + cx, + ); + return; + } } + move_active_item( + &self.active_pane, + &destination, + action.focus, + true, + window, + cx, + ) } pub fn activate_next_pane(&mut self, window: &mut Window, cx: &mut App) { @@ -3841,24 +3848,31 @@ impl Workspace { }; if action.clone { - clone_active_item( - self.database_id(), - &self.active_pane, - &destination, - action.focus, - window, - cx, - ) - } else { - move_active_item( - &self.active_pane, - &destination, - action.focus, - true, - window, - cx, - ); + if self + .active_pane + .read(cx) + .active_item() + .is_some_and(|item| item.can_split(cx)) + { + clone_active_item( + self.database_id(), + &self.active_pane, + &destination, + action.focus, + window, + cx, + ); + return; + } } + move_active_item( + &self.active_pane, + &destination, + action.focus, + true, + window, + cx, + ); } pub fn bounding_box_for_pane(&self, pane: &Entity) -> Option> { @@ -4141,6 +4155,9 @@ impl Workspace { let Some(item) = pane.read(cx).active_item() else { return Task::ready(None); }; + if !item.can_split(cx) { + return Task::ready(None); + } let task = item.clone_on_split(self.database_id(), window, cx); cx.spawn_in(window, async move |this, cx| { if let Some(clone) = task.await { @@ -8225,6 +8242,9 @@ pub fn clone_active_item( let Some(active_item) = source.read(cx).active_item() else { return; }; + if !active_item.can_split(cx) { + return; + } let destination = destination.downgrade(); let task = active_item.clone_on_split(workspace_id, window, cx); window diff --git a/crates/zed/src/zed/component_preview.rs b/crates/zed/src/zed/component_preview.rs index 153d66f04e0b0aafe4b8d8dd14cedb05f44b496f..d62f39ef6306593eba4b5fe6bff427db036e82dc 100644 --- a/crates/zed/src/zed/component_preview.rs +++ b/crates/zed/src/zed/component_preview.rs @@ -715,6 +715,10 @@ impl Item for ComponentPreview { false } + fn can_split(&self) -> bool { + true + } + fn clone_on_split( &self, _workspace_id: Option,