@@ -284,3 +284,170 @@ pub enum Shell {
title_override: Option<SharedString>,
},
}
+
+#[cfg(target_os = "windows")]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum WindowsShellType {
+ Powershell,
+ Cmd,
+ Other,
+}
+
+/// ShellBuilder is used to turn a user-requested task into a
+/// program that can be executed by the shell.
+pub struct ShellBuilder {
+ program: String,
+ args: Vec<String>,
+}
+
+impl ShellBuilder {
+ /// Create a new ShellBuilder as configured.
+ pub fn new(is_local: bool, shell: &Shell) -> Self {
+ let (program, args) = match shell {
+ Shell::System => {
+ if is_local {
+ (Self::system_shell(), Vec::new())
+ } else {
+ ("\"${SHELL:-sh}\"".to_string(), Vec::new())
+ }
+ }
+ Shell::Program(shell) => (shell.clone(), Vec::new()),
+ Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()),
+ };
+ Self { program, args }
+ }
+}
+
+#[cfg(not(target_os = "windows"))]
+impl ShellBuilder {
+ /// Returns the label to show in the terminal tab
+ pub fn command_label(&self, command_label: &str) -> String {
+ format!("{} -i -c '{}'", self.program, command_label)
+ }
+
+ /// Returns the program and arguments to run this task in a shell.
+ pub fn build(mut self, task_command: String, task_args: &Vec<String>) -> (String, Vec<String>) {
+ let combined_command = task_args
+ .into_iter()
+ .fold(task_command, |mut command, arg| {
+ command.push(' ');
+ command.push_str(&arg);
+ command
+ });
+ self.args
+ .extend(["-i".to_owned(), "-c".to_owned(), combined_command]);
+
+ (self.program, self.args)
+ }
+
+ fn system_shell() -> String {
+ std::env::var("SHELL").unwrap_or("/bin/sh".to_string())
+ }
+}
+
+#[cfg(target_os = "windows")]
+impl ShellBuilder {
+ /// Returns the label to show in the terminal tab
+ pub fn command_label(&self, command_label: &str) -> String {
+ match self.windows_shell_type() {
+ WindowsShellType::Powershell => {
+ format!("{} -C '{}'", self.program, command_label)
+ }
+ WindowsShellType::Cmd => {
+ format!("{} /C '{}'", self.program, command_label)
+ }
+ WindowsShellType::Other => {
+ format!("{} -i -c '{}'", self.program, command_label)
+ }
+ }
+ }
+
+ /// Returns the program and arguments to run this task in a shell.
+ pub fn build(mut self, task_command: String, task_args: &Vec<String>) -> (String, Vec<String>) {
+ let combined_command = task_args
+ .into_iter()
+ .fold(task_command, |mut command, arg| {
+ command.push(' ');
+ command.push_str(&self.to_windows_shell_variable(arg.to_string()));
+ command
+ });
+
+ match self.windows_shell_type() {
+ WindowsShellType::Powershell => self.args.extend(["-C".to_owned(), combined_command]),
+ WindowsShellType::Cmd => self.args.extend(["/C".to_owned(), combined_command]),
+ WindowsShellType::Other => {
+ self.args
+ .extend(["-i".to_owned(), "-c".to_owned(), combined_command])
+ }
+ }
+
+ (self.program, self.args)
+ }
+ fn windows_shell_type(&self) -> WindowsShellType {
+ if self.program == "powershell"
+ || self.program.ends_with("powershell.exe")
+ || self.program == "pwsh"
+ || self.program.ends_with("pwsh.exe")
+ {
+ WindowsShellType::Powershell
+ } else if self.program == "cmd" || self.program.ends_with("cmd.exe") {
+ WindowsShellType::Cmd
+ } else {
+ // Someother shell detected, the user might install and use a
+ // unix-like shell.
+ WindowsShellType::Other
+ }
+ }
+
+ // `alacritty_terminal` uses this as default on Windows. See:
+ // https://github.com/alacritty/alacritty/blob/0d4ab7bca43213d96ddfe40048fc0f922543c6f8/alacritty_terminal/src/tty/windows/mod.rs#L130
+ fn system_shell() -> String {
+ "powershell".to_owned()
+ }
+
+ fn to_windows_shell_variable(&self, input: String) -> String {
+ match self.windows_shell_type() {
+ WindowsShellType::Powershell => Self::to_powershell_variable(input),
+ WindowsShellType::Cmd => Self::to_cmd_variable(input),
+ WindowsShellType::Other => input,
+ }
+ }
+
+ fn to_cmd_variable(input: String) -> String {
+ if let Some(var_str) = input.strip_prefix("${") {
+ if var_str.find(':').is_none() {
+ // If the input starts with "${", remove the trailing "}"
+ format!("%{}%", &var_str[..var_str.len() - 1])
+ } else {
+ // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
+ // which will result in the task failing to run in such cases.
+ input
+ }
+ } else if let Some(var_str) = input.strip_prefix('$') {
+ // If the input starts with "$", directly append to "$env:"
+ format!("%{}%", var_str)
+ } else {
+ // If no prefix is found, return the input as is
+ input
+ }
+ }
+
+ fn to_powershell_variable(input: String) -> String {
+ if let Some(var_str) = input.strip_prefix("${") {
+ if var_str.find(':').is_none() {
+ // If the input starts with "${", remove the trailing "}"
+ format!("$env:{}", &var_str[..var_str.len() - 1])
+ } else {
+ // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
+ // which will result in the task failing to run in such cases.
+ input
+ }
+ } else if let Some(var_str) = input.strip_prefix('$') {
+ // If the input starts with "$", directly append to "$env:"
+ format!("$env:{}", var_str)
+ } else {
+ // If no prefix is found, return the input as is
+ input
+ }
+ }
+}
@@ -20,7 +20,7 @@ use itertools::Itertools;
use project::{terminals::TerminalKind, Fs, Project, ProjectEntryId};
use search::{buffer_search::DivRegistrar, BufferSearchBar};
use settings::Settings;
-use task::{RevealStrategy, RevealTarget, Shell, SpawnInTerminal, TaskId};
+use task::{RevealStrategy, RevealTarget, ShellBuilder, SpawnInTerminal, TaskId};
use terminal::{
terminal_settings::{TerminalDockPosition, TerminalSettings},
Terminal,
@@ -447,167 +447,68 @@ impl TerminalPanel {
.detach_and_log_err(cx);
}
- fn spawn_task(&mut self, spawn_in_terminal: &SpawnInTerminal, cx: &mut ViewContext<Self>) {
- let mut spawn_task = spawn_in_terminal.clone();
+ fn spawn_task(&mut self, task: &SpawnInTerminal, cx: &mut ViewContext<Self>) {
let Ok(is_local) = self
.workspace
.update(cx, |workspace, cx| workspace.project().read(cx).is_local())
else {
return;
};
- if let ControlFlow::Break(_) =
- Self::fill_command(is_local, spawn_in_terminal, &mut spawn_task)
- {
- return;
- }
- let spawn_task = spawn_task;
- let allow_concurrent_runs = spawn_in_terminal.allow_concurrent_runs;
- let use_new_terminal = spawn_in_terminal.use_new_terminal;
+ let builder = ShellBuilder::new(is_local, &task.shell);
+ let command_label = builder.command_label(&task.command_label);
+ let (command, args) = builder.build(task.command.clone(), &task.args);
- if allow_concurrent_runs && use_new_terminal {
- self.spawn_in_new_terminal(spawn_task, cx)
- .detach_and_log_err(cx);
- return;
- }
+ let task = SpawnInTerminal {
+ command_label,
+ command,
+ args,
+ ..task.clone()
+ };
- let terminals_for_task = self.terminals_for_task(&spawn_in_terminal.full_label, cx);
- if terminals_for_task.is_empty() {
- self.spawn_in_new_terminal(spawn_task, cx)
- .detach_and_log_err(cx);
+ if task.allow_concurrent_runs && task.use_new_terminal {
+ self.spawn_in_new_terminal(task, cx).detach_and_log_err(cx);
return;
}
- let (existing_item_index, task_pane, existing_terminal) = terminals_for_task
- .last()
- .expect("covered no terminals case above")
- .clone();
- let id = spawn_in_terminal.id.clone();
- cx.spawn(move |this, mut cx| async move {
- if allow_concurrent_runs {
- debug_assert!(
- !use_new_terminal,
- "Should have handled 'allow_concurrent_runs && use_new_terminal' case above"
- );
- this.update(&mut cx, |terminal_panel, cx| {
- terminal_panel.replace_terminal(
- spawn_task,
- task_pane,
- existing_item_index,
- existing_terminal,
- cx,
- )
- })?
- .await;
- } else {
- this.update(&mut cx, |this, cx| {
- this.deferred_tasks.insert(
- id,
- cx.spawn(|terminal_panel, mut cx| async move {
- wait_for_terminals_tasks(terminals_for_task, &mut cx).await;
- let Ok(Some(new_terminal_task)) =
- terminal_panel.update(&mut cx, |terminal_panel, cx| {
- if use_new_terminal {
- terminal_panel
- .spawn_in_new_terminal(spawn_task, cx)
- .detach_and_log_err(cx);
- None
- } else {
- Some(terminal_panel.replace_terminal(
- spawn_task,
- task_pane,
- existing_item_index,
- existing_terminal,
- cx,
- ))
- }
- })
- else {
- return;
- };
- new_terminal_task.await;
- }),
- );
- })
- .ok();
- }
- anyhow::Result::<_, anyhow::Error>::Ok(())
- })
- .detach()
- }
- pub fn fill_command(
- is_local: bool,
- spawn_in_terminal: &SpawnInTerminal,
- spawn_task: &mut SpawnInTerminal,
- ) -> ControlFlow<()> {
- let Some((shell, mut user_args)) = (match spawn_in_terminal.shell.clone() {
- Shell::System => {
- if is_local {
- retrieve_system_shell().map(|shell| (shell, Vec::new()))
- } else {
- Some(("\"${SHELL:-sh}\"".to_string(), Vec::new()))
- }
- }
- Shell::Program(shell) => Some((shell, Vec::new())),
- Shell::WithArguments { program, args, .. } => Some((program, args)),
- }) else {
- return ControlFlow::Break(());
+ let mut terminals_for_task = self.terminals_for_task(&task.full_label, cx);
+ let Some(existing) = terminals_for_task.pop() else {
+ self.spawn_in_new_terminal(task, cx).detach_and_log_err(cx);
+ return;
};
- #[cfg(target_os = "windows")]
- let windows_shell_type = to_windows_shell_type(&shell);
- #[cfg(not(target_os = "windows"))]
- {
- spawn_task.command_label = format!("{shell} -i -c '{}'", spawn_task.command_label);
- }
- #[cfg(target_os = "windows")]
- {
- use crate::terminal_panel::WindowsShellType;
- match windows_shell_type {
- WindowsShellType::Powershell => {
- spawn_task.command_label = format!("{shell} -C '{}'", spawn_task.command_label)
- }
- WindowsShellType::Cmd => {
- spawn_task.command_label = format!("{shell} /C '{}'", spawn_task.command_label)
- }
- WindowsShellType::Other => {
- spawn_task.command_label =
- format!("{shell} -i -c '{}'", spawn_task.command_label)
- }
- }
+ let (existing_item_index, task_pane, existing_terminal) = existing;
+ if task.allow_concurrent_runs {
+ self.replace_terminal(task, task_pane, existing_item_index, existing_terminal, cx)
+ .detach();
+ return;
}
- let task_command = std::mem::replace(&mut spawn_task.command, shell);
- let task_args = std::mem::take(&mut spawn_task.args);
- let combined_command = task_args
- .into_iter()
- .fold(task_command, |mut command, arg| {
- command.push(' ');
- #[cfg(not(target_os = "windows"))]
- command.push_str(&arg);
- #[cfg(target_os = "windows")]
- command.push_str(&to_windows_shell_variable(windows_shell_type, arg));
- command
- });
- #[cfg(not(target_os = "windows"))]
- user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command]);
- #[cfg(target_os = "windows")]
- {
- use crate::terminal_panel::WindowsShellType;
- match windows_shell_type {
- WindowsShellType::Powershell => {
- user_args.extend(["-C".to_owned(), combined_command])
- }
- WindowsShellType::Cmd => user_args.extend(["/C".to_owned(), combined_command]),
- WindowsShellType::Other => {
- user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command])
+ self.deferred_tasks.insert(
+ task.id.clone(),
+ cx.spawn(|terminal_panel, mut cx| async move {
+ wait_for_terminals_tasks(terminals_for_task, &mut cx).await;
+ let task = terminal_panel.update(&mut cx, |terminal_panel, cx| {
+ if task.use_new_terminal {
+ terminal_panel
+ .spawn_in_new_terminal(task, cx)
+ .detach_and_log_err(cx);
+ None
+ } else {
+ Some(terminal_panel.replace_terminal(
+ task,
+ task_pane,
+ existing_item_index,
+ existing_terminal,
+ cx,
+ ))
+ }
+ });
+ if let Ok(Some(task)) = task {
+ task.await;
}
- }
- }
- spawn_task.args = user_args;
- // Set up shell args unconditionally, as tasks are always spawned inside of a shell.
-
- ControlFlow::Continue(())
+ }),
+ );
}
pub fn spawn_in_new_terminal(
@@ -1438,97 +1339,3 @@ impl Render for InlineAssistTabBarButton {
})
}
}
-
-fn retrieve_system_shell() -> Option<String> {
- #[cfg(not(target_os = "windows"))]
- {
- use anyhow::Context;
- use util::ResultExt;
-
- std::env::var("SHELL")
- .context("Error finding SHELL in env.")
- .log_err()
- }
- // `alacritty_terminal` uses this as default on Windows. See:
- // https://github.com/alacritty/alacritty/blob/0d4ab7bca43213d96ddfe40048fc0f922543c6f8/alacritty_terminal/src/tty/windows/mod.rs#L130
- #[cfg(target_os = "windows")]
- return Some("powershell".to_owned());
-}
-
-#[cfg(target_os = "windows")]
-fn to_windows_shell_variable(shell_type: WindowsShellType, input: String) -> String {
- match shell_type {
- WindowsShellType::Powershell => to_powershell_variable(input),
- WindowsShellType::Cmd => to_cmd_variable(input),
- WindowsShellType::Other => input,
- }
-}
-
-#[cfg(target_os = "windows")]
-fn to_windows_shell_type(shell: &str) -> WindowsShellType {
- if shell == "powershell"
- || shell.ends_with("powershell.exe")
- || shell == "pwsh"
- || shell.ends_with("pwsh.exe")
- {
- WindowsShellType::Powershell
- } else if shell == "cmd" || shell.ends_with("cmd.exe") {
- WindowsShellType::Cmd
- } else {
- // Someother shell detected, the user might install and use a
- // unix-like shell.
- WindowsShellType::Other
- }
-}
-
-/// Convert `${SOME_VAR}`, `$SOME_VAR` to `%SOME_VAR%`.
-#[inline]
-#[cfg(target_os = "windows")]
-fn to_cmd_variable(input: String) -> String {
- if let Some(var_str) = input.strip_prefix("${") {
- if var_str.find(':').is_none() {
- // If the input starts with "${", remove the trailing "}"
- format!("%{}%", &var_str[..var_str.len() - 1])
- } else {
- // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
- // which will result in the task failing to run in such cases.
- input
- }
- } else if let Some(var_str) = input.strip_prefix('$') {
- // If the input starts with "$", directly append to "$env:"
- format!("%{}%", var_str)
- } else {
- // If no prefix is found, return the input as is
- input
- }
-}
-
-/// Convert `${SOME_VAR}`, `$SOME_VAR` to `$env:SOME_VAR`.
-#[inline]
-#[cfg(target_os = "windows")]
-fn to_powershell_variable(input: String) -> String {
- if let Some(var_str) = input.strip_prefix("${") {
- if var_str.find(':').is_none() {
- // If the input starts with "${", remove the trailing "}"
- format!("$env:{}", &var_str[..var_str.len() - 1])
- } else {
- // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
- // which will result in the task failing to run in such cases.
- input
- }
- } else if let Some(var_str) = input.strip_prefix('$') {
- // If the input starts with "$", directly append to "$env:"
- format!("$env:{}", var_str)
- } else {
- // If no prefix is found, return the input as is
- input
- }
-}
-
-#[cfg(target_os = "windows")]
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum WindowsShellType {
- Powershell,
- Cmd,
- Other,
-}