Allow opening non-extant files (#9256)

Conrad Irwin created

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)).

Change summary

crates/cli/src/main.rs                      |  51 +++----
crates/copilot/src/copilot.rs               |   4 
crates/editor/src/items.rs                  |   2 
crates/fs/src/fs.rs                         |  20 +++
crates/language/src/buffer.rs               |  37 +++--
crates/project/src/project.rs               |  44 ++++--
crates/semantic_index/src/semantic_index.rs |  15 +
crates/workspace/src/pane.rs                |   6 
crates/workspace/src/workspace.rs           |  34 ++---
crates/worktree/src/worktree.rs             | 139 +++++++++++++---------
crates/zed/src/zed.rs                       |  42 ++++++
11 files changed, 242 insertions(+), 152 deletions(-)

Detailed changes

crates/cli/src/main.rs 🔗

@@ -5,9 +5,9 @@ use clap::Parser;
 use cli::{CliRequest, CliResponse};
 use serde::Deserialize;
 use std::{
+    env,
     ffi::OsStr,
-    fs::{self, OpenOptions},
-    io,
+    fs::{self},
     path::{Path, PathBuf},
 };
 use util::paths::PathLikeWithPosition;
@@ -62,14 +62,26 @@ fn main() -> Result<()> {
         return Ok(());
     }
 
