Add File.disk_state
enum to clarify filesystem states (#20776)
Motivation for this is to make things more understandable while figuring out #20775. This is intended to be a refactoring that does not affect behavior, but there are a few tricky spots: * Previously `File.mtime()` (now `File.disk_state().mtime()`) would return last known modification time for deleted files. Looking at uses, I believe this will not affect anything. If there are behavior changes here I believe they would be improvements. * `BufferEvent::DirtyChanged` is now only emitted if dirtiness actually changed, rather than if it may have changed. This should only be an efficiency improvement. Release Notes: - N/A Co-authored-by: Mikayla Maki <mikayla@zed.dev>
This commit is contained in:
parent
df1d0dec0a
commit
d99f5fe83e
10 changed files with 161 additions and 134 deletions
|
@ -388,7 +388,7 @@ pet-core = { git = "https://github.com/microsoft/python-environment-tools.git",
|
||||||
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||||
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||||
postage = { version = "0.5", features = ["futures-traits"] }
|
postage = { version = "0.5", features = ["futures-traits"] }
|
||||||
pretty_assertions = "1.3.0"
|
pretty_assertions = { version = "1.3.0", features = ["unstable"] }
|
||||||
profiling = "1"
|
profiling = "1"
|
||||||
prost = "0.9"
|
prost = "0.9"
|
||||||
prost-build = "0.9"
|
prost-build = "0.9"
|
||||||
|
|
|
@ -1323,11 +1323,8 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||||
match (host_file, guest_file) {
|
match (host_file, guest_file) {
|
||||||
(Some(host_file), Some(guest_file)) => {
|
(Some(host_file), Some(guest_file)) => {
|
||||||
assert_eq!(guest_file.path(), host_file.path());
|
assert_eq!(guest_file.path(), host_file.path());
|
||||||
assert_eq!(guest_file.is_deleted(), host_file.is_deleted());
|
assert_eq!(guest_file.disk_state(), host_file.disk_state(),
|
||||||
assert_eq!(
|
"guest {} disk_state does not match host {} for path {:?} in project {}",
|
||||||
guest_file.mtime(),
|
|
||||||
host_file.mtime(),
|
|
||||||
"guest {} mtime does not match host {} for path {:?} in project {}",
|
|
||||||
guest_user_id,
|
guest_user_id,
|
||||||
host_user_id,
|
host_user_id,
|
||||||
guest_file.path(),
|
guest_file.path(),
|
||||||
|
|
|
@ -1229,8 +1229,10 @@ mod tests {
|
||||||
Some(self)
|
Some(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mtime(&self) -> Option<std::time::SystemTime> {
|
fn disk_state(&self) -> language::DiskState {
|
||||||
unimplemented!()
|
language::DiskState::Present {
|
||||||
|
mtime: std::time::UNIX_EPOCH,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn path(&self) -> &Arc<Path> {
|
fn path(&self) -> &Arc<Path> {
|
||||||
|
@ -1245,10 +1247,6 @@ mod tests {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_deleted(&self) -> bool {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn std::any::Any {
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,8 @@ use gpui::{
|
||||||
VisualContext, WeakView, WindowContext,
|
VisualContext, WeakView, WindowContext,
|
||||||
};
|
};
|
||||||
use language::{
|
use language::{
|
||||||
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, Point, SelectionGoal,
|
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, DiskState, Point,
|
||||||
|
SelectionGoal,
|
||||||
};
|
};
|
||||||
use lsp::DiagnosticSeverity;
|
use lsp::DiagnosticSeverity;
|
||||||
use multi_buffer::AnchorRangeExt;
|
use multi_buffer::AnchorRangeExt;
|
||||||
|
@ -641,7 +642,7 @@ impl Item for Editor {
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.as_singleton()
|
.as_singleton()
|
||||||
.and_then(|buffer| buffer.read(cx).file())
|
.and_then(|buffer| buffer.read(cx).file())
|
||||||
.map_or(false, |file| file.is_deleted() && file.is_created());
|
.map_or(false, |file| file.disk_state() == DiskState::Deleted);
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
|
|
|
@ -104,6 +104,7 @@ pub struct Buffer {
|
||||||
text: TextBuffer,
|
text: TextBuffer,
|
||||||
diff_base: Option<BufferDiffBase>,
|
diff_base: Option<BufferDiffBase>,
|
||||||
git_diff: git::diff::BufferDiff,
|
git_diff: git::diff::BufferDiff,
|
||||||
|
/// Filesystem state, `None` when there is no path.
|
||||||
file: Option<Arc<dyn File>>,
|
file: Option<Arc<dyn File>>,
|
||||||
/// The mtime of the file when this buffer was last loaded from
|
/// The mtime of the file when this buffer was last loaded from
|
||||||
/// or saved to disk.
|
/// or saved to disk.
|
||||||
|
@ -371,8 +372,9 @@ pub trait File: Send + Sync {
|
||||||
self.as_local().is_some()
|
self.as_local().is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the file's mtime.
|
/// Returns whether the file is new, exists in storage, or has been deleted. Includes metadata
|
||||||
fn mtime(&self) -> Option<SystemTime>;
|
/// only available in some states, such as modification time.
|
||||||
|
fn disk_state(&self) -> DiskState;
|
||||||
|
|
||||||
/// Returns the path of this file relative to the worktree's root directory.
|
/// Returns the path of this file relative to the worktree's root directory.
|
||||||
fn path(&self) -> &Arc<Path>;
|
fn path(&self) -> &Arc<Path>;
|
||||||
|
@ -390,14 +392,6 @@ pub trait File: Send + Sync {
|
||||||
/// This is needed for looking up project-specific settings.
|
/// This is needed for looking up project-specific settings.
|
||||||
fn worktree_id(&self, cx: &AppContext) -> WorktreeId;
|
fn worktree_id(&self, cx: &AppContext) -> WorktreeId;
|
||||||
|
|
||||||
/// Returns whether the file has been deleted.
|
|
||||||
fn is_deleted(&self) -> bool;
|
|
||||||
|
|
||||||
/// Returns whether the file existed on disk at one point
|
|
||||||
fn is_created(&self) -> bool {
|
|
||||||
self.mtime().is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts this file into an [`Any`] trait object.
|
/// Converts this file into an [`Any`] trait object.
|
||||||
fn as_any(&self) -> &dyn Any;
|
fn as_any(&self) -> &dyn Any;
|
||||||
|
|
||||||
|
@ -408,6 +402,34 @@ pub trait File: Send + Sync {
|
||||||
fn is_private(&self) -> bool;
|
fn is_private(&self) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The file's storage status - whether it's stored (`Present`), and if so when it was last
|
||||||
|
/// modified. In the case where the file is not stored, it can be either `New` or `Deleted`. In the
|
||||||
|
/// UI these two states are distinguished. For example, the buffer tab does not display a deletion
|
||||||
|
/// indicator for new files.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum DiskState {
|
||||||
|
/// File created in Zed that has not been saved.
|
||||||
|
New,
|
||||||
|
/// File present on the filesystem.
|
||||||
|
Present {
|
||||||
|
/// Last known mtime (modification time).
|
||||||
|
mtime: SystemTime,
|
||||||
|
},
|
||||||
|
/// Deleted file that was previously present.
|
||||||
|
Deleted,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiskState {
|
||||||
|
/// Returns the file's last known modification time on disk.
|
||||||
|
pub fn mtime(self) -> Option<SystemTime> {
|
||||||
|
match self {
|
||||||
|
DiskState::New => None,
|
||||||
|
DiskState::Present { mtime } => Some(mtime),
|
||||||
|
DiskState::Deleted => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The file associated with a buffer, in the case where the file is on the local disk.
|
/// The file associated with a buffer, in the case where the file is on the local disk.
|
||||||
pub trait LocalFile: File {
|
pub trait LocalFile: File {
|
||||||
/// Returns the absolute path of this file
|
/// Returns the absolute path of this file
|
||||||
|
@ -750,7 +772,7 @@ impl Buffer {
|
||||||
file: Option<Arc<dyn File>>,
|
file: Option<Arc<dyn File>>,
|
||||||
capability: Capability,
|
capability: Capability,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let saved_mtime = file.as_ref().and_then(|file| file.mtime());
|
let saved_mtime = file.as_ref().and_then(|file| file.disk_state().mtime());
|
||||||
let snapshot = buffer.snapshot();
|
let snapshot = buffer.snapshot();
|
||||||
let git_diff = git::diff::BufferDiff::new(&snapshot);
|
let git_diff = git::diff::BufferDiff::new(&snapshot);
|
||||||
let syntax_map = Mutex::new(SyntaxMap::new(&snapshot));
|
let syntax_map = Mutex::new(SyntaxMap::new(&snapshot));
|
||||||
|
@ -1014,7 +1036,7 @@ impl Buffer {
|
||||||
self.reload_task = Some(cx.spawn(|this, mut cx| async move {
|
self.reload_task = Some(cx.spawn(|this, mut cx| async move {
|
||||||
let Some((new_mtime, new_text)) = this.update(&mut cx, |this, cx| {
|
let Some((new_mtime, new_text)) = this.update(&mut cx, |this, cx| {
|
||||||
let file = this.file.as_ref()?.as_local()?;
|
let file = this.file.as_ref()?.as_local()?;
|
||||||
Some((file.mtime(), file.load(cx)))
|
Some((file.disk_state().mtime(), file.load(cx)))
|
||||||
})?
|
})?
|
||||||
else {
|
else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
@ -1070,6 +1092,7 @@ impl Buffer {
|
||||||
/// Updates the [`File`] backing this buffer. This should be called when
|
/// Updates the [`File`] backing this buffer. This should be called when
|
||||||
/// the file has changed or has been deleted.
|
/// the file has changed or has been deleted.
|
||||||
pub fn file_updated(&mut self, new_file: Arc<dyn File>, cx: &mut ModelContext<Self>) {
|
pub fn file_updated(&mut self, new_file: Arc<dyn File>, cx: &mut ModelContext<Self>) {
|
||||||
|
let was_dirty = self.is_dirty();
|
||||||
let mut file_changed = false;
|
let mut file_changed = false;
|
||||||
|
|
||||||
if let Some(old_file) = self.file.as_ref() {
|
if let Some(old_file) = self.file.as_ref() {
|
||||||
|
@ -1077,21 +1100,12 @@ impl Buffer {
|
||||||
file_changed = true;
|
file_changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if new_file.is_deleted() {
|
let old_state = old_file.disk_state();
|
||||||
if !old_file.is_deleted() {
|
let new_state = new_file.disk_state();
|
||||||
file_changed = true;
|
if old_state != new_state {
|
||||||
if !self.is_dirty() {
|
file_changed = true;
|
||||||
cx.emit(BufferEvent::DirtyChanged);
|
if !was_dirty && matches!(new_state, DiskState::Present { .. }) {
|
||||||
}
|
cx.emit(BufferEvent::ReloadNeeded)
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let new_mtime = new_file.mtime();
|
|
||||||
if new_mtime != old_file.mtime() {
|
|
||||||
file_changed = true;
|
|
||||||
|
|
||||||
if !self.is_dirty() {
|
|
||||||
cx.emit(BufferEvent::ReloadNeeded);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -1101,6 +1115,9 @@ impl Buffer {
|
||||||
self.file = Some(new_file);
|
self.file = Some(new_file);
|
||||||
if file_changed {
|
if file_changed {
|
||||||
self.non_text_state_update_count += 1;
|
self.non_text_state_update_count += 1;
|
||||||
|
if was_dirty != self.is_dirty() {
|
||||||
|
cx.emit(BufferEvent::DirtyChanged);
|
||||||
|
}
|
||||||
cx.emit(BufferEvent::FileHandleChanged);
|
cx.emit(BufferEvent::FileHandleChanged);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
@ -1742,15 +1759,10 @@ impl Buffer {
|
||||||
pub fn is_dirty(&self) -> bool {
|
pub fn is_dirty(&self) -> bool {
|
||||||
self.capability != Capability::ReadOnly
|
self.capability != Capability::ReadOnly
|
||||||
&& (self.has_conflict
|
&& (self.has_conflict
|
||||||
|| self.has_unsaved_edits()
|
|| self.file.as_ref().map_or(false, |file| {
|
||||||
|| self
|
matches!(file.disk_state(), DiskState::New | DiskState::Deleted)
|
||||||
.file
|
})
|
||||||
.as_ref()
|
|| self.has_unsaved_edits())
|
||||||
.map_or(false, |file| file.is_deleted() || !file.is_created()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_deleted(&self) -> bool {
|
|
||||||
self.file.as_ref().map_or(false, |file| file.is_deleted())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if the buffer and its file have both changed since the buffer
|
/// Checks if the buffer and its file have both changed since the buffer
|
||||||
|
@ -1762,7 +1774,13 @@ impl Buffer {
|
||||||
let Some(file) = self.file.as_ref() else {
|
let Some(file) = self.file.as_ref() else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
file.is_deleted() || (file.mtime() > self.saved_mtime && self.has_unsaved_edits())
|
match file.disk_state() {
|
||||||
|
DiskState::New | DiskState::Deleted => true,
|
||||||
|
DiskState::Present { mtime } => match self.saved_mtime {
|
||||||
|
Some(saved_mtime) => mtime > saved_mtime && self.has_unsaved_edits(),
|
||||||
|
None => true,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets a [`Subscription`] that tracks all of the changes to the buffer's text.
|
/// Gets a [`Subscription`] that tracks all of the changes to the buffer's text.
|
||||||
|
@ -4403,7 +4421,7 @@ impl File for TestFile {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mtime(&self) -> Option<SystemTime> {
|
fn disk_state(&self) -> DiskState {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4415,10 +4433,6 @@ impl File for TestFile {
|
||||||
WorktreeId::from_usize(0)
|
WorktreeId::from_usize(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_deleted(&self) -> bool {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn std::any::Any {
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,9 +10,9 @@ use itertools::Itertools;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{language_settings, LanguageSettings},
|
language_settings::{language_settings, LanguageSettings},
|
||||||
AutoindentMode, Buffer, BufferChunks, BufferRow, BufferSnapshot, Capability, CharClassifier,
|
AutoindentMode, Buffer, BufferChunks, BufferRow, BufferSnapshot, Capability, CharClassifier,
|
||||||
CharKind, Chunk, CursorShape, DiagnosticEntry, File, IndentGuide, IndentSize, Language,
|
CharKind, Chunk, CursorShape, DiagnosticEntry, DiskState, File, IndentGuide, IndentSize,
|
||||||
LanguageScope, OffsetRangeExt, OffsetUtf16, Outline, OutlineItem, Point, PointUtf16, Selection,
|
Language, LanguageScope, OffsetRangeExt, OffsetUtf16, Outline, OutlineItem, Point, PointUtf16,
|
||||||
TextDimension, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _,
|
Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _,
|
||||||
TransactionId, Unclipped,
|
TransactionId, Unclipped,
|
||||||
};
|
};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
@ -2035,7 +2035,9 @@ impl MultiBuffer {
|
||||||
edited |= buffer_edited;
|
edited |= buffer_edited;
|
||||||
non_text_state_updated |= buffer_non_text_state_updated;
|
non_text_state_updated |= buffer_non_text_state_updated;
|
||||||
is_dirty |= buffer.is_dirty();
|
is_dirty |= buffer.is_dirty();
|
||||||
has_deleted_file |= buffer.file().map_or(false, |file| file.is_deleted());
|
has_deleted_file |= buffer
|
||||||
|
.file()
|
||||||
|
.map_or(false, |file| file.disk_state() == DiskState::Deleted);
|
||||||
has_conflict |= buffer.has_conflict();
|
has_conflict |= buffer.has_conflict();
|
||||||
}
|
}
|
||||||
if edited {
|
if edited {
|
||||||
|
|
|
@ -20,7 +20,7 @@ use language::{
|
||||||
deserialize_line_ending, deserialize_version, serialize_line_ending, serialize_version,
|
deserialize_line_ending, deserialize_version, serialize_line_ending, serialize_version,
|
||||||
split_operations,
|
split_operations,
|
||||||
},
|
},
|
||||||
Buffer, BufferEvent, Capability, File as _, Language, Operation,
|
Buffer, BufferEvent, Capability, DiskState, File as _, Language, Operation,
|
||||||
};
|
};
|
||||||
use rpc::{proto, AnyProtoClient, ErrorExt as _, TypedEnvelope};
|
use rpc::{proto, AnyProtoClient, ErrorExt as _, TypedEnvelope};
|
||||||
use smol::channel::Receiver;
|
use smol::channel::Receiver;
|
||||||
|
@ -434,7 +434,10 @@ impl LocalBufferStore {
|
||||||
let line_ending = buffer.line_ending();
|
let line_ending = buffer.line_ending();
|
||||||
let version = buffer.version();
|
let version = buffer.version();
|
||||||
let buffer_id = buffer.remote_id();
|
let buffer_id = buffer.remote_id();
|
||||||
if buffer.file().is_some_and(|file| !file.is_created()) {
|
if buffer
|
||||||
|
.file()
|
||||||
|
.is_some_and(|file| file.disk_state() == DiskState::New)
|
||||||
|
{
|
||||||
has_changed_file = true;
|
has_changed_file = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -444,7 +447,7 @@ impl LocalBufferStore {
|
||||||
|
|
||||||
cx.spawn(move |this, mut cx| async move {
|
cx.spawn(move |this, mut cx| async move {
|
||||||
let new_file = save.await?;
|
let new_file = save.await?;
|
||||||
let mtime = new_file.mtime;
|
let mtime = new_file.disk_state().mtime();
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
if let Some((downstream_client, project_id)) = this.downstream_client(cx) {
|
if let Some((downstream_client, project_id)) = this.downstream_client(cx) {
|
||||||
if has_changed_file {
|
if has_changed_file {
|
||||||
|
@ -658,37 +661,30 @@ impl LocalBufferStore {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_file = if let Some(entry) = old_file
|
let snapshot_entry = old_file
|
||||||
.entry_id
|
.entry_id
|
||||||
.and_then(|entry_id| snapshot.entry_for_id(entry_id))
|
.and_then(|entry_id| snapshot.entry_for_id(entry_id))
|
||||||
{
|
.or_else(|| snapshot.entry_for_path(old_file.path.as_ref()));
|
||||||
|
|
||||||
|
let new_file = if let Some(entry) = snapshot_entry {
|
||||||
File {
|
File {
|
||||||
|
disk_state: match entry.mtime {
|
||||||
|
Some(mtime) => DiskState::Present { mtime },
|
||||||
|
None => old_file.disk_state,
|
||||||
|
},
|
||||||
is_local: true,
|
is_local: true,
|
||||||
entry_id: Some(entry.id),
|
entry_id: Some(entry.id),
|
||||||
mtime: entry.mtime,
|
|
||||||
path: entry.path.clone(),
|
path: entry.path.clone(),
|
||||||
worktree: worktree.clone(),
|
worktree: worktree.clone(),
|
||||||
is_deleted: false,
|
|
||||||
is_private: entry.is_private,
|
|
||||||
}
|
|
||||||
} else if let Some(entry) = snapshot.entry_for_path(old_file.path.as_ref()) {
|
|
||||||
File {
|
|
||||||
is_local: true,
|
|
||||||
entry_id: Some(entry.id),
|
|
||||||
mtime: entry.mtime,
|
|
||||||
path: entry.path.clone(),
|
|
||||||
worktree: worktree.clone(),
|
|
||||||
is_deleted: false,
|
|
||||||
is_private: entry.is_private,
|
is_private: entry.is_private,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
File {
|
File {
|
||||||
|
disk_state: DiskState::Deleted,
|
||||||
is_local: true,
|
is_local: true,
|
||||||
entry_id: old_file.entry_id,
|
entry_id: old_file.entry_id,
|
||||||
path: old_file.path.clone(),
|
path: old_file.path.clone(),
|
||||||
mtime: old_file.mtime,
|
|
||||||
worktree: worktree.clone(),
|
worktree: worktree.clone(),
|
||||||
is_deleted: true,
|
|
||||||
is_private: old_file.is_private,
|
is_private: old_file.is_private,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -867,10 +863,9 @@ impl BufferStoreImpl for Model<LocalBufferStore> {
|
||||||
Some(Arc::new(File {
|
Some(Arc::new(File {
|
||||||
worktree,
|
worktree,
|
||||||
path,
|
path,
|
||||||
mtime: None,
|
disk_state: DiskState::New,
|
||||||
entry_id: None,
|
entry_id: None,
|
||||||
is_local: true,
|
is_local: true,
|
||||||
is_deleted: false,
|
|
||||||
is_private: false,
|
is_private: false,
|
||||||
})),
|
})),
|
||||||
Capability::ReadWrite,
|
Capability::ReadWrite,
|
||||||
|
|
|
@ -9,7 +9,7 @@ use gpui::{
|
||||||
hash, prelude::*, AppContext, EventEmitter, Img, Model, ModelContext, Subscription, Task,
|
hash, prelude::*, AppContext, EventEmitter, Img, Model, ModelContext, Subscription, Task,
|
||||||
WeakModel,
|
WeakModel,
|
||||||
};
|
};
|
||||||
use language::File;
|
use language::{DiskState, File};
|
||||||
use rpc::{AnyProtoClient, ErrorExt as _};
|
use rpc::{AnyProtoClient, ErrorExt as _};
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::num::NonZeroU64;
|
use std::num::NonZeroU64;
|
||||||
|
@ -74,11 +74,12 @@ impl ImageItem {
|
||||||
file_changed = true;
|
file_changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !new_file.is_deleted() {
|
let old_state = old_file.disk_state();
|
||||||
let new_mtime = new_file.mtime();
|
let new_state = new_file.disk_state();
|
||||||
if new_mtime != old_file.mtime() {
|
if old_state != new_state {
|
||||||
file_changed = true;
|
file_changed = true;
|
||||||
cx.emit(ImageItemEvent::ReloadNeeded);
|
if matches!(new_state, DiskState::Present { .. }) {
|
||||||
|
cx.emit(ImageItemEvent::ReloadNeeded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -503,37 +504,30 @@ impl LocalImageStore {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_file = if let Some(entry) = old_file
|
let snapshot_entry = old_file
|
||||||
.entry_id
|
.entry_id
|
||||||
.and_then(|entry_id| snapshot.entry_for_id(entry_id))
|
.and_then(|entry_id| snapshot.entry_for_id(entry_id))
|
||||||
{
|
.or_else(|| snapshot.entry_for_path(old_file.path.as_ref()));
|
||||||
|
|
||||||
|
let new_file = if let Some(entry) = snapshot_entry {
|
||||||
worktree::File {
|
worktree::File {
|
||||||
|
disk_state: match entry.mtime {
|
||||||
|
Some(mtime) => DiskState::Present { mtime },
|
||||||
|
None => old_file.disk_state,
|
||||||
|
},
|
||||||
is_local: true,
|
is_local: true,
|
||||||
entry_id: Some(entry.id),
|
entry_id: Some(entry.id),
|
||||||
mtime: entry.mtime,
|
|
||||||
path: entry.path.clone(),
|
path: entry.path.clone(),
|
||||||
worktree: worktree.clone(),
|
worktree: worktree.clone(),
|
||||||
is_deleted: false,
|
|
||||||
is_private: entry.is_private,
|
|
||||||
}
|
|
||||||
} else if let Some(entry) = snapshot.entry_for_path(old_file.path.as_ref()) {
|
|
||||||
worktree::File {
|
|
||||||
is_local: true,
|
|
||||||
entry_id: Some(entry.id),
|
|
||||||
mtime: entry.mtime,
|
|
||||||
path: entry.path.clone(),
|
|
||||||
worktree: worktree.clone(),
|
|
||||||
is_deleted: false,
|
|
||||||
is_private: entry.is_private,
|
is_private: entry.is_private,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
worktree::File {
|
worktree::File {
|
||||||
|
disk_state: DiskState::Deleted,
|
||||||
is_local: true,
|
is_local: true,
|
||||||
entry_id: old_file.entry_id,
|
entry_id: old_file.entry_id,
|
||||||
path: old_file.path.clone(),
|
path: old_file.path.clone(),
|
||||||
mtime: old_file.mtime,
|
|
||||||
worktree: worktree.clone(),
|
worktree: worktree.clone(),
|
||||||
is_deleted: true,
|
|
||||||
is_private: old_file.is_private,
|
is_private: old_file.is_private,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,12 +5,12 @@ use gpui::{AppContext, SemanticVersion, UpdateGlobal};
|
||||||
use http_client::Url;
|
use http_client::Url;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{language_settings, AllLanguageSettings, LanguageSettingsContent},
|
language_settings::{language_settings, AllLanguageSettings, LanguageSettingsContent},
|
||||||
tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticSet, FakeLspAdapter,
|
tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticSet, DiskState, FakeLspAdapter,
|
||||||
LanguageConfig, LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint,
|
LanguageConfig, LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint,
|
||||||
};
|
};
|
||||||
use lsp::{DiagnosticSeverity, NumberOrString};
|
use lsp::{DiagnosticSeverity, NumberOrString};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::{assert_eq, assert_matches};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
use std::os;
|
use std::os;
|
||||||
|
@ -3239,10 +3239,22 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
|
||||||
Path::new("b/c/file5")
|
Path::new("b/c/file5")
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(!buffer2.read(cx).file().unwrap().is_deleted());
|
assert_matches!(
|
||||||
assert!(!buffer3.read(cx).file().unwrap().is_deleted());
|
buffer2.read(cx).file().unwrap().disk_state(),
|
||||||
assert!(!buffer4.read(cx).file().unwrap().is_deleted());
|
DiskState::Present { .. }
|
||||||
assert!(buffer5.read(cx).file().unwrap().is_deleted());
|
);
|
||||||
|
assert_matches!(
|
||||||
|
buffer3.read(cx).file().unwrap().disk_state(),
|
||||||
|
DiskState::Present { .. }
|
||||||
|
);
|
||||||
|
assert_matches!(
|
||||||
|
buffer4.read(cx).file().unwrap().disk_state(),
|
||||||
|
DiskState::Present { .. }
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
buffer5.read(cx).file().unwrap().disk_state(),
|
||||||
|
DiskState::Deleted
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update the remote worktree. Check that it becomes consistent with the
|
// Update the remote worktree. Check that it becomes consistent with the
|
||||||
|
@ -3416,7 +3428,11 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
events.lock().clear();
|
events.lock().clear();
|
||||||
buffer.did_save(buffer.version(), buffer.file().unwrap().mtime(), cx);
|
buffer.did_save(
|
||||||
|
buffer.version(),
|
||||||
|
buffer.file().unwrap().disk_state().mtime(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// after saving, the buffer is not dirty, and emits a saved event.
|
// after saving, the buffer is not dirty, and emits a saved event.
|
||||||
|
|
|
@ -29,6 +29,7 @@ use gpui::{
|
||||||
Task,
|
Task,
|
||||||
};
|
};
|
||||||
use ignore::IgnoreStack;
|
use ignore::IgnoreStack;
|
||||||
|
use language::DiskState;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use paths::local_settings_folder_relative_path;
|
use paths::local_settings_folder_relative_path;
|
||||||
use postage::{
|
use postage::{
|
||||||
|
@ -1313,9 +1314,10 @@ impl LocalWorktree {
|
||||||
entry_id: None,
|
entry_id: None,
|
||||||
worktree,
|
worktree,
|
||||||
path,
|
path,
|
||||||
mtime: Some(metadata.mtime),
|
disk_state: DiskState::Present {
|
||||||
|
mtime: metadata.mtime,
|
||||||
|
},
|
||||||
is_local: true,
|
is_local: true,
|
||||||
is_deleted: false,
|
|
||||||
is_private,
|
is_private,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1374,9 +1376,10 @@ impl LocalWorktree {
|
||||||
entry_id: None,
|
entry_id: None,
|
||||||
worktree,
|
worktree,
|
||||||
path,
|
path,
|
||||||
mtime: Some(metadata.mtime),
|
disk_state: DiskState::Present {
|
||||||
|
mtime: metadata.mtime,
|
||||||
|
},
|
||||||
is_local: true,
|
is_local: true,
|
||||||
is_deleted: false,
|
|
||||||
is_private,
|
is_private,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1512,10 +1515,11 @@ impl LocalWorktree {
|
||||||
Ok(Arc::new(File {
|
Ok(Arc::new(File {
|
||||||
worktree,
|
worktree,
|
||||||
path,
|
path,
|
||||||
mtime: Some(metadata.mtime),
|
disk_state: DiskState::Present {
|
||||||
|
mtime: metadata.mtime,
|
||||||
|
},
|
||||||
entry_id: None,
|
entry_id: None,
|
||||||
is_local: true,
|
is_local: true,
|
||||||
is_deleted: false,
|
|
||||||
is_private,
|
is_private,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@ -3178,10 +3182,9 @@ impl fmt::Debug for Snapshot {
|
||||||
pub struct File {
|
pub struct File {
|
||||||
pub worktree: Model<Worktree>,
|
pub worktree: Model<Worktree>,
|
||||||
pub path: Arc<Path>,
|
pub path: Arc<Path>,
|
||||||
pub mtime: Option<SystemTime>,
|
pub disk_state: DiskState,
|
||||||
pub entry_id: Option<ProjectEntryId>,
|
pub entry_id: Option<ProjectEntryId>,
|
||||||
pub is_local: bool,
|
pub is_local: bool,
|
||||||
pub is_deleted: bool,
|
|
||||||
pub is_private: bool,
|
pub is_private: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3194,8 +3197,8 @@ impl language::File for File {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mtime(&self) -> Option<SystemTime> {
|
fn disk_state(&self) -> DiskState {
|
||||||
self.mtime
|
self.disk_state
|
||||||
}
|
}
|
||||||
|
|
||||||
fn path(&self) -> &Arc<Path> {
|
fn path(&self) -> &Arc<Path> {
|
||||||
|
@ -3238,10 +3241,6 @@ impl language::File for File {
|
||||||
self.worktree.read(cx).id()
|
self.worktree.read(cx).id()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_deleted(&self) -> bool {
|
|
||||||
self.is_deleted
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn Any {
|
fn as_any(&self) -> &dyn Any {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
@ -3251,8 +3250,8 @@ impl language::File for File {
|
||||||
worktree_id: self.worktree.read(cx).id().to_proto(),
|
worktree_id: self.worktree.read(cx).id().to_proto(),
|
||||||
entry_id: self.entry_id.map(|id| id.to_proto()),
|
entry_id: self.entry_id.map(|id| id.to_proto()),
|
||||||
path: self.path.to_string_lossy().into(),
|
path: self.path.to_string_lossy().into(),
|
||||||
mtime: self.mtime.map(|time| time.into()),
|
mtime: self.disk_state.mtime().map(|time| time.into()),
|
||||||
is_deleted: self.is_deleted,
|
is_deleted: self.disk_state == DiskState::Deleted,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3293,10 +3292,13 @@ impl File {
|
||||||
Arc::new(Self {
|
Arc::new(Self {
|
||||||
worktree,
|
worktree,
|
||||||
path: entry.path.clone(),
|
path: entry.path.clone(),
|
||||||
mtime: entry.mtime,
|
disk_state: if let Some(mtime) = entry.mtime {
|
||||||
|
DiskState::Present { mtime }
|
||||||
|
} else {
|
||||||
|
DiskState::New
|
||||||
|
},
|
||||||
entry_id: Some(entry.id),
|
entry_id: Some(entry.id),
|
||||||
is_local: true,
|
is_local: true,
|
||||||
is_deleted: false,
|
|
||||||
is_private: entry.is_private,
|
is_private: entry.is_private,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -3316,13 +3318,22 @@ impl File {
|
||||||
return Err(anyhow!("worktree id does not match file"));
|
return Err(anyhow!("worktree id does not match file"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let disk_state = if proto.is_deleted {
|
||||||
|
DiskState::Deleted
|
||||||
|
} else {
|
||||||
|
if let Some(mtime) = proto.mtime.map(&Into::into) {
|
||||||
|
DiskState::Present { mtime }
|
||||||
|
} else {
|
||||||
|
DiskState::New
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
worktree,
|
worktree,
|
||||||
path: Path::new(&proto.path).into(),
|
path: Path::new(&proto.path).into(),
|
||||||
mtime: proto.mtime.map(|time| time.into()),
|
disk_state,
|
||||||
entry_id: proto.entry_id.map(ProjectEntryId::from_proto),
|
entry_id: proto.entry_id.map(ProjectEntryId::from_proto),
|
||||||
is_local: false,
|
is_local: false,
|
||||||
is_deleted: proto.is_deleted,
|
|
||||||
is_private: false,
|
is_private: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -3336,10 +3347,9 @@ impl File {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn project_entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
|
pub fn project_entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
|
||||||
if self.is_deleted {
|
match self.disk_state {
|
||||||
None
|
DiskState::Deleted => None,
|
||||||
} else {
|
_ => self.entry_id,
|
||||||
self.entry_id
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue