Do not send edits over the wire

Kirill Bulatov created

Change summary

crates/editor/src/editor.rs       |  37 +++++-
crates/project/src/lsp_command.rs |  86 +++++++--------
crates/project/src/project.rs     | 179 ++++++++++++++++++++++++++++----
crates/rpc/proto/zed.proto        |   9 -
4 files changed, 225 insertions(+), 86 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -2123,9 +2123,10 @@ impl Editor {
             this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
 
             // When buffer contents is updated and caret is moved, try triggering on type formatting.
-            if settings::get::<EditorSettings>(cx).use_on_type_format && text.len() == 1 {
-                let input_char = text.chars().next().expect("single char input");
-                if let Some(on_type_format_task) = this.trigger_on_type_format(input_char, cx) {
+            if settings::get::<EditorSettings>(cx).use_on_type_format {
+                if let Some(on_type_format_task) =
+                    this.trigger_on_type_formatting(text.to_string(), cx)
+                {
                     on_type_format_task.detach_and_log_err(cx);
                 }
             }
@@ -2508,20 +2509,42 @@ impl Editor {
         }
     }
 
-    fn trigger_on_type_format(
+    fn trigger_on_type_formatting(
         &self,
-        input: char,
+        input: String,
         cx: &mut ViewContext<Self>,
     ) -> Option<Task<Result<()>>> {
+        if input.len() != 1 {
+            return None;
+        }
+
+        let transaction_title = format!("OnTypeFormatting after {input}");
+        let workspace = self.workspace(cx)?;
         let project = self.project.as_ref()?;
         let position = self.selections.newest_anchor().head();
         let (buffer, buffer_position) = self
             .buffer
             .read(cx)
             .text_anchor_for_position(position.clone(), cx)?;
+        let on_type_formatting = project.update(cx, |project, cx| {
+            project.on_type_format(buffer, buffer_position, input, cx)
+        });
+
+        Some(cx.spawn(|editor, mut cx| async move {
+            let project_transaction = on_type_formatting.await?;
+            Self::open_project_transaction(
+                &editor,
+                workspace.downgrade(),
+                project_transaction,
+                transaction_title,
+                cx.clone(),
+            )
+            .await?;
 
-        Some(project.update(cx, |project, cx| {
-            project.on_type_format(buffer.clone(), buffer_position, input, cx)
+            editor.update(&mut cx, |editor, cx| {
+                editor.refresh_document_highlights(cx);
+            })?;
+            Ok(())
         }))
     }
 

crates/project/src/lsp_command.rs 🔗

@@ -2,7 +2,7 @@ use crate::{
     DocumentHighlight, Hover, HoverBlock, HoverBlockKind, Location, LocationLink, Project,
     ProjectTransaction,
 };
-use anyhow::{anyhow, Context, Result};
+use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 use client::proto::{self, PeerId};
 use fs::LineEnding;
@@ -123,6 +123,7 @@ pub(crate) struct OnTypeFormatting {
     pub position: PointUtf16,
     pub trigger: String,
     pub options: FormattingOptions,
+    pub push_to_history: bool,
 }
 
 pub(crate) struct FormattingOptions {
@@ -1627,7 +1628,7 @@ impl LspCommand for GetCodeActions {
 
 #[async_trait(?Send)]
 impl LspCommand for OnTypeFormatting {
-    type Response = Vec<(Range<Anchor>, String)>;
+    type Response = ProjectTransaction;
     type LspRequest = lsp::request::OnTypeFormatting;
     type ProtoRequest = proto::OnTypeFormatting;
 
@@ -1667,14 +1668,23 @@ impl LspCommand for OnTypeFormatting {
         buffer: ModelHandle<Buffer>,
         server_id: LanguageServerId,
         mut cx: AsyncAppContext,
-    ) -> Result<Vec<(Range<Anchor>, String)>> {
-        cx.update(|cx| {
-            project.update(cx, |project, cx| {
-                project.edits_from_lsp(&buffer, message.into_iter().flatten(), server_id, None, cx)
-            })
-        })
-        .await
-        .context("LSP edits conversion")
+    ) -> Result<ProjectTransaction> {
+        if let Some(edits) = message {
+            let (lsp_adapter, lsp_server) =
+                language_server_for_buffer(&project, &buffer, server_id, &mut cx)?;
+            Project::deserialize_edits(
+                project,
+                buffer,
+                edits,
+                self.push_to_history,
+                lsp_adapter,
+                lsp_server,
+                &mut cx,
+            )
+            .await
+        } else {
+            Ok(ProjectTransaction::default())
+        }
     }
 
     fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::OnTypeFormatting {
@@ -1714,58 +1724,38 @@ impl LspCommand for OnTypeFormatting {
             position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)),
             trigger: message.trigger.clone(),
             options: lsp_formatting_options(tab_size.get()).into(),
+            push_to_history: false,
         })
     }
 
     fn response_to_proto(
-        response: Vec<(Range<Anchor>, String)>,
-        _: &mut Project,
-        _: PeerId,
-        buffer_version: &clock::Global,
-        _: &mut AppContext,
+        response: ProjectTransaction,
+        project: &mut Project,
+        peer_id: PeerId,
+        _: &clock::Global,
+        cx: &mut AppContext,
     ) -> proto::OnTypeFormattingResponse {
+        let transaction = project.serialize_project_transaction_for_peer(response, peer_id, cx);
         proto::OnTypeFormattingResponse {
-            entries: response
-                .into_iter()
-                .map(
-                    |(response_range, new_text)| proto::OnTypeFormattingResponseEntry {
-                        start: Some(language::proto::serialize_anchor(&response_range.start)),
-                        end: Some(language::proto::serialize_anchor(&response_range.end)),
-                        new_text,
-                    },
-                )
-                .collect(),
-            version: serialize_version(&buffer_version),
+            transaction: Some(transaction),
         }
     }
 
     async fn response_from_proto(
         self,
         message: proto::OnTypeFormattingResponse,
-        _: ModelHandle<Project>,
-        buffer: ModelHandle<Buffer>,
+        project: ModelHandle<Project>,
+        _: ModelHandle<Buffer>,
         mut cx: AsyncAppContext,
-    ) -> Result<Vec<(Range<Anchor>, String)>> {
-        buffer
-            .update(&mut cx, |buffer, _| {
-                buffer.wait_for_version(deserialize_version(&message.version))
-            })
-            .await?;
-        message
-            .entries
-            .into_iter()
-            .map(|entry| {
-                let start = entry
-                    .start
-                    .and_then(language::proto::deserialize_anchor)
-                    .ok_or_else(|| anyhow!("invalid start"))?;
-                let end = entry
-                    .end
-                    .and_then(language::proto::deserialize_anchor)
-                    .ok_or_else(|| anyhow!("invalid end"))?;
-                Ok((start..end, entry.new_text))
+    ) -> Result<ProjectTransaction> {
+        let message = message
+            .transaction
+            .ok_or_else(|| anyhow!("missing transaction"))?;
+        project
+            .update(&mut cx, |project, cx| {
+                project.deserialize_project_transaction(message, self.push_to_history, cx)
             })
-            .collect()
+            .await
     }
 
     fn buffer_id_from_proto(message: &proto::OnTypeFormatting) -> u64 {

crates/project/src/project.rs 🔗

@@ -417,6 +417,7 @@ impl Project {
         client.add_model_request_handler(Self::handle_delete_project_entry);
         client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion);
         client.add_model_request_handler(Self::handle_apply_code_action);
+        client.add_model_request_handler(Self::handle_on_type_formatting);
         client.add_model_request_handler(Self::handle_reload_buffers);
         client.add_model_request_handler(Self::handle_synchronize_buffers);
         client.add_model_request_handler(Self::handle_format_buffers);
@@ -429,7 +430,6 @@ impl Project {
         client.add_model_request_handler(Self::handle_lsp_command::<GetReferences>);
         client.add_model_request_handler(Self::handle_lsp_command::<PrepareRename>);
         client.add_model_request_handler(Self::handle_lsp_command::<PerformRename>);
-        client.add_model_request_handler(Self::handle_lsp_command::<OnTypeFormatting>);
         client.add_model_request_handler(Self::handle_search_project);
         client.add_model_request_handler(Self::handle_get_project_symbols);
         client.add_model_request_handler(Self::handle_open_buffer_for_symbol);
@@ -4035,6 +4035,118 @@ impl Project {
         }
     }
 
+    fn apply_on_type_formatting(
+        &self,
+        buffer: ModelHandle<Buffer>,
+        position: Anchor,
+        trigger: String,
+        push_to_history: bool,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<ProjectTransaction>> {
+        if self.is_local() {
+            cx.spawn(|this, mut cx| async move {
+                // Do not allow multiple concurrent formatting requests for the
+                // same buffer.
+                this.update(&mut cx, |this, cx| {
+                    this.buffers_being_formatted
+                        .insert(buffer.read(cx).remote_id())
+                });
+
+                let _cleanup = defer({
+                    let this = this.clone();
+                    let mut cx = cx.clone();
+                    let closure_buffer = buffer.clone();
+                    move || {
+                        this.update(&mut cx, |this, cx| {
+                            this.buffers_being_formatted
+                                .remove(&closure_buffer.read(cx).remote_id());
+                        });
+                    }
+                });
+
+                buffer
+                    .update(&mut cx, |buffer, _| {
+                        buffer.wait_for_edits(Some(position.timestamp))
+                    })
+                    .await?;
+                this.update(&mut cx, |this, cx| {
+                    let position = position.to_point_utf16(buffer.read(cx));
+                    this.on_type_format(buffer, position, trigger, cx)
+                })
+                .await
+            })
+        } else if let Some(project_id) = self.remote_id() {
+            let client = self.client.clone();
+            let request = proto::OnTypeFormatting {
+                project_id,
+                buffer_id: buffer.read(cx).remote_id(),
+                position: Some(serialize_anchor(&position)),
+                trigger,
+                version: serialize_version(&buffer.read(cx).version()),
+            };
+            cx.spawn(|this, mut cx| async move {
+                let response = client
+                    .request(request)
+                    .await?
+                    .transaction
+                    .ok_or_else(|| anyhow!("missing transaction"))?;
+                this.update(&mut cx, |this, cx| {
+                    this.deserialize_project_transaction(response, push_to_history, cx)
+                })
+                .await
+            })
+        } else {
+            Task::ready(Err(anyhow!("project does not have a remote id")))
+        }
+    }
+
+    async fn deserialize_edits(
+        this: ModelHandle<Self>,
+        buffer_to_edit: ModelHandle<Buffer>,
+        edits: Vec<lsp::TextEdit>,
+        push_to_history: bool,
+        _: Arc<CachedLspAdapter>,
+        language_server: Arc<LanguageServer>,
+        cx: &mut AsyncAppContext,
+    ) -> Result<ProjectTransaction> {
+        let edits = this
+            .update(cx, |this, cx| {
+                this.edits_from_lsp(
+                    &buffer_to_edit,
+                    edits,
+                    language_server.server_id(),
+                    None,
+                    cx,
+                )
+            })
+            .await?;
+
+        let transaction = buffer_to_edit.update(cx, |buffer, cx| {
+            buffer.finalize_last_transaction();
+            buffer.start_transaction();
+            for (range, text) in edits {
+                buffer.edit([(range, text)], None, cx);
+            }
+
+            if buffer.end_transaction(cx).is_some() {
+                let transaction = buffer.finalize_last_transaction().unwrap().clone();
+                if !push_to_history {
+                    buffer.forget_transaction(transaction.id);
+                }
+                Some(transaction)
+            } else {
+                None
+            }
+        });
+
+        let mut project_transaction = ProjectTransaction::default();
+        if let Some(transaction) = transaction {
+            project_transaction.0.insert(buffer_to_edit, transaction);
+        }
+
+        Ok(project_transaction)
+    }
+
     async fn deserialize_workspace_edit(
         this: ModelHandle<Self>,
         edit: lsp::WorkspaceEdit,
@@ -4204,39 +4316,24 @@ impl Project {
         &self,
         buffer: ModelHandle<Buffer>,
         position: T,
-        input: char,
+        trigger: String,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<()>> {
+    ) -> Task<Result<ProjectTransaction>> {
         let tab_size = buffer.read_with(cx, |buffer, cx| {
             let language_name = buffer.language().map(|language| language.name());
             language_settings(language_name.as_deref(), cx).tab_size
         });
         let position = position.to_point_utf16(buffer.read(cx));
-        let edits_task = self.request_lsp(
+        self.request_lsp(
             buffer.clone(),
             OnTypeFormatting {
                 position,
-                trigger: input.to_string(),
+                trigger,
                 options: lsp_command::lsp_formatting_options(tab_size.get()).into(),
+                push_to_history: true,
             },
             cx,
-        );
-
-        cx.spawn(|_project, mut cx| async move {
-            let edits = edits_task
-                .await
-                .context("requesting OnTypeFormatting edits for char '{new_char}'")?;
-
-            if !edits.is_empty() {
-                cx.update(|cx| {
-                    buffer.update(cx, |buffer, cx| {
-                        buffer.edit(edits, None, cx);
-                    });
-                });
-            }
-
-            Ok(())
-        })
+        )
     }
 
     #[allow(clippy::type_complexity)]
@@ -5809,6 +5906,42 @@ impl Project {
         })
     }
 
+    async fn handle_on_type_formatting(
+        this: ModelHandle<Self>,
+        envelope: TypedEnvelope<proto::OnTypeFormatting>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::OnTypeFormattingResponse> {
+        let sender_id = envelope.original_sender_id()?;
+        let on_type_formatting = this.update(&mut cx, |this, cx| {
+            let buffer = this
+                .opened_buffers
+                .get(&envelope.payload.buffer_id)
+                .and_then(|buffer| buffer.upgrade(cx))
+                .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?;
+            let position = envelope
+                .payload
+                .position
+                .and_then(deserialize_anchor)
+                .ok_or_else(|| anyhow!("invalid position"))?;
+            Ok::<_, anyhow::Error>(this.apply_on_type_formatting(
+                buffer,
+                position,
+                envelope.payload.trigger.clone(),
+                false,
+                cx,
+            ))
+        })?;
+
+        let project_transaction = on_type_formatting.await?;
+        let project_transaction = this.update(&mut cx, |this, cx| {
+            this.serialize_project_transaction_for_peer(project_transaction, sender_id, cx)
+        });
+        Ok(proto::OnTypeFormattingResponse {
+            transaction: Some(project_transaction),
+        })
+    }
+
     async fn handle_lsp_command<T: LspCommand>(
         this: ModelHandle<Self>,
         envelope: TypedEnvelope<T::ProtoRequest>,
@@ -6379,7 +6512,7 @@ impl Project {
     }
 
     #[allow(clippy::type_complexity)]
-    pub fn edits_from_lsp(
+    fn edits_from_lsp(
         &mut self,
         buffer: &ModelHandle<Buffer>,
         lsp_edits: impl 'static + Send + IntoIterator<Item = lsp::TextEdit>,

crates/rpc/proto/zed.proto 🔗

@@ -682,14 +682,7 @@ message OnTypeFormatting {
 }
 
 message OnTypeFormattingResponse {
-    repeated OnTypeFormattingResponseEntry entries = 1;
-    repeated VectorClockEntry version = 2;
-}
-
-message OnTypeFormattingResponseEntry {
-    Anchor start = 1;
-    Anchor end = 2;
-    string new_text = 3;
+    ProjectTransaction transaction = 1;
 }
 
 message PerformRenameResponse {