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:
1 parent
2f718bac80
commit
058ea457a7
5 files changed
+30
-7
No files matched your search
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user