Allow opening non-extant files (#9256)

Fixes #7400



Release Notes:

- Improved the `zed` command to not create files until you save them in
the editor ([#7400](https://github.com/zed-industries/zed/issues/7400)).
This commit is contained in:
Conrad Irwin 2024-03-12 22:30:04 -06:00 committed by GitHub
parent e792c1a5c5
commit 646f69583a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 242 additions and 152 deletions

View file

@ -5,9 +5,9 @@ use clap::Parser;
use cli::{CliRequest, CliResponse}; use cli::{CliRequest, CliResponse};
use serde::Deserialize; use serde::Deserialize;
use std::{ use std::{
env,
ffi::OsStr, ffi::OsStr,
fs::{self, OpenOptions}, fs::{self},
io,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use util::paths::PathLikeWithPosition; use util::paths::PathLikeWithPosition;
@ -62,14 +62,26 @@ fn main() -> Result<()> {
return Ok(()); return Ok(());
} }
for path in args let curdir = env::current_dir()?;
.paths_with_position let mut paths = vec![];
.iter() for path in args.paths_with_position {
.map(|path_with_position| &path_with_position.path_like) let canonicalized = path.map_path_like(|path| match fs::canonicalize(&path) {
{ Ok(path) => Ok(path),
if !path.exists() { Err(e) => {
touch(path.as_path())?; if let Some(mut parent) = path.parent() {
} if parent == Path::new("") {
parent = &curdir;
}
match fs::canonicalize(parent) {
Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
Err(_) => Err(e),
}
} else {
Err(e)
}
}
})?;
paths.push(canonicalized.to_string(|path| path.display().to_string()))
} }
let (tx, rx) = bundle.launch()?; let (tx, rx) = bundle.launch()?;
@ -82,17 +94,7 @@ fn main() -> Result<()> {
}; };
tx.send(CliRequest::Open { tx.send(CliRequest::Open {
paths: args paths,
.paths_with_position
.into_iter()
.map(|path_with_position| {
let path_with_position = path_with_position.map_path_like(|path| {
fs::canonicalize(&path)
.with_context(|| format!("path {path:?} canonicalization"))
})?;
Ok(path_with_position.to_string(|path| path.display().to_string()))
})
.collect::<Result<_>>()?,
wait: args.wait, wait: args.wait,
open_new_workspace, open_new_workspace,
})?; })?;
@ -120,13 +122,6 @@ enum Bundle {
}, },
} }
fn touch(path: &Path) -> io::Result<()> {
match OpenOptions::new().create(true).write(true).open(path) {
Ok(_) => Ok(()),
Err(e) => Err(e),
}
}
fn locate_bundle() -> Result<PathBuf> { fn locate_bundle() -> Result<PathBuf> {
let cli_path = std::env::current_exe()?.canonicalize()?; let cli_path = std::env::current_exe()?.canonicalize()?;
let mut app_path = cli_path.clone(); let mut app_path = cli_path.clone();

View file

@ -1220,7 +1220,7 @@ mod tests {
Some(self) Some(self)
} }
fn mtime(&self) -> std::time::SystemTime { fn mtime(&self) -> Option<std::time::SystemTime> {
unimplemented!() unimplemented!()
} }
@ -1272,7 +1272,7 @@ mod tests {
_: &clock::Global, _: &clock::Global,
_: language::RopeFingerprint, _: language::RopeFingerprint,
_: language::LineEnding, _: language::LineEnding,
_: std::time::SystemTime, _: Option<std::time::SystemTime>,
_: &mut AppContext, _: &mut AppContext,
) { ) {
unimplemented!() unimplemented!()

View file

@ -1280,7 +1280,7 @@ mod tests {
unimplemented!() unimplemented!()
} }
fn mtime(&self) -> SystemTime { fn mtime(&self) -> Option<SystemTime> {
unimplemented!() unimplemented!()
} }

View file

@ -56,6 +56,7 @@ pub trait Fs: Send + Sync {
async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>; async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>;
async fn canonicalize(&self, path: &Path) -> Result<PathBuf>; async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
async fn is_file(&self, path: &Path) -> bool; async fn is_file(&self, path: &Path) -> bool;
async fn is_dir(&self, path: &Path) -> bool;
async fn metadata(&self, path: &Path) -> Result<Option<Metadata>>; async fn metadata(&self, path: &Path) -> Result<Option<Metadata>>;
async fn read_link(&self, path: &Path) -> Result<PathBuf>; async fn read_link(&self, path: &Path) -> Result<PathBuf>;
async fn read_dir( async fn read_dir(
@ -264,6 +265,12 @@ impl Fs for RealFs {
.map_or(false, |metadata| metadata.is_file()) .map_or(false, |metadata| metadata.is_file())
} }
async fn is_dir(&self, path: &Path) -> bool {
smol::fs::metadata(path)
.await
.map_or(false, |metadata| metadata.is_dir())
}
async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> { async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
let symlink_metadata = match smol::fs::symlink_metadata(path).await { let symlink_metadata = match smol::fs::symlink_metadata(path).await {
Ok(metadata) => metadata, Ok(metadata) => metadata,
@ -500,7 +507,12 @@ impl FakeFsState {
fn read_path(&self, target: &Path) -> Result<Arc<Mutex<FakeFsEntry>>> { fn read_path(&self, target: &Path) -> Result<Arc<Mutex<FakeFsEntry>>> {
Ok(self Ok(self
.try_read_path(target, true) .try_read_path(target, true)
.ok_or_else(|| anyhow!("path does not exist: {}", target.display()))? .ok_or_else(|| {
anyhow!(io::Error::new(
io::ErrorKind::NotFound,
format!("not found: {}", target.display())
))
})?
.0) .0)
} }
@ -1260,6 +1272,12 @@ impl Fs for FakeFs {
} }
} }
async fn is_dir(&self, path: &Path) -> bool {
self.metadata(path)
.await
.is_ok_and(|metadata| metadata.is_some_and(|metadata| metadata.is_dir))
}
async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> { async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
self.simulate_random_delay().await; self.simulate_random_delay().await;
let path = normalize_path(path); let path = normalize_path(path);

View file

@ -37,7 +37,7 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
str, str,
sync::Arc, sync::Arc,
time::{Duration, Instant, SystemTime, UNIX_EPOCH}, time::{Duration, Instant, SystemTime},
vec, vec,
}; };
use sum_tree::TreeMap; use sum_tree::TreeMap;
@ -83,7 +83,7 @@ pub struct Buffer {
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.
saved_mtime: SystemTime, saved_mtime: Option<SystemTime>,
/// The version vector when this buffer was last loaded from /// The version vector when this buffer was last loaded from
/// or saved to disk. /// or saved to disk.
saved_version: clock::Global, saved_version: clock::Global,
@ -358,7 +358,7 @@ pub trait File: Send + Sync {
} }
/// Returns the file's mtime. /// Returns the file's mtime.
fn mtime(&self) -> SystemTime; fn mtime(&self) -> Option<SystemTime>;
/// 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>;
@ -379,6 +379,11 @@ pub trait File: Send + Sync {
/// Returns whether the file has been deleted. /// Returns whether the file has been deleted.
fn is_deleted(&self) -> bool; 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;
@ -404,7 +409,7 @@ pub trait LocalFile: File {
version: &clock::Global, version: &clock::Global,
fingerprint: RopeFingerprint, fingerprint: RopeFingerprint,
line_ending: LineEnding, line_ending: LineEnding,
mtime: SystemTime, mtime: Option<SystemTime>,
cx: &mut AppContext, cx: &mut AppContext,
); );
@ -573,10 +578,7 @@ impl Buffer {
)); ));
this.saved_version = proto::deserialize_version(&message.saved_version); this.saved_version = proto::deserialize_version(&message.saved_version);
this.file_fingerprint = proto::deserialize_fingerprint(&message.saved_version_fingerprint)?; this.file_fingerprint = proto::deserialize_fingerprint(&message.saved_version_fingerprint)?;
this.saved_mtime = message this.saved_mtime = message.saved_mtime.map(|time| time.into());
.saved_mtime
.ok_or_else(|| anyhow!("invalid saved_mtime"))?
.into();
Ok(this) Ok(this)
} }
@ -590,7 +592,7 @@ impl Buffer {
line_ending: proto::serialize_line_ending(self.line_ending()) as i32, line_ending: proto::serialize_line_ending(self.line_ending()) as i32,
saved_version: proto::serialize_version(&self.saved_version), saved_version: proto::serialize_version(&self.saved_version),
saved_version_fingerprint: proto::serialize_fingerprint(self.file_fingerprint), saved_version_fingerprint: proto::serialize_fingerprint(self.file_fingerprint),
saved_mtime: Some(self.saved_mtime.into()), saved_mtime: self.saved_mtime.map(|time| time.into()),
} }
} }
@ -664,11 +666,7 @@ impl Buffer {
file: Option<Arc<dyn File>>, file: Option<Arc<dyn File>>,
capability: Capability, capability: Capability,
) -> Self { ) -> Self {
let saved_mtime = if let Some(file) = file.as_ref() { let saved_mtime = file.as_ref().and_then(|file| file.mtime());
file.mtime()
} else {
UNIX_EPOCH
};
Self { Self {
saved_mtime, saved_mtime,
@ -754,7 +752,7 @@ impl Buffer {
} }
/// The mtime of the buffer's file when the buffer was last saved or reloaded from disk. /// The mtime of the buffer's file when the buffer was last saved or reloaded from disk.
pub fn saved_mtime(&self) -> SystemTime { pub fn saved_mtime(&self) -> Option<SystemTime> {
self.saved_mtime self.saved_mtime
} }
@ -786,7 +784,7 @@ impl Buffer {
&mut self, &mut self,
version: clock::Global, version: clock::Global,
fingerprint: RopeFingerprint, fingerprint: RopeFingerprint,
mtime: SystemTime, mtime: Option<SystemTime>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
self.saved_version = version; self.saved_version = version;
@ -861,7 +859,7 @@ impl Buffer {
version: clock::Global, version: clock::Global,
fingerprint: RopeFingerprint, fingerprint: RopeFingerprint,
line_ending: LineEnding, line_ending: LineEnding,
mtime: SystemTime, mtime: Option<SystemTime>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
self.saved_version = version; self.saved_version = version;
@ -1547,7 +1545,10 @@ impl Buffer {
/// Checks if the buffer has unsaved changes. /// Checks if the buffer has unsaved changes.
pub fn is_dirty(&self) -> bool { pub fn is_dirty(&self) -> bool {
(self.has_conflict || self.changed_since_saved_version()) (self.has_conflict || self.changed_since_saved_version())
|| self.file.as_ref().map_or(false, |file| file.is_deleted()) || self
.file
.as_ref()
.map_or(false, |file| file.is_deleted() || !file.is_created())
} }
/// 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

View file

@ -75,7 +75,7 @@ use std::{
env, env,
ffi::OsStr, ffi::OsStr,
hash::Hash, hash::Hash,
mem, io, mem,
num::NonZeroU32, num::NonZeroU32,
ops::Range, ops::Range,
path::{self, Component, Path, PathBuf}, path::{self, Component, Path, PathBuf},
@ -1801,13 +1801,13 @@ impl Project {
let (mut tx, rx) = postage::watch::channel(); let (mut tx, rx) = postage::watch::channel();
entry.insert(rx.clone()); entry.insert(rx.clone());
let project_path = project_path.clone();
let load_buffer = if worktree.read(cx).is_local() { let load_buffer = if worktree.read(cx).is_local() {
self.open_local_buffer_internal(&project_path.path, &worktree, cx) self.open_local_buffer_internal(project_path.path.clone(), worktree, cx)
} else { } else {
self.open_remote_buffer_internal(&project_path.path, &worktree, cx) self.open_remote_buffer_internal(&project_path.path, &worktree, cx)
}; };
let project_path = project_path.clone();
cx.spawn(move |this, mut cx| async move { cx.spawn(move |this, mut cx| async move {
let load_result = load_buffer.await; let load_result = load_buffer.await;
*tx.borrow_mut() = Some(this.update(&mut cx, |this, _| { *tx.borrow_mut() = Some(this.update(&mut cx, |this, _| {
@ -1832,17 +1832,32 @@ impl Project {
fn open_local_buffer_internal( fn open_local_buffer_internal(
&mut self, &mut self,
path: &Arc<Path>, path: Arc<Path>,
worktree: &Model<Worktree>, worktree: Model<Worktree>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Result<Model<Buffer>>> { ) -> Task<Result<Model<Buffer>>> {
let buffer_id = self.next_buffer_id.next(); let buffer_id = self.next_buffer_id.next();
let load_buffer = worktree.update(cx, |worktree, cx| { let load_buffer = worktree.update(cx, |worktree, cx| {
let worktree = worktree.as_local_mut().unwrap(); let worktree = worktree.as_local_mut().unwrap();
worktree.load_buffer(buffer_id, path, cx) worktree.load_buffer(buffer_id, &path, cx)
}); });
fn is_not_found_error(error: &anyhow::Error) -> bool {
error
.root_cause()
.downcast_ref::<io::Error>()
.is_some_and(|err| err.kind() == io::ErrorKind::NotFound)
}
cx.spawn(move |this, mut cx| async move { cx.spawn(move |this, mut cx| async move {
let buffer = load_buffer.await?; let buffer = match load_buffer.await {
Ok(buffer) => Ok(buffer),
Err(error) if is_not_found_error(&error) => {
worktree.update(&mut cx, |worktree, cx| {
let worktree = worktree.as_local_mut().unwrap();
worktree.new_buffer(buffer_id, path, cx)
})
}
Err(e) => Err(e),
}?;
this.update(&mut cx, |this, cx| this.register_buffer(&buffer, cx))??; this.update(&mut cx, |this, cx| this.register_buffer(&buffer, cx))??;
Ok(buffer) Ok(buffer)
}) })
@ -8005,7 +8020,7 @@ impl Project {
project_id, project_id,
buffer_id: buffer_id.into(), buffer_id: buffer_id.into(),
version: serialize_version(buffer.saved_version()), version: serialize_version(buffer.saved_version()),
mtime: Some(buffer.saved_mtime().into()), mtime: buffer.saved_mtime().map(|time| time.into()),
fingerprint: language::proto::serialize_fingerprint(buffer.saved_version_fingerprint()), fingerprint: language::proto::serialize_fingerprint(buffer.saved_version_fingerprint()),
}) })
} }
@ -8098,7 +8113,7 @@ impl Project {
project_id, project_id,
buffer_id: buffer_id.into(), buffer_id: buffer_id.into(),
version: language::proto::serialize_version(buffer.saved_version()), version: language::proto::serialize_version(buffer.saved_version()),
mtime: Some(buffer.saved_mtime().into()), mtime: buffer.saved_mtime().map(|time| time.into()),
fingerprint: language::proto::serialize_fingerprint( fingerprint: language::proto::serialize_fingerprint(
buffer.saved_version_fingerprint(), buffer.saved_version_fingerprint(),
), ),
@ -8973,11 +8988,7 @@ impl Project {
let fingerprint = deserialize_fingerprint(&envelope.payload.fingerprint)?; let fingerprint = deserialize_fingerprint(&envelope.payload.fingerprint)?;
let version = deserialize_version(&envelope.payload.version); let version = deserialize_version(&envelope.payload.version);
let buffer_id = BufferId::new(envelope.payload.buffer_id)?; let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
let mtime = envelope let mtime = envelope.payload.mtime.map(|time| time.into());
.payload
.mtime
.ok_or_else(|| anyhow!("missing mtime"))?
.into();
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
let buffer = this let buffer = this
@ -9011,10 +9022,7 @@ impl Project {
proto::LineEnding::from_i32(payload.line_ending) proto::LineEnding::from_i32(payload.line_ending)
.ok_or_else(|| anyhow!("missing line ending"))?, .ok_or_else(|| anyhow!("missing line ending"))?,
); );
let mtime = payload let mtime = payload.mtime.map(|time| time.into());
.mtime
.ok_or_else(|| anyhow!("missing mtime"))?
.into();
let buffer_id = BufferId::new(payload.buffer_id)?; let buffer_id = BufferId::new(payload.buffer_id)?;
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
let buffer = this let buffer = this

View file

@ -173,13 +173,16 @@ impl WorktreeState {
let Some(entry) = worktree.entry_for_id(*entry_id) else { let Some(entry) = worktree.entry_for_id(*entry_id) else {
continue; continue;
}; };
let Some(mtime) = entry.mtime else {
continue;
};
if entry.is_ignored || entry.is_symlink || entry.is_external || entry.is_dir() { if entry.is_ignored || entry.is_symlink || entry.is_external || entry.is_dir() {
continue; continue;
} }
changed_paths.insert( changed_paths.insert(
path.clone(), path.clone(),
ChangedPathInfo { ChangedPathInfo {
mtime: entry.mtime, mtime,
is_deleted: *change == PathChange::Removed, is_deleted: *change == PathChange::Removed,
}, },
); );
@ -594,18 +597,18 @@ impl SemanticIndex {
{ {
continue; continue;
} }
let Some(new_mtime) = file.mtime else {
continue;
};
let stored_mtime = file_mtimes.remove(&file.path.to_path_buf()); let stored_mtime = file_mtimes.remove(&file.path.to_path_buf());
let already_stored = stored_mtime let already_stored = stored_mtime == Some(new_mtime);
.map_or(false, |existing_mtime| {
existing_mtime == file.mtime
});
if !already_stored { if !already_stored {
changed_paths.insert( changed_paths.insert(
file.path.clone(), file.path.clone(),
ChangedPathInfo { ChangedPathInfo {
mtime: file.mtime, mtime: new_mtime,
is_deleted: false, is_deleted: false,
}, },
); );

View file

@ -2108,7 +2108,11 @@ impl NavHistoryState {
fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String { fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
let path = buffer_path let path = buffer_path
.as_ref() .as_ref()
.and_then(|p| p.path.to_str()) .and_then(|p| {
p.path
.to_str()
.and_then(|s| if s == "" { None } else { Some(s) })
})
.unwrap_or("This buffer"); .unwrap_or("This buffer");
let path = truncate_and_remove_front(path, 80); let path = truncate_and_remove_front(path, 80);
format!("{path} contains unsaved edits. Do you want to save it?") format!("{path} contains unsaved edits. Do you want to save it?")

View file

@ -1447,18 +1447,12 @@ impl Workspace {
OpenVisible::None => Some(false), OpenVisible::None => Some(false),
OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() { OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() {
Some(Some(metadata)) => Some(!metadata.is_dir), Some(Some(metadata)) => Some(!metadata.is_dir),
Some(None) => { Some(None) => Some(true),
log::error!("No metadata for file {abs_path:?}");
None
}
None => None, None => None,
}, },
OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() { OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() {
Some(Some(metadata)) => Some(metadata.is_dir), Some(Some(metadata)) => Some(metadata.is_dir),
Some(None) => { Some(None) => Some(false),
log::error!("No metadata for file {abs_path:?}");
None
}
None => None, None => None,
}, },
}; };
@ -1486,15 +1480,7 @@ impl Workspace {
let pane = pane.clone(); let pane = pane.clone();
let task = cx.spawn(move |mut cx| async move { let task = cx.spawn(move |mut cx| async move {
let (worktree, project_path) = project_path?; let (worktree, project_path) = project_path?;
if fs.is_file(&abs_path).await { if fs.is_dir(&abs_path).await {
Some(
this.update(&mut cx, |this, cx| {
this.open_path(project_path, pane, true, cx)
})
.log_err()?
.await,
)
} else {
this.update(&mut cx, |workspace, cx| { this.update(&mut cx, |workspace, cx| {
let worktree = worktree.read(cx); let worktree = worktree.read(cx);
let worktree_abs_path = worktree.abs_path(); let worktree_abs_path = worktree.abs_path();
@ -1517,6 +1503,14 @@ impl Workspace {
}) })
.log_err()?; .log_err()?;
None None
} else {
Some(
this.update(&mut cx, |this, cx| {
this.open_path(project_path, pane, true, cx)
})
.log_err()?
.await,
)
} }
}); });
tasks.push(task); tasks.push(task);
@ -3731,7 +3725,9 @@ fn open_items(
let fs = app_state.fs.clone(); let fs = app_state.fs.clone();
async move { async move {
let file_project_path = project_path?; let file_project_path = project_path?;
if fs.is_file(&abs_path).await { if fs.is_dir(&abs_path).await {
None
} else {
Some(( Some((
ix, ix,
workspace workspace
@ -3741,8 +3737,6 @@ fn open_items(
.log_err()? .log_err()?
.await, .await,
)) ))
} else {
None
} }
} }
}) })

View file

@ -761,6 +761,32 @@ impl LocalWorktree {
}) })
} }
pub fn new_buffer(
&mut self,
buffer_id: BufferId,
path: Arc<Path>,
cx: &mut ModelContext<Worktree>,
) -> Model<Buffer> {
let text_buffer = text::Buffer::new(0, buffer_id, "".into());
let worktree = cx.handle();
cx.new_model(|_| {
Buffer::build(
text_buffer,
None,
Some(Arc::new(File {
worktree,
path,
mtime: None,
entry_id: None,
is_local: true,
is_deleted: false,
is_private: false,
})),
Capability::ReadWrite,
)
})
}
pub fn diagnostics_for_path( pub fn diagnostics_for_path(
&self, &self,
path: &Path, path: &Path,
@ -1088,7 +1114,7 @@ impl LocalWorktree {
entry_id: None, entry_id: None,
worktree, worktree,
path, path,
mtime: metadata.mtime, mtime: Some(metadata.mtime),
is_local: true, is_local: true,
is_deleted: false, is_deleted: false,
is_private, is_private,
@ -1105,7 +1131,7 @@ impl LocalWorktree {
&self, &self,
buffer_handle: Model<Buffer>, buffer_handle: Model<Buffer>,
path: Arc<Path>, path: Arc<Path>,
has_changed_file: bool, mut has_changed_file: bool,
cx: &mut ModelContext<Worktree>, cx: &mut ModelContext<Worktree>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
let buffer = buffer_handle.read(cx); let buffer = buffer_handle.read(cx);
@ -1114,6 +1140,10 @@ impl LocalWorktree {
let buffer_id: u64 = buffer.remote_id().into(); let buffer_id: u64 = buffer.remote_id().into();
let project_id = self.share.as_ref().map(|share| share.project_id); let project_id = self.share.as_ref().map(|share| share.project_id);
if buffer.file().is_some_and(|file| !file.is_created()) {
has_changed_file = true;
}
let text = buffer.as_rope().clone(); let text = buffer.as_rope().clone();
let fingerprint = text.fingerprint(); let fingerprint = text.fingerprint();
let version = buffer.version(); let version = buffer.version();
@ -1141,7 +1171,7 @@ impl LocalWorktree {
.with_context(|| { .with_context(|| {
format!("Excluded buffer {path:?} got removed during saving") format!("Excluded buffer {path:?} got removed during saving")
})?; })?;
(None, metadata.mtime, path, is_private) (None, Some(metadata.mtime), path, is_private)
} }
}; };
@ -1177,7 +1207,7 @@ impl LocalWorktree {
project_id, project_id,
buffer_id, buffer_id,
version: serialize_version(&version), version: serialize_version(&version),
mtime: Some(mtime.into()), mtime: mtime.map(|time| time.into()),
fingerprint: serialize_fingerprint(fingerprint), fingerprint: serialize_fingerprint(fingerprint),
})?; })?;
} }
@ -1585,10 +1615,7 @@ impl RemoteWorktree {
.await?; .await?;
let version = deserialize_version(&response.version); let version = deserialize_version(&response.version);
let fingerprint = deserialize_fingerprint(&response.fingerprint)?; let fingerprint = deserialize_fingerprint(&response.fingerprint)?;
let mtime = response let mtime = response.mtime.map(|mtime| mtime.into());
.mtime
.ok_or_else(|| anyhow!("missing mtime"))?
.into();
buffer_handle.update(&mut cx, |buffer, cx| { buffer_handle.update(&mut cx, |buffer, cx| {
buffer.did_save(version.clone(), fingerprint, mtime, cx); buffer.did_save(version.clone(), fingerprint, mtime, cx);
@ -2733,10 +2760,13 @@ impl BackgroundScannerState {
let Ok(repo_path) = entry.path.strip_prefix(&work_directory.0) else { let Ok(repo_path) = entry.path.strip_prefix(&work_directory.0) else {
continue; continue;
}; };
let Some(mtime) = entry.mtime else {
continue;
};
let repo_path = RepoPath(repo_path.to_path_buf()); let repo_path = RepoPath(repo_path.to_path_buf());
let git_file_status = combine_git_statuses( let git_file_status = combine_git_statuses(
staged_statuses.get(&repo_path).copied(), staged_statuses.get(&repo_path).copied(),
repo.unstaged_status(&repo_path, entry.mtime), repo.unstaged_status(&repo_path, mtime),
); );
if entry.git_status != git_file_status { if entry.git_status != git_file_status {
entry.git_status = git_file_status; entry.git_status = git_file_status;
@ -2850,7 +2880,7 @@ 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: SystemTime, pub mtime: Option<SystemTime>,
pub entry_id: Option<ProjectEntryId>, pub entry_id: Option<ProjectEntryId>,
pub is_local: bool, pub is_local: bool,
pub is_deleted: bool, pub is_deleted: bool,
@ -2866,7 +2896,7 @@ impl language::File for File {
} }
} }
fn mtime(&self) -> SystemTime { fn mtime(&self) -> Option<SystemTime> {
self.mtime self.mtime
} }
@ -2923,7 +2953,7 @@ impl language::File for File {
worktree_id: self.worktree.entity_id().as_u64(), worktree_id: self.worktree.entity_id().as_u64(),
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: Some(self.mtime.into()), mtime: self.mtime.map(|time| time.into()),
is_deleted: self.is_deleted, is_deleted: self.is_deleted,
} }
} }
@ -2957,7 +2987,7 @@ impl language::LocalFile for File {
version: &clock::Global, version: &clock::Global,
fingerprint: RopeFingerprint, fingerprint: RopeFingerprint,
line_ending: LineEnding, line_ending: LineEnding,
mtime: SystemTime, mtime: Option<SystemTime>,
cx: &mut AppContext, cx: &mut AppContext,
) { ) {
let worktree = self.worktree.read(cx).as_local().unwrap(); let worktree = self.worktree.read(cx).as_local().unwrap();
@ -2968,7 +2998,7 @@ impl language::LocalFile for File {
project_id, project_id,
buffer_id: buffer_id.into(), buffer_id: buffer_id.into(),
version: serialize_version(version), version: serialize_version(version),
mtime: Some(mtime.into()), mtime: mtime.map(|time| time.into()),
fingerprint: serialize_fingerprint(fingerprint), fingerprint: serialize_fingerprint(fingerprint),
line_ending: serialize_line_ending(line_ending) as i32, line_ending: serialize_line_ending(line_ending) as i32,
}) })
@ -3008,7 +3038,7 @@ impl File {
Ok(Self { Ok(Self {
worktree, worktree,
path: Path::new(&proto.path).into(), path: Path::new(&proto.path).into(),
mtime: proto.mtime.ok_or_else(|| anyhow!("no timestamp"))?.into(), mtime: proto.mtime.map(|time| time.into()),
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_deleted: proto.is_deleted,
@ -3039,7 +3069,7 @@ pub struct Entry {
pub kind: EntryKind, pub kind: EntryKind,
pub path: Arc<Path>, pub path: Arc<Path>,
pub inode: u64, pub inode: u64,
pub mtime: SystemTime, pub mtime: Option<SystemTime>,
pub is_symlink: bool, pub is_symlink: bool,
/// Whether this entry is ignored by Git. /// Whether this entry is ignored by Git.
@ -3109,7 +3139,7 @@ impl Entry {
}, },
path, path,
inode: metadata.inode, inode: metadata.inode,
mtime: metadata.mtime, mtime: Some(metadata.mtime),
is_symlink: metadata.is_symlink, is_symlink: metadata.is_symlink,
is_ignored: false, is_ignored: false,
is_external: false, is_external: false,
@ -3118,6 +3148,10 @@ impl Entry {
} }
} }
pub fn is_created(&self) -> bool {
self.mtime.is_some()
}
pub fn is_dir(&self) -> bool { pub fn is_dir(&self) -> bool {
self.kind.is_dir() self.kind.is_dir()
} }
@ -3456,7 +3490,7 @@ impl BackgroundScanner {
Ok(path) => path, Ok(path) => path,
Err(err) => { Err(err) => {
log::error!("failed to canonicalize root path: {}", err); log::error!("failed to canonicalize root path: {}", err);
return false; return true;
} }
}; };
let abs_paths = request let abs_paths = request
@ -3878,13 +3912,13 @@ impl BackgroundScanner {
&job.containing_repository &job.containing_repository
{ {
if let Ok(repo_path) = child_entry.path.strip_prefix(&repository_dir.0) { if let Ok(repo_path) = child_entry.path.strip_prefix(&repository_dir.0) {
let repo_path = RepoPath(repo_path.into()); if let Some(mtime) = child_entry.mtime {
child_entry.git_status = combine_git_statuses( let repo_path = RepoPath(repo_path.into());
staged_statuses.get(&repo_path).copied(), child_entry.git_status = combine_git_statuses(
repository staged_statuses.get(&repo_path).copied(),
.lock() repository.lock().unstaged_status(&repo_path, mtime),
.unstaged_status(&repo_path, child_entry.mtime), );
); }
} }
} }
} }
@ -4018,9 +4052,11 @@ impl BackgroundScanner {
if !is_dir && !fs_entry.is_ignored && !fs_entry.is_external { if !is_dir && !fs_entry.is_ignored && !fs_entry.is_external {
if let Some((work_dir, repo)) = state.snapshot.local_repo_for_path(path) { if let Some((work_dir, repo)) = state.snapshot.local_repo_for_path(path) {
if let Ok(repo_path) = path.strip_prefix(work_dir.0) { if let Ok(repo_path) = path.strip_prefix(work_dir.0) {
let repo_path = RepoPath(repo_path.into()); if let Some(mtime) = fs_entry.mtime {
let repo = repo.repo_ptr.lock(); let repo_path = RepoPath(repo_path.into());
fs_entry.git_status = repo.status(&repo_path, fs_entry.mtime); let repo = repo.repo_ptr.lock();
fs_entry.git_status = repo.status(&repo_path, mtime);
}
} }
} }
} }
@ -4664,7 +4700,7 @@ impl<'a> From<&'a Entry> for proto::Entry {
is_dir: entry.is_dir(), is_dir: entry.is_dir(),
path: entry.path.to_string_lossy().into(), path: entry.path.to_string_lossy().into(),
inode: entry.inode, inode: entry.inode,
mtime: Some(entry.mtime.into()), mtime: entry.mtime.map(|time| time.into()),
is_symlink: entry.is_symlink, is_symlink: entry.is_symlink,
is_ignored: entry.is_ignored, is_ignored: entry.is_ignored,
is_external: entry.is_external, is_external: entry.is_external,
@ -4677,33 +4713,26 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
type Error = anyhow::Error; type Error = anyhow::Error;
fn try_from((root_char_bag, entry): (&'a CharBag, proto::Entry)) -> Result<Self> { fn try_from((root_char_bag, entry): (&'a CharBag, proto::Entry)) -> Result<Self> {
if let Some(mtime) = entry.mtime { let kind = if entry.is_dir {
let kind = if entry.is_dir { EntryKind::Dir
EntryKind::Dir
} else {
let mut char_bag = *root_char_bag;
char_bag.extend(entry.path.chars().map(|c| c.to_ascii_lowercase()));
EntryKind::File(char_bag)
};
let path: Arc<Path> = PathBuf::from(entry.path).into();
Ok(Entry {
id: ProjectEntryId::from_proto(entry.id),
kind,
path,
inode: entry.inode,
mtime: mtime.into(),
is_symlink: entry.is_symlink,
is_ignored: entry.is_ignored,
is_external: entry.is_external,
git_status: git_status_from_proto(entry.git_status),
is_private: false,
})
} else { } else {
Err(anyhow!( let mut char_bag = *root_char_bag;
"missing mtime in remote worktree entry {:?}", char_bag.extend(entry.path.chars().map(|c| c.to_ascii_lowercase()));
entry.path EntryKind::File(char_bag)
)) };
} let path: Arc<Path> = PathBuf::from(entry.path).into();
Ok(Entry {
id: ProjectEntryId::from_proto(entry.id),
kind,
path,
inode: entry.inode,
mtime: entry.mtime.map(|time| time.into()),
is_symlink: entry.is_symlink,
is_ignored: entry.is_ignored,
is_external: entry.is_external,
git_status: git_status_from_proto(entry.git_status),
is_private: false,
})
} }
} }

View file

@ -875,6 +875,41 @@ mod tests {
WorkspaceHandle, WorkspaceHandle,
}; };
#[gpui::test]
async fn test_open_non_existing_file(cx: &mut TestAppContext) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/root",
json!({
"a": {
},
}),
)
.await;
cx.update(|cx| {
open_paths(
&[PathBuf::from("/root/a/new")],
app_state.clone(),
workspace::OpenOptions::default(),
cx,
)
})
.await
.unwrap();
assert_eq!(cx.read(|cx| cx.windows().len()), 1);
let workspace = cx.windows()[0].downcast::<Workspace>().unwrap();
workspace
.update(cx, |workspace, cx| {
assert!(workspace.active_item_as::<Editor>(cx).is_some())
})
.unwrap();
}
#[gpui::test] #[gpui::test]
async fn test_open_paths_action(cx: &mut TestAppContext) { async fn test_open_paths_action(cx: &mut TestAppContext) {
let app_state = init_test(cx); let app_state = init_test(cx);
@ -1657,6 +1692,9 @@ mod tests {
}, },
"excluded_dir": { "excluded_dir": {
"file": "excluded file contents", "file": "excluded file contents",
"ignored_subdir": {
"file": "ignored subfile contents",
},
}, },
}), }),
) )
@ -2305,7 +2343,7 @@ mod tests {
(file3.clone(), DisplayPoint::new(0, 0), 0.) (file3.clone(), DisplayPoint::new(0, 0), 0.)
); );
// Go back to an item that has been closed and removed from disk, ensuring it gets skipped. // Go back to an item that has been closed and removed from disk
workspace workspace
.update(cx, |_, cx| { .update(cx, |_, cx| {
pane.update(cx, |pane, cx| { pane.update(cx, |pane, cx| {
@ -2331,7 +2369,7 @@ mod tests {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
active_location(&workspace, cx), active_location(&workspace, cx),
(file1.clone(), DisplayPoint::new(10, 0), 0.) (file2.clone(), DisplayPoint::new(0, 0), 0.)
); );
workspace workspace
.update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))