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 <antonio@zed.dev>
This commit is contained in:
Max Brunsfeld 2025-03-28 14:17:16 -07:00 committed by GitHub
parent 5c0adde7bb
commit 9445005bff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 43 additions and 11 deletions

View file

@ -1950,13 +1950,14 @@ impl Buffer {
if self.capability == Capability::ReadOnly { if self.capability == Capability::ReadOnly {
return false; return false;
} }
if self.has_conflict || self.has_unsaved_edits() { if self.has_conflict {
return true; return true;
} }
match self.file.as_ref().map(|f| f.disk_state()) { match self.file.as_ref().map(|f| f.disk_state()) {
Some(DiskState::New) => !self.is_empty(), Some(DiskState::New) | Some(DiskState::Deleted) => {
Some(DiskState::Deleted) => true, !self.is_empty() && self.has_unsaved_edits()
_ => false, }
_ => self.has_unsaved_edits(),
} }
} }
@ -1977,7 +1978,7 @@ impl Buffer {
} }
None => true, None => true,
}, },
DiskState::Deleted => true, DiskState::Deleted => false,
} }
} }

View file

@ -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 events = Arc::new(Mutex::new(Vec::new()));
let buffer2 = project let buffer2 = project
.update(cx, |p, cx| p.open_local_buffer(path!("/dir/file2"), cx)) .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| { buffer2.update(cx, |_, cx| {
cx.subscribe(&buffer2, { cx.subscribe(&buffer2, {
let events = events.clone(); let events = events.clone();
move |_, _, event, _| events.lock().push(event.clone()) move |_, _, event, _| match event {
BufferEvent::Operation { .. } => {}
_ => events.lock().push(event.clone()),
}
}) })
.detach(); .detach();
}); });
@ -3902,12 +3905,37 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
.await .await
.unwrap(); .unwrap();
cx.executor().run_until_parked(); 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!( assert_eq!(
*events.lock(), *events.lock(),
&[ &[
language::BufferEvent::DirtyChanged, language::BufferEvent::Edited,
language::BufferEvent::FileHandleChanged language::BufferEvent::DirtyChanged
] ]
); );
@ -3920,7 +3948,10 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
buffer3.update(cx, |_, cx| { buffer3.update(cx, |_, cx| {
cx.subscribe(&buffer3, { cx.subscribe(&buffer3, {
let events = events.clone(); let events = events.clone();
move |_, _, event, _| events.lock().push(event.clone()) move |_, _, event, _| match event {
BufferEvent::Operation { .. } => {}
_ => events.lock().push(event.clone()),
}
}) })
.detach(); .detach();
}); });