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

Attempt 2 for https://github.com/zed-industries/zed/pull/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-30 13:55:19 +00:00
committed by GitHub
parent c7aa805398
commit 0c73252c9d
4 changed files with 527 additions and 510 deletions
+180 -174
View File
@@ -139,142 +139,145 @@ 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));
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 separator = shell_kind.sequential_commands_separator();
let activation_script =
activation_script.join(&format!("{separator} "));
let to_run = format_to_run();
let shell = remote_client
.read(cx)
.shell()
.unwrap_or_else(get_default_system_shell);
let arg = format!("{activation_script}{separator} {to_run}");
let args = shell_kind.args_for_shell(false, 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 separator = shell_kind.sequential_commands_separator();
let activation_script =
activation_script.join(&format!("{separator} "));
let to_run = format_to_run();
let shell = remote_client
.read(cx)
.shell()
.unwrap_or_else(get_default_system_shell);
let arg = format!("{activation_script}{separator} {to_run}");
let args = shell_kind.args_for_shell(false, arg);
create_remote_shell(
Some((&shell, &args)),
create_remote_shell(
Some((&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
})
})
}
@@ -355,53 +358,55 @@ 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),
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();
}
};
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
})
})?
.detach();
terminal_handle
})
})
}
@@ -422,13 +427,14 @@ impl Project {
cwd
};
let new_terminal = 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());
@@ -447,8 +453,8 @@ impl Project {
.detach();
terminal_handle
});
Task::ready(new_terminal)
})
})
}
pub fn terminal_settings<'a>(