Add failing test for unwanted reload during format-on-save

Max Brunsfeld created

Change summary

crates/project/tests/integration/project_tests.rs | 101 +++++++++++++++++
1 file changed, 101 insertions(+)

Detailed changes

crates/project/tests/integration/project_tests.rs 🔗

@@ -6094,6 +6094,107 @@ async fn test_dirty_buffer_reloads_after_undo(cx: &mut gpui::TestAppContext) {
     });
 }
 
+#[gpui::test]
+async fn test_save_does_not_reload_when_format_removes_user_edits(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree(
+        path!("/dir"),
+        json!({
+            "file.txt": "hello\nworld\n",
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
+    let buffer = project
+        .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file.txt"), cx))
+        .await
+        .unwrap();
+
+    buffer.read_with(cx, |buffer, _| {
+        assert_eq!(buffer.text(), "hello\nworld\n");
+        assert!(!buffer.is_dirty());
+    });
+
+    // User adds trailing whitespace — the only edit.
+    buffer.update(cx, |buffer, cx| {
+        buffer.edit([(5..5, "  ")], None, cx);
+    });
+
+    buffer.read_with(cx, |buffer, _| {
+        assert_eq!(buffer.text(), "hello  \nworld\n");
+        assert!(buffer.is_dirty());
+    });
+
+    // An external tool writes different content to the file while the buffer is dirty.
+    fs.save(
+        path!("/dir/file.txt").as_ref(),
+        &"EXTERNAL CONTENT\n".into(),
+        Default::default(),
+    )
+    .await
+    .unwrap();
+    cx.executor().run_until_parked();
+
+    // The buffer has a conflict: file mtime changed and buffer has unsaved edits.
+    buffer.read_with(cx, |buffer, _| {
+        assert!(buffer.is_dirty());
+        assert!(buffer.has_conflict());
+        assert_eq!(buffer.text(), "hello  \nworld\n");
+        assert_ne!(
+            buffer.file().unwrap().disk_state().mtime(),
+            buffer.saved_mtime(),
+            "disk mtime should differ from saved mtime after external write"
+        );
+    });
+
+    // The user triggers a save, which formats the buffer then saves it.
+    let buffers = [buffer.clone()].into_iter().collect::<HashSet<_>>();
+    project
+        .update(cx, |project, cx| {
+            project.format(
+                buffers,
+                project::lsp_store::LspFormatTarget::Buffers,
+                true,
+                project::lsp_store::FormatTrigger::Save,
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+
+    // After formatting, the trailing whitespace was removed.
+    // The buffer text should match what will be saved to disk.
+    let formatted_text = buffer.read_with(cx, |buffer, _| buffer.text());
+    assert_eq!(formatted_text, "hello\nworld\n");
+    cx.executor().run_until_parked();
+
+    // The buffer text must still be the formatted text — not reloaded from disk.
+    buffer.read_with(cx, |buffer, _| {
+        assert_eq!(
+            buffer.text(),
+            formatted_text,
+            "buffer should not have been reloaded from disk during format-on-save"
+        );
+    });
+
+    // Now save to disk.
+    project
+        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
+        .await
+        .unwrap();
+    cx.executor().run_until_parked();
+
+    // After save, the buffer should be clean and match disk.
+    buffer.read_with(cx, |buffer, _| {
+        assert_eq!(buffer.text(), "hello\nworld\n");
+        assert!(!buffer.is_dirty());
+        assert!(!buffer.has_conflict());
+    });
+}
+
 #[gpui::test]
 async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
     init_test(cx);