Add size to DiskState to detect file changes (#49436)

## Summary

This fix addresses the cross-platform root cause identified in issue
#38109 where open buffers go stale or empty when external tools write
files.

## The Problem

The buffer's `file_updated()` method was only comparing `mtime` to
determine if a buffer needed to be reloaded. This caused a race
condition when external tools write files using `std::fs::write()`,
which uses `O_TRUNC` and creates a brief window where the file is 0
bytes:

1. Scanner re-stats → sees 0 bytes, mtime T
2. `file_updated()` sees mtime changed → emits `ReloadNeeded`
3. Buffer reloads to empty, stamps `saved_mtime = T`
4. Tool finishes writing → file has content, but mtime is still T (or
same-second granularity)
5. Scanner re-stats → mtime T matches `saved_mtime` → **no reload
triggered**
6. Buffer permanently stuck empty

## The Fix

Release Notes:

- Add the file `size` to `DiskState::Present`, so that even when mtime
stays the same, size changes (0 → N bytes) will trigger a reload. This
is the same fix that was identified in the issue by @lex00.

## Changes

- `crates/language/src/buffer.rs`: Add `size: u64` to
`DiskState::Present`, add `size()` method
- `crates/worktree/src/worktree.rs`: Pass size when constructing File
and DiskState::Present
- `crates/project/src/buffer_store.rs`: Pass size when constructing File
- `crates/project/src/image_store.rs`: Pass size when constructing File
- `crates/copilot/src/copilot.rs`: Update test mock

## Test plan

- [ ] Open a file in Zed
- [ ] Write to that file from an external tool (e.g., `echo "content" >
file`)
- [ ] Verify the buffer updates correctly without needing to reload

Fixes #38109

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Ben Kunkle <ben.kunkle@gmail.com>
Co-authored-by: Jakub Konka <kubkon@jakubkonka.com>
This commit is contained in:
authored and GitHub committed 2026-03-10 13:55:57 -05:00
1 parent 2f718bac80
commit 058ea457a7
5 files changed
+30 -7

No files matched your search

+1
View File
@@ -1779,6 +1779,7 @@ mod tests {
fn disk_state(&self) -> language::DiskState {
language::DiskState::Present {
mtime: ::fs::MTime::from_seconds_and_nanos(100, 42),
size: 0,
}
}
+13 -3
View File
@@ -435,7 +435,7 @@ pub enum DiskState {
/// File created in Zed that has not been saved.
New,
/// File present on the filesystem.
Present { mtime: MTime },
Present { mtime: MTime, size: u64 },
/// Deleted file that was previously present.
Deleted,
/// An old version of a file that was previously present
@@ -448,7 +448,17 @@ impl DiskState {
pub fn mtime(self) -> Option<MTime> {
match self {
DiskState::New => None,
DiskState::Present { mtime } => Some(mtime),
DiskState::Present { mtime, .. } => Some(mtime),
DiskState::Deleted => None,
DiskState::Historic { .. } => None,
}
}
/// Returns the file's size on disk in bytes.
pub fn size(self) -> Option<u64> {
match self {
DiskState::New => None,
DiskState::Present { size, .. } => Some(size),
DiskState::Deleted => None,
DiskState::Historic { .. } => None,
}
@@ -2377,7 +2387,7 @@ impl Buffer {
};
match file.disk_state() {
DiskState::New => false,
DiskState::Present { mtime } => match self.saved_mtime {
DiskState::Present { mtime, .. } => match self.saved_mtime {
Some(saved_mtime) => {
mtime.bad_is_greater_than(saved_mtime) && self.has_unsaved_edits()
}
+4 -1
View File
@@ -527,7 +527,10 @@ impl LocalBufferStore {
let new_file = if let Some(entry) = snapshot_entry {
File {
disk_state: match entry.mtime {
Some(mtime) => DiskState::Present { mtime },
Some(mtime) => DiskState::Present {
mtime,
size: entry.size,
},
None => old_file.disk_state,
},
is_local: true,
+4 -1
View File
@@ -808,7 +808,10 @@ impl LocalImageStore {
let new_file = if let Some(entry) = snapshot_entry {
worktree::File {
disk_state: match entry.mtime {
Some(mtime) => DiskState::Present { mtime },
Some(mtime) => DiskState::Present {
mtime,
size: entry.size,
},
None => old_file.disk_state,
},
is_local: true,
+8 -2
View File
@@ -1322,6 +1322,7 @@ impl LocalWorktree {
path,
disk_state: DiskState::Present {
mtime: metadata.mtime,
size: metadata.len,
},
is_local: true,
is_private,
@@ -1378,6 +1379,7 @@ impl LocalWorktree {
path,
disk_state: DiskState::Present {
mtime: metadata.mtime,
size: metadata.len,
},
is_local: true,
is_private,
@@ -1575,6 +1577,7 @@ impl LocalWorktree {
path,
disk_state: DiskState::Present {
mtime: metadata.mtime,
size: metadata.len,
},
entry_id: None,
is_local: true,
@@ -3289,7 +3292,10 @@ impl File {
worktree,
path: entry.path.clone(),
disk_state: if let Some(mtime) = entry.mtime {
DiskState::Present { mtime }
DiskState::Present {
mtime,
size: entry.size,
}
} else {
DiskState::New
},
@@ -3318,7 +3324,7 @@ impl File {
} else if proto.is_deleted {
DiskState::Deleted
} else if let Some(mtime) = proto.mtime.map(&Into::into) {
DiskState::Present { mtime }
DiskState::Present { mtime, size: 0 }
} else {
DiskState::New
};