From 9445005bffe6c2bd4996deb86768e82cfc42a31d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 28 Mar 2025 14:17:16 -0700 Subject: [PATCH] Don't consider empty deleted files to be dirty or conflicting (#27701) When a file is deleted outside of Zed, but it doesn't have any unsaved changes, it shouldn't be considered "dirty" (prompting you before you close it). Release Notes: - Fixed an bug where unchanged buffers were marked as conflicting if their files were deleted outside of Zed. --------- Co-authored-by: Antonio --- crates/language/src/buffer.rs | 11 ++++---- crates/project/src/project_tests.rs | 43 +++++++++++++++++++++++++---- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index fb68ab1316..d074351d98 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1950,13 +1950,14 @@ impl Buffer { if self.capability == Capability::ReadOnly { return false; } - if self.has_conflict || self.has_unsaved_edits() { + if self.has_conflict { return true; } match self.file.as_ref().map(|f| f.disk_state()) { - Some(DiskState::New) => !self.is_empty(), - Some(DiskState::Deleted) => true, - _ => false, + Some(DiskState::New) | Some(DiskState::Deleted) => { + !self.is_empty() && self.has_unsaved_edits() + } + _ => self.has_unsaved_edits(), } } @@ -1977,7 +1978,7 @@ impl Buffer { } None => true, }, - DiskState::Deleted => true, + DiskState::Deleted => false, } } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index caa59c11c5..c2547df27e 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -3884,7 +3884,7 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { ] ); - // When a file is deleted, the buffer is considered dirty. + // When a file is deleted, it is not considered dirty. let events = Arc::new(Mutex::new(Vec::new())); let buffer2 = project .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file2"), cx)) @@ -3893,7 +3893,10 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { buffer2.update(cx, |_, cx| { cx.subscribe(&buffer2, { let events = events.clone(); - move |_, _, event, _| events.lock().push(event.clone()) + move |_, _, event, _| match event { + BufferEvent::Operation { .. } => {} + _ => events.lock().push(event.clone()), + } }) .detach(); }); @@ -3902,12 +3905,37 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { .await .unwrap(); cx.executor().run_until_parked(); - buffer2.update(cx, |buffer, _| assert!(buffer.is_dirty())); + buffer2.update(cx, |buffer, _| assert!(!buffer.is_dirty())); + assert_eq!( + mem::take(&mut *events.lock()), + &[language::BufferEvent::FileHandleChanged] + ); + + // Buffer becomes dirty when edited. + buffer2.update(cx, |buffer, cx| { + buffer.edit([(2..3, "")], None, cx); + assert_eq!(buffer.is_dirty(), true); + }); + assert_eq!( + mem::take(&mut *events.lock()), + &[ + language::BufferEvent::Edited, + language::BufferEvent::DirtyChanged + ] + ); + + // Buffer becomes clean again when all of its content is removed, because + // the file was deleted. + buffer2.update(cx, |buffer, cx| { + buffer.edit([(0..2, "")], None, cx); + assert_eq!(buffer.is_empty(), true); + assert_eq!(buffer.is_dirty(), false); + }); assert_eq!( *events.lock(), &[ - language::BufferEvent::DirtyChanged, - language::BufferEvent::FileHandleChanged + language::BufferEvent::Edited, + language::BufferEvent::DirtyChanged ] ); @@ -3920,7 +3948,10 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { buffer3.update(cx, |_, cx| { cx.subscribe(&buffer3, { let events = events.clone(); - move |_, _, event, _| events.lock().push(event.clone()) + move |_, _, event, _| match event { + BufferEvent::Operation { .. } => {} + _ => events.lock().push(event.clone()), + } }) .detach(); });