zed_extension_api: Add simple `process` API (#25399)

Marshall Bowers created

This PR adds a simple API for working with processes to the extension
API.

The API is designed to mirror Rust's
[`std::process::Command`](https://doc.rust-lang.org/std/process/struct.Command.html).

Release Notes:

- N/A

Change summary

crates/extension_api/src/extension_api.rs               |  1 
crates/extension_api/src/process.rs                     | 44 +++++++++++
crates/extension_api/wit/since_v0.3.0/common.wit        |  3 
crates/extension_api/wit/since_v0.3.0/extension.wit     | 17 ---
crates/extension_api/wit/since_v0.3.0/process.wit       | 29 +++++++
crates/extension_host/src/wasm_host/wit/since_v0_3_0.rs | 29 +++++++
6 files changed, 109 insertions(+), 14 deletions(-)

Detailed changes

crates/extension_api/src/extension_api.rs 🔗

@@ -1,6 +1,7 @@
 //! The Zed Rust Extension API allows you write extensions for [Zed](https://zed.dev/) in Rust.
 
 pub mod http_client;
+pub mod process;
 pub mod settings;
 
 use core::fmt;

crates/extension_api/src/process.rs 🔗

@@ -0,0 +1,44 @@
+//! A module for working with processes.
+
+use crate::wit::zed::extension::process;
+pub use crate::wit::zed::extension::process::{Command, Output};
+
+impl Command {
+    pub fn new(program: impl Into<String>) -> Self {
+        Self {
+            command: program.into(),
+            args: Vec::new(),
+            env: Vec::new(),
+        }
+    }
+
+    pub fn arg(mut self, arg: impl Into<String>) -> Self {
+        self.args.push(arg.into());
+        self
+    }
+
+    pub fn args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
+        self.args.extend(args.into_iter().map(Into::into));
+        self
+    }
+
+    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
+        self.env.push((key.into(), value.into()));
+        self
+    }
+
+    pub fn envs(
+        mut self,
+        envs: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
+    ) -> Self {
+        self.env.extend(
+            envs.into_iter()
+                .map(|(key, value)| (key.into(), value.into())),
+        );
+        self
+    }
+
+    pub fn output(&mut self) -> Result<Output, String> {
+        process::run_command(self)
+    }
+}

crates/extension_api/wit/since_v0.3.0/extension.wit 🔗

@@ -4,10 +4,12 @@ world extension {
     import github;
     import http-client;
     import platform;
+    import process;
     import nodejs;
 
-    use common.{range};
+    use common.{env-vars, range};
     use lsp.{completion, symbol};
+    use process.{command};
     use slash-command.{slash-command, slash-command-argument-completion, slash-command-output};
 
     /// Initializes the extension.
@@ -56,19 +58,6 @@ world extension {
     /// Updates the installation status for the given language server.
     import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status);
 
-    /// A list of environment variables.
-    type env-vars = list<tuple<string, string>>;
-
-    /// A command.
-    record command {
-        /// The command to execute.
-        command: string,
-        /// The arguments to pass to the command.
-        args: list<string>,
-        /// The environment variables to set for the command.
-        env: env-vars,
-    }
-
     /// A Zed worktree.
     resource worktree {
         /// Returns the ID of the worktree.

crates/extension_api/wit/since_v0.3.0/process.wit 🔗

@@ -0,0 +1,29 @@
+interface process {
+    use common.{env-vars};
+
+    /// A command.
+    record command {
+        /// The command to execute.
+        command: string,
+        /// The arguments to pass to the command.
+        args: list<string>,
+        /// The environment variables to set for the command.
+        env: env-vars,
+    }
+
+    /// The output of a finished process.
+    record output {
+        /// The status (exit code) of the process.
+        ///
+        /// On Unix, this will be `None` if the process was terminated by a signal.
+        status: option<s32>,
+        /// The data that the process wrote to stdout.
+        stdout: list<u8>,
+        /// The data that the process wrote to stderr.
+        stderr: list<u8>,
+    }
+
+    /// Executes the given command as a child process, waiting for it to finish
+    /// and collecting all of its output.
+    run-command: func(command: command) -> result<output, string>;
+}

crates/extension_host/src/wasm_host/wit/since_v0_3_0.rs 🔗

@@ -576,6 +576,35 @@ impl platform::Host for WasmState {
     }
 }
 
+impl From<std::process::Output> for process::Output {
+    fn from(output: std::process::Output) -> Self {
+        Self {
+            status: output.status.code(),
+            stdout: output.stdout,
+            stderr: output.stderr,
+        }
+    }
+}
+
+impl process::Host for WasmState {
+    async fn run_command(
+        &mut self,
+        command: process::Command,
+    ) -> wasmtime::Result<Result<process::Output, String>> {
+        maybe!(async {
+            let output = util::command::new_smol_command(command.command.as_str())
+                .args(&command.args)
+                .envs(command.env)
+                .output()
+                .await?;
+
+            Ok(output.into())
+        })
+        .await
+        .to_wasmtime_result()
+    }
+}
+
 #[async_trait]
 impl slash_command::Host for WasmState {}