lsp-config: Allow setting a server's environment variables (#27213)

David Barsky created

Closes https://github.com/zed-industries/zed/issues/14334, allowing
users to set environment variables for a language server binary like:

```json
"lsp": {
  "rust-analyzer": {
    "binary": {
      "path": "/Users/dbarsky/.cargo/bin/rust-analyzer",
      "env": {
        "RA_PROFILE": "*>100"
      }
    },
  }
}
```

The newly introduced environment variables are merged with the shell
environment. Perhaps more controversially, I've _also_ removed the
trimming/`stderr:`-prefixing of language server logs. This because
rust-analyzer has some nice, tree-shaped profiling built-in, and it
prevents us from printing profiles like this:

<details>
<img width="1147" alt="Screenshot 2025-03-20 at 12 09 14 PM"
src="https://github.com/user-attachments/assets/b7066651-6394-492b-b745-906c66d3c7b2"
/>
</details>

Release Notes:

- Added the ability to set a language server's environment variables.
- Removed the `stderr`-prefix of a language server's stderr logs.

Change summary

crates/extension_host/src/wasm_host/wit/since_v0_3_0.rs |  2 
crates/language_tools/src/lsp_log.rs                    |  1 
crates/project/src/lsp_store.rs                         | 19 ++++++++--
crates/project/src/project_settings.rs                  |  2 +
4 files changed, 17 insertions(+), 7 deletions(-)

Detailed changes

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

@@ -651,7 +651,7 @@ impl ExtensionImports for WasmState {
                             binary: settings.binary.map(|binary| settings::CommandSettings {
                                 path: binary.path,
                                 arguments: binary.arguments,
-                                env: None,
+                                env: binary.env,
                             }),
                             settings: settings.settings,
                             initialization_options: settings.initialization_options,

crates/language_tools/src/lsp_log.rs 🔗

@@ -540,7 +540,6 @@ impl LogStore {
             IoKind::StdOut => true,
             IoKind::StdIn => false,
             IoKind::StdErr => {
-                let message = format!("stderr: {}", message.trim());
                 self.add_language_server_log(language_server_id, MessageType::LOG, &message, cx);
                 return Some(());
             }

crates/project/src/lsp_store.rs 🔗

@@ -382,10 +382,14 @@ impl LocalLspStore {
 
         if settings.as_ref().is_some_and(|b| b.path.is_some()) {
             let settings = settings.unwrap();
+
             return cx.spawn(async move |_| {
+                let mut env = delegate.shell_env().await;
+                env.extend(settings.env.unwrap_or_default());
+
                 Ok(LanguageServerBinary {
                     path: PathBuf::from(&settings.path.unwrap()),
-                    env: Some(delegate.shell_env().await),
+                    env: Some(env),
                     arguments: settings
                         .arguments
                         .unwrap_or_default()
@@ -412,12 +416,17 @@ impl LocalLspStore {
             delegate.update_status(adapter.name.clone(), BinaryStatus::None);
 
             let mut binary = binary_result?;
-            if let Some(arguments) = settings.and_then(|b| b.arguments) {
-                binary.arguments = arguments.into_iter().map(Into::into).collect();
+            let mut shell_env = delegate.shell_env().await;
+
+            if let Some(settings) = settings {
+                if let Some(arguments) = settings.arguments {
+                    binary.arguments = arguments.into_iter().map(Into::into).collect();
+                }
+                if let Some(env) = settings.env {
+                    shell_env.extend(env);
+                }
             }
 
-            let mut shell_env = delegate.shell_env().await;
-            shell_env.extend(binary.env.unwrap_or_default());
             binary.env = Some(shell_env);
             Ok(binary)
         })

crates/project/src/project_settings.rs 🔗

@@ -273,6 +273,8 @@ const fn true_value() -> bool {
 pub struct BinarySettings {
     pub path: Option<String>,
     pub arguments: Option<Vec<String>>,
+    // this can't be an FxHashMap because the extension APIs require the default SipHash
+    pub env: Option<std::collections::HashMap<String, String, std::hash::RandomState>>,
     pub ignore_system_version: Option<bool>,
 }