Have proper undo for both client and host

Kirill Bulatov created

Change summary

crates/collab/src/tests/integration_tests.rs | 45 ++++++++++++++++++++-
crates/editor/src/editor.rs                  | 30 +++++++++++--
2 files changed, 66 insertions(+), 9 deletions(-)

Detailed changes

crates/collab/src/tests/integration_tests.rs 🔗

@@ -7464,9 +7464,6 @@ async fn test_on_input_format_from_host_to_guest(
             }]))
         },
     );
-    // .next()
-    // .await
-    // .unwrap();
 
     // Open the buffer on the guest and see that the formattings worked
     let buffer_b = project_b
@@ -7486,6 +7483,27 @@ async fn test_on_input_format_from_host_to_guest(
     buffer_b.read_with(cx_b, |buffer, _| {
         assert_eq!(buffer.text(), "fn main() { a>~< }")
     });
+
+    // Undo should remove LSP edits first
+    editor_a.update(cx_a, |editor, cx| {
+        assert_eq!(editor.text(cx), "fn main() { a>~< }");
+        editor.undo(&Undo, cx);
+        assert_eq!(editor.text(cx), "fn main() { a> }");
+    });
+    cx_b.foreground().run_until_parked();
+    buffer_b.read_with(cx_b, |buffer, _| {
+        assert_eq!(buffer.text(), "fn main() { a> }")
+    });
+
+    editor_a.update(cx_a, |editor, cx| {
+        assert_eq!(editor.text(cx), "fn main() { a> }");
+        editor.undo(&Undo, cx);
+        assert_eq!(editor.text(cx), "fn main() { a }");
+    });
+    cx_b.foreground().run_until_parked();
+    buffer_b.read_with(cx_b, |buffer, _| {
+        assert_eq!(buffer.text(), "fn main() { a }")
+    });
 }
 
 #[gpui::test(iterations = 10)]
@@ -7595,6 +7613,27 @@ async fn test_on_input_format_from_guest_to_host(
     buffer_a.read_with(cx_a, |buffer, _| {
         assert_eq!(buffer.text(), "fn main() { a:~: }")
     });
+
+    // Undo should remove LSP edits first
+    editor_b.update(cx_b, |editor, cx| {
+        assert_eq!(editor.text(cx), "fn main() { a:~: }");
+        editor.undo(&Undo, cx);
+        assert_eq!(editor.text(cx), "fn main() { a: }");
+    });
+    cx_a.foreground().run_until_parked();
+    buffer_a.read_with(cx_a, |buffer, _| {
+        assert_eq!(buffer.text(), "fn main() { a: }")
+    });
+
+    editor_b.update(cx_b, |editor, cx| {
+        assert_eq!(editor.text(cx), "fn main() { a: }");
+        editor.undo(&Undo, cx);
+        assert_eq!(editor.text(cx), "fn main() { a }");
+    });
+    cx_a.foreground().run_until_parked();
+    buffer_a.read_with(cx_a, |buffer, _| {
+        assert_eq!(buffer.text(), "fn main() { a }")
+    });
 }
 
 #[derive(Debug, Eq, PartialEq)]

crates/editor/src/editor.rs 🔗

@@ -2524,15 +2524,33 @@ impl Editor {
             .buffer
             .read(cx)
             .text_anchor_for_position(position.clone(), cx)?;
+
+        // OnTypeFormatting retuns a list of edits, no need to pass them between Zed instances,
+        // hence we do LSP request & edit on host side only — add formats to host's history.
+        let push_to_lsp_host_history = true;
+        // If this is not the host, append its history with new edits.
+        let push_to_client_history = project.read(cx).is_remote();
+
         let on_type_formatting = project.update(cx, |project, cx| {
-            project.on_type_format(buffer, buffer_position, input, true, cx)
+            project.on_type_format(
+                buffer.clone(),
+                buffer_position,
+                input,
+                push_to_lsp_host_history,
+                cx,
+            )
         });
-
         Some(cx.spawn(|editor, mut cx| async move {
-            on_type_formatting.await?;
-            editor.update(&mut cx, |editor, cx| {
-                editor.refresh_document_highlights(cx);
-            })?;
+            if let Some(transaction) = on_type_formatting.await? {
+                if push_to_client_history {
+                    buffer.update(&mut cx, |buffer, _| {
+                        buffer.push_transaction(transaction, Instant::now());
+                    });
+                }
+                editor.update(&mut cx, |editor, cx| {
+                    editor.refresh_document_highlights(cx);
+                })?;
+            }
             Ok(())
         }))
     }