diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index bf0c3fe85d..61a1e47a5c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2078,7 +2078,7 @@ impl Editor { })?; let apply_code_actions = workspace.project().update(cx, |project, cx| { - project.apply_code_action(buffer, action, cx) + project.apply_code_action(buffer, action, true, cx) }); Some(cx.spawn(|workspace, mut cx| async move { let buffers = apply_code_actions.await?; diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 8e409f5e3e..dc7a50151b 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -216,6 +216,13 @@ pub trait File { cx: &mut MutableAppContext, ) -> Task>>; + fn code_actions( + &self, + buffer_id: u64, + position: Anchor, + cx: &mut MutableAppContext, + ) -> Task>>>; + fn buffer_updated(&self, buffer_id: u64, operation: Operation, cx: &mut MutableAppContext); fn buffer_removed(&self, buffer_id: u64, cx: &mut MutableAppContext); @@ -304,6 +311,15 @@ impl File for FakeFile { Task::ready(Ok(Default::default())) } + fn code_actions( + &self, + _: u64, + _: Anchor, + _: &mut MutableAppContext, + ) -> Task>>> { + Task::ready(Ok(Default::default())) + } + fn buffer_updated(&self, _: u64, _: Operation, _: &mut MutableAppContext) {} fn buffer_removed(&self, _: u64, _: &mut MutableAppContext) {} @@ -1350,10 +1366,29 @@ impl Buffer { } } + pub fn push_transaction( + &mut self, + edit_ids: impl IntoIterator, + now: Instant, + ) { + self.text.push_transaction(edit_ids, now); + } + pub fn avoid_grouping_next_transaction(&mut self) { self.text.avoid_grouping_next_transaction(); } + pub fn forget_transaction(&mut self, transaction_id: TransactionId) { + self.text.forget_transaction(transaction_id); + } + + pub fn wait_for_edits( + &mut self, + edit_ids: impl IntoIterator, + ) -> impl Future { + self.text.wait_for_edits(edit_ids) + } + pub fn set_active_selections( &mut self, selections: Arc<[Selection]>, @@ -1873,6 +1908,8 @@ impl Buffer { } else { return Task::ready(Ok(Default::default())); }; + let position = position.to_point_utf16(self); + let anchor = self.anchor_after(position); if let Some(file) = file.as_local() { let server = if let Some(language_server) = self.language_server.as_ref() { @@ -1881,8 +1918,6 @@ impl Buffer { return Task::ready(Ok(Default::default())); }; let abs_path = file.abs_path(cx); - let position = position.to_point_utf16(self); - let anchor = self.anchor_after(position); cx.foreground().spawn(async move { let actions = server @@ -1922,8 +1957,7 @@ impl Buffer { Ok(actions) }) } else { - log::info!("code actions are not implemented for guests"); - Task::ready(Ok(Default::default())) + file.code_actions(self.remote_id(), anchor, cx.as_mut()) } } @@ -1959,7 +1993,7 @@ impl Buffer { let edits = this.apply_lsp_edits(additional_edits, None, cx); if let Some(transaction_id) = this.end_transaction(cx) { if !push_to_history { - this.text.forget_transaction(transaction_id); + this.forget_transaction(transaction_id); } } Ok(edits?.into_iter().map(|(_, edit_id)| edit_id).collect()) @@ -1976,12 +2010,13 @@ impl Buffer { ); cx.spawn(|this, mut cx| async move { let edit_ids = apply_edits.await?; - this.update(&mut cx, |this, _| this.text.wait_for_edits(&edit_ids)) - .await; + this.update(&mut cx, |this, _| { + this.wait_for_edits(edit_ids.iter().copied()) + }) + .await; if push_to_history { this.update(&mut cx, |this, _| { - this.text - .push_transaction(edit_ids.iter().copied(), Instant::now()); + this.push_transaction(edit_ids.iter().copied(), Instant::now()); }); } Ok(edit_ids) diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 82787ec571..2f23a1242e 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -1,12 +1,13 @@ use crate::{ - diagnostic_set::DiagnosticEntry, Completion, CompletionLabel, Diagnostic, Language, Operation, + diagnostic_set::DiagnosticEntry, CodeAction, Completion, CompletionLabel, Diagnostic, Language, + Operation, }; use anyhow::{anyhow, Result}; use clock::ReplicaId; use collections::HashSet; use lsp::DiagnosticSeverity; use rpc::proto; -use std::sync::Arc; +use std::{ops::Range, sync::Arc}; use text::*; pub use proto::{Buffer, BufferState, SelectionSet}; @@ -411,3 +412,62 @@ pub fn deserialize_completion( lsp_completion, }) } + +pub fn serialize_code_action(action: &CodeAction) -> proto::CodeAction { + proto::CodeAction { + position: Some(serialize_anchor(&action.position)), + lsp_action: serde_json::to_vec(&action.lsp_action).unwrap(), + } +} + +pub fn deserialize_code_action(action: proto::CodeAction) -> Result> { + let position = action + .position + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid position"))?; + let lsp_action = serde_json::from_slice(&action.lsp_action)?; + Ok(CodeAction { + position, + lsp_action, + }) +} + +pub fn serialize_code_action_edit( + edit_id: clock::Local, + old_range: &Range, +) -> proto::CodeActionEdit { + proto::CodeActionEdit { + id: Some(serialize_edit_id(edit_id)), + old_start: Some(serialize_anchor(&old_range.start)), + old_end: Some(serialize_anchor(&old_range.end)), + } +} + +pub fn deserialize_code_action_edit( + edit: proto::CodeActionEdit, +) -> Result<(Range, clock::Local)> { + let old_start = edit + .old_start + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid old_start"))?; + let old_end = edit + .old_end + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid old_end"))?; + let edit_id = deserialize_edit_id(edit.id.ok_or_else(|| anyhow!("invalid edit_id"))?); + Ok((old_start..old_end, edit_id)) +} + +pub fn serialize_edit_id(edit_id: clock::Local) -> proto::EditId { + proto::EditId { + replica_id: edit_id.replica_id as u32, + local_timestamp: edit_id.value, + } +} + +pub fn deserialize_edit_id(edit_id: proto::EditId) -> clock::Local { + clock::Local { + replica_id: edit_id.replica_id as ReplicaId, + value: edit_id.local_timestamp, + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f1798b491b..1e71e44e48 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -26,6 +26,7 @@ use std::{ ops::Range, path::{Path, PathBuf}, sync::{atomic::AtomicBool, Arc}, + time::Instant, }; use util::{post_inc, ResultExt, TryFutureExt as _}; @@ -341,6 +342,8 @@ impl Project { cx, Self::handle_apply_additional_edits_for_completion, ), + client.subscribe_to_entity(remote_id, cx, Self::handle_get_code_actions), + client.subscribe_to_entity(remote_id, cx, Self::handle_apply_code_action), client.subscribe_to_entity(remote_id, cx, Self::handle_get_definition), ]); } @@ -1169,6 +1172,7 @@ impl Project { &self, buffer_handle: ModelHandle, mut action: CodeAction, + push_to_history: bool, cx: &mut ModelContext, ) -> Task, Vec<(Range, clock::Local)>>>> { @@ -1299,7 +1303,19 @@ impl Project { lsp::OneOf::Left(edit) => edit, lsp::OneOf::Right(edit) => edit.text_edit, }); - buffer.apply_lsp_edits(edits, op.text_document.version, cx) + if !push_to_history { + buffer.avoid_grouping_next_transaction(); + } + buffer.start_transaction(); + let edits = + buffer.apply_lsp_edits(edits, op.text_document.version, cx); + if let Some(transaction_id) = buffer.end_transaction(cx) { + if !push_to_history { + buffer.forget_transaction(transaction_id); + } + } + + edits })?; edited_buffers .entry(buffer_to_edit) @@ -1309,51 +1325,49 @@ impl Project { } } + Ok(edited_buffers) + }) + } else if let Some(project_id) = self.remote_id() { + let client = self.client.clone(); + let request = proto::ApplyCodeAction { + project_id, + buffer_id: buffer_handle.read(cx).remote_id(), + action: Some(language::proto::serialize_code_action(&action)), + }; + cx.spawn(|this, mut cx| async move { + let response = client.request(request).await?; + let mut edited_buffers = HashMap::default(); + for buffer_edit in response.buffer_edits { + let buffer = buffer_edit + .buffer + .ok_or_else(|| anyhow!("invalid buffer"))?; + let buffer = this.update(&mut cx, |this, cx| { + this.deserialize_remote_buffer(buffer, cx) + })?; + + let buffer_edits = edited_buffers.entry(buffer.clone()).or_insert(Vec::new()); + for edit in buffer_edit.edits { + buffer_edits.push(language::proto::deserialize_code_action_edit(edit)?); + } + + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_edits(buffer_edits.iter().map(|e| e.1)) + }) + .await; + + if push_to_history { + buffer.update(&mut cx, |buffer, _| { + buffer + .push_transaction(buffer_edits.iter().map(|e| e.1), Instant::now()); + }); + } + } Ok(edited_buffers) }) } else { - log::info!("applying code actions is not implemented for guests"); - Task::ready(Ok(Default::default())) + Task::ready(Err(anyhow!("project does not have a remote id"))) } - // 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( @@ -1951,7 +1965,7 @@ impl Project { envelope .payload .completion - .ok_or_else(|| anyhow!("invalid position"))?, + .ok_or_else(|| anyhow!("invalid completion"))?, language, )?; cx.spawn(|_, mut cx| async move { @@ -1966,10 +1980,7 @@ impl Project { proto::ApplyCompletionAdditionalEditsResponse { additional_edits: edit_ids .into_iter() - .map(|edit_id| proto::AdditionalEdit { - replica_id: edit_id.replica_id as u32, - local_timestamp: edit_id.value, - }) + .map(language::proto::serialize_edit_id) .collect(), }, ), @@ -1985,6 +1996,99 @@ impl Project { Ok(()) } + fn handle_get_code_actions( + &mut self, + envelope: TypedEnvelope, + rpc: Arc, + cx: &mut ModelContext, + ) -> Result<()> { + let receipt = envelope.receipt(); + let sender_id = envelope.original_sender_id()?; + let buffer = self + .shared_buffers + .get(&sender_id) + .and_then(|shared_buffers| shared_buffers.get(&envelope.payload.buffer_id).cloned()) + .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?; + let position = envelope + .payload + .position + .and_then(language::proto::deserialize_anchor) + .ok_or_else(|| anyhow!("invalid position"))?; + cx.spawn(|_, mut cx| async move { + match buffer + .update(&mut cx, |buffer, cx| buffer.code_actions(position, cx)) + .await + { + Ok(completions) => rpc.respond( + receipt, + proto::GetCodeActionsResponse { + actions: completions + .iter() + .map(language::proto::serialize_code_action) + .collect(), + }, + ), + Err(error) => rpc.respond_with_error( + receipt, + proto::Error { + message: error.to_string(), + }, + ), + } + }) + .detach_and_log_err(cx); + Ok(()) + } + + fn handle_apply_code_action( + &mut self, + envelope: TypedEnvelope, + rpc: Arc, + cx: &mut ModelContext, + ) -> Result<()> { + let receipt = envelope.receipt(); + let sender_id = envelope.original_sender_id()?; + let buffer = self + .shared_buffers + .get(&sender_id) + .and_then(|shared_buffers| shared_buffers.get(&envelope.payload.buffer_id).cloned()) + .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?; + let action = language::proto::deserialize_code_action( + envelope + .payload + .action + .ok_or_else(|| anyhow!("invalid action"))?, + )?; + let apply_code_action = self.apply_code_action(buffer, action, false, cx); + cx.spawn(|this, mut cx| async move { + match apply_code_action.await { + Ok(edited_buffers) => this.update(&mut cx, |this, cx| { + let buffer_edits = edited_buffers + .into_iter() + .map(|(buffer, edits)| proto::CodeActionBufferEdits { + buffer: Some(this.serialize_buffer_for_peer(&buffer, sender_id, cx)), + edits: edits + .into_iter() + .map(|(range, edit_id)| { + language::proto::serialize_code_action_edit(edit_id, &range) + }) + .collect(), + }) + .collect(); + rpc.respond(receipt, proto::ApplyCodeActionResponse { buffer_edits }) + }), + Err(error) => rpc.respond_with_error( + receipt, + proto::Error { + message: error.to_string(), + }, + ), + } + }) + .detach_and_log_err(cx); + Ok(()) + } + pub fn handle_get_definition( &mut self, envelope: TypedEnvelope, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 643c26aa71..f30f8f868b 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1469,14 +1469,43 @@ impl language::File for File { Ok(response .additional_edits .into_iter() - .map(|edit| clock::Local { - replica_id: edit.replica_id as ReplicaId, - value: edit.local_timestamp, - }) + .map(language::proto::deserialize_edit_id) .collect()) }) } + fn code_actions( + &self, + buffer_id: u64, + position: Anchor, + cx: &mut MutableAppContext, + ) -> Task>>> { + let worktree = self.worktree.read(cx); + let worktree = if let Some(worktree) = worktree.as_remote() { + worktree + } else { + return Task::ready(Err(anyhow!( + "remote code actions requested on a local worktree" + ))); + }; + let rpc = worktree.client.clone(); + let project_id = worktree.project_id; + cx.foreground().spawn(async move { + let response = rpc + .request(proto::GetCodeActions { + project_id, + buffer_id, + position: Some(language::proto::serialize_anchor(&position)), + }) + .await?; + response + .actions + .into_iter() + .map(language::proto::deserialize_code_action) + .collect() + }) + } + fn buffer_updated(&self, buffer_id: u64, operation: Operation, cx: &mut MutableAppContext) { self.worktree.update(cx, |worktree, cx| { worktree.send_buffer_update(buffer_id, operation, cx); diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 2f2364fc24..f61aaf38e6 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -44,22 +44,26 @@ message Envelope { GetCompletionsResponse get_completions_response = 36; ApplyCompletionAdditionalEdits apply_completion_additional_edits = 37; ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 38; + GetCodeActions get_code_actions = 39; + GetCodeActionsResponse get_code_actions_response = 40; + ApplyCodeAction apply_code_action = 41; + ApplyCodeActionResponse apply_code_action_response = 42; - GetChannels get_channels = 39; - GetChannelsResponse get_channels_response = 40; - JoinChannel join_channel = 41; - JoinChannelResponse join_channel_response = 42; - LeaveChannel leave_channel = 43; - SendChannelMessage send_channel_message = 44; - SendChannelMessageResponse send_channel_message_response = 45; - ChannelMessageSent channel_message_sent = 46; - GetChannelMessages get_channel_messages = 47; - GetChannelMessagesResponse get_channel_messages_response = 48; + GetChannels get_channels = 43; + GetChannelsResponse get_channels_response = 44; + JoinChannel join_channel = 45; + JoinChannelResponse join_channel_response = 46; + LeaveChannel leave_channel = 47; + SendChannelMessage send_channel_message = 48; + SendChannelMessageResponse send_channel_message_response = 49; + ChannelMessageSent channel_message_sent = 50; + GetChannelMessages get_channel_messages = 51; + GetChannelMessagesResponse get_channel_messages_response = 52; - UpdateContacts update_contacts = 49; + UpdateContacts update_contacts = 53; - GetUsers get_users = 50; - GetUsersResponse get_users_response = 51; + GetUsers get_users = 54; + GetUsersResponse get_users_response = 55; } } @@ -224,12 +228,7 @@ message ApplyCompletionAdditionalEdits { } message ApplyCompletionAdditionalEditsResponse { - repeated AdditionalEdit additional_edits = 1; -} - -message AdditionalEdit { - uint32 replica_id = 1; - uint32 local_timestamp = 2; + repeated EditId additional_edits = 1; } message Completion { @@ -239,6 +238,47 @@ message Completion { bytes lsp_completion = 4; } +message GetCodeActions { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor position = 3; +} + +message GetCodeActionsResponse { + repeated CodeAction actions = 1; +} + +message ApplyCodeAction { + uint64 project_id = 1; + uint64 buffer_id = 2; + CodeAction action = 3; +} + +message ApplyCodeActionResponse { + repeated CodeActionBufferEdits buffer_edits = 1; +} + +message CodeAction { + Anchor position = 1; + bytes lsp_action = 2; +} + +message CodeActionBufferEdits { + Buffer buffer = 1; + repeated CodeActionEdit edits = 2; +} + +message CodeActionEdit { + EditId id = 1; + Anchor old_start = 2; + Anchor old_end = 3; +} + +message EditId { + uint32 replica_id = 1; + uint32 local_timestamp = 2; +} + message UpdateDiagnosticSummary { uint64 project_id = 1; uint64 worktree_id = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index a940795265..a22027173f 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -122,6 +122,8 @@ macro_rules! entity_messages { messages!( Ack, AddProjectCollaborator, + ApplyCodeAction, + ApplyCodeActionResponse, ApplyCompletionAdditionalEdits, ApplyCompletionAdditionalEditsResponse, BufferReloaded, @@ -136,6 +138,8 @@ messages!( GetChannelMessagesResponse, GetChannels, GetChannelsResponse, + GetCodeActions, + GetCodeActionsResponse, GetCompletions, GetCompletionsResponse, GetDefinition, @@ -171,6 +175,7 @@ messages!( ); request_messages!( + (ApplyCodeAction, ApplyCodeActionResponse), ( ApplyCompletionAdditionalEdits, ApplyCompletionAdditionalEditsResponse @@ -178,6 +183,7 @@ request_messages!( (FormatBuffer, Ack), (GetChannelMessages, GetChannelMessagesResponse), (GetChannels, GetChannelsResponse), + (GetCodeActions, GetCodeActionsResponse), (GetCompletions, GetCompletionsResponse), (GetDefinition, GetDefinitionResponse), (GetUsers, GetUsersResponse), @@ -197,6 +203,7 @@ request_messages!( entity_messages!( project_id, AddProjectCollaborator, + ApplyCodeAction, ApplyCompletionAdditionalEdits, BufferReloaded, BufferSaved, @@ -204,6 +211,7 @@ entity_messages!( DiskBasedDiagnosticsUpdated, DiskBasedDiagnosticsUpdating, FormatBuffer, + GetCodeActions, GetCompletions, GetDefinition, JoinProject, diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 672d6054de..ee2fb6a94d 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -85,6 +85,8 @@ impl Server { .add_handler(Server::format_buffer) .add_handler(Server::get_completions) .add_handler(Server::apply_additional_edits_for_completion) + .add_handler(Server::get_code_actions) + .add_handler(Server::apply_code_action) .add_handler(Server::get_channels) .add_handler(Server::get_users) .add_handler(Server::join_channel) @@ -737,6 +739,52 @@ impl Server { Ok(()) } + async fn get_code_actions( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let host; + { + let state = self.state(); + let project = state + .read_project(request.payload.project_id, request.sender_id) + .ok_or_else(|| anyhow!(NO_SUCH_PROJECT))?; + host = project.host_connection_id; + } + + let sender = request.sender_id; + let receipt = request.receipt(); + let response = self + .peer + .forward_request(sender, host, request.payload.clone()) + .await?; + self.peer.respond(receipt, response)?; + Ok(()) + } + + async fn apply_code_action( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let host; + { + let state = self.state(); + let project = state + .read_project(request.payload.project_id, request.sender_id) + .ok_or_else(|| anyhow!(NO_SUCH_PROJECT))?; + host = project.host_connection_id; + } + + let sender = request.sender_id; + let receipt = request.receipt(); + let response = self + .peer + .forward_request(sender, host, request.payload.clone()) + .await?; + self.peer.respond(receipt, response)?; + Ok(()) + } + async fn update_buffer( self: Arc, request: TypedEnvelope, diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 1bbd75867e..b1f513c375 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -570,7 +570,6 @@ impl Buffer { Self { remote_id, replica_id, - history: History::new("".into()), deferred_ops: OperationQueue::new(), deferred_replicas: Default::default(), @@ -1294,13 +1293,13 @@ impl Buffer { pub fn wait_for_edits( &mut self, - edit_ids: &[clock::Local], + edit_ids: impl IntoIterator, ) -> impl 'static + Future { let mut futures = Vec::new(); for edit_id in edit_ids { - if !self.version.observed(*edit_id) { + if !self.version.observed(edit_id) { let (tx, rx) = oneshot::channel(); - self.edit_id_resolvers.entry(*edit_id).or_default().push(tx); + self.edit_id_resolvers.entry(edit_id).or_default().push(tx); futures.push(rx); } }