Start work on applying code actions that use commands

Max Brunsfeld and Keith Simmons created

Co-Authored-By: Keith Simmons <keith@zed.dev>

Change summary

crates/project/src/project.rs | 326 +++++++++++++++++++++++++++---------
1 file changed, 243 insertions(+), 83 deletions(-)

Detailed changes

crates/project/src/project.rs 🔗

@@ -143,6 +143,7 @@ enum LanguageServerEvent {
         token: String,
     },
     DiagnosticsUpdate(lsp::PublishDiagnosticsParams),
+    WorkspaceEdit(lsp::ApplyWorkspaceEditParams),
 }
 
 pub struct LanguageServerStatus {
@@ -1367,6 +1368,24 @@ impl Project {
                         })
                         .detach();
 
+                    language_server
+                        .on_request::<lsp::request::ApplyWorkspaceEdit, _, _>({
+                            let language_server_events_tx = language_server_events_tx.clone();
+                            move |params, _| {
+                                language_server_events_tx
+                                    .try_send(LanguageServerEvent::WorkspaceEdit(params))
+                                    .ok();
+                                async move {
+                                    Ok(lsp::ApplyWorkspaceEditResponse {
+                                        applied: true,
+                                        failed_change: None,
+                                        failure_reason: None,
+                                    })
+                                }
+                            }
+                        })
+                        .detach();
+
                     language_server
                         .on_notification::<lsp::notification::Progress, _>(move |params, _| {
                             let token = match params.token {
@@ -1416,12 +1435,20 @@ impl Project {
                     // Process all the LSP events.
                     cx.spawn(|mut cx| {
                         let this = this.downgrade();
+                        let adapter = adapter.clone();
+                        let language_server = language_server.clone();
                         async move {
                             while let Ok(event) = language_server_events_rx.recv().await {
                                 let this = this.upgrade(&cx)?;
-                                this.update(&mut cx, |this, cx| {
-                                    this.on_lsp_event(server_id, event, &language, cx)
-                                });
+                                Self::on_lsp_event(
+                                    this,
+                                    server_id,
+                                    &adapter,
+                                    &language_server,
+                                    event,
+                                    &mut cx,
+                                )
+                                .await;
 
                                 // Don't starve the main thread when lots of events arrive all at once.
                                 smol::future::yield_now().await;
@@ -1585,109 +1612,142 @@ impl Project {
         .detach();
     }
 
-    fn on_lsp_event(
-        &mut self,
+    async fn on_lsp_event(
+        this: ModelHandle<Self>,
         language_server_id: usize,
+        adapter: &Arc<dyn LspAdapter>,
+        language_server: &Arc<LanguageServer>,
         event: LanguageServerEvent,
-        language: &Arc<Language>,
-        cx: &mut ModelContext<Self>,
+        cx: &mut AsyncAppContext,
     ) {
-        let disk_diagnostics_token = language.disk_based_diagnostics_progress_token();
-        let language_server_status =
-            if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
-                status
-            } else {
-                return;
-            };
-
+        let disk_based_diagnostics_progress_token = adapter.disk_based_diagnostics_progress_token();
         match event {
             LanguageServerEvent::WorkStart { token } => {
-                if Some(token.as_str()) == disk_diagnostics_token {
-                    language_server_status.pending_diagnostic_updates += 1;
-                    if language_server_status.pending_diagnostic_updates == 1 {
-                        self.disk_based_diagnostics_started(cx);
-                        self.broadcast_language_server_update(
+                this.update(cx, |this, cx| {
+                    let language_server_status = if let Some(status) =
+                        this.language_server_statuses.get_mut(&language_server_id)
+                    {
+                        status
+                    } else {
+                        return;
+                    };
+
+                    if Some(token.as_str()) == disk_based_diagnostics_progress_token {
+                        language_server_status.pending_diagnostic_updates += 1;
+                        if language_server_status.pending_diagnostic_updates == 1 {
+                            this.disk_based_diagnostics_started(cx);
+                            this.broadcast_language_server_update(
                             language_server_id,
                             proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(
                                 proto::LspDiskBasedDiagnosticsUpdating {},
                             ),
                         );
+                        }
+                    } else {
+                        this.on_lsp_work_start(language_server_id, token.clone(), cx);
+                        this.broadcast_language_server_update(
+                            language_server_id,
+                            proto::update_language_server::Variant::WorkStart(
+                                proto::LspWorkStart { token },
+                            ),
+                        );
                     }
-                } else {
-                    self.on_lsp_work_start(language_server_id, token.clone(), cx);
-                    self.broadcast_language_server_update(
-                        language_server_id,
-                        proto::update_language_server::Variant::WorkStart(proto::LspWorkStart {
-                            token,
-                        }),
-                    );
-                }
+                });
             }
             LanguageServerEvent::WorkProgress { token, progress } => {
-                if Some(token.as_str()) != disk_diagnostics_token {
-                    self.on_lsp_work_progress(
-                        language_server_id,
-                        token.clone(),
-                        progress.clone(),
-                        cx,
-                    );
-                    self.broadcast_language_server_update(
-                        language_server_id,
-                        proto::update_language_server::Variant::WorkProgress(
-                            proto::LspWorkProgress {
-                                token,
-                                message: progress.message,
-                                percentage: progress.percentage.map(|p| p as u32),
-                            },
-                        ),
-                    );
-                }
+                this.update(cx, |this, cx| {
+                    if Some(token.as_str()) != disk_based_diagnostics_progress_token {
+                        this.on_lsp_work_progress(
+                            language_server_id,
+                            token.clone(),
+                            progress.clone(),
+                            cx,
+                        );
+                        this.broadcast_language_server_update(
+                            language_server_id,
+                            proto::update_language_server::Variant::WorkProgress(
+                                proto::LspWorkProgress {
+                                    token,
+                                    message: progress.message,
+                                    percentage: progress.percentage.map(|p| p as u32),
+                                },
+                            ),
+                        );
+                    }
+                });
             }
             LanguageServerEvent::WorkEnd { token } => {
-                if Some(token.as_str()) == disk_diagnostics_token {
-                    language_server_status.pending_diagnostic_updates -= 1;
-                    if language_server_status.pending_diagnostic_updates == 0 {
-                        self.disk_based_diagnostics_finished(cx);
-                        self.broadcast_language_server_update(
+                this.update(cx, |this, cx| {
+                    if Some(token.as_str()) == disk_based_diagnostics_progress_token {
+                        let language_server_status = if let Some(status) =
+                            this.language_server_statuses.get_mut(&language_server_id)
+                        {
+                            status
+                        } else {
+                            return;
+                        };
+
+                        language_server_status.pending_diagnostic_updates -= 1;
+                        if language_server_status.pending_diagnostic_updates == 0 {
+                            this.disk_based_diagnostics_finished(cx);
+                            this.broadcast_language_server_update(
+                                language_server_id,
+                                proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
+                                    proto::LspDiskBasedDiagnosticsUpdated {},
+                                ),
+                            );
+                        }
+                    } else {
+                        this.on_lsp_work_end(language_server_id, token.clone(), cx);
+                        this.broadcast_language_server_update(
+                            language_server_id,
+                            proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd {
+                                token,
+                            }),
+                        );
+                    }
+                });
+            }
+            LanguageServerEvent::DiagnosticsUpdate(mut params) => {
+                this.update(cx, |this, cx| {
+                    adapter.process_diagnostics(&mut params);
+
+                    if disk_based_diagnostics_progress_token.is_none() {
+                        this.disk_based_diagnostics_started(cx);
+                        this.broadcast_language_server_update(
+                            language_server_id,
+                            proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(
+                                proto::LspDiskBasedDiagnosticsUpdating {},
+                            ),
+                        );
+                    }
+                    this.update_diagnostics(params, adapter.disk_based_diagnostic_sources(), cx)
+                        .log_err();
+                    if disk_based_diagnostics_progress_token.is_none() {
+                        this.disk_based_diagnostics_finished(cx);
+                        this.broadcast_language_server_update(
                             language_server_id,
                             proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
                                 proto::LspDiskBasedDiagnosticsUpdated {},
                             ),
                         );
                     }
-                } else {
-                    self.on_lsp_work_end(language_server_id, token.clone(), cx);
-                    self.broadcast_language_server_update(
-                        language_server_id,
-                        proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd {
-                            token,
-                        }),
-                    );
-                }
+                });
             }
-            LanguageServerEvent::DiagnosticsUpdate(mut params) => {
-                language.process_diagnostics(&mut params);
+            LanguageServerEvent::WorkspaceEdit(params) => {
+                let transaction = Self::deserialize_workspace_edit(
+                    this,
+                    params.edit,
+                    false,
+                    adapter.clone(),
+                    language_server.clone(),
+                    cx,
+                )
+                .await
+                .log_err();
 
-                if disk_diagnostics_token.is_none() {
-                    self.disk_based_diagnostics_started(cx);
-                    self.broadcast_language_server_update(
-                        language_server_id,
-                        proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(
-                            proto::LspDiskBasedDiagnosticsUpdating {},
-                        ),
-                    );
-                }
-                self.update_diagnostics(params, language.disk_based_diagnostic_sources(), cx)
-                    .log_err();
-                if disk_diagnostics_token.is_none() {
-                    self.disk_based_diagnostics_finished(cx);
-                    self.broadcast_language_server_update(
-                        language_server_id,
-                        proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
-                            proto::LspDiskBasedDiagnosticsUpdated {},
-                        ),
-                    );
-                }
+                // Check if there is a code action currently running, using the state that is
+                // set in `start_code_action`. If so, then store the transaction for later use.
             }
         }
     }
@@ -2679,6 +2739,16 @@ impl Project {
                         &mut cx,
                     )
                     .await
+                } else if let Some(command) = action.lsp_action.command {
+                    this.update(&mut cx, |this, _| this.start_code_action());
+                    lang_server
+                        .request::<lsp::request::ExecuteCommand>(lsp::ExecuteCommandParams {
+                            command: command.command,
+                            arguments: command.arguments.unwrap_or_default(),
+                            ..Default::default()
+                        })
+                        .await?;
+                    Ok(this.update(&mut cx, |this, cx| this.finish_code_action(cx)))
                 } else {
                     Ok(ProjectTransaction::default())
                 }
@@ -2837,6 +2907,17 @@ impl Project {
         Ok(project_transaction)
     }
 
+    fn start_code_action(&mut self) {
+        // Set some state that will be read inside of `on_lsp_event` when handling a `WorkspaceEdit`
+        // event, and will cause the `ProjectTransaction` to be stored.
+    }
+
+    fn finish_code_action(&mut self, cx: &mut ModelContext<Self>) -> ProjectTransaction {
+        // Retrieve all stored `ProjectTransactions` that have been received since `start_code_action`
+        // was called, and combine them together.
+        Default::default()
+    }
+
     pub fn prepare_rename<T: ToPointUtf16>(
         &self,
         buffer: ModelHandle<Buffer>,
@@ -5992,6 +6073,85 @@ mod tests {
         }
     }
 
+    #[gpui::test]
+    async fn test_apply_code_action(cx: &mut gpui::TestAppContext) {
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "TypeScript".into(),
+                path_suffixes: vec!["ts".to_string()],
+                ..Default::default()
+            },
+            None,
+        );
+        let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
+
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/dir",
+            json!({
+                "a.ts": "",
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs, cx);
+        project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+        let (tree, _) = project
+            .update(cx, |project, cx| {
+                project.find_or_create_local_worktree("/dir", true, cx)
+            })
+            .await
+            .unwrap();
+        let worktree_id = tree.read_with(cx, |tree, _| tree.id());
+        cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+            .await;
+
+        let buffer = project
+            .update(cx, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx))
+            .await
+            .unwrap();
+
+        let fake_server = fake_language_servers.next().await.unwrap();
+
+        let actions = project.update(cx, |project, cx| project.code_actions(&buffer, 0..0, cx));
+        fake_server
+            .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
+                Ok(Some(vec![
+                    lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
+                        title: "The code action".into(),
+                        command: Some(lsp::Command {
+                            title: "The command".into(),
+                            command: "_the/command".into(),
+                            arguments: Some(vec![json!("the-argument")]),
+                        }),
+                        ..Default::default()
+                    }),
+                    lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
+                        title: "two".into(),
+                        ..Default::default()
+                    }),
+                ]))
+            })
+            .next()
+            .await;
+
+        let action = actions.await.unwrap()[0].clone();
+        let apply = project.update(cx, |project, cx| {
+            project.apply_code_action(buffer.clone(), action, true, cx)
+        });
+        fake_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
+            |action, _| async move { Ok(action) },
+        );
+        fake_server
+            .handle_request::<lsp::request::ExecuteCommand, _, _>(move |params, cx| async move {
+                // fake_server.send();
+                Ok(Some(json!(null)))
+            })
+            .next()
+            .await;
+    }
+
     #[gpui::test]
     async fn test_save_file(cx: &mut gpui::TestAppContext) {
         let fs = FakeFs::new(cx.background());