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,