formatting: Use project environment to find external formatters (#18611)

Thorsten Ball and Bennet created

Closes #18261

This makes sure that we find external formatters in the project
environment.

TODO:

- [x] Use a different type for the triplet of `(buffer_handle,
buffer_path, buffer_env)`. Something like `FormattableBuffer`.
- [x] Test this!!

Release Notes:

- Fixed external formatters not being found, even when they were
available in the `$PATH` of a project.

---------

Co-authored-by: Bennet <bennet@zed.dev>

Change summary

crates/project/src/lsp_store.rs | 135 ++++++++++++++++++++++------------
1 file changed, 87 insertions(+), 48 deletions(-)

Detailed changes

crates/project/src/lsp_store.rs 🔗

@@ -158,7 +158,7 @@ impl LocalLspStore {
 
     async fn format_locally(
         lsp_store: WeakModel<LspStore>,
-        mut buffers_with_paths: Vec<(Model<Buffer>, Option<PathBuf>)>,
+        mut buffers: Vec<FormattableBuffer>,
         push_to_history: bool,
         trigger: FormatTrigger,
         mut cx: AsyncAppContext,
@@ -167,22 +167,22 @@ impl LocalLspStore {
         // same buffer.
         lsp_store.update(&mut cx, |this, cx| {
             let this = this.as_local_mut().unwrap();
-            buffers_with_paths.retain(|(buffer, _)| {
+            buffers.retain(|buffer| {
                 this.buffers_being_formatted
-                    .insert(buffer.read(cx).remote_id())
+                    .insert(buffer.handle.read(cx).remote_id())
             });
         })?;
 
         let _cleanup = defer({
             let this = lsp_store.clone();
             let mut cx = cx.clone();
-            let buffers = &buffers_with_paths;
+            let buffers = &buffers;
             move || {
                 this.update(&mut cx, |this, cx| {
                     let this = this.as_local_mut().unwrap();
-                    for (buffer, _) in buffers {
+                    for buffer in buffers {
                         this.buffers_being_formatted
-                            .remove(&buffer.read(cx).remote_id());
+                            .remove(&buffer.handle.read(cx).remote_id());
                     }
                 })
                 .ok();
@@ -190,10 +190,10 @@ impl LocalLspStore {
         });
 
         let mut project_transaction = ProjectTransaction::default();
-        for (buffer, buffer_abs_path) in &buffers_with_paths {
+        for buffer in &buffers {
             let (primary_adapter_and_server, adapters_and_servers) =
                 lsp_store.update(&mut cx, |lsp_store, cx| {
-                    let buffer = buffer.read(cx);
+                    let buffer = buffer.handle.read(cx);
 
                     let adapters_and_servers = lsp_store
                         .language_servers_for_buffer(buffer, cx)
@@ -207,7 +207,7 @@ impl LocalLspStore {
                     (primary_adapter, adapters_and_servers)
                 })?;
 
-            let settings = buffer.update(&mut cx, |buffer, cx| {
+            let settings = buffer.handle.update(&mut cx, |buffer, cx| {
                 language_settings(buffer.language(), buffer.file(), cx).clone()
             })?;
 
@@ -218,13 +218,14 @@ impl LocalLspStore {
             let trailing_whitespace_diff = if remove_trailing_whitespace {
                 Some(
                     buffer
+                        .handle
                         .update(&mut cx, |b, cx| b.remove_trailing_whitespace(cx))?
                         .await,
                 )
             } else {
                 None
             };
-            let whitespace_transaction_id = buffer.update(&mut cx, |buffer, cx| {
+            let whitespace_transaction_id = buffer.handle.update(&mut cx, |buffer, cx| {
                 buffer.finalize_last_transaction();
                 buffer.start_transaction();
                 if let Some(diff) = trailing_whitespace_diff {
@@ -246,7 +247,7 @@ impl LocalLspStore {
                     &lsp_store,
                     &adapters_and_servers,
                     code_actions,
-                    buffer,
+                    &buffer.handle,
                     push_to_history,
                     &mut project_transaction,
                     &mut cx,
@@ -261,9 +262,9 @@ impl LocalLspStore {
                 primary_adapter_and_server.map(|(_adapter, server)| server.clone());
             let server_and_buffer = primary_language_server
                 .as_ref()
-                .zip(buffer_abs_path.as_ref());
+                .zip(buffer.abs_path.as_ref());
 
-            let prettier_settings = buffer.read_with(&cx, |buffer, cx| {
+            let prettier_settings = buffer.handle.read_with(&cx, |buffer, cx| {
                 language_settings(buffer.language(), buffer.file(), cx)
                     .prettier
                     .clone()
@@ -288,7 +289,6 @@ impl LocalLspStore {
                                                     server_and_buffer,
                                                     lsp_store.clone(),
                                                     buffer,
-                                                    buffer_abs_path,
                                                     &settings,
                                                     &adapters_and_servers,
                                                     push_to_history,
@@ -302,7 +302,6 @@ impl LocalLspStore {
                                                     server_and_buffer,
                                                     lsp_store.clone(),
                                                     buffer,
-                                                    buffer_abs_path,
                                                     &settings,
                                                     &adapters_and_servers,
                                                     push_to_history,
@@ -325,7 +324,6 @@ impl LocalLspStore {
                                                 server_and_buffer,
                                                 lsp_store.clone(),
                                                 buffer,
-                                                buffer_abs_path,
                                                 &settings,
                                                 &adapters_and_servers,
                                                 push_to_history,
@@ -351,7 +349,6 @@ impl LocalLspStore {
                                         server_and_buffer,
                                         lsp_store.clone(),
                                         buffer,
-                                        buffer_abs_path,
                                         &settings,
                                         &adapters_and_servers,
                                         push_to_history,
@@ -379,7 +376,6 @@ impl LocalLspStore {
                                             server_and_buffer,
                                             lsp_store.clone(),
                                             buffer,
-                                            buffer_abs_path,
                                             &settings,
                                             &adapters_and_servers,
                                             push_to_history,
@@ -393,7 +389,6 @@ impl LocalLspStore {
                                             server_and_buffer,
                                             lsp_store.clone(),
                                             buffer,
-                                            buffer_abs_path,
                                             &settings,
                                             &adapters_and_servers,
                                             push_to_history,
@@ -418,7 +413,6 @@ impl LocalLspStore {
                                         server_and_buffer,
                                         lsp_store.clone(),
                                         buffer,
-                                        buffer_abs_path,
                                         &settings,
                                         &adapters_and_servers,
                                         push_to_history,
@@ -438,7 +432,7 @@ impl LocalLspStore {
                 }
             }
 
-            buffer.update(&mut cx, |b, cx| {
+            buffer.handle.update(&mut cx, |b, cx| {
                 // If the buffer had its whitespace formatted and was edited while the language-specific
                 // formatting was being computed, avoid applying the language-specific formatting, because
                 // it can't be grouped with the whitespace formatting in the undo history.
@@ -467,7 +461,7 @@ impl LocalLspStore {
 
                     if let Some(transaction_id) = whitespace_transaction_id {
                         b.group_until_transaction(transaction_id);
-                    } else if let Some(transaction) = project_transaction.0.get(buffer) {
+                    } else if let Some(transaction) = project_transaction.0.get(&buffer.handle) {
                         b.group_until_transaction(transaction.id)
                     }
                 }
@@ -476,7 +470,9 @@ impl LocalLspStore {
                     if !push_to_history {
                         b.forget_transaction(transaction.id);
                     }
-                    project_transaction.0.insert(buffer.clone(), transaction);
+                    project_transaction
+                        .0
+                        .insert(buffer.handle.clone(), transaction);
                 }
             })?;
         }
@@ -489,8 +485,7 @@ impl LocalLspStore {
         formatter: &Formatter,
         primary_server_and_buffer: Option<(&Arc<LanguageServer>, &PathBuf)>,
         lsp_store: WeakModel<LspStore>,
-        buffer: &Model<Buffer>,
-        buffer_abs_path: &Option<PathBuf>,
+        buffer: &FormattableBuffer,
         settings: &LanguageSettings,
         adapters_and_servers: &[(Arc<CachedLspAdapter>, Arc<LanguageServer>)],
         push_to_history: bool,
@@ -514,7 +509,7 @@ impl LocalLspStore {
                     Some(FormatOperation::Lsp(
                         LspStore::format_via_lsp(
                             &lsp_store,
-                            buffer,
+                            &buffer.handle,
                             buffer_abs_path,
                             language_server,
                             settings,
@@ -531,27 +526,20 @@ impl LocalLspStore {
                 let prettier = lsp_store.update(cx, |lsp_store, _cx| {
                     lsp_store.prettier_store().unwrap().downgrade()
                 })?;
-                prettier_store::format_with_prettier(&prettier, buffer, cx)
+                prettier_store::format_with_prettier(&prettier, &buffer.handle, cx)
                     .await
                     .transpose()
                     .ok()
                     .flatten()
             }
             Formatter::External { command, arguments } => {
-                let buffer_abs_path = buffer_abs_path.as_ref().map(|path| path.as_path());
-                Self::format_via_external_command(
-                    buffer,
-                    buffer_abs_path,
-                    command,
-                    arguments.as_deref(),
-                    cx,
-                )
-                .await
-                .context(format!(
-                    "failed to format via external command {:?}",
-                    command
-                ))?
-                .map(FormatOperation::External)
+                Self::format_via_external_command(buffer, command, arguments.as_deref(), cx)
+                    .await
+                    .context(format!(
+                        "failed to format via external command {:?}",
+                        command
+                    ))?
+                    .map(FormatOperation::External)
             }
             Formatter::CodeActions(code_actions) => {
                 let code_actions = deserialize_code_actions(code_actions);
@@ -560,7 +548,7 @@ impl LocalLspStore {
                         &lsp_store,
                         adapters_and_servers,
                         code_actions,
-                        buffer,
+                        &buffer.handle,
                         push_to_history,
                         transaction,
                         cx,
@@ -574,13 +562,12 @@ impl LocalLspStore {
     }
 
     async fn format_via_external_command(
-        buffer: &Model<Buffer>,
-        buffer_abs_path: Option<&Path>,
+        buffer: &FormattableBuffer,
         command: &str,
         arguments: Option<&[String]>,
         cx: &mut AsyncAppContext,
     ) -> Result<Option<Diff>> {
-        let working_dir_path = buffer.update(cx, |buffer, cx| {
+        let working_dir_path = buffer.handle.update(cx, |buffer, cx| {
             let file = File::from_dyn(buffer.file())?;
             let worktree = file.worktree.read(cx);
             let mut worktree_path = worktree.abs_path().to_path_buf();
@@ -597,13 +584,17 @@ impl LocalLspStore {
             child.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
         }
 
+        if let Some(buffer_env) = buffer.env.as_ref() {
+            child.envs(buffer_env);
+        }
+
         if let Some(working_dir_path) = working_dir_path {
             child.current_dir(working_dir_path);
         }
 
         if let Some(arguments) = arguments {
             child.args(arguments.iter().map(|arg| {
-                if let Some(buffer_abs_path) = buffer_abs_path {
+                if let Some(buffer_abs_path) = buffer.abs_path.as_ref() {
                     arg.replace("{buffer_path}", &buffer_abs_path.to_string_lossy())
                 } else {
                     arg.replace("{buffer_path}", "Untitled")
@@ -621,7 +612,9 @@ impl LocalLspStore {
             .stdin
             .as_mut()
             .ok_or_else(|| anyhow!("failed to acquire stdin"))?;
-        let text = buffer.update(cx, |buffer, _| buffer.as_rope().clone())?;
+        let text = buffer
+            .handle
+            .update(cx, |buffer, _| buffer.as_rope().clone())?;
         for chunk in text.chunks() {
             stdin.write_all(chunk.as_bytes()).await?;
         }
@@ -640,12 +633,19 @@ impl LocalLspStore {
         let stdout = String::from_utf8(output.stdout)?;
         Ok(Some(
             buffer
+                .handle
                 .update(cx, |buffer, cx| buffer.diff(stdout, cx))?
                 .await,
         ))
     }
 }
 
+pub struct FormattableBuffer {
+    handle: Model<Buffer>,
+    abs_path: Option<PathBuf>,
+    env: Option<HashMap<String, String>>,
+}
+
 pub struct RemoteLspStore {
     upstream_client: AnyProtoClient,
     upstream_project_id: u64,
@@ -5028,6 +5028,28 @@ impl LspStore {
             .and_then(|local| local.last_formatting_failure.as_deref())
     }
 
+    pub fn environment_for_buffer(
+        &self,
+        buffer: &Model<Buffer>,
+        cx: &mut ModelContext<Self>,
+    ) -> Shared<Task<Option<HashMap<String, String>>>> {
+        let worktree_id = buffer.read(cx).file().map(|file| file.worktree_id(cx));
+        let worktree_abs_path = worktree_id.and_then(|worktree_id| {
+            self.worktree_store
+                .read(cx)
+                .worktree_for_id(worktree_id, cx)
+                .map(|entry| entry.read(cx).abs_path().clone())
+        });
+
+        if let Some(environment) = &self.as_local().map(|local| local.environment.clone()) {
+            environment.update(cx, |env, cx| {
+                env.get_environment(worktree_id, worktree_abs_path, cx)
+            })
+        } else {
+            Task::ready(None).shared()
+        }
+    }
+
     pub fn format(
         &mut self,
         buffers: HashSet<Model<Buffer>>,
@@ -5042,14 +5064,31 @@ impl LspStore {
                     let buffer = buffer_handle.read(cx);
                     let buffer_abs_path = File::from_dyn(buffer.file())
                         .and_then(|file| file.as_local().map(|f| f.abs_path(cx)));
+
                     (buffer_handle, buffer_abs_path)
                 })
                 .collect::<Vec<_>>();
 
             cx.spawn(move |lsp_store, mut cx| async move {
+                let mut formattable_buffers = Vec::with_capacity(buffers_with_paths.len());
+
+                for (handle, abs_path) in buffers_with_paths {
+                    let env = lsp_store
+                        .update(&mut cx, |lsp_store, cx| {
+                            lsp_store.environment_for_buffer(&handle, cx)
+                        })?
+                        .await;
+
+                    formattable_buffers.push(FormattableBuffer {
+                        handle,
+                        abs_path,
+                        env,
+                    });
+                }
+
                 let result = LocalLspStore::format_locally(
                     lsp_store.clone(),
-                    buffers_with_paths,
+                    formattable_buffers,
                     push_to_history,
                     trigger,
                     cx.clone(),