project: Spawn terminal process on background executor (#40774)

We were spawning the process on the foreground thread before which can
block an arbitrary amount of time. Likewise we no longer block
deserialization on the terminal loading.

Release Notes:

- Improved startup time on systems with slow process spawning
capabilities
This commit is contained in:
Lukas Wirth
2025-10-21 13:10:21 +00:00
committed by GitHub
parent 0be70e24d6
commit 0eccdfe61f
28 changed files with 744 additions and 657 deletions
+4 -2
View File
@@ -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 {
+25 -19
View File
@@ -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())
+1 -1
View File
@@ -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| {
+3 -3
View File
@@ -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(
+3 -3
View File
@@ -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>) {
+3 -3
View File
@@ -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 {
+2 -2
View File
@@ -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(
+5 -5
View File
@@ -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,
}
}))
})))
}
}
+7 -3
View File
@@ -625,12 +625,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 {
+3 -3
View File
@@ -176,15 +176,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 {
@@ -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))))
}
}
+4 -4
View File
@@ -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
}))
})))
}
}
@@ -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
}))
})))
}
}
+3 -3
View File
@@ -385,14 +385,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)) {
+197 -188
View File
@@ -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<Cow<str>> = 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<Cow<str>> = 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));
command.into_iter().chain(args).join(" ")
} else {
// todo: this breaks for remotes to windows
format!("exec {shell} -l")
}
};
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,
)),
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();
)?,
},
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 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);
(
Shell::WithArguments {
program: shell,
args,
title_override: None,
},
env,
)
}
let args = shell_kind.args_for_shell(false, arg);
(
Shell::WithArguments {
program: shell,
args,
title_override: None,
_ => (
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));
terminal_handle
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();
terminal_handle
})
})
}
@@ -364,75 +369,36 @@ impl Project {
})
.await
.unwrap_or_default();
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),
}
};
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 (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();
}
})
.detach();
terminal_handle
})
})?
})
}
pub fn clone_terminal(
&mut self,
terminal: &Entity<Terminal>,
cx: &mut Context<'_, Project>,
cwd: Option<PathBuf>,
) -> Result<Entity<Terminal>> {
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));
self.terminals
this.terminals
.local_handles
.push(terminal_handle.downgrade());
@@ -452,6 +418,49 @@ impl Project {
terminal_handle
})
})
}
pub fn clone_terminal(
&mut self,
terminal: &Entity<Terminal>,
cx: &mut Context<'_, Project>,
cwd: Option<PathBuf>,
) -> Task<Result<Entity<Terminal>>> {
let local_path = if self.is_via_remote_server() {
None
} else {
cwd
};
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));
project
.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();
terminal_handle
})
})
}
pub fn terminal_settings<'a>(
+4 -2
View File
@@ -709,11 +709,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 {
+6 -2
View File
@@ -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(
@@ -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!(
+271 -264
View File
@@ -423,232 +423,233 @@ impl TerminalBuilder {
completion_tx: Option<Sender<Option<ExitStatus>>>,
cx: &App,
activation_script: Vec<String>,
) -> Result<TerminalBuilder> {
// 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());
}
) -> Task<Result<TerminalBuilder>> {
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(),
release_channel::AppVersion::global(cx).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<Vec<String>>,
title_override: Option<SharedString>,
}
impl ShellParams {
fn new(
#[derive(Default)]
struct ShellParams {
program: String,
args: Option<Vec<String>>,
title_override: Option<SharedString>,
) -> Self {
log::info!("Using {program} as shell");
Self {
}
impl ShellParams {
fn new(
program: String,
args: Option<Vec<String>>,
title_override: Option<SharedString>,
) -> 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
}
}
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());
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());
#[cfg(windows)]
let shell_program = shell_params.as_ref().map(|params| {
use util::ResultExt;
#[cfg(windows)]
let shell_program = shell_params.as_ref().map(|params| {
use util::ResultExt;
Self::resolve_path(&params.program)
.log_err()
.unwrap_or(params.program.clone())
});
// 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(&params.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,
}
};
// 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 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()
};
//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 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 term = Arc::new(FairMutex::new(term));
//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,
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(),
)
});
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()
};
//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 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
if !activation_script.is_empty() && no_task {
for activation_script in activation_script {
terminal.write_to_pty(activation_script.into_bytes());
let no_task = task.is_none();
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<PathBuf>) -> Result<TerminalBuilder> {
pub fn clone_builder(&self, cx: &App, cwd: Option<PathBuf>) -> Task<Result<TerminalBuilder>> {
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::<Event>();
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::<Event>();
cx.update(|cx| {
+56 -46
View File
@@ -214,14 +214,6 @@ async fn deserialize_pane_group(
}
SerializedPaneGroup::Pane(serialized_pane) => {
let active = serialized_pane.active;
let new_items = deserialize_terminal_views(
workspace_id,
project.clone(),
workspace.clone(),
serialized_pane.children.as_slice(),
cx,
)
.await;
let pane = panel
.update_in(cx, |terminal_panel, window, cx| {
@@ -236,56 +228,71 @@ async fn deserialize_pane_group(
.log_err()?;
let active_item = serialized_pane.active_item;
let pinned_count = serialized_pane.pinned_count;
let terminal = pane
.update_in(cx, |pane, window, cx| {
populate_pane_items(pane, new_items, active_item, window, cx);
pane.set_pinned_count(pinned_count);
let new_items = deserialize_terminal_views(
workspace_id,
project.clone(),
workspace.clone(),
serialized_pane.children.as_slice(),
cx,
);
cx.spawn({
let pane = pane.downgrade();
async move |cx| {
let new_items = new_items.await;
let items = pane.update_in(cx, |pane, window, cx| {
populate_pane_items(pane, new_items, active_item, window, cx);
pane.set_pinned_count(pinned_count);
pane.items_len()
});
// Avoid blank panes in splits
if pane.items_len() == 0 {
if items.is_ok_and(|items| items == 0) {
let working_directory = workspace
.update(cx, |workspace, cx| default_working_directory(workspace, cx))
.ok()
.flatten();
let terminal = project.update(cx, |project, cx| {
project.create_terminal_shell(working_directory, cx)
});
Some(Some(terminal))
} else {
Some(None)
let Some(terminal) = project
.update(cx, |project, cx| {
project.create_terminal_shell(working_directory, cx)
})
.log_err()
else {
return;
};
let terminal = terminal.await.log_err();
pane.update_in(cx, |pane, window, cx| {
if let Some(terminal) = terminal {
let terminal_view = Box::new(cx.new(|cx| {
TerminalView::new(
terminal,
workspace.clone(),
Some(workspace_id),
project.downgrade(),
window,
cx,
)
}));
pane.add_item(terminal_view, true, false, None, window, cx);
}
})
.ok();
}
})
.ok()
.flatten()?;
if let Some(terminal) = terminal {
let terminal = terminal.await.ok()?;
pane.update_in(cx, |pane, window, cx| {
let terminal_view = Box::new(cx.new(|cx| {
TerminalView::new(
terminal,
workspace.clone(),
Some(workspace_id),
project.downgrade(),
window,
cx,
)
}));
pane.add_item(terminal_view, true, false, None, window, cx);
})
.ok()?;
}
}
})
.detach();
Some((Member::Pane(pane.clone()), active.then_some(pane)))
}
}
}
async fn deserialize_terminal_views(
fn deserialize_terminal_views(
workspace_id: WorkspaceId,
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
item_ids: &[u64],
cx: &mut AsyncWindowContext,
) -> Vec<Entity<TerminalView>> {
let mut items = Vec::with_capacity(item_ids.len());
) -> impl Future<Output = Vec<Entity<TerminalView>>> + use<> {
let mut deserialized_items = item_ids
.iter()
.map(|item_id| {
@@ -302,12 +309,15 @@ async fn deserialize_terminal_views(
.unwrap_or_else(|e| Task::ready(Err(e.context("no window present"))))
})
.collect::<FuturesUnordered<_>>();
while let Some(item) = deserialized_items.next().await {
if let Some(item) = item.log_err() {
items.push(item);
async move {
let mut items = Vec::with_capacity(deserialized_items.len());
while let Some(item) = deserialized_items.next().await {
if let Some(item) = item.log_err() {
items.push(item);
}
}
items
}
items
}
#[derive(Debug, Serialize, Deserialize)]
+2 -2
View File
@@ -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()?
+24 -21
View File
@@ -1220,28 +1220,31 @@ 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)
) -> Task<Option<Entity<Self>>> {
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 {
+15 -11
View File
@@ -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 {
+12 -5
View File
@@ -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);
}
+4 -4
View File
@@ -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)) {
+5 -3
View File
@@ -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))))
}
}
+60 -42
View File
@@ -3630,7 +3630,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();
}
}
@@ -3997,7 +3998,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);
}
@@ -4138,21 +4140,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>) {
@@ -8177,19 +8185,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)]
@@ -8696,25 +8712,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| {
@@ -10531,7 +10546,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!(
+14 -6
View File
@@ -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| {
@@ -3466,7 +3468,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,
+3 -3
View File
@@ -721,7 +721,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,
{
@@ -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)) {