From 0a66dd1ab836d4a5fb988f59f8da5e7633c68796 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Tue, 21 Oct 2025 10:37:37 +0200 Subject: [PATCH] project: Spawn terminal process on background thread --- 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/project/src/terminals.rs | 355 ++++++------ crates/repl/src/notebook/notebook_ui.rs | 6 +- crates/search/src/project_search.rs | 8 +- crates/terminal/src/terminal.rs | 531 +++++++++--------- crates/terminal_view/src/terminal_panel.rs | 4 +- crates/terminal_view/src/terminal_view.rs | 45 +- 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 | 45 +- crates/zed/src/zed/component_preview.rs | 6 +- 27 files changed, 682 insertions(+), 608 deletions(-) diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index c2b624ecfca2bed4c9480bf681fe17b43b569685..e463c0eb48816021bc2665d385804c926f1c63f4 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -581,11 +581,13 @@ impl Item for AgentDiffPane { _workspace_id: Option, window: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> 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 { diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index ab72ce3605b7c93bac05dc6321b44b7abb964d93..07cf866a3513d27894307216e904b130eb023e22 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/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()) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index f080b1cc4a56a7597115a35aac3c329f9039421c..cc2c01b7857a1efefd88b47d2ea199fc571051ea 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/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| { diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 4e4bd2ca958d20225a7188b1f7f601e879e22835..18847363bf1b012acb7916bb7b6a9c0adde4de28 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -498,8 +498,8 @@ impl Item for ChannelView { _: Option, window: &mut Window, cx: &mut Context, - ) -> Option> { - Some(cx.new(|cx| { + ) -> Task>> { + 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( diff --git a/crates/diagnostics/src/buffer_diagnostics.rs b/crates/diagnostics/src/buffer_diagnostics.rs index e25d3a7702e02c93e38f5808434aff57e743defe..1a7a97c68691d7bdf941322660eddeb70fa15037 100644 --- a/crates/diagnostics/src/buffer_diagnostics.rs +++ b/crates/diagnostics/src/buffer_diagnostics.rs @@ -693,11 +693,11 @@ impl Item for BufferDiagnosticsEditor { _workspace_id: Option, window: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> 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) { diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 47e2a9539b7e362bb9b968d8e39cda30d3f17e78..b96e8f891fda3cc470c7091eba8e92847b59562b 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -732,11 +732,11 @@ impl Item for ProjectDiagnosticsEditor { _workspace_id: Option, window: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> 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 { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 708efbbe979dd153dbafde265e56bc2ddd725f76..128b894eddc868c93c187070d6deafec5bcfe96f 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -762,11 +762,11 @@ impl Item for Editor { _workspace_id: Option, window: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> 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( diff --git a/crates/git_ui/src/commit_view.rs b/crates/git_ui/src/commit_view.rs index 9738e13984a0b032b09a218990f3466052e9fa61..2430796c89f68e9b5032c3d05f7106b6f6de0bec 100644 --- a/crates/git_ui/src/commit_view.rs +++ b/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, window: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> 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, } - })) + }))) } } diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 5cd2e5e3b07bb8bf503cada7f0ca2f8dd0388a4e..f4ee0b8934ae63433eb5d94d52e213b4458c76cd 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -625,12 +625,16 @@ impl Item for ProjectDiff { _workspace_id: Option, window: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> 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 { diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index ff68de39e87c7246e0aa07e96a4d0d7e3c2ad523..fab0166efea5d4199973b66dd63f68b8bb0f2e1c 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -176,15 +176,15 @@ impl Item for ImageView { _workspace_id: Option, _: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> 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 { diff --git a/crates/language_tools/src/key_context_view.rs b/crates/language_tools/src/key_context_view.rs index 1d3cd451f19ce6bc28540f10b2a91a7a6319214a..7b0b71059e9998914ce511b47e26d1fd0c3abfe5 100644 --- a/crates/language_tools/src/key_context_view.rs +++ b/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, window: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> where Self: Sized, { - Some(cx.new(|cx| KeyContextView::new(window, cx))) + Task::ready(Some(cx.new(|cx| KeyContextView::new(window, cx)))) } } diff --git a/crates/language_tools/src/lsp_log_view.rs b/crates/language_tools/src/lsp_log_view.rs index e834dd6aec003930d68ed745f67aff50b2c8f66b..b1e24303c47e722460d20023d4f7444a8b006406 100644 --- a/crates/language_tools/src/lsp_log_view.rs +++ b/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, window: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> 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 - })) + }))) } } diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 464d518c2e9c697d292d7bffda7ee7bae68dd254..9e5a0374f54f97fc3d75ebc68e2247bf4b904f0c 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/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, window: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> 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 - })) + }))) } } diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 9273f0d7d87851b5118d7835244074502fc128c7..e252966814f7eaa381982b4c73583f9e2b051ad2 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -385,14 +385,14 @@ impl Item for Onboarding { _workspace_id: Option, _: &mut Window, cx: &mut Context, - ) -> Option> { - Some(cx.new(|cx| Onboarding { + ) -> Task>> { + 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::(move |_, cx| cx.notify()), - })) + }))) } fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index c9594a4b55d44608fc80c2e64d8e39ebbeacee13..dd69050a53d8e5477e5bf8be5e5d3a2a86e92af5 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -144,141 +144,146 @@ impl Project { .await .unwrap_or_default(); - project.update(cx, move |this, cx| { - let format_to_run = || { - if let Some(command) = &spawn_task.command { - let mut command: Option> = shell_kind.try_quote(command); - if let Some(command) = &mut command - && command.starts_with('"') - && let Some(prefix) = shell_kind.command_prefix() - { - *command = Cow::Owned(format!("{prefix}{command}")); - } + let builder = project + .update(cx, move |_, cx| { + let format_to_run = || { + if let Some(command) = &spawn_task.command { + let mut command: Option> = shell_kind.try_quote(command); + if let Some(command) = &mut command + && command.starts_with('"') + && let Some(prefix) = shell_kind.command_prefix() + { + *command = Cow::Owned(format!("{prefix}{command}")); + } - let args = spawn_task - .args - .iter() - .filter_map(|arg| shell_kind.try_quote(&arg)); + let args = spawn_task + .args + .iter() + .filter_map(|arg| shell_kind.try_quote(&arg)); - command.into_iter().chain(args).join(" ") - } else { - // todo: this breaks for remotes to windows - format!("exec {shell} -l") - } - }; - - let (shell, env) = { - env.extend(spawn_task.env); - match remote_client { - Some(remote_client) => match activation_script.clone() { - activation_script if !activation_script.is_empty() => { - let activation_script = activation_script.join("; "); - let to_run = format_to_run(); - let args = - vec!["-c".to_owned(), format!("{activation_script}; {to_run}")]; - create_remote_shell( - Some(( - &remote_client - .read(cx) - .shell() - .unwrap_or_else(get_default_system_shell), - &args, - )), + command.into_iter().chain(args).join(" ") + } else { + // todo: this breaks for remotes to windows + format!("exec {shell} -l") + } + }; + + let (shell, env) = { + env.extend(spawn_task.env); + match remote_client { + Some(remote_client) => match activation_script.clone() { + activation_script if !activation_script.is_empty() => { + let activation_script = activation_script.join("; "); + let to_run = format_to_run(); + let args = vec![ + "-c".to_owned(), + format!("{activation_script}; {to_run}"), + ]; + create_remote_shell( + Some(( + &remote_client + .read(cx) + .shell() + .unwrap_or_else(get_default_system_shell), + &args, + )), + env, + path, + remote_client, + cx, + )? + } + _ => create_remote_shell( + spawn_task + .command + .as_ref() + .map(|command| (command, &spawn_task.args)), env, path, remote_client, cx, - )? - } - _ => create_remote_shell( - spawn_task - .command - .as_ref() - .map(|command| (command, &spawn_task.args)), - env, - path, - remote_client, - cx, - )?, - }, - None => match activation_script.clone() { - activation_script if !activation_script.is_empty() => { - let separator = shell_kind.sequential_commands_separator(); - let activation_script = - activation_script.join(&format!("{separator} ")); - let to_run = format_to_run(); - - let mut arg = format!("{activation_script}{separator} {to_run}"); - if shell_kind == ShellKind::Cmd { - // We need to put the entire command in quotes since otherwise CMD tries to execute them - // as separate commands rather than chaining one after another. - arg = format!("\"{arg}\""); - } + )?, + }, + None => match activation_script.clone() { + activation_script if !activation_script.is_empty() => { + let separator = shell_kind.sequential_commands_separator(); + let activation_script = + activation_script.join(&format!("{separator} ")); + let to_run = format_to_run(); + + let mut arg = + format!("{activation_script}{separator} {to_run}"); + if shell_kind == ShellKind::Cmd { + // We need to put the entire command in quotes since otherwise CMD tries to execute them + // as separate commands rather than chaining one after another. + arg = format!("\"{arg}\""); + } - let args = shell_kind.args_for_shell(false, arg); + let args = shell_kind.args_for_shell(false, arg); - ( - Shell::WithArguments { - program: shell, - args, - title_override: None, + ( + Shell::WithArguments { + program: shell, + args, + title_override: None, + }, + env, + ) + } + _ => ( + if let Some(program) = spawn_task.command { + Shell::WithArguments { + program, + args: spawn_task.args, + title_override: None, + } + } else { + Shell::System }, env, - ) - } - _ => ( - if let Some(program) = spawn_task.command { - Shell::WithArguments { - program, - args: spawn_task.args, - title_override: None, - } - } else { - Shell::System - }, - env, - ), - }, - } - }; - TerminalBuilder::new( - local_path.map(|path| path.to_path_buf()), - task_state, - shell, - env, - settings.cursor_shape, - settings.alternate_scroll, - settings.max_scroll_history_lines, - is_via_remote, - cx.entity_id().as_u64(), - Some(completion_tx), - cx, - activation_script, - ) - .map(|builder| { - let terminal_handle = cx.new(|cx| builder.subscribe(cx)); - - this.terminals - .local_handles - .push(terminal_handle.downgrade()); - - let id = terminal_handle.entity_id(); - cx.observe_release(&terminal_handle, move |project, _terminal, cx| { - let handles = &mut project.terminals.local_handles; - - if let Some(index) = handles - .iter() - .position(|terminal| terminal.entity_id() == id) - { - handles.remove(index); - cx.notify(); + ), + }, } - }) - .detach(); + }; + anyhow::Ok(TerminalBuilder::new( + local_path.map(|path| path.to_path_buf()), + task_state, + shell, + env, + settings.cursor_shape, + settings.alternate_scroll, + settings.max_scroll_history_lines, + is_via_remote, + cx.entity_id().as_u64(), + Some(completion_tx), + cx, + activation_script, + )) + })?? + .await?; + project.update(cx, move |this, cx| { + let terminal_handle = cx.new(|cx| builder.subscribe(cx)); + + this.terminals + .local_handles + .push(terminal_handle.downgrade()); + + let id = terminal_handle.entity_id(); + cx.observe_release(&terminal_handle, move |project, _terminal, cx| { + let handles = &mut project.terminals.local_handles; - terminal_handle + if let Some(index) = handles + .iter() + .position(|terminal| terminal.entity_id() == id) + { + handles.remove(index); + cx.notify(); + } }) - })? + .detach(); + + terminal_handle + }) }) } @@ -364,53 +369,55 @@ impl Project { }) .await .unwrap_or_default(); - project.update(cx, move |this, cx| { - let (shell, env) = { - match remote_client { - Some(remote_client) => { - create_remote_shell(None, env, path, remote_client, cx)? - } - None => (settings.shell, env), - } - }; - TerminalBuilder::new( - local_path.map(|path| path.to_path_buf()), - None, - shell, - env, - settings.cursor_shape, - settings.alternate_scroll, - settings.max_scroll_history_lines, - is_via_remote, - cx.entity_id().as_u64(), - None, - cx, - activation_script, - ) - .map(|builder| { - let terminal_handle = cx.new(|cx| builder.subscribe(cx)); - - this.terminals - .local_handles - .push(terminal_handle.downgrade()); - - let id = terminal_handle.entity_id(); - cx.observe_release(&terminal_handle, move |project, _terminal, cx| { - let handles = &mut project.terminals.local_handles; - - if let Some(index) = handles - .iter() - .position(|terminal| terminal.entity_id() == id) - { - handles.remove(index); - cx.notify(); + let builder = project + .update(cx, move |_, cx| { + let (shell, env) = { + match remote_client { + Some(remote_client) => { + create_remote_shell(None, env, path, remote_client, cx)? + } + None => (settings.shell, env), } - }) - .detach(); + }; + anyhow::Ok(TerminalBuilder::new( + local_path.map(|path| path.to_path_buf()), + None, + shell, + env, + settings.cursor_shape, + settings.alternate_scroll, + settings.max_scroll_history_lines, + is_via_remote, + cx.entity_id().as_u64(), + None, + cx, + activation_script, + )) + })?? + .await?; + project.update(cx, move |this, cx| { + let terminal_handle = cx.new(|cx| builder.subscribe(cx)); + + this.terminals + .local_handles + .push(terminal_handle.downgrade()); + + let id = terminal_handle.entity_id(); + cx.observe_release(&terminal_handle, move |project, _terminal, cx| { + let handles = &mut project.terminals.local_handles; - terminal_handle + if let Some(index) = handles + .iter() + .position(|terminal| terminal.entity_id() == id) + { + handles.remove(index); + cx.notify(); + } }) - })? + .detach(); + + terminal_handle + }) }) } @@ -419,20 +426,21 @@ impl Project { terminal: &Entity, cx: &mut Context<'_, Project>, cwd: Option, - ) -> Result> { + ) -> Task>> { let local_path = if self.is_via_remote_server() { None } else { cwd }; - terminal - .read(cx) - .clone_builder(cx, local_path) - .map(|builder| { - let terminal_handle = cx.new(|cx| builder.subscribe(cx)); + let builder = terminal.read(cx).clone_builder(cx, local_path); + cx.spawn(async |project, cx| { + let terminal = builder.await?; + project.update(cx, |project, cx| { + let terminal_handle = cx.new(|cx| terminal.subscribe(cx)); - self.terminals + project + .terminals .local_handles .push(terminal_handle.downgrade()); @@ -452,6 +460,7 @@ impl Project { terminal_handle }) + }) } pub fn terminal_settings<'a>( diff --git a/crates/repl/src/notebook/notebook_ui.rs b/crates/repl/src/notebook/notebook_ui.rs index 6f92fe511528097d363063bf837ad3b7efa83318..7e523a46ddf2dfce9921a3c907de19fb91221f9b 100644 --- a/crates/repl/src/notebook/notebook_ui.rs +++ b/crates/repl/src/notebook/notebook_ui.rs @@ -709,11 +709,13 @@ impl Item for NotebookEditor { _workspace_id: Option, window: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> 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 { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index c292d05df75ad329c608d456aaf07a9d1d3af044..e4ff5e6e540fa5626699e98725c5713a09e7cce8 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -572,12 +572,14 @@ impl Item for ProjectSearchView { _workspace_id: Option, window: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> 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( @@ -3694,6 +3696,7 @@ pub mod tests { ) }) .unwrap() + .await .unwrap(); assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1); @@ -3889,6 +3892,7 @@ pub mod tests { ) }) .unwrap() + .await .unwrap(); assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1); assert!( diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 960812da00060a987fbaafe6f248a0d582d60e4f..e35a3ae4e14dec8c510e4858adaeabb789436de0 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -423,232 +423,233 @@ impl TerminalBuilder { completion_tx: Option>>, cx: &App, activation_script: Vec, - ) -> Result { - // If the parent environment doesn't have a locale set - // (As is the case when launched from a .app on MacOS), - // and the Project doesn't have a locale set, then - // set a fallback for our child environment to use. - if std::env::var("LANG").is_err() { - env.entry("LANG".to_string()) - .or_insert_with(|| "en_US.UTF-8".to_string()); - } - - env.insert("ZED_TERM".to_string(), "true".to_string()); - env.insert("TERM_PROGRAM".to_string(), "zed".to_string()); - env.insert("TERM".to_string(), "xterm-256color".to_string()); - env.insert("COLORTERM".to_string(), "truecolor".to_string()); - env.insert( - "TERM_PROGRAM_VERSION".to_string(), - release_channel::AppVersion::global(cx).to_string(), - ); - - #[derive(Default)] - struct ShellParams { - program: String, - args: Option>, - title_override: Option, - } - - impl ShellParams { - fn new( + ) -> Task> { + let version = release_channel::AppVersion::global(cx); + cx.background_spawn(async move { + // If the parent environment doesn't have a locale set + // (As is the case when launched from a .app on MacOS), + // and the Project doesn't have a locale set, then + // set a fallback for our child environment to use. + if std::env::var("LANG").is_err() { + env.entry("LANG".to_string()) + .or_insert_with(|| "en_US.UTF-8".to_string()); + } + + env.insert("ZED_TERM".to_string(), "true".to_string()); + env.insert("TERM_PROGRAM".to_string(), "zed".to_string()); + env.insert("TERM".to_string(), "xterm-256color".to_string()); + env.insert("COLORTERM".to_string(), "truecolor".to_string()); + env.insert("TERM_PROGRAM_VERSION".to_string(), version.to_string()); + + #[derive(Default)] + struct ShellParams { program: String, args: Option>, title_override: Option, - ) -> Self { - log::info!("Using {program} as shell"); - Self { - program, - args, - title_override, - } } - } - let shell_params = match shell.clone() { - Shell::System => { - if cfg!(windows) { - Some(ShellParams::new( - util::shell::get_windows_system_shell(), - None, - None, - )) - } else { - None + impl ShellParams { + fn new( + program: String, + args: Option>, + title_override: Option, + ) -> Self { + log::info!("Using {program} as shell"); + Self { + program, + args, + title_override, + } } } - Shell::Program(program) => Some(ShellParams::new(program, None, None)), - Shell::WithArguments { - program, - args, - title_override, - } => Some(ShellParams::new(program, Some(args), title_override)), - }; - let terminal_title_override = shell_params.as_ref().and_then(|e| e.title_override.clone()); - #[cfg(windows)] - let shell_program = shell_params.as_ref().map(|params| { - use util::ResultExt; + let shell_params = match shell.clone() { + Shell::System => { + if cfg!(windows) { + Some(ShellParams::new( + util::shell::get_windows_system_shell(), + None, + None, + )) + } else { + None + } + } + Shell::Program(program) => Some(ShellParams::new(program, None, None)), + Shell::WithArguments { + program, + args, + title_override, + } => Some(ShellParams::new(program, Some(args), title_override)), + }; + let terminal_title_override = + shell_params.as_ref().and_then(|e| e.title_override.clone()); - Self::resolve_path(¶ms.program) - .log_err() - .unwrap_or(params.program.clone()) - }); + #[cfg(windows)] + let shell_program = shell_params.as_ref().map(|params| { + use util::ResultExt; - // Note: when remoting, this shell_kind will scrutinize `ssh` or - // `wsl.exe` as a shell and fall back to posix or powershell based on - // the compilation target. This is fine right now due to the restricted - // way we use the return value, but would become incorrect if we - // supported remoting into windows. - let shell_kind = shell.shell_kind(cfg!(windows)); - - let pty_options = { - let alac_shell = shell_params.as_ref().map(|params| { - alacritty_terminal::tty::Shell::new( - params.program.clone(), - params.args.clone().unwrap_or_default(), - ) + Self::resolve_path(¶ms.program) + .log_err() + .unwrap_or(params.program.clone()) }); - alacritty_terminal::tty::Options { - shell: alac_shell, - working_directory: working_directory.clone(), - drain_on_exit: true, - env: env.clone().into_iter().collect(), - // We do not want to escape arguments if we are using CMD as our shell. - // If we do we end up with too many quotes/escaped quotes for CMD to handle. - #[cfg(windows)] - escape_args: shell_kind != util::shell::ShellKind::Cmd, - } - }; - - let default_cursor_style = AlacCursorStyle::from(cursor_shape); - let scrolling_history = if task.is_some() { - // Tasks like `cargo build --all` may produce a lot of output, ergo allow maximum scrolling. - // After the task finishes, we do not allow appending to that terminal, so small tasks output should not - // cause excessive memory usage over time. - MAX_SCROLL_HISTORY_LINES - } else { - max_scroll_history_lines - .unwrap_or(DEFAULT_SCROLL_HISTORY_LINES) - .min(MAX_SCROLL_HISTORY_LINES) - }; - let config = Config { - scrolling_history, - default_cursor_style, - ..Config::default() - }; + // Note: when remoting, this shell_kind will scrutinize `ssh` or + // `wsl.exe` as a shell and fall back to posix or powershell based on + // the compilation target. This is fine right now due to the restricted + // way we use the return value, but would become incorrect if we + // supported remoting into windows. + let shell_kind = shell.shell_kind(cfg!(windows)); + + let pty_options = { + let alac_shell = shell_params.as_ref().map(|params| { + alacritty_terminal::tty::Shell::new( + params.program.clone(), + params.args.clone().unwrap_or_default(), + ) + }); - //Spawn a task so the Alacritty EventLoop can communicate with us - //TODO: Remove with a bounded sender which can be dispatched on &self - let (events_tx, events_rx) = unbounded(); - //Set up the terminal... - let mut term = Term::new( - config.clone(), - &TerminalBounds::default(), - ZedListener(events_tx.clone()), - ); + alacritty_terminal::tty::Options { + shell: alac_shell, + working_directory: working_directory.clone(), + drain_on_exit: true, + env: env.clone().into_iter().collect(), + // We do not want to escape arguments if we are using CMD as our shell. + // If we do we end up with too many quotes/escaped quotes for CMD to handle. + #[cfg(windows)] + escape_args: shell_kind != util::shell::ShellKind::Cmd, + } + }; - //Alacritty defaults to alternate scrolling being on, so we just need to turn it off. - if let AlternateScroll::Off = alternate_scroll { - term.unset_private_mode(PrivateMode::Named(NamedPrivateMode::AlternateScroll)); - } + let default_cursor_style = AlacCursorStyle::from(cursor_shape); + let scrolling_history = if task.is_some() { + // Tasks like `cargo build --all` may produce a lot of output, ergo allow maximum scrolling. + // After the task finishes, we do not allow appending to that terminal, so small tasks output should not + // cause excessive memory usage over time. + MAX_SCROLL_HISTORY_LINES + } else { + max_scroll_history_lines + .unwrap_or(DEFAULT_SCROLL_HISTORY_LINES) + .min(MAX_SCROLL_HISTORY_LINES) + }; + let config = Config { + scrolling_history, + default_cursor_style, + ..Config::default() + }; - let term = Arc::new(FairMutex::new(term)); + //Spawn a task so the Alacritty EventLoop can communicate with us + //TODO: Remove with a bounded sender which can be dispatched on &self + let (events_tx, events_rx) = unbounded(); + //Set up the terminal... + let mut term = Term::new( + config.clone(), + &TerminalBounds::default(), + ZedListener(events_tx.clone()), + ); - //Setup the pty... - let pty = match tty::new(&pty_options, TerminalBounds::default().into(), window_id) { - Ok(pty) => pty, - Err(error) => { - bail!(TerminalError { - directory: working_directory, - program: shell_params.as_ref().map(|params| params.program.clone()), - args: shell_params.as_ref().and_then(|params| params.args.clone()), - title_override: terminal_title_override, - source: error, - }); + //Alacritty defaults to alternate scrolling being on, so we just need to turn it off. + if let AlternateScroll::Off = alternate_scroll { + term.unset_private_mode(PrivateMode::Named(NamedPrivateMode::AlternateScroll)); } - }; - let pty_info = PtyProcessInfo::new(&pty); + let term = Arc::new(FairMutex::new(term)); - //And connect them together - let event_loop = EventLoop::new( - term.clone(), - ZedListener(events_tx), - pty, - pty_options.drain_on_exit, - false, - ) - .context("failed to create event loop")?; + //Setup the pty... + let pty = match tty::new(&pty_options, TerminalBounds::default().into(), window_id) { + Ok(pty) => pty, + Err(error) => { + bail!(TerminalError { + directory: working_directory, + program: shell_params.as_ref().map(|params| params.program.clone()), + args: shell_params.as_ref().and_then(|params| params.args.clone()), + title_override: terminal_title_override, + source: error, + }); + } + }; - //Kick things off - let pty_tx = event_loop.channel(); - let _io_thread = event_loop.spawn(); // DANGER + let pty_info = PtyProcessInfo::new(&pty); - let no_task = task.is_none(); + //And connect them together + let event_loop = EventLoop::new( + term.clone(), + ZedListener(events_tx), + pty, + pty_options.drain_on_exit, + false, + ) + .context("failed to create event loop")?; - let terminal = Terminal { - task, - terminal_type: TerminalType::Pty { - pty_tx: Notifier(pty_tx), - info: pty_info, - }, - completion_tx, - term, - term_config: config, - title_override: terminal_title_override, - events: VecDeque::with_capacity(10), //Should never get this high. - last_content: Default::default(), - last_mouse: None, - matches: Vec::new(), - selection_head: None, - breadcrumb_text: String::new(), - scroll_px: px(0.), - next_link_id: 0, - selection_phase: SelectionPhase::Ended, - hyperlink_regex_searches: RegexSearches::new(), - vi_mode_enabled: false, - is_ssh_terminal, - last_mouse_move_time: Instant::now(), - last_hyperlink_search_position: None, - #[cfg(windows)] - shell_program, - activation_script: activation_script.clone(), - template: CopyTemplate { - shell, - env, - cursor_shape, - alternate_scroll, - max_scroll_history_lines, - window_id, - }, - child_exited: None, - }; + //Kick things off + let pty_tx = event_loop.channel(); + let _io_thread = event_loop.spawn(); // DANGER + + let no_task = task.is_none(); - if !activation_script.is_empty() && no_task { - for activation_script in activation_script { - terminal.write_to_pty(activation_script.into_bytes()); + let terminal = Terminal { + task, + terminal_type: TerminalType::Pty { + pty_tx: Notifier(pty_tx), + info: pty_info, + }, + completion_tx, + term, + term_config: config, + title_override: terminal_title_override, + events: VecDeque::with_capacity(10), //Should never get this high. + last_content: Default::default(), + last_mouse: None, + matches: Vec::new(), + selection_head: None, + breadcrumb_text: String::new(), + scroll_px: px(0.), + next_link_id: 0, + selection_phase: SelectionPhase::Ended, + hyperlink_regex_searches: RegexSearches::new(), + vi_mode_enabled: false, + is_ssh_terminal, + last_mouse_move_time: Instant::now(), + last_hyperlink_search_position: None, + #[cfg(windows)] + shell_program, + activation_script: activation_script.clone(), + template: CopyTemplate { + shell, + env, + cursor_shape, + alternate_scroll, + max_scroll_history_lines, + window_id, + }, + child_exited: None, + }; + + if !activation_script.is_empty() && no_task { + for activation_script in activation_script { + terminal.write_to_pty(activation_script.into_bytes()); + // Simulate enter key press + // NOTE(PowerShell): using `\r\n` will put PowerShell in a continuation mode (infamous >> character) + // and generally mess up the rendering. + terminal.write_to_pty(b"\x0d"); + } + // In order to clear the screen at this point, we have two options: + // 1. We can send a shell-specific command such as "clear" or "cls" + // 2. We can "echo" a marker message that we will then catch when handling a Wakeup event + // and clear the screen using `terminal.clear()` method + // We cannot issue a `terminal.clear()` command at this point as alacritty is evented + // and while we have sent the activation script to the pty, it will be executed asynchronously. + // Therefore, we somehow need to wait for the activation script to finish executing before we + // can proceed with clearing the screen. + terminal.write_to_pty(shell_kind.clear_screen_command().as_bytes()); // Simulate enter key press - // NOTE(PowerShell): using `\r\n` will put PowerShell in a continuation mode (infamous >> character) - // and generally mess up the rendering. terminal.write_to_pty(b"\x0d"); } - // In order to clear the screen at this point, we have two options: - // 1. We can send a shell-specific command such as "clear" or "cls" - // 2. We can "echo" a marker message that we will then catch when handling a Wakeup event - // and clear the screen using `terminal.clear()` method - // We cannot issue a `terminal.clear()` command at this point as alacritty is evented - // and while we have sent the activation script to the pty, it will be executed asynchronously. - // Therefore, we somehow need to wait for the activation script to finish executing before we - // can proceed with clearing the screen. - terminal.write_to_pty(shell_kind.clear_screen_command().as_bytes()); - // Simulate enter key press - terminal.write_to_pty(b"\x0d"); - } - Ok(TerminalBuilder { - terminal, - events_rx, + Ok(TerminalBuilder { + terminal, + events_rx, + }) }) } @@ -2153,7 +2154,7 @@ impl Terminal { self.vi_mode_enabled } - pub fn clone_builder(&self, cx: &App, cwd: Option) -> Result { + pub fn clone_builder(&self, cx: &App, cwd: Option) -> Task> { let working_directory = self.working_directory().or_else(|| cwd); TerminalBuilder::new( working_directory, @@ -2389,28 +2390,30 @@ mod tests { let (completion_tx, completion_rx) = smol::channel::unbounded(); let (program, args) = ShellBuilder::new(&Shell::System, false) .build(Some("echo".to_owned()), &["hello".to_owned()]); - let terminal = cx.new(|cx| { - TerminalBuilder::new( - None, - None, - task::Shell::WithArguments { - program, - args, - title_override: None, - }, - HashMap::default(), - CursorShape::default(), - AlternateScroll::On, - None, - false, - 0, - Some(completion_tx), - cx, - vec![], - ) - .unwrap() - .subscribe(cx) - }); + let builder = cx + .update(|cx| { + TerminalBuilder::new( + None, + None, + task::Shell::WithArguments { + program, + args, + title_override: None, + }, + HashMap::default(), + CursorShape::default(), + AlternateScroll::On, + None, + false, + 0, + Some(completion_tx), + cx, + vec![], + ) + }) + .await + .unwrap(); + let terminal = cx.new(|cx| builder.subscribe(cx)); assert_eq!( completion_rx.recv().await.unwrap(), Some(ExitStatus::default()) @@ -2439,25 +2442,27 @@ mod tests { cx.executor().allow_parking(); let (completion_tx, completion_rx) = smol::channel::unbounded(); + let builder = cx + .update(|cx| { + TerminalBuilder::new( + None, + None, + task::Shell::System, + HashMap::default(), + CursorShape::default(), + AlternateScroll::On, + None, + false, + 0, + Some(completion_tx), + cx, + Vec::new(), + ) + }) + .await + .unwrap(); // Build an empty command, which will result in a tty shell spawned. - let terminal = cx.new(|cx| { - TerminalBuilder::new( - None, - None, - task::Shell::System, - HashMap::default(), - CursorShape::default(), - AlternateScroll::On, - None, - false, - 0, - Some(completion_tx), - cx, - Vec::new(), - ) - .unwrap() - .subscribe(cx) - }); + let terminal = cx.new(|cx| builder.subscribe(cx)); let (event_tx, event_rx) = smol::channel::unbounded::(); cx.update(|cx| { @@ -2508,28 +2513,30 @@ mod tests { let (completion_tx, completion_rx) = smol::channel::unbounded(); let (program, args) = ShellBuilder::new(&Shell::System, false) .build(Some("asdasdasdasd".to_owned()), &["@@@@@".to_owned()]); - let terminal = cx.new(|cx| { - TerminalBuilder::new( - None, - None, - task::Shell::WithArguments { - program, - args, - title_override: None, - }, - HashMap::default(), - CursorShape::default(), - AlternateScroll::On, - None, - false, - 0, - Some(completion_tx), - cx, - Vec::new(), - ) - .unwrap() - .subscribe(cx) - }); + let builder = cx + .update(|cx| { + TerminalBuilder::new( + None, + None, + task::Shell::WithArguments { + program, + args, + title_override: None, + }, + HashMap::default(), + CursorShape::default(), + AlternateScroll::On, + None, + false, + 0, + Some(completion_tx), + cx, + Vec::new(), + ) + }) + .await + .unwrap(); + let terminal = cx.new(|cx| builder.subscribe(cx)); let (event_tx, event_rx) = smol::channel::unbounded::(); cx.update(|cx| { diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index de66bb1ed64851a1101d434e3a7b54a8ae725cfb..df30ea4ddf5611b286c0608c7e6d51d4ff7f9e00 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -462,11 +462,11 @@ impl TerminalPanel { cx.spawn_in(window, async move |panel, cx| { let terminal = project .update(cx, |project, cx| match terminal_view { - Some(view) => Task::ready(project.clone_terminal( + Some(view) => project.clone_terminal( &view.read(cx).terminal.clone(), cx, working_directory, - )), + ), None => project.create_terminal_shell(working_directory, cx), }) .ok()? diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index e89e435b0c5dd2ba064a9904dfacec6870d8d512..65eb993d208629f16c705bbac55c3dc3e0f08261 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1220,28 +1220,31 @@ impl Item for TerminalView { workspace_id: Option, window: &mut Window, cx: &mut Context, - ) -> Option> { - 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) + ) -> Task>> { + let Ok(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) + }) else { + return Task::ready(None); + }; + cx.spawn_in(window, async move |this, cx| { + let terminal = terminal.await.log_err()?; + this.update_in(cx, |this, window, cx| { + cx.new(|cx| { + TerminalView::new( + terminal, + this.workspace.clone(), + workspace_id, + this.project.clone(), + window, + cx, + ) + }) }) - .ok()? - .log_err()?; - - Some(cx.new(|cx| { - TerminalView::new( - terminal, - self.workspace.clone(), - workspace_id, - self.project.clone(), - window, - cx, - ) - })) + .ok() + }) } fn is_dirty(&self, cx: &gpui::App) -> bool { diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index bc755d851036d10f04b76866b3d7b94673f2df84..42d452a68ee72491a53e9da535d7713c735912f5 100644 --- a/crates/workspace/src/item.rs +++ b/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 + Render + Sized { _workspace_id: Option, _window: &mut Window, _: &mut Context, - ) -> Option> + ) -> Task>> 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, window: &mut Window, cx: &mut App, - ) -> Option>; + ) -> Task>>; fn added_to_pane( &self, workspace: &mut Workspace, @@ -635,9 +636,12 @@ impl ItemHandle for Entity { workspace_id: Option, window: &mut Window, cx: &mut App, - ) -> Option> { - self.update(cx, |item, cx| item.clone_on_split(workspace_id, window, cx)) - .map(|handle| Box::new(handle) as Box) + ) -> Task>> { + 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) + }) } fn added_to_pane( @@ -1504,11 +1508,11 @@ pub mod test { _workspace_id: Option, _: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> 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 { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 3dcba4aa4ebc38bc7c0006c8402e38d4e12ac016..3e39a8d18faa367798db7a65689bcca59b07a5ae 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -3295,11 +3295,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); } diff --git a/crates/workspace/src/shared_screen.rs b/crates/workspace/src/shared_screen.rs index d77be8ed7632dd113e10a03552da79735d82fa6c..34c7d27df73b8b832e9b5b23b832a15161644e3a 100644 --- a/crates/workspace/src/shared_screen.rs +++ b/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, window: &mut Window, cx: &mut Context, - ) -> Option> { - Some(cx.new(|cx| Self { + ) -> Task>> { + 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)) { diff --git a/crates/workspace/src/theme_preview.rs b/crates/workspace/src/theme_preview.rs index 09a5415ca063d0aab2b2fab97abff3533e113b0b..00f083f353daab677265a2410823c69be0bc5e8f 100644 --- a/crates/workspace/src/theme_preview.rs +++ b/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, window: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> where Self: Sized, { - Some(cx.new(|cx| Self::new(window, cx))) + Task::ready(Some(cx.new(|cx| Self::new(window, cx)))) } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3c8c94ce0e932dc6773c8f6c168c95563b60c879..ee6e5a5d93891bdd79160ead5aba3c674a787dbb 100644 --- a/crates/workspace/src/workspace.rs +++ b/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, - ) -> Option> { - 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>> { + 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) { @@ -8170,19 +8178,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)] @@ -8689,25 +8705,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| { @@ -10524,7 +10539,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!( diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 1416a74e1697324213c11e1bbc51fd2d8a6bf91b..b3d2a920c5b75125059b7d08b100cf95ff23ef3d 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -2836,14 +2836,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| { @@ -3459,25 +3461,28 @@ mod tests { // Open the same newly-created file in another pane item. The new editor should reuse // the same buffer. cx.dispatch_action(window.into(), NewFile); - window + let (task1, task2) = window .update(cx, |workspace, window, cx| { - workspace.split_and_clone( - workspace.active_pane().clone(), - SplitDirection::Right, - window, - cx, - ); - workspace.open_path( - (worktree.read(cx).id(), rel_path("the-new-name.rs")), - None, - true, - window, - cx, + ( + workspace.split_and_clone( + workspace.active_pane().clone(), + SplitDirection::Right, + window, + cx, + ), + workspace.open_path( + (worktree.read(cx).id(), rel_path("the-new-name.rs")), + None, + true, + window, + cx, + ), ) }) - .unwrap() - .await .unwrap(); + task1.await.unwrap(); + task2.await.unwrap(); + cx.run_until_parked(); let editor2 = window .update(cx, |workspace, _, cx| { workspace diff --git a/crates/zed/src/zed/component_preview.rs b/crates/zed/src/zed/component_preview.rs index 7a287cf3d83f24e7f4d42221bda420053a975860..75369bbe0324b2fd4bbe9279ed253faac52487c8 100644 --- a/crates/zed/src/zed/component_preview.rs +++ b/crates/zed/src/zed/component_preview.rs @@ -721,7 +721,7 @@ impl Item for ComponentPreview { _workspace_id: Option, window: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> where Self: Sized, { @@ -743,13 +743,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)) {