Apply code actions remotely

Antonio Scandurra created

Change summary

crates/editor/src/editor.rs    |   2 
crates/language/src/buffer.rs  |  53 ++++++++-
crates/language/src/proto.rs   |  64 +++++++++++
crates/project/src/project.rs  | 198 +++++++++++++++++++++++++++--------
crates/project/src/worktree.rs |  37 ++++++
crates/rpc/proto/zed.proto     |  84 +++++++++++----
crates/rpc/src/proto.rs        |   8 +
crates/server/src/rpc.rs       |  48 ++++++++
crates/text/src/text.rs        |   7 
9 files changed, 412 insertions(+), 89 deletions(-)

Detailed changes

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?;

crates/language/src/buffer.rs 🔗

@@ -216,6 +216,13 @@ pub trait File {
         cx: &mut MutableAppContext,
     ) -> Task<Result<Vec<clock::Local>>>;
 
+    fn code_actions(
+        &self,
+        buffer_id: u64,
+        position: Anchor,
+        cx: &mut MutableAppContext,
+    ) -> Task<Result<Vec<CodeAction<Anchor>>>>;
+
     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<Result<Vec<CodeAction<Anchor>>>> {
+        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<Item = clock::Local>,
+        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<Item = clock::Local>,
+    ) -> impl Future<Output = ()> {
+        self.text.wait_for_edits(edit_ids)
+    }
+
     pub fn set_active_selections(
         &mut self,
         selections: Arc<[Selection<Anchor>]>,
@@ -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)

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<Anchor>) -> 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<CodeAction<Anchor>> {
+    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<Anchor>,
+) -> 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<Anchor>, 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,
+    }
+}

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<Buffer>,
         mut action: CodeAction<language::Anchor>,
+        push_to_history: bool,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<HashMap<ModelHandle<Buffer>, Vec<(Range<language::Anchor>, 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::<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(
@@ -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,51 @@ 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(),
+                    },
+                ),
+                Err(error) => rpc.respond_with_error(
+                    receipt,
+                    proto::Error {
+                        message: error.to_string(),
+                    },
+                ),
+            }
+        })
+        .detach_and_log_err(cx);
+        Ok(())
+    }
+
+    fn handle_get_code_actions(
+        &mut self,
+        envelope: TypedEnvelope<proto::GetCodeActions>,
+        rpc: Arc<Client>,
+        cx: &mut ModelContext<Self>,
+    ) -> 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(),
                     },
                 ),
@@ -1985,6 +2040,55 @@ impl Project {
         Ok(())
     }
 
+    fn handle_apply_code_action(
+        &mut self,
+        envelope: TypedEnvelope<proto::ApplyCodeAction>,
+        rpc: Arc<Client>,
+        cx: &mut ModelContext<Self>,
+    ) -> 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<proto::GetDefinition>,

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<Result<Vec<language::CodeAction<Anchor>>>> {
+        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);

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;
-
-        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;
-
-        UpdateContacts update_contacts = 49;
-
-        GetUsers get_users = 50;
-        GetUsersResponse get_users_response = 51;
+        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 = 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 = 53;
+
+        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;

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,

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<Server>,
+        request: TypedEnvelope<proto::GetCodeActions>,
+    ) -> 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<Server>,
+        request: TypedEnvelope<proto::ApplyCodeAction>,
+    ) -> 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<Server>,
         request: TypedEnvelope<proto::UpdateBuffer>,

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<Item = clock::Local>,
     ) -> impl 'static + Future<Output = ()> {
         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);
             }
         }