From 646f69583a96aab4d3bcecdb03a0f59b8772c93e Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 12 Mar 2024 22:30:04 -0600 Subject: [PATCH] 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)). --- 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(-) diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 121d9ef611..c5c484bb4f 100644 --- a/crates/cli/src/main.rs +++ b/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::>()?, + 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 { let cli_path = std::env::current_exe()?.canonicalize()?; let mut app_path = cli_path.clone(); diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index e6552cdc33..fb0d7c4543 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1220,7 +1220,7 @@ mod tests { Some(self) } - fn mtime(&self) -> std::time::SystemTime { + fn mtime(&self) -> Option { unimplemented!() } @@ -1272,7 +1272,7 @@ mod tests { _: &clock::Global, _: language::RopeFingerprint, _: language::LineEnding, - _: std::time::SystemTime, + _: Option, _: &mut AppContext, ) { unimplemented!() diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 2078baf5aa..d256b0eb16 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1280,7 +1280,7 @@ mod tests { unimplemented!() } - fn mtime(&self) -> SystemTime { + fn mtime(&self) -> Option { unimplemented!() } diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 88f77da608..522cecb586 100644 --- a/crates/fs/src/fs.rs +++ b/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; async fn is_file(&self, path: &Path) -> bool; + async fn is_dir(&self, path: &Path) -> bool; async fn metadata(&self, path: &Path) -> Result>; async fn read_link(&self, path: &Path) -> Result; 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> { 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>> { 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> { self.simulate_random_delay().await; let path = normalize_path(path); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index c4f1a650bb..13a81a52cc 100644 --- a/crates/language/src/buffer.rs +++ b/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>, /// The mtime of the file when this buffer was last loaded from /// or saved to disk. - saved_mtime: SystemTime, + saved_mtime: Option, /// 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; /// Returns the path of this file relative to the worktree's root directory. fn path(&self) -> &Arc; @@ -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, 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>, 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 { self.saved_mtime } @@ -786,7 +784,7 @@ impl Buffer { &mut self, version: clock::Global, fingerprint: RopeFingerprint, - mtime: SystemTime, + mtime: Option, cx: &mut ModelContext, ) { self.saved_version = version; @@ -861,7 +859,7 @@ impl Buffer { version: clock::Global, fingerprint: RopeFingerprint, line_ending: LineEnding, - mtime: SystemTime, + mtime: Option, cx: &mut ModelContext, ) { 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 diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 06fcc68db6..46c2ccd2a4 100644 --- a/crates/project/src/project.rs +++ b/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, - worktree: &Model, + path: Arc, + worktree: Model, cx: &mut ModelContext, ) -> Task>> { 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::() + .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 diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index e57da7bc9b..8f5da4ffd8 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/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, }, ); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 5422a47f49..a7d46264ee 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2108,7 +2108,11 @@ impl NavHistoryState { fn dirty_message_for(buffer_path: Option) -> 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?") diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 87cfdd9c13..8198cab533 100644 --- a/crates/workspace/src/workspace.rs +++ b/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 } } }) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 4302754dca..68db7a4272 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -761,6 +761,32 @@ impl LocalWorktree { }) } + pub fn new_buffer( + &mut self, + buffer_id: BufferId, + path: Arc, + cx: &mut ModelContext, + ) -> Model { + 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, path: Arc, - has_changed_file: bool, + mut has_changed_file: bool, cx: &mut ModelContext, ) -> Task> { 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, pub path: Arc, - pub mtime: SystemTime, + pub mtime: Option, pub entry_id: Option, 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 { 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, 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, pub inode: u64, - pub mtime: SystemTime, + pub mtime: Option, 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 { - 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 = 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 = 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, + }) } } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index a9deb6818c..68d6b385e0 100644 --- a/crates/zed/src/zed.rs +++ b/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::().unwrap(); + workspace + .update(cx, |workspace, cx| { + assert!(workspace.active_item_as::(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))