workspace: Make `Item::clone_on_split` async (#41211)

Lukas Wirth created

Split out from https://github.com/zed-industries/zed/pull/40774 to
reduce the size of the reland of that PR (once I figure out the cause of
the issue)

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Change summary

crates/agent_ui/src/agent_diff.rs             |   6 
crates/collab/src/tests/following_tests.rs    |  44 +++++---
crates/collab/src/tests/integration_tests.rs  |   2 
crates/collab_ui/src/channel_view.rs          |   6 
crates/diagnostics/src/buffer_diagnostics.rs  |   6 
crates/diagnostics/src/diagnostics.rs         |   6 
crates/editor/src/items.rs                    |   4 
crates/git_ui/src/commit_view.rs              |  10 +-
crates/git_ui/src/project_diff.rs             |  10 +
crates/image_viewer/src/image_viewer.rs       |   6 
crates/language_tools/src/key_context_view.rs |   7 
crates/language_tools/src/lsp_log_view.rs     |   8 
crates/language_tools/src/syntax_tree_view.rs |   8 
crates/onboarding/src/onboarding.rs           |   6 
crates/repl/src/notebook/notebook_ui.rs       |   6 
crates/search/src/project_search.rs           |   8 +
crates/terminal_view/src/terminal_view.rs     |  46 ++++----
crates/workspace/src/item.rs                  |  26 +++--
crates/workspace/src/pane.rs                  |  17 ++-
crates/workspace/src/shared_screen.rs         |   8 
crates/workspace/src/theme_preview.rs         |   8 +
crates/workspace/src/workspace.rs             | 102 ++++++++++++--------
crates/zed/src/zed.rs                         |  20 ++-
crates/zed/src/zed/component_preview.rs       |   6 
24 files changed, 218 insertions(+), 158 deletions(-)

Detailed changes

crates/agent_ui/src/agent_diff.rs 🔗

@@ -581,11 +581,13 @@ impl Item for AgentDiffPane {
         _workspace_id: Option<workspace::WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>>
+    ) -> Task<Option<Entity<Self>>>
     where
         Self: Sized,
     {
-        Some(cx.new(|cx| Self::new(self.thread.clone(), self.workspace.clone(), window, cx)))
+        Task::ready(Some(cx.new(|cx| {
+            Self::new(self.thread.clone(), self.workspace.clone(), window, cx)
+        })))
     }
 
     fn is_dirty(&self, cx: &App) -> bool {

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

@@ -776,26 +776,30 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
         .unwrap();
 
     // Clients A and B follow each other in split panes
-    workspace_a.update_in(cx_a, |workspace, window, cx| {
-        workspace.split_and_clone(
-            workspace.active_pane().clone(),
-            SplitDirection::Right,
-            window,
-            cx,
-        );
-    });
+    workspace_a
+        .update_in(cx_a, |workspace, window, cx| {
+            workspace.split_and_clone(
+                workspace.active_pane().clone(),
+                SplitDirection::Right,
+                window,
+                cx,
+            )
+        })
+        .await;
     workspace_a.update_in(cx_a, |workspace, window, cx| {
         workspace.follow(client_b.peer_id().unwrap(), window, cx)
     });
     executor.run_until_parked();
-    workspace_b.update_in(cx_b, |workspace, window, cx| {
-        workspace.split_and_clone(
-            workspace.active_pane().clone(),
-            SplitDirection::Right,
-            window,
-            cx,
-        );
-    });
+    workspace_b
+        .update_in(cx_b, |workspace, window, cx| {
+            workspace.split_and_clone(
+                workspace.active_pane().clone(),
+                SplitDirection::Right,
+                window,
+                cx,
+            )
+        })
+        .await;
     workspace_b.update_in(cx_b, |workspace, window, cx| {
         workspace.follow(client_a.peer_id().unwrap(), window, cx)
     });
@@ -1369,9 +1373,11 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
     );
 
     // When client B activates a different pane, it continues following client A in the original pane.
-    workspace_b.update_in(cx_b, |workspace, window, cx| {
-        workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, window, cx)
-    });
+    workspace_b
+        .update_in(cx_b, |workspace, window, cx| {
+            workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, window, cx)
+        })
+        .await;
     assert_eq!(
         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
         Some(leader_id.into())

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

@@ -6748,7 +6748,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
     pane.update(cx, |pane, cx| {
         pane.split(workspace::SplitDirection::Right, cx);
     });
-
+    cx.run_until_parked();
     let right_pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
     pane.update(cx, |pane, cx| {

crates/collab_ui/src/channel_view.rs 🔗

@@ -498,8 +498,8 @@ impl Item for ChannelView {
         _: Option<WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>> {
-        Some(cx.new(|cx| {
+    ) -> Task<Option<Entity<Self>>> {
+        Task::ready(Some(cx.new(|cx| {
             Self::new(
                 self.project.clone(),
                 self.workspace.clone(),
@@ -508,7 +508,7 @@ impl Item for ChannelView {
                 window,
                 cx,
             )
-        }))
+        })))
     }
 
     fn navigate(

crates/diagnostics/src/buffer_diagnostics.rs 🔗

@@ -693,11 +693,11 @@ impl Item for BufferDiagnosticsEditor {
         _workspace_id: Option<workspace::WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>>
+    ) -> Task<Option<Entity<Self>>>
     where
         Self: Sized,
     {
-        Some(cx.new(|cx| {
+        Task::ready(Some(cx.new(|cx| {
             BufferDiagnosticsEditor::new(
                 self.project_path.clone(),
                 self.project.clone(),
@@ -706,7 +706,7 @@ impl Item for BufferDiagnosticsEditor {
                 window,
                 cx,
             )
-        }))
+        })))
     }
 
     fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {

crates/diagnostics/src/diagnostics.rs 🔗

@@ -732,11 +732,11 @@ impl Item for ProjectDiagnosticsEditor {
         _workspace_id: Option<workspace::WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>>
+    ) -> Task<Option<Entity<Self>>>
     where
         Self: Sized,
     {
-        Some(cx.new(|cx| {
+        Task::ready(Some(cx.new(|cx| {
             ProjectDiagnosticsEditor::new(
                 self.include_warnings,
                 self.project.clone(),
@@ -744,7 +744,7 @@ impl Item for ProjectDiagnosticsEditor {
                 window,
                 cx,
             )
-        }))
+        })))
     }
 
     fn is_dirty(&self, cx: &App) -> bool {

crates/editor/src/items.rs 🔗

@@ -762,11 +762,11 @@ impl Item for Editor {
         _workspace_id: Option<WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Editor>>
+    ) -> Task<Option<Entity<Editor>>>
     where
         Self: Sized,
     {
-        Some(cx.new(|cx| self.clone(window, cx)))
+        Task::ready(Some(cx.new(|cx| self.clone(window, cx))))
     }
 
     fn set_nav_history(

crates/git_ui/src/commit_view.rs 🔗

@@ -4,8 +4,8 @@ use editor::{Editor, EditorEvent, MultiBuffer, SelectionEffects, multibuffer_con
 use git::repository::{CommitDetails, CommitDiff, RepoPath};
 use gpui::{
     Action, AnyElement, AnyView, App, AppContext as _, AsyncApp, AsyncWindowContext, Context,
-    Entity, EventEmitter, FocusHandle, Focusable, IntoElement, PromptLevel, Render, WeakEntity,
-    Window, actions,
+    Entity, EventEmitter, FocusHandle, Focusable, IntoElement, PromptLevel, Render, Task,
+    WeakEntity, Window, actions,
 };
 use language::{
     Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, OffsetRangeExt as _,
@@ -561,11 +561,11 @@ impl Item for CommitView {
         _workspace_id: Option<workspace::WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>>
+    ) -> Task<Option<Entity<Self>>>
     where
         Self: Sized,
     {
-        Some(cx.new(|cx| {
+        Task::ready(Some(cx.new(|cx| {
             let editor = cx.new(|cx| {
                 self.editor
                     .update(cx, |editor, cx| editor.clone(window, cx))
@@ -577,7 +577,7 @@ impl Item for CommitView {
                 commit: self.commit.clone(),
                 stash: self.stash,
             }
-        }))
+        })))
     }
 }
 

crates/git_ui/src/project_diff.rs 🔗

@@ -714,12 +714,16 @@ impl Item for ProjectDiff {
         _workspace_id: Option<workspace::WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>>
+    ) -> Task<Option<Entity<Self>>>
     where
         Self: Sized,
     {
-        let workspace = self.workspace.upgrade()?;
-        Some(cx.new(|cx| ProjectDiff::new(self.project.clone(), workspace, window, cx)))
+        let Some(workspace) = self.workspace.upgrade() else {
+            return Task::ready(None);
+        };
+        Task::ready(Some(cx.new(|cx| {
+            ProjectDiff::new(self.project.clone(), workspace, window, cx)
+        })))
     }
 
     fn is_dirty(&self, cx: &App) -> bool {

crates/image_viewer/src/image_viewer.rs 🔗

@@ -179,15 +179,15 @@ impl Item for ImageView {
         _workspace_id: Option<WorkspaceId>,
         _: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>>
+    ) -> Task<Option<Entity<Self>>>
     where
         Self: Sized,
     {
-        Some(cx.new(|cx| Self {
+        Task::ready(Some(cx.new(|cx| Self {
             image_item: self.image_item.clone(),
             project: self.project.clone(),
             focus_handle: cx.focus_handle(),
-        }))
+        })))
     }
 
     fn has_deleted_file(&self, cx: &App) -> bool {

crates/language_tools/src/key_context_view.rs 🔗

@@ -1,6 +1,7 @@
 use gpui::{
     Action, App, AppContext as _, Entity, EventEmitter, FocusHandle, Focusable,
-    KeyBindingContextPredicate, KeyContext, Keystroke, MouseButton, Render, Subscription, actions,
+    KeyBindingContextPredicate, KeyContext, Keystroke, MouseButton, Render, Subscription, Task,
+    actions,
 };
 use itertools::Itertools;
 use serde_json::json;
@@ -157,11 +158,11 @@ impl Item for KeyContextView {
         _workspace_id: Option<workspace::WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>>
+    ) -> Task<Option<Entity<Self>>>
     where
         Self: Sized,
     {
-        Some(cx.new(|cx| KeyContextView::new(window, cx)))
+        Task::ready(Some(cx.new(|cx| KeyContextView::new(window, cx))))
     }
 }
 

crates/language_tools/src/lsp_log_view.rs 🔗

@@ -3,7 +3,7 @@ use copilot::Copilot;
 use editor::{Editor, EditorEvent, actions::MoveToEnd, scroll::Autoscroll};
 use gpui::{
     AnyView, App, Context, Corner, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
-    ParentElement, Render, Styled, Subscription, WeakEntity, Window, actions, div,
+    ParentElement, Render, Styled, Subscription, Task, WeakEntity, Window, actions, div,
 };
 use itertools::Itertools;
 use language::{LanguageServerId, language_settings::SoftWrap};
@@ -763,11 +763,11 @@ impl Item for LspLogView {
         _workspace_id: Option<WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>>
+    ) -> Task<Option<Entity<Self>>>
     where
         Self: Sized,
     {
-        Some(cx.new(|cx| {
+        Task::ready(Some(cx.new(|cx| {
             let mut new_view = Self::new(self.project.clone(), self.log_store.clone(), window, cx);
             if let Some(server_id) = self.current_server_id {
                 match self.active_entry_kind {
@@ -778,7 +778,7 @@ impl Item for LspLogView {
                 }
             }
             new_view
-        }))
+        })))
     }
 }
 

crates/language_tools/src/syntax_tree_view.rs 🔗

@@ -3,7 +3,7 @@ use editor::{Anchor, Editor, ExcerptId, SelectionEffects, scroll::Autoscroll};
 use gpui::{
     App, AppContext as _, Context, Div, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
     Hsla, InteractiveElement, IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent,
-    ParentElement, Render, ScrollStrategy, SharedString, Styled, UniformListScrollHandle,
+    ParentElement, Render, ScrollStrategy, SharedString, Styled, Task, UniformListScrollHandle,
     WeakEntity, Window, actions, div, rems, uniform_list,
 };
 use language::{Buffer, OwnedSyntaxLayer};
@@ -573,17 +573,17 @@ impl Item for SyntaxTreeView {
         _: Option<workspace::WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>>
+    ) -> Task<Option<Entity<Self>>>
     where
         Self: Sized,
     {
-        Some(cx.new(|cx| {
+        Task::ready(Some(cx.new(|cx| {
             let mut clone = Self::new(self.workspace_handle.clone(), None, window, cx);
             if let Some(editor) = &self.editor {
                 clone.set_editor(editor.editor.clone(), window, cx)
             }
             clone
-        }))
+        })))
     }
 }
 

crates/onboarding/src/onboarding.rs 🔗

@@ -383,14 +383,14 @@ impl Item for Onboarding {
         _workspace_id: Option<WorkspaceId>,
         _: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>> {
-        Some(cx.new(|cx| Onboarding {
+    ) -> Task<Option<Entity<Self>>> {
+        Task::ready(Some(cx.new(|cx| Onboarding {
             workspace: self.workspace.clone(),
             user_store: self.user_store.clone(),
             scroll_handle: ScrollHandle::new(),
             focus_handle: cx.focus_handle(),
             _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
-        }))
+        })))
     }
 
     fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {

crates/repl/src/notebook/notebook_ui.rs 🔗

@@ -699,11 +699,13 @@ impl Item for NotebookEditor {
         _workspace_id: Option<workspace::WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>>
+    ) -> Task<Option<Entity<Self>>>
     where
         Self: Sized,
     {
-        Some(cx.new(|cx| Self::new(self.project.clone(), self.notebook_item.clone(), window, cx)))
+        Task::ready(Some(cx.new(|cx| {
+            Self::new(self.project.clone(), self.notebook_item.clone(), window, cx)
+        })))
     }
 
     fn buffer_kind(&self, _: &App) -> workspace::item::ItemBufferKind {

crates/search/src/project_search.rs 🔗

@@ -572,12 +572,14 @@ impl Item for ProjectSearchView {
         _workspace_id: Option<WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>>
+    ) -> Task<Option<Entity<Self>>>
     where
         Self: Sized,
     {
         let model = self.entity.update(cx, |model, cx| model.clone(cx));
-        Some(cx.new(|cx| Self::new(self.workspace.clone(), model, window, cx, None)))
+        Task::ready(Some(cx.new(|cx| {
+            Self::new(self.workspace.clone(), model, window, cx, None)
+        })))
     }
 
     fn added_to_workspace(
@@ -3677,6 +3679,7 @@ pub mod tests {
                 )
             })
             .unwrap()
+            .await
             .unwrap();
         assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1);
 
@@ -3872,6 +3875,7 @@ pub mod tests {
                 )
             })
             .unwrap()
+            .await
             .unwrap();
         assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1);
         assert!(

crates/terminal_view/src/terminal_view.rs 🔗

@@ -38,7 +38,7 @@ use ui::{
     prelude::*,
     scrollbars::{self, GlobalSetting, ScrollbarVisibility},
 };
-use util::ResultExt;
+use util::{ResultExt, maybe};
 use workspace::{
     CloseActiveItem, NewCenterTerminal, NewTerminal, ToolbarItemLocation, Workspace, WorkspaceId,
     delete_unloaded_items,
@@ -1218,27 +1218,29 @@ impl Item for TerminalView {
         workspace_id: Option<WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>> {
-        let terminal = self
-            .project
-            .update(cx, |project, cx| {
-                let cwd = project
-                    .active_project_directory(cx)
-                    .map(|it| it.to_path_buf());
-                project.clone_terminal(self.terminal(), cx, cwd)
-            })
-            .ok()?
-            .log_err()?;
-
-        Some(cx.new(|cx| {
-            TerminalView::new(
-                terminal,
-                self.workspace.clone(),
-                workspace_id,
-                self.project.clone(),
-                window,
-                cx,
-            )
+    ) -> Task<Option<Entity<Self>>> {
+        Task::ready(maybe!({
+            let terminal = self
+                .project
+                .update(cx, |project, cx| {
+                    let cwd = project
+                        .active_project_directory(cx)
+                        .map(|it| it.to_path_buf());
+                    project.clone_terminal(self.terminal(), cx, cwd)
+                })
+                .ok()?
+                .log_err()?;
+
+            Some(cx.new(|cx| {
+                TerminalView::new(
+                    terminal,
+                    self.workspace.clone(),
+                    workspace_id,
+                    self.project.clone(),
+                    window,
+                    cx,
+                )
+            }))
         }))
     }
 

crates/workspace/src/item.rs 🔗

@@ -11,8 +11,9 @@ use anyhow::Result;
 use client::{Client, proto};
 use futures::{StreamExt, channel::mpsc};
 use gpui::{
-    Action, AnyElement, AnyView, App, Context, Entity, EntityId, EventEmitter, FocusHandle,
-    Focusable, Font, HighlightStyle, Pixels, Point, Render, SharedString, Task, WeakEntity, Window,
+    Action, AnyElement, AnyView, App, AppContext, Context, Entity, EntityId, EventEmitter,
+    FocusHandle, Focusable, Font, HighlightStyle, Pixels, Point, Render, SharedString, Task,
+    WeakEntity, Window,
 };
 use project::{Project, ProjectEntryId, ProjectPath};
 pub use settings::{
@@ -217,11 +218,11 @@ pub trait Item: Focusable + EventEmitter<Self::Event> + Render + Sized {
         _workspace_id: Option<WorkspaceId>,
         _window: &mut Window,
         _: &mut Context<Self>,
-    ) -> Option<Entity<Self>>
+    ) -> Task<Option<Entity<Self>>>
     where
         Self: Sized,
     {
-        None
+        Task::ready(None)
     }
     fn is_dirty(&self, _: &App) -> bool {
         false
@@ -422,7 +423,7 @@ pub trait ItemHandle: 'static + Send {
         workspace_id: Option<WorkspaceId>,
         window: &mut Window,
         cx: &mut App,
-    ) -> Option<Box<dyn ItemHandle>>;
+    ) -> Task<Option<Box<dyn ItemHandle>>>;
     fn added_to_pane(
         &self,
         workspace: &mut Workspace,
@@ -635,9 +636,12 @@ impl<T: Item> ItemHandle for Entity<T> {
         workspace_id: Option<WorkspaceId>,
         window: &mut Window,
         cx: &mut App,
-    ) -> Option<Box<dyn ItemHandle>> {
-        self.update(cx, |item, cx| item.clone_on_split(workspace_id, window, cx))
-            .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
+    ) -> Task<Option<Box<dyn ItemHandle>>> {
+        let task = self.update(cx, |item, cx| item.clone_on_split(workspace_id, window, cx));
+        cx.background_spawn(async move {
+            task.await
+                .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
+        })
     }
 
     fn added_to_pane(
@@ -1504,11 +1508,11 @@ pub mod test {
             _workspace_id: Option<WorkspaceId>,
             _: &mut Window,
             cx: &mut Context<Self>,
-        ) -> Option<Entity<Self>>
+        ) -> Task<Option<Entity<Self>>>
         where
             Self: Sized,
         {
-            Some(cx.new(|cx| Self {
+            Task::ready(Some(cx.new(|cx| Self {
                 state: self.state.clone(),
                 label: self.label.clone(),
                 save_count: self.save_count,
@@ -1525,7 +1529,7 @@ pub mod test {
                 workspace_id: self.workspace_id,
                 focus_handle: cx.focus_handle(),
                 serialize: None,
-            }))
+            })))
         }
 
         fn is_dirty(&self, _: &App) -> bool {

crates/workspace/src/pane.rs 🔗

@@ -3292,11 +3292,18 @@ impl Pane {
                         else {
                             return;
                         };
-                        if let Some(item) = item.clone_on_split(database_id, window, cx) {
-                            to_pane.update(cx, |pane, cx| {
-                                pane.add_item(item, true, true, None, window, 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);
                     }

crates/workspace/src/shared_screen.rs 🔗

@@ -6,7 +6,7 @@ use call::{RemoteVideoTrack, RemoteVideoTrackView, Room};
 use client::{User, proto::PeerId};
 use gpui::{
     AppContext as _, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
-    ParentElement, Render, SharedString, Styled, div,
+    ParentElement, Render, SharedString, Styled, Task, div,
 };
 use std::sync::Arc;
 use ui::{Icon, IconName, prelude::*};
@@ -114,14 +114,14 @@ impl Item for SharedScreen {
         _workspace_id: Option<WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>> {
-        Some(cx.new(|cx| Self {
+    ) -> Task<Option<Entity<Self>>> {
+        Task::ready(Some(cx.new(|cx| Self {
             view: self.view.update(cx, |view, cx| view.clone(window, cx)),
             peer_id: self.peer_id,
             user: self.user.clone(),
             nav_history: Default::default(),
             focus: cx.focus_handle(),
-        }))
+        })))
     }
 
     fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {

crates/workspace/src/theme_preview.rs 🔗

@@ -1,5 +1,7 @@
 #![allow(unused, dead_code)]
-use gpui::{AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Hsla, actions, hsla};
+use gpui::{
+    AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Hsla, Task, actions, hsla,
+};
 use strum::IntoEnumIterator;
 use theme::all_theme_colors;
 use ui::{
@@ -100,11 +102,11 @@ impl Item for ThemePreview {
         _workspace_id: Option<crate::WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>>
+    ) -> Task<Option<Entity<Self>>>
     where
         Self: Sized,
     {
-        Some(cx.new(|cx| Self::new(window, cx)))
+        Task::ready(Some(cx.new(|cx| Self::new(window, cx))))
     }
 }
 

crates/workspace/src/workspace.rs 🔗

@@ -3627,7 +3627,8 @@ impl Workspace {
         if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
             window.focus(&pane.focus_handle(cx));
         } else {
-            self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, window, cx);
+            self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, window, cx)
+                .detach();
         }
     }
 
@@ -3994,7 +3995,8 @@ impl Workspace {
                 clone_active_item,
             } => {
                 if *clone_active_item {
-                    self.split_and_clone(pane.clone(), *direction, window, cx);
+                    self.split_and_clone(pane.clone(), *direction, window, cx)
+                        .detach();
                 } else {
                     self.split_and_move(pane.clone(), *direction, window, cx);
                 }
@@ -4135,21 +4137,27 @@ impl Workspace {
         direction: SplitDirection,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Pane>> {
-        let item = pane.read(cx).active_item()?;
-        let maybe_pane_handle =
-            if let Some(clone) = item.clone_on_split(self.database_id(), window, cx) {
-                let new_pane = self.add_pane(window, cx);
-                new_pane.update(cx, |pane, cx| {
-                    pane.add_item(clone, true, true, None, window, cx)
-                });
-                self.center.split(&pane, &new_pane, direction).unwrap();
-                cx.notify();
-                Some(new_pane)
+    ) -> Task<Option<Entity<Pane>>> {
+        let Some(item) = pane.read(cx).active_item() else {
+            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 {
+                this.update_in(cx, |this, window, cx| {
+                    let new_pane = this.add_pane(window, cx);
+                    new_pane.update(cx, |pane, cx| {
+                        pane.add_item(clone, true, true, None, window, cx)
+                    });
+                    this.center.split(&pane, &new_pane, direction).unwrap();
+                    cx.notify();
+                    new_pane
+                })
+                .ok()
             } else {
                 None
-            };
-        maybe_pane_handle
+            }
+        })
     }
 
     pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -8217,19 +8225,27 @@ pub fn clone_active_item(
     let Some(active_item) = source.read(cx).active_item() else {
         return;
     };
-    destination.update(cx, |target_pane, cx| {
-        let Some(clone) = active_item.clone_on_split(workspace_id, window, cx) else {
-            return;
-        };
-        target_pane.add_item(
-            clone,
-            focus_destination,
-            focus_destination,
-            Some(target_pane.items_len()),
-            window,
-            cx,
-        );
-    });
+    let destination = destination.downgrade();
+    let task = active_item.clone_on_split(workspace_id, window, cx);
+    window
+        .spawn(cx, async move |cx| {
+            let Some(clone) = task.await else {
+                return;
+            };
+            destination
+                .update_in(cx, |target_pane, window, cx| {
+                    target_pane.add_item(
+                        clone,
+                        focus_destination,
+                        focus_destination,
+                        Some(target_pane.items_len()),
+                        window,
+                        cx,
+                    );
+                })
+                .log_err();
+        })
+        .detach();
 }
 
 #[derive(Debug)]
@@ -8736,25 +8752,24 @@ mod tests {
                 cx,
             );
 
-            let right_pane = workspace
-                .split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx)
-                .unwrap();
+            let right_pane =
+                workspace.split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx);
 
-            right_pane.update(cx, |pane, cx| {
-                pane.add_item(
-                    single_entry_items[1].boxed_clone(),
-                    true,
-                    true,
-                    None,
-                    window,
-                    cx,
-                );
-                pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
+            let boxed_clone = single_entry_items[1].boxed_clone();
+            let right_pane = window.spawn(cx, async move |cx| {
+                right_pane.await.inspect(|right_pane| {
+                    right_pane
+                        .update_in(cx, |pane, window, cx| {
+                            pane.add_item(boxed_clone, true, true, None, window, cx);
+                            pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
+                        })
+                        .unwrap();
+                })
             });
 
             (left_pane, right_pane)
         });
-
+        let right_pane = right_pane.await.unwrap();
         cx.focus(&right_pane);
 
         let mut close = right_pane.update_in(cx, |pane, window, cx| {
@@ -10571,7 +10586,10 @@ mod tests {
                 window,
                 cx,
             );
+        });
+        cx.run_until_parked();
 
+        workspace.update(cx, |workspace, cx| {
             assert_eq!(workspace.panes.len(), 3, "Two new panes were created");
             for pane in workspace.panes() {
                 assert_eq!(

crates/zed/src/zed.rs 🔗

@@ -2854,14 +2854,16 @@ mod tests {
         });
 
         // Split the pane with the first entry, then open the second entry again.
-        window
+        let (task1, task2) = window
             .update(cx, |w, window, cx| {
-                w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, window, cx);
-                w.open_path(file2.clone(), None, true, window, cx)
+                (
+                    w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, window, cx),
+                    w.open_path(file2.clone(), None, true, window, cx),
+                )
             })
-            .unwrap()
-            .await
             .unwrap();
+        task1.await.unwrap();
+        task2.await.unwrap();
 
         window
             .read_with(cx, |w, cx| {
@@ -3484,7 +3486,13 @@ mod tests {
                     SplitDirection::Right,
                     window,
                     cx,
-                );
+                )
+            })
+            .unwrap()
+            .await
+            .unwrap();
+        window
+            .update(cx, |workspace, window, cx| {
                 workspace.open_path(
                     (worktree.read(cx).id(), rel_path("the-new-name.rs")),
                     None,

crates/zed/src/zed/component_preview.rs 🔗

@@ -720,7 +720,7 @@ impl Item for ComponentPreview {
         _workspace_id: Option<WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<gpui::Entity<Self>>
+    ) -> Task<Option<gpui::Entity<Self>>>
     where
         Self: Sized,
     {
@@ -742,13 +742,13 @@ impl Item for ComponentPreview {
             cx,
         );
 
-        match self_result {
+        Task::ready(match self_result {
             Ok(preview) => Some(cx.new(|_cx| preview)),
             Err(e) => {
                 log::error!("Failed to clone component preview: {}", e);
                 None
             }
-        }
+        })
     }
 
     fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {