Add support for xonsh shell (#39834)

Merlin04 and Jakub Konka created

Closes #39506

Release Notes:

- Fixed environment variable capture when login shell is
[xonsh](https://xon.sh/)

---------

Co-authored-by: Jakub Konka <kubkon@jakubkonka.com>

Change summary

crates/languages/src/python.rs   |  2 ++
crates/task/src/shell_builder.rs |  6 ++++--
crates/util/src/shell.rs         |  9 ++++++++-
crates/util/src/shell_env.rs     | 11 ++++++++++-
4 files changed, 24 insertions(+), 4 deletions(-)

Detailed changes

crates/languages/src/python.rs 🔗

@@ -1189,6 +1189,7 @@ impl ToolchainLister for PythonToolchainProvider {
                         ShellKind::Nushell => "activate.nu",
                         ShellKind::PowerShell => "activate.ps1",
                         ShellKind::Cmd => "activate.bat",
+                        ShellKind::Xonsh => "activate.xsh",
                     };
                     let path = prefix.join(BINARY_DIR).join(activate_script_name);
 
@@ -1215,6 +1216,7 @@ impl ToolchainLister for PythonToolchainProvider {
                     ShellKind::Tcsh => None,
                     ShellKind::Cmd => None,
                     ShellKind::Rc => None,
+                    ShellKind::Xonsh => None,
                 })
             }
             _ => {}

crates/task/src/shell_builder.rs 🔗

@@ -56,7 +56,8 @@ impl ShellBuilder {
                 | ShellKind::Fish
                 | ShellKind::Csh
                 | ShellKind::Tcsh
-                | ShellKind::Rc => {
+                | ShellKind::Rc
+                | ShellKind::Xonsh => {
                     let interactivity = self.interactive.then_some("-i ").unwrap_or_default();
                     format!(
                         "{PROGRAM} {interactivity}-c '{command_to_use_in_label}'",
@@ -91,7 +92,8 @@ impl ShellBuilder {
                     | ShellKind::Fish
                     | ShellKind::Csh
                     | ShellKind::Tcsh
-                    | ShellKind::Rc => {
+                    | ShellKind::Rc
+                    | ShellKind::Xonsh => {
                         combined_command.insert(0, '(');
                         combined_command.push_str(") </dev/null");
                     }

crates/util/src/shell.rs 🔗

@@ -11,6 +11,7 @@ pub enum ShellKind {
     PowerShell,
     Nushell,
     Cmd,
+    Xonsh,
 }
 
 pub fn get_system_shell() -> String {
@@ -165,6 +166,7 @@ impl fmt::Display for ShellKind {
             ShellKind::Nushell => write!(f, "nu"),
             ShellKind::Cmd => write!(f, "cmd"),
             ShellKind::Rc => write!(f, "rc"),
+            ShellKind::Xonsh => write!(f, "xonsh"),
         }
     }
 }
@@ -197,6 +199,8 @@ impl ShellKind {
             ShellKind::Tcsh
         } else if program == "rc" {
             ShellKind::Rc
+        } else if program == "xonsh" {
+            ShellKind::Xonsh
         } else if program == "sh" || program == "bash" {
             ShellKind::Posix
         } else {
@@ -220,6 +224,7 @@ impl ShellKind {
             Self::Tcsh => input.to_owned(),
             Self::Rc => input.to_owned(),
             Self::Nushell => Self::to_nushell_variable(input),
+            Self::Xonsh => input.to_owned(),
         }
     }
 
@@ -345,7 +350,8 @@ impl ShellKind {
             | ShellKind::Fish
             | ShellKind::Csh
             | ShellKind::Tcsh
-            | ShellKind::Rc => interactive
+            | ShellKind::Rc
+            | ShellKind::Xonsh => interactive
                 .then(|| "-i".to_owned())
                 .into_iter()
                 .chain(["-c".to_owned(), combined_command])
@@ -387,6 +393,7 @@ impl ShellKind {
             ShellKind::Csh => "source",
             ShellKind::Tcsh => "source",
             ShellKind::Posix | ShellKind::Rc => "source",
+            ShellKind::Xonsh => "source",
         }
     }
 

crates/util/src/shell_env.rs 🔗

@@ -46,10 +46,14 @@ async fn capture_unix(
     // See: https://github.com/zed-industries/zed/pull/32136#issuecomment-2999645482
     const FD_STDIN: std::os::fd::RawFd = 0;
     const FD_STDOUT: std::os::fd::RawFd = 1;
+    const FD_STDERR: std::os::fd::RawFd = 2;
 
     let (fd_num, redir) = match shell_kind {
         ShellKind::Rc => (FD_STDIN, format!(">[1={}]", FD_STDIN)), // `[1=0]`
         ShellKind::Nushell | ShellKind::Tcsh => (FD_STDOUT, "".to_string()),
+        // xonsh doesn't support redirecting to stdin, and control sequences are printed to
+        // stdout on startup
+        ShellKind::Xonsh => (FD_STDERR, "o>e".to_string()),
         _ => (FD_STDIN, format!(">&{}", FD_STDIN)), // `>&0`
     };
     command.stdin(Stdio::null());
@@ -133,7 +137,12 @@ async fn capture_windows(
 
     let shell_kind = ShellKind::new(shell_path);
     let env_output = match shell_kind {
-        ShellKind::Posix | ShellKind::Csh | ShellKind::Tcsh | ShellKind::Rc | ShellKind::Fish => {
+        ShellKind::Posix
+        | ShellKind::Csh
+        | ShellKind::Tcsh
+        | ShellKind::Rc
+        | ShellKind::Fish
+        | ShellKind::Xonsh => {
             return Err(anyhow::anyhow!("unsupported shell kind"));
         }
         ShellKind::PowerShell => {