diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c8cd2bbc676bd99aa62edd99650607c57e9edf6e..186f117d0873c0c4e598bcbc3639aa61d0137d97 100644 --- a/crates/editor/src/editor.rs +++ b/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]>, 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, @@ -2051,23 +2051,35 @@ impl Editor { } fn confirm_code_action( - &mut self, + workspace: &mut Workspace, ConfirmCodeAction(action_ix): &ConfirmCodeAction, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> Option>> { - 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::(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 { diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index bbcc4a47a77e9b20e46cfdbfb1f4f0cc712e5986..5e37d25c1900dfdd6cac9d2412baa911045e2076 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/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, - ) -> Task>> + ) -> Task>>> 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( diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index d3e41f06297b013be6f0df07541159e544a659e2..c626a78f9016a18ee2d2f06ebd94d2589e68987f 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -118,6 +118,12 @@ pub struct Completion { pub lsp_completion: lsp::CompletionItem, } +#[derive(Clone, Debug)] +pub struct CodeAction { + pub position: T, + pub lsp_action: lsp::CodeAction, +} + struct LanguageServerState { server: Arc, latest_snapshot: watch::Sender>, @@ -1852,7 +1858,7 @@ impl Buffer { &self, position: T, cx: &mut ModelContext, - ) -> Task>> + ) -> Task>>> 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::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 } diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 287d825c8f89687892acc7743931d3b4aff81b35..f0bf4b47cb25d5bf0e0efd2bace07e5bb97c5dd6 100644 --- a/crates/lsp/src/lsp.rs +++ b/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 { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 55a3583f6e12d4cf87884805cba76a529b453cd6..4bac937e65c924fde75511cc32470e1c7b4ab4bb 100644 --- a/crates/project/src/project.rs +++ b/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, + mut action: CodeAction, + cx: &mut ModelContext, + ) -> Task> { + 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::(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::(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, diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index f246b13f5d9f4bed88c291235360400a460436a3..172f77dd6b267a7a5cf37bf8c5e6f7983fa74875 100644 --- a/crates/server/src/rpc.rs +++ b/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() }"); });