-    for path in args
-        .paths_with_position
-        .iter()
-        .map(|path_with_position| &path_with_position.path_like)
-    {
-        if !path.exists() {
-            touch(path.as_path())?;
-        }
+    let curdir = env::current_dir()?;
+    let mut paths = vec![];
+    for path in args.paths_with_position {
+        let canonicalized = path.map_path_like(|path| match fs::canonicalize(&path) {
+            Ok(path) => Ok(path),
+            Err(e) => {
+                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()?;
@@ -82,17 +94,7 @@ fn main() -> Result<()> {
     };
 
     tx.send(CliRequest::Open {
-        paths: args
-            .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<_>>()?,
+        paths,
         wait: args.wait,
         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> {
     let cli_path = std::env::current_exe()?.canonicalize()?;
     let mut app_path = cli_path.clone();

crates/copilot/src/copilot.rs 🔗

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

crates/editor/src/items.rs 🔗

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

crates/fs/src/fs.rs 🔗

@@ -56,6 +56,7 @@ pub trait Fs: Send + Sync {
     async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>;
     async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
     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 read_link(&self, path: &Path) -> Result<PathBuf>;
     async fn read_dir(
@@ -264,6 +265,12 @@ impl Fs for RealFs {
             .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>> {
         let symlink_metadata = match smol::fs::symlink_metadata(path).await {
             Ok(metadata) => metadata,
@@ -500,7 +507,12 @@ impl FakeFsState {
     fn read_path(&self, target: &Path) -> Result<Arc<Mutex<FakeFsEntry>>> {
         Ok(self
             .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)
     }
 
@@ -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>> {
         self.simulate_random_delay().await;
         let path = normalize_path(path);

crates/language/src/buffer.rs 🔗

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

crates/project/src/project.rs 🔗

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

crates/semantic_index/src/semantic_index.rs 🔗

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

crates/workspace/src/pane.rs 🔗

@@ -2108,7 +2108,11 @@ impl NavHistoryState {
 fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
     let path = buffer_path
         .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");
     let path = truncate_and_remove_front(path, 80);
     format!("{path} contains unsaved edits. Do you want to save it?")

crates/workspace/src/workspace.rs 🔗

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

crates/worktree/src/worktree.rs 🔗

@@ -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(
         &self,
         path: &Path,
@@ -1088,7 +1114,7 @@ impl LocalWorktree {
                             entry_id: None,
                             worktree,
                             path,
-                            mtime: metadata.mtime,
+                            mtime: Some(metadata.mtime),
                             is_local: true,
                             is_deleted: false,
                             is_private,
@@ -1105,7 +1131,7 @@ impl LocalWorktree {
         &self,
         buffer_handle: Model<Buffer>,
         path: Arc<Path>,
-        has_changed_file: bool,
+        mut has_changed_file: bool,
         cx: &mut ModelContext<Worktree>,
     ) -> Task<Result<()>> {
         let buffer = buffer_handle.read(cx);
@@ -1114,6 +1140,10 @@ impl LocalWorktree {
         let buffer_id: u64 = buffer.remote_id().into();
         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 fingerprint = text.fingerprint();
         let version = buffer.version();
@@ -1141,7 +1171,7 @@ impl LocalWorktree {
                         .with_context(|| {
                             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,
                     buffer_id,
                     version: serialize_version(&version),
-                    mtime: Some(mtime.into()),
+                    mtime: mtime.map(|time| time.into()),
                     fingerprint: serialize_fingerprint(fingerprint),
                 })?;
             }
@@ -1585,10 +1615,7 @@ impl RemoteWorktree {
                 .await?;
             let version = deserialize_version(&response.version);
             let fingerprint = deserialize_fingerprint(&response.fingerprint)?;
-            let mtime = response
-                .mtime
-                .ok_or_else(|| anyhow!("missing mtime"))?
-                .into();
+            let mtime = response.mtime.map(|mtime| mtime.into());
 
             buffer_handle.update(&mut cx, |buffer, 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 {
                 continue;
             };
+            let Some(mtime) = entry.mtime else {
+                continue;
+            };
             let repo_path = RepoPath(repo_path.to_path_buf());
             let git_file_status = combine_git_statuses(
                 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 {
                 entry.git_status = git_file_status;
@@ -2850,7 +2880,7 @@ impl fmt::Debug for Snapshot {
 pub struct File {
     pub worktree: Model<Worktree>,
     pub path: Arc<Path>,
-    pub mtime: SystemTime,
+    pub mtime: Option<SystemTime>,
     pub entry_id: Option<ProjectEntryId>,
     pub is_local: 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
     }
 
@@ -2923,7 +2953,7 @@ impl language::File for File {
             worktree_id: self.worktree.entity_id().as_u64(),
             entry_id: self.entry_id.map(|id| id.to_proto()),
             path: self.path.to_string_lossy().into(),
-            mtime: Some(self.mtime.into()),
+            mtime: self.mtime.map(|time| time.into()),
             is_deleted: self.is_deleted,
         }
     }
@@ -2957,7 +2987,7 @@ impl language::LocalFile for File {
         version: &clock::Global,
         fingerprint: RopeFingerprint,
         line_ending: LineEnding,
-        mtime: SystemTime,
+        mtime: Option<SystemTime>,
         cx: &mut AppContext,
     ) {
         let worktree = self.worktree.read(cx).as_local().unwrap();
@@ -2968,7 +2998,7 @@ impl language::LocalFile for File {
                     project_id,
                     buffer_id: buffer_id.into(),
                     version: serialize_version(version),
-                    mtime: Some(mtime.into()),
+                    mtime: mtime.map(|time| time.into()),
                     fingerprint: serialize_fingerprint(fingerprint),
                     line_ending: serialize_line_ending(line_ending) as i32,
                 })
@@ -3008,7 +3038,7 @@ impl File {
         Ok(Self {
             worktree,
             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),
             is_local: false,
             is_deleted: proto.is_deleted,
@@ -3039,7 +3069,7 @@ pub struct Entry {
     pub kind: EntryKind,
     pub path: Arc<Path>,
     pub inode: u64,
-    pub mtime: SystemTime,
+    pub mtime: Option<SystemTime>,
     pub is_symlink: bool,
 
     /// Whether this entry is ignored by Git.
@@ -3109,7 +3139,7 @@ impl Entry {
             },
             path,
             inode: metadata.inode,
-            mtime: metadata.mtime,
+            mtime: Some(metadata.mtime),
             is_symlink: metadata.is_symlink,
             is_ignored: 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 {
         self.kind.is_dir()
     }
@@ -3456,7 +3490,7 @@ impl BackgroundScanner {
             Ok(path) => path,
             Err(err) => {
                 log::error!("failed to canonicalize root path: {}", err);
-                return false;
+                return true;
             }
         };
         let abs_paths = request
@@ -3878,13 +3912,13 @@ impl BackgroundScanner {
                         &job.containing_repository
                     {
                         if let Ok(repo_path) = child_entry.path.strip_prefix(&repository_dir.0) {
-                            let repo_path = RepoPath(repo_path.into());
-                            child_entry.git_status = combine_git_statuses(
-                                staged_statuses.get(&repo_path).copied(),
-                                repository
-                                    .lock()
-                                    .unstaged_status(&repo_path, child_entry.mtime),
-                            );
+                            if let Some(mtime) = child_entry.mtime {
+                                let repo_path = RepoPath(repo_path.into());
+                                child_entry.git_status = combine_git_statuses(
+                                    staged_statuses.get(&repo_path).copied(),
+                                    repository.lock().unstaged_status(&repo_path, mtime),
+                                );
+                            }
                         }
                     }
                 }
@@ -4018,9 +4052,11 @@ impl BackgroundScanner {
                     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 Ok(repo_path) = path.strip_prefix(work_dir.0) {
-                                let repo_path = RepoPath(repo_path.into());
-                                let repo = repo.repo_ptr.lock();
-                                fs_entry.git_status = repo.status(&repo_path, fs_entry.mtime);
+                                if let Some(mtime) = fs_entry.mtime {
+                                    let repo_path = RepoPath(repo_path.into());
+                                    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(),
             path: entry.path.to_string_lossy().into(),
             inode: entry.inode,
-            mtime: Some(entry.mtime.into()),
+            mtime: entry.mtime.map(|time| time.into()),
             is_symlink: entry.is_symlink,
             is_ignored: entry.is_ignored,
             is_external: entry.is_external,
@@ -4677,33 +4713,26 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
     type Error = anyhow::Error;
 
     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 {
-                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,
-            })
+        let kind = if entry.is_dir {
+            EntryKind::Dir
         } else {
-            Err(anyhow!(
-                "missing mtime in remote worktree entry {:?}",
-                entry.path
-            ))
-        }
+            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: 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,
+        })
     }
 }
 

crates/zed/src/zed.rs 🔗

@@ -875,6 +875,41 @@ mod tests {
         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]
     async fn test_open_paths_action(cx: &mut TestAppContext) {
         let app_state = init_test(cx);
@@ -1657,6 +1692,9 @@ mod tests {
                     },
                     "excluded_dir": {
                         "file": "excluded file contents",
+                        "ignored_subdir": {
+                            "file": "ignored subfile contents",
+                        },
                     },
                 }),
             )
@@ -2305,7 +2343,7 @@ mod tests {
             (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
             .update(cx, |_, cx| {
                 pane.update(cx, |pane, cx| {
@@ -2331,7 +2369,7 @@ mod tests {
             .unwrap();
         assert_eq!(
             active_location(&workspace, cx),
-            (file1.clone(), DisplayPoint::new(10, 0), 0.)
+            (file2.clone(), DisplayPoint::new(0, 0), 0.)
         );
         workspace
             .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))