WIP: Start on applying code actions

Antonio Scandurra created

Change summary

crates/editor/src/editor.rs       | 50 ++++++++++++-------
crates/editor/src/multi_buffer.rs | 20 +++++-
crates/language/src/buffer.rs     | 23 +++++++-
crates/lsp/src/lsp.rs             |  4 +
crates/project/src/project.rs     | 85 ++++++++++++++++++++++++++++++++
crates/server/src/rpc.rs          |  4 
6 files changed, 154 insertions(+), 32 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -30,7 +30,7 @@ use gpui::{
 use items::BufferItemHandle;
 use itertools::Itertools as _;
 use language::{
-    AnchorRangeExt as _, BracketPair, Buffer, Completion, CompletionLabel, Diagnostic,
+    AnchorRangeExt as _, BracketPair, Buffer, CodeAction, Completion, CompletionLabel, Diagnostic,
     DiagnosticSeverity, Language, Point, Selection, SelectionGoal, TransactionId,
 };
 use multi_buffer::MultiBufferChunks;
@@ -654,7 +654,7 @@ impl CompletionsMenu {
 }
 
 struct CodeActionsMenu {
-    actions: Arc<[lsp::CodeAction]>,
+    actions: Arc<[CodeAction<Anchor>]>,
     selected_item: usize,
     list: UniformListState,
 }
@@ -699,7 +699,7 @@ impl CodeActionsMenu {
                             settings.style.autocomplete.item
                         };
 
-                        Text::new(action.title.clone(), settings.style.text.clone())
+                        Text::new(action.lsp_action.title.clone(), settings.style.text.clone())
                             .with_soft_wrap(false)
                             .contained()
                             .with_style(item_style)
@@ -717,7 +717,7 @@ impl CodeActionsMenu {
             self.actions
                 .iter()
                 .enumerate()
-                .max_by_key(|(_, action)| action.title.chars().count())
+                .max_by_key(|(_, action)| action.lsp_action.title.chars().count())
                 .map(|(ix, _)| ix),
         )
         .contained()
@@ -1935,7 +1935,7 @@ impl Editor {
         self.completion_tasks.push((id, task));
     }
 
-    fn confirm_completion(
+    pub fn confirm_completion(
         &mut self,
         ConfirmCompletion(completion_ix): &ConfirmCompletion,
         cx: &mut ViewContext<Self>,
@@ -2051,23 +2051,35 @@ impl Editor {
     }
 
     fn confirm_code_action(
-        &mut self,
+        workspace: &mut Workspace,
         ConfirmCodeAction(action_ix): &ConfirmCodeAction,
-        cx: &mut ViewContext<Self>,
+        cx: &mut ViewContext<Workspace>,
     ) -> Option<Task<Result<()>>> {
-        let actions_menu = if let ContextMenu::CodeActions(menu) = self.hide_context_menu(cx)? {
-            menu
-        } else {
-            return None;
-        };
-
-        let action = actions_menu
-            .actions
-            .get(action_ix.unwrap_or(actions_menu.selected_item))?;
-
-        dbg!(action);
+        let active_item = workspace.active_item(cx)?;
+        let editor = active_item.act_as::<Self>(cx)?;
+        let (buffer, action) = editor.update(cx, |editor, cx| {
+            let actions_menu =
+                if let ContextMenu::CodeActions(menu) = editor.hide_context_menu(cx)? {
+                    menu
+                } else {
+                    return None;
+                };
+            let action_ix = action_ix.unwrap_or(actions_menu.selected_item);
+            let action = actions_menu.actions.get(action_ix)?.clone();
+            let (buffer, position) = editor
+                .buffer
+                .read(cx)
+                .text_anchor_for_position(action.position, cx);
+            let action = CodeAction {
+                position,
+                lsp_action: action.lsp_action,
+            };
+            Some((buffer, action))
+        })?;
 
-        None
+        Some(workspace.project().update(cx, |project, cx| {
+            project.apply_code_action(buffer, action, cx)
+        }))
     }
 
     pub fn showing_context_menu(&self) -> bool {

crates/editor/src/multi_buffer.rs 🔗

@@ -5,11 +5,11 @@ use anyhow::Result;
 use clock::ReplicaId;
 use collections::{HashMap, HashSet};
 use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
-pub use language::Completion;
 use language::{
     Buffer, BufferChunks, BufferSnapshot, Chunk, DiagnosticEntry, Event, File, Language, Outline,
     OutlineItem, Selection, ToOffset as _, ToPoint as _, ToPointUtf16 as _, TransactionId,
 };
+pub use language::{CodeAction, Completion};
 use std::{
     cell::{Ref, RefCell},
     cmp, fmt, io,
@@ -864,15 +864,25 @@ impl MultiBuffer {
         &self,
         position: T,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<Vec<lsp::CodeAction>>>
+    ) -> Task<Result<Vec<CodeAction<Anchor>>>>
     where
         T: ToOffset,
     {
         let anchor = self.read(cx).anchor_before(position);
         let buffer = self.buffers.borrow()[&anchor.buffer_id].buffer.clone();
-        let code_actions =
-            buffer.update(cx, |buffer, cx| buffer.code_actions(anchor.text_anchor, cx));
-        cx.spawn(|this, cx| async move { code_actions.await })
+        let code_actions = buffer.update(cx, |buffer, cx| {
+            buffer.code_actions(anchor.text_anchor.clone(), cx)
+        });
+        cx.foreground().spawn(async move {
+            Ok(code_actions
+                .await?
+                .into_iter()
+                .map(|action| CodeAction {
+                    position: anchor.clone(),
+                    lsp_action: action.lsp_action,
+                })
+                .collect())
+        })
     }
 
     pub fn completions<T>(

crates/language/src/buffer.rs 🔗

@@ -118,6 +118,12 @@ pub struct Completion<T> {
     pub lsp_completion: lsp::CompletionItem,
 }
 
+#[derive(Clone, Debug)]
+pub struct CodeAction<T> {
+    pub position: T,
+    pub lsp_action: lsp::CodeAction,
+}
+
 struct LanguageServerState {
     server: Arc<LanguageServer>,
     latest_snapshot: watch::Sender<Option<LanguageServerSnapshot>>,
@@ -1852,7 +1858,7 @@ impl Buffer {
         &self,
         position: T,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<Vec<lsp::CodeAction>>>
+    ) -> Task<Result<Vec<CodeAction<Anchor>>>>
     where
         T: ToPointUtf16,
     {
@@ -1870,8 +1876,9 @@ impl Buffer {
             };
             let abs_path = file.abs_path(cx);
             let position = position.to_point_utf16(self);
+            let anchor = self.anchor_after(position);
 
-            cx.spawn(|this, mut cx| async move {
+            cx.foreground().spawn(async move {
                 let actions = server
                     .request::<lsp::request::CodeActionRequest>(lsp::CodeActionParams {
                         text_document: lsp::TextDocumentIdentifier::new(
@@ -1896,8 +1903,16 @@ impl Buffer {
                     .unwrap_or_default()
                     .into_iter()
                     .filter_map(|entry| {
-                        if let lsp::CodeActionOrCommand::CodeAction(action) = entry {
-                            Some(action)
+                        if let lsp::CodeActionOrCommand::CodeAction(lsp_action) = entry {
+                            if lsp_action.data.is_none() {
+                                log::warn!("skipping code action without data {lsp_action:?}");
+                                None
+                            } else {
+                                Some(CodeAction {
+                                    position: anchor.clone(),
+                                    lsp_action,
+                                })
+                            }
                         } else {
                             None
                         }

crates/lsp/src/lsp.rs 🔗

@@ -247,6 +247,10 @@ impl LanguageServer {
                                 ],
                             },
                         }),
+                        data_support: Some(true),
+                        resolve_support: Some(CodeActionCapabilityResolveSupport {
+                            properties: vec!["edit".to_string()],
+                        }),
                         ..Default::default()
                     }),
                     completion: Some(CompletionClientCapabilities {

crates/project/src/project.rs 🔗

@@ -15,8 +15,8 @@ use gpui::{
 use language::{
     point_from_lsp,
     proto::{deserialize_anchor, serialize_anchor},
-    range_from_lsp, Bias, Buffer, Diagnostic, DiagnosticEntry, File as _, Language,
-    LanguageRegistry, PointUtf16, ToOffset,
+    range_from_lsp, Bias, Buffer, CodeAction, Diagnostic, DiagnosticEntry, File as _, Language,
+    LanguageRegistry, PointUtf16, ToLspPosition, ToOffset, ToPointUtf16,
 };
 use lsp::{DiagnosticSeverity, LanguageServer};
 use postage::{prelude::Stream, watch};
@@ -1151,6 +1151,87 @@ impl Project {
         }
     }
 
+    pub fn apply_code_action(
+        &self,
+        buffer: ModelHandle<Buffer>,
+        mut action: CodeAction<language::Anchor>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        if self.is_local() {
+            let buffer = buffer.read(cx);
+            let server = if let Some(language_server) = buffer.language_server() {
+                language_server.clone()
+            } else {
+                return Task::ready(Ok(Default::default()));
+            };
+            let position = action.position.to_point_utf16(buffer).to_lsp_position();
+
+            cx.spawn(|this, mut cx| async move {
+                let range = action
+                    .lsp_action
+                    .data
+                    .as_mut()
+                    .and_then(|d| d.get_mut("codeActionParams"))
+                    .and_then(|d| d.get_mut("range"))
+                    .ok_or_else(|| anyhow!("code action has no range"))?;
+                *range = serde_json::to_value(&lsp::Range::new(position, position)).unwrap();
+                let action = server
+                    .request::<lsp::request::CodeActionResolveRequest>(action.lsp_action)
+                    .await?;
+                let edit = action
+                    .edit
+                    .ok_or_else(|| anyhow!("code action has no edit"));
+                // match edit {
+                //     Ok(edit) => edit.,
+                //     Err(_) => todo!(),
+                // }
+                Ok(Default::default())
+            })
+        } else {
+            log::info!("applying code actions is not implemented for guests");
+            Task::ready(Ok(Default::default()))
+        }
+        // let file = if let Some(file) = self.file.as_ref() {
+        //     file
+        // } else {
+        //     return Task::ready(Ok(Default::default()));
+        // };
+
+        // if file.is_local() {
+        //     let server = if let Some(language_server) = self.language_server.as_ref() {
+        //         language_server.server.clone()
+        //     } else {
+        //         return Task::ready(Ok(Default::default()));
+        //     };
+        //     let position = action.position.to_point_utf16(self).to_lsp_position();
+
+        //     cx.spawn(|this, mut cx| async move {
+        //         let range = action
+        //             .lsp_action
+        //             .data
+        //             .as_mut()
+        //             .and_then(|d| d.get_mut("codeActionParams"))
+        //             .and_then(|d| d.get_mut("range"))
+        //             .ok_or_else(|| anyhow!("code action has no range"))?;
+        //         *range = serde_json::to_value(&lsp::Range::new(position, position)).unwrap();
+        //         let action = server
+        //             .request::<lsp::request::CodeActionResolveRequest>(action.lsp_action)
+        //             .await?;
+        //         let edit = action
+        //             .edit
+        //             .ok_or_else(|| anyhow!("code action has no edit"));
+        //         match edit {
+        //             Ok(edit) => edit.,
+        //             Err(_) => todo!(),
+        //         }
+        //         Ok(Default::default())
+        //     })
+        // } else {
+        //     log::info!("applying code actions is not implemented for guests");
+        //     Task::ready(Ok(Default::default()))
+        // }
+    }
+
     pub fn find_or_create_local_worktree(
         &self,
         abs_path: impl AsRef<Path>,

crates/server/src/rpc.rs 🔗

@@ -1237,7 +1237,7 @@ mod tests {
             self, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Credentials,
             EstablishConnectionError, UserStore,
         },
-        editor::{Editor, EditorSettings, Input, MultiBuffer},
+        editor::{ConfirmCompletion, Editor, EditorSettings, Input, MultiBuffer},
         fs::{FakeFs, Fs as _},
         language::{
             tree_sitter_rust, AnchorRangeExt, Diagnostic, DiagnosticEntry, Language,
@@ -2469,7 +2469,7 @@ mod tests {
         editor_b.next_notification(&cx_b).await;
         editor_b.update(&mut cx_b, |editor, cx| {
             assert!(editor.showing_context_menu());
-            editor.confirm_completion(Some(0), cx);
+            editor.confirm_completion(&ConfirmCompletion(Some(0)), cx);
             assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
         });