Better absolute path handling (#19727)
Closes #19866 This PR supersedes #19228, as #19228 encountered too many merge conflicts. After some exploration, I found that for paths with the `\\?\` prefix, we can safely remove it and consistently use the clean paths in all cases. Previously, in #19228, I thought we would still need the `\\?\` prefix for IO operations to handle long paths better. However, this turns out to be unnecessary because Rust automatically manages this for us when calling IO-related APIs. For details, refer to Rust's internal function [`get_long_path`](017ae1b21f/library/std/src/sys/path/windows.rs (L225-L233)
). Therefore, we can always store and use paths without the `\\?\` prefix. This PR introduces a `SanitizedPath` structure, which represents a path stripped of the `\\?\` prefix. To prevent untrimmed paths from being mistakenly passed into `Worktree`, the type of `Worktree`’s `abs_path` member variable has been changed to `SanitizedPath`. Additionally, this PR reverts the changes of #15856 and #18726. After testing, it appears that the issues those PRs addressed can be resolved by this PR. ### Existing Issue To keep the scope of modifications manageable, `Worktree::abs_path` has retained its current signature as `fn abs_path(&self) -> Arc<Path>`, rather than returning a `SanitizedPath`. Updating the method to return `SanitizedPath`—which may better resolve path inconsistencies—would likely introduce extensive changes similar to those in #19228. Currently, the limitation is as follows: ```rust let abs_path: &Arc<Path> = snapshot.abs_path(); let some_non_trimmed_path = Path::new("\\\\?\\C:\\Users\\user\\Desktop\\project"); // The caller performs some actions here: some_non_trimmed_path.strip_prefix(abs_path); // This fails some_non_trimmed_path.starts_with(abs_path); // This fails too ``` The final two lines will fail because `snapshot.abs_path()` returns a clean path without the `\\?\` prefix. I have identified two relevant instances that may face this issue: - [lsp_store.rs#L3578](0173479d18/crates/project/src/lsp_store.rs (L3578)
) - [worktree.rs#L4338](0173479d18/crates/worktree/src/worktree.rs (L4338)
) Switching `Worktree::abs_path` to return `SanitizedPath` would resolve these issues but would also lead to many code changes. Any suggestions or feedback on this approach are very welcome. cc @SomeoneToIgnore Release Notes: - N/A
This commit is contained in:
parent
d0bafce86b
commit
cff9ae0bbc
12 changed files with 189 additions and 113 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -3752,6 +3752,12 @@ dependencies = [
|
||||||
"phf",
|
"phf",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dunce"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dwrote"
|
name = "dwrote"
|
||||||
version = "0.11.2"
|
version = "0.11.2"
|
||||||
|
@ -13689,6 +13695,7 @@ dependencies = [
|
||||||
"async-fs 1.6.0",
|
"async-fs 1.6.0",
|
||||||
"collections",
|
"collections",
|
||||||
"dirs 4.0.0",
|
"dirs 4.0.0",
|
||||||
|
"dunce",
|
||||||
"futures 0.3.31",
|
"futures 0.3.31",
|
||||||
"futures-lite 1.13.0",
|
"futures-lite 1.13.0",
|
||||||
"git2",
|
"git2",
|
||||||
|
|
12
Cargo.toml
12
Cargo.toml
|
@ -228,7 +228,9 @@ git = { path = "crates/git" }
|
||||||
git_hosting_providers = { path = "crates/git_hosting_providers" }
|
git_hosting_providers = { path = "crates/git_hosting_providers" }
|
||||||
go_to_line = { path = "crates/go_to_line" }
|
go_to_line = { path = "crates/go_to_line" }
|
||||||
google_ai = { path = "crates/google_ai" }
|
google_ai = { path = "crates/google_ai" }
|
||||||
gpui = { path = "crates/gpui", default-features = false, features = ["http_client"]}
|
gpui = { path = "crates/gpui", default-features = false, features = [
|
||||||
|
"http_client",
|
||||||
|
] }
|
||||||
gpui_macros = { path = "crates/gpui_macros" }
|
gpui_macros = { path = "crates/gpui_macros" }
|
||||||
html_to_markdown = { path = "crates/html_to_markdown" }
|
html_to_markdown = { path = "crates/html_to_markdown" }
|
||||||
http_client = { path = "crates/http_client" }
|
http_client = { path = "crates/http_client" }
|
||||||
|
@ -403,10 +405,10 @@ parking_lot = "0.12.1"
|
||||||
pathdiff = "0.2"
|
pathdiff = "0.2"
|
||||||
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||||
pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||||
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||||
pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||||
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||||
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||||
postage = { version = "0.5", features = ["futures-traits"] }
|
postage = { version = "0.5", features = ["futures-traits"] }
|
||||||
pretty_assertions = { version = "1.3.0", features = ["unstable"] }
|
pretty_assertions = { version = "1.3.0", features = ["unstable"] }
|
||||||
profiling = "1"
|
profiling = "1"
|
||||||
|
|
|
@ -452,18 +452,16 @@ impl Fs for RealFs {
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> {
|
async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> {
|
||||||
|
use util::paths::SanitizedPath;
|
||||||
use windows::{
|
use windows::{
|
||||||
core::HSTRING,
|
core::HSTRING,
|
||||||
Storage::{StorageDeleteOption, StorageFile},
|
Storage::{StorageDeleteOption, StorageFile},
|
||||||
};
|
};
|
||||||
// todo(windows)
|
// todo(windows)
|
||||||
// When new version of `windows-rs` release, make this operation `async`
|
// When new version of `windows-rs` release, make this operation `async`
|
||||||
let path = path.canonicalize()?.to_string_lossy().to_string();
|
let path = SanitizedPath::from(path.canonicalize()?);
|
||||||
let path_str = path.trim_start_matches("\\\\?\\");
|
let path_string = path.to_string();
|
||||||
if path_str.is_empty() {
|
let file = StorageFile::GetFileFromPathAsync(&HSTRING::from(path_string))?.get()?;
|
||||||
anyhow::bail!("File path is empty!");
|
|
||||||
}
|
|
||||||
let file = StorageFile::GetFileFromPathAsync(&HSTRING::from(path_str))?.get()?;
|
|
||||||
file.DeleteAsync(StorageDeleteOption::Default)?.get()?;
|
file.DeleteAsync(StorageDeleteOption::Default)?.get()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -480,19 +478,17 @@ impl Fs for RealFs {
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
async fn trash_dir(&self, path: &Path, _options: RemoveOptions) -> Result<()> {
|
async fn trash_dir(&self, path: &Path, _options: RemoveOptions) -> Result<()> {
|
||||||
|
use util::paths::SanitizedPath;
|
||||||
use windows::{
|
use windows::{
|
||||||
core::HSTRING,
|
core::HSTRING,
|
||||||
Storage::{StorageDeleteOption, StorageFolder},
|
Storage::{StorageDeleteOption, StorageFolder},
|
||||||
};
|
};
|
||||||
|
|
||||||
let path = path.canonicalize()?.to_string_lossy().to_string();
|
|
||||||
let path_str = path.trim_start_matches("\\\\?\\");
|
|
||||||
if path_str.is_empty() {
|
|
||||||
anyhow::bail!("Folder path is empty!");
|
|
||||||
}
|
|
||||||
// todo(windows)
|
// todo(windows)
|
||||||
// When new version of `windows-rs` release, make this operation `async`
|
// When new version of `windows-rs` release, make this operation `async`
|
||||||
let folder = StorageFolder::GetFolderFromPathAsync(&HSTRING::from(path_str))?.get()?;
|
let path = SanitizedPath::from(path.canonicalize()?);
|
||||||
|
let path_string = path.to_string();
|
||||||
|
let folder = StorageFolder::GetFolderFromPathAsync(&HSTRING::from(path_string))?.get()?;
|
||||||
folder.DeleteAsync(StorageDeleteOption::Default)?.get()?;
|
folder.DeleteAsync(StorageDeleteOption::Default)?.get()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ use std::{
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use ::util::ResultExt;
|
use ::util::{paths::SanitizedPath, ResultExt};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use async_task::Runnable;
|
use async_task::Runnable;
|
||||||
use futures::channel::oneshot::{self, Receiver};
|
use futures::channel::oneshot::{self, Receiver};
|
||||||
|
@ -645,13 +645,11 @@ fn file_save_dialog(directory: PathBuf) -> Result<Option<PathBuf>> {
|
||||||
let dialog: IFileSaveDialog = unsafe { CoCreateInstance(&FileSaveDialog, None, CLSCTX_ALL)? };
|
let dialog: IFileSaveDialog = unsafe { CoCreateInstance(&FileSaveDialog, None, CLSCTX_ALL)? };
|
||||||
if !directory.to_string_lossy().is_empty() {
|
if !directory.to_string_lossy().is_empty() {
|
||||||
if let Some(full_path) = directory.canonicalize().log_err() {
|
if let Some(full_path) = directory.canonicalize().log_err() {
|
||||||
let full_path = full_path.to_string_lossy();
|
let full_path = SanitizedPath::from(full_path);
|
||||||
let full_path_str = full_path.trim_start_matches("\\\\?\\");
|
let full_path_string = full_path.to_string();
|
||||||
if !full_path_str.is_empty() {
|
let path_item: IShellItem =
|
||||||
let path_item: IShellItem =
|
unsafe { SHCreateItemFromParsingName(&HSTRING::from(full_path_string), None)? };
|
||||||
unsafe { SHCreateItemFromParsingName(&HSTRING::from(full_path_str), None)? };
|
unsafe { dialog.SetFolder(&path_item).log_err() };
|
||||||
unsafe { dialog.SetFolder(&path_item).log_err() };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
unsafe {
|
unsafe {
|
||||||
|
|
|
@ -5577,7 +5577,7 @@ impl LspStore {
|
||||||
|
|
||||||
let worktree = worktree_handle.read(cx);
|
let worktree = worktree_handle.read(cx);
|
||||||
let worktree_id = worktree.id();
|
let worktree_id = worktree.id();
|
||||||
let worktree_path = worktree.abs_path();
|
let root_path = worktree.abs_path();
|
||||||
let key = (worktree_id, adapter.name.clone());
|
let key = (worktree_id, adapter.name.clone());
|
||||||
|
|
||||||
if self.language_server_ids.contains_key(&key) {
|
if self.language_server_ids.contains_key(&key) {
|
||||||
|
@ -5599,7 +5599,6 @@ impl LspStore {
|
||||||
as Arc<dyn LspAdapterDelegate>;
|
as Arc<dyn LspAdapterDelegate>;
|
||||||
|
|
||||||
let server_id = self.languages.next_language_server_id();
|
let server_id = self.languages.next_language_server_id();
|
||||||
let root_path = worktree_path.clone();
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"attempting to start language server {:?}, path: {root_path:?}, id: {server_id}",
|
"attempting to start language server {:?}, path: {root_path:?}, id: {server_id}",
|
||||||
adapter.name.0
|
adapter.name.0
|
||||||
|
|
|
@ -23,7 +23,7 @@ use smol::{
|
||||||
stream::StreamExt,
|
stream::StreamExt,
|
||||||
};
|
};
|
||||||
use text::ReplicaId;
|
use text::ReplicaId;
|
||||||
use util::ResultExt;
|
use util::{paths::SanitizedPath, ResultExt};
|
||||||
use worktree::{Entry, ProjectEntryId, Worktree, WorktreeId, WorktreeSettings};
|
use worktree::{Entry, ProjectEntryId, Worktree, WorktreeId, WorktreeSettings};
|
||||||
|
|
||||||
use crate::{search::SearchQuery, ProjectPath};
|
use crate::{search::SearchQuery, ProjectPath};
|
||||||
|
@ -52,7 +52,7 @@ pub struct WorktreeStore {
|
||||||
worktrees_reordered: bool,
|
worktrees_reordered: bool,
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
loading_worktrees:
|
loading_worktrees:
|
||||||
HashMap<Arc<Path>, Shared<Task<Result<Model<Worktree>, Arc<anyhow::Error>>>>>,
|
HashMap<SanitizedPath, Shared<Task<Result<Model<Worktree>, Arc<anyhow::Error>>>>>,
|
||||||
state: WorktreeStoreState,
|
state: WorktreeStoreState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,11 +147,12 @@ impl WorktreeStore {
|
||||||
|
|
||||||
pub fn find_worktree(
|
pub fn find_worktree(
|
||||||
&self,
|
&self,
|
||||||
abs_path: &Path,
|
abs_path: impl Into<SanitizedPath>,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> Option<(Model<Worktree>, PathBuf)> {
|
) -> Option<(Model<Worktree>, PathBuf)> {
|
||||||
|
let abs_path: SanitizedPath = abs_path.into();
|
||||||
for tree in self.worktrees() {
|
for tree in self.worktrees() {
|
||||||
if let Ok(relative_path) = abs_path.strip_prefix(tree.read(cx).abs_path()) {
|
if let Ok(relative_path) = abs_path.as_path().strip_prefix(tree.read(cx).abs_path()) {
|
||||||
return Some((tree.clone(), relative_path.into()));
|
return Some((tree.clone(), relative_path.into()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -192,12 +193,12 @@ impl WorktreeStore {
|
||||||
|
|
||||||
pub fn create_worktree(
|
pub fn create_worktree(
|
||||||
&mut self,
|
&mut self,
|
||||||
abs_path: impl AsRef<Path>,
|
abs_path: impl Into<SanitizedPath>,
|
||||||
visible: bool,
|
visible: bool,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<Model<Worktree>>> {
|
) -> Task<Result<Model<Worktree>>> {
|
||||||
let path: Arc<Path> = abs_path.as_ref().into();
|
let abs_path: SanitizedPath = abs_path.into();
|
||||||
if !self.loading_worktrees.contains_key(&path) {
|
if !self.loading_worktrees.contains_key(&abs_path) {
|
||||||
let task = match &self.state {
|
let task = match &self.state {
|
||||||
WorktreeStoreState::Remote {
|
WorktreeStoreState::Remote {
|
||||||
upstream_client, ..
|
upstream_client, ..
|
||||||
|
@ -205,20 +206,26 @@ impl WorktreeStore {
|
||||||
if upstream_client.is_via_collab() {
|
if upstream_client.is_via_collab() {
|
||||||
Task::ready(Err(Arc::new(anyhow!("cannot create worktrees via collab"))))
|
Task::ready(Err(Arc::new(anyhow!("cannot create worktrees via collab"))))
|
||||||
} else {
|
} else {
|
||||||
self.create_ssh_worktree(upstream_client.clone(), abs_path, visible, cx)
|
self.create_ssh_worktree(
|
||||||
|
upstream_client.clone(),
|
||||||
|
abs_path.clone(),
|
||||||
|
visible,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WorktreeStoreState::Local { fs } => {
|
WorktreeStoreState::Local { fs } => {
|
||||||
self.create_local_worktree(fs.clone(), abs_path, visible, cx)
|
self.create_local_worktree(fs.clone(), abs_path.clone(), visible, cx)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
self.loading_worktrees.insert(path.clone(), task.shared());
|
self.loading_worktrees
|
||||||
|
.insert(abs_path.clone(), task.shared());
|
||||||
}
|
}
|
||||||
let task = self.loading_worktrees.get(&path).unwrap().clone();
|
let task = self.loading_worktrees.get(&abs_path).unwrap().clone();
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let result = task.await;
|
let result = task.await;
|
||||||
this.update(&mut cx, |this, _| this.loading_worktrees.remove(&path))
|
this.update(&mut cx, |this, _| this.loading_worktrees.remove(&abs_path))
|
||||||
.ok();
|
.ok();
|
||||||
match result {
|
match result {
|
||||||
Ok(worktree) => Ok(worktree),
|
Ok(worktree) => Ok(worktree),
|
||||||
|
@ -230,12 +237,11 @@ impl WorktreeStore {
|
||||||
fn create_ssh_worktree(
|
fn create_ssh_worktree(
|
||||||
&mut self,
|
&mut self,
|
||||||
client: AnyProtoClient,
|
client: AnyProtoClient,
|
||||||
abs_path: impl AsRef<Path>,
|
abs_path: impl Into<SanitizedPath>,
|
||||||
visible: bool,
|
visible: bool,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<Model<Worktree>, Arc<anyhow::Error>>> {
|
) -> Task<Result<Model<Worktree>, Arc<anyhow::Error>>> {
|
||||||
let path_key: Arc<Path> = abs_path.as_ref().into();
|
let mut abs_path = Into::<SanitizedPath>::into(abs_path).to_string();
|
||||||
let mut abs_path = path_key.clone().to_string_lossy().to_string();
|
|
||||||
// If we start with `/~` that means the ssh path was something like `ssh://user@host/~/home-dir-folder/`
|
// If we start with `/~` that means the ssh path was something like `ssh://user@host/~/home-dir-folder/`
|
||||||
// in which case want to strip the leading the `/`.
|
// in which case want to strip the leading the `/`.
|
||||||
// On the host-side, the `~` will get expanded.
|
// On the host-side, the `~` will get expanded.
|
||||||
|
@ -293,12 +299,12 @@ impl WorktreeStore {
|
||||||
fn create_local_worktree(
|
fn create_local_worktree(
|
||||||
&mut self,
|
&mut self,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
abs_path: impl AsRef<Path>,
|
abs_path: impl Into<SanitizedPath>,
|
||||||
visible: bool,
|
visible: bool,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<Model<Worktree>, Arc<anyhow::Error>>> {
|
) -> Task<Result<Model<Worktree>, Arc<anyhow::Error>>> {
|
||||||
let next_entry_id = self.next_entry_id.clone();
|
let next_entry_id = self.next_entry_id.clone();
|
||||||
let path: Arc<Path> = abs_path.as_ref().into();
|
let path: SanitizedPath = abs_path.into();
|
||||||
|
|
||||||
cx.spawn(move |this, mut cx| async move {
|
cx.spawn(move |this, mut cx| async move {
|
||||||
let worktree = Worktree::local(path.clone(), visible, fs, next_entry_id, &mut cx).await;
|
let worktree = Worktree::local(path.clone(), visible, fs, next_entry_id, &mut cx).await;
|
||||||
|
@ -308,7 +314,7 @@ impl WorktreeStore {
|
||||||
|
|
||||||
if visible {
|
if visible {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
cx.add_recent_document(&path);
|
cx.add_recent_document(path.as_path());
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
|
|
|
@ -798,7 +798,6 @@ fn possible_open_paths_metadata(
|
||||||
cx.background_executor().spawn(async move {
|
cx.background_executor().spawn(async move {
|
||||||
let mut paths_with_metadata = Vec::with_capacity(potential_paths.len());
|
let mut paths_with_metadata = Vec::with_capacity(potential_paths.len());
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
let mut fetch_metadata_tasks = potential_paths
|
let mut fetch_metadata_tasks = potential_paths
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|potential_path| async {
|
.map(|potential_path| async {
|
||||||
|
@ -814,20 +813,6 @@ fn possible_open_paths_metadata(
|
||||||
})
|
})
|
||||||
.collect::<FuturesUnordered<_>>();
|
.collect::<FuturesUnordered<_>>();
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
let mut fetch_metadata_tasks = potential_paths
|
|
||||||
.iter()
|
|
||||||
.map(|potential_path| async {
|
|
||||||
let metadata = fs.metadata(potential_path).await.ok().flatten();
|
|
||||||
let path = PathBuf::from(
|
|
||||||
potential_path
|
|
||||||
.to_string_lossy()
|
|
||||||
.trim_start_matches("\\\\?\\"),
|
|
||||||
);
|
|
||||||
(PathWithPosition { path, row, column }, metadata)
|
|
||||||
})
|
|
||||||
.collect::<FuturesUnordered<_>>();
|
|
||||||
|
|
||||||
while let Some((path, metadata)) = fetch_metadata_tasks.next().await {
|
while let Some((path, metadata)) = fetch_metadata_tasks.next().await {
|
||||||
if let Some(metadata) = metadata {
|
if let Some(metadata) = metadata {
|
||||||
paths_with_metadata.push((path, metadata));
|
paths_with_metadata.push((path, metadata));
|
||||||
|
|
|
@ -37,6 +37,7 @@ unicase.workspace = true
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
tendril = "0.4.3"
|
tendril = "0.4.3"
|
||||||
|
dunce = "1.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
git2.workspace = true
|
git2.workspace = true
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
use std::sync::OnceLock;
|
use std::sync::{Arc, OnceLock};
|
||||||
use std::{
|
use std::{
|
||||||
ffi::OsStr,
|
ffi::OsStr,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
@ -95,6 +95,46 @@ impl<T: AsRef<Path>> PathExt for T {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Due to the issue of UNC paths on Windows, which can cause bugs in various parts of Zed, introducing this `SanitizedPath`
|
||||||
|
/// leverages Rust's type system to ensure that all paths entering Zed are always "sanitized" by removing the `\\\\?\\` prefix.
|
||||||
|
/// On non-Windows operating systems, this struct is effectively a no-op.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct SanitizedPath(Arc<Path>);
|
||||||
|
|
||||||
|
impl SanitizedPath {
|
||||||
|
pub fn starts_with(&self, prefix: &SanitizedPath) -> bool {
|
||||||
|
self.0.starts_with(&prefix.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_path(&self) -> &Arc<Path> {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_string(&self) -> String {
|
||||||
|
self.0.to_string_lossy().to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SanitizedPath> for Arc<Path> {
|
||||||
|
fn from(sanitized_path: SanitizedPath) -> Self {
|
||||||
|
sanitized_path.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: AsRef<Path>> From<T> for SanitizedPath {
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
fn from(path: T) -> Self {
|
||||||
|
let path = path.as_ref();
|
||||||
|
SanitizedPath(path.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn from(path: T) -> Self {
|
||||||
|
let path = path.as_ref();
|
||||||
|
SanitizedPath(dunce::simplified(path).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A delimiter to use in `path_query:row_number:column_number` strings parsing.
|
/// A delimiter to use in `path_query:row_number:column_number` strings parsing.
|
||||||
pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
|
pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
|
||||||
|
|
||||||
|
@ -805,4 +845,22 @@ mod tests {
|
||||||
"Path matcher should match {path:?}"
|
"Path matcher should match {path:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn test_sanitized_path() {
|
||||||
|
let path = Path::new("C:\\Users\\someone\\test_file.rs");
|
||||||
|
let sanitized_path = SanitizedPath::from(path);
|
||||||
|
assert_eq!(
|
||||||
|
sanitized_path.to_string(),
|
||||||
|
"C:\\Users\\someone\\test_file.rs"
|
||||||
|
);
|
||||||
|
|
||||||
|
let path = Path::new("\\\\?\\C:\\Users\\someone\\test_file.rs");
|
||||||
|
let sanitized_path = SanitizedPath::from(path);
|
||||||
|
assert_eq!(
|
||||||
|
sanitized_path.to_string(),
|
||||||
|
"C:\\Users\\someone\\test_file.rs"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,7 +97,7 @@ use ui::{
|
||||||
IntoElement, ParentElement as _, Pixels, SharedString, Styled as _, ViewContext,
|
IntoElement, ParentElement as _, Pixels, SharedString, Styled as _, ViewContext,
|
||||||
VisualContext as _, WindowContext,
|
VisualContext as _, WindowContext,
|
||||||
};
|
};
|
||||||
use util::{ResultExt, TryFutureExt};
|
use util::{paths::SanitizedPath, ResultExt, TryFutureExt};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
pub use workspace_settings::{
|
pub use workspace_settings::{
|
||||||
AutosaveSetting, RestoreOnStartupBehavior, TabBarSettings, WorkspaceSettings,
|
AutosaveSetting, RestoreOnStartupBehavior, TabBarSettings, WorkspaceSettings,
|
||||||
|
@ -2024,7 +2024,7 @@ impl Workspace {
|
||||||
};
|
};
|
||||||
|
|
||||||
let this = this.clone();
|
let this = this.clone();
|
||||||
let abs_path = abs_path.clone();
|
let abs_path: Arc<Path> = SanitizedPath::from(abs_path.clone()).into();
|
||||||
let fs = fs.clone();
|
let fs = fs.clone();
|
||||||
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 {
|
||||||
|
@ -2033,7 +2033,7 @@ impl Workspace {
|
||||||
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();
|
||||||
let entry_id = if abs_path == worktree_abs_path.as_ref() {
|
let entry_id = if abs_path.as_ref() == worktree_abs_path.as_ref() {
|
||||||
worktree.root_entry()
|
worktree.root_entry()
|
||||||
} else {
|
} else {
|
||||||
abs_path
|
abs_path
|
||||||
|
|
|
@ -66,7 +66,7 @@ use std::{
|
||||||
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
|
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
|
||||||
use text::{LineEnding, Rope};
|
use text::{LineEnding, Rope};
|
||||||
use util::{
|
use util::{
|
||||||
paths::{home_dir, PathMatcher},
|
paths::{home_dir, PathMatcher, SanitizedPath},
|
||||||
ResultExt,
|
ResultExt,
|
||||||
};
|
};
|
||||||
pub use worktree_settings::WorktreeSettings;
|
pub use worktree_settings::WorktreeSettings;
|
||||||
|
@ -149,7 +149,7 @@ pub struct RemoteWorktree {
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Snapshot {
|
pub struct Snapshot {
|
||||||
id: WorktreeId,
|
id: WorktreeId,
|
||||||
abs_path: Arc<Path>,
|
abs_path: SanitizedPath,
|
||||||
root_name: String,
|
root_name: String,
|
||||||
root_char_bag: CharBag,
|
root_char_bag: CharBag,
|
||||||
entries_by_path: SumTree<Entry>,
|
entries_by_path: SumTree<Entry>,
|
||||||
|
@ -356,7 +356,7 @@ enum ScanState {
|
||||||
scanning: bool,
|
scanning: bool,
|
||||||
},
|
},
|
||||||
RootUpdated {
|
RootUpdated {
|
||||||
new_path: Option<Arc<Path>>,
|
new_path: Option<SanitizedPath>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -654,8 +654,8 @@ impl Worktree {
|
||||||
|
|
||||||
pub fn abs_path(&self) -> Arc<Path> {
|
pub fn abs_path(&self) -> Arc<Path> {
|
||||||
match self {
|
match self {
|
||||||
Worktree::Local(worktree) => worktree.abs_path.clone(),
|
Worktree::Local(worktree) => worktree.abs_path.clone().into(),
|
||||||
Worktree::Remote(worktree) => worktree.abs_path.clone(),
|
Worktree::Remote(worktree) => worktree.abs_path.clone().into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1026,6 +1026,7 @@ impl LocalWorktree {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn contains_abs_path(&self, path: &Path) -> bool {
|
pub fn contains_abs_path(&self, path: &Path) -> bool {
|
||||||
|
let path = SanitizedPath::from(path);
|
||||||
path.starts_with(&self.abs_path)
|
path.starts_with(&self.abs_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1066,13 +1067,13 @@ impl LocalWorktree {
|
||||||
let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
|
let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
|
||||||
let background_scanner = cx.background_executor().spawn({
|
let background_scanner = cx.background_executor().spawn({
|
||||||
let abs_path = &snapshot.abs_path;
|
let abs_path = &snapshot.abs_path;
|
||||||
let abs_path = if cfg!(target_os = "windows") {
|
#[cfg(target_os = "windows")]
|
||||||
abs_path
|
let abs_path = abs_path
|
||||||
.canonicalize()
|
.as_path()
|
||||||
.unwrap_or_else(|_| abs_path.to_path_buf())
|
.canonicalize()
|
||||||
} else {
|
.unwrap_or_else(|_| abs_path.as_path().to_path_buf());
|
||||||
abs_path.to_path_buf()
|
#[cfg(not(target_os = "windows"))]
|
||||||
};
|
let abs_path = abs_path.as_path().to_path_buf();
|
||||||
let background = cx.background_executor().clone();
|
let background = cx.background_executor().clone();
|
||||||
async move {
|
async move {
|
||||||
let (events, watcher) = fs.watch(&abs_path, FS_WATCH_LATENCY).await;
|
let (events, watcher) = fs.watch(&abs_path, FS_WATCH_LATENCY).await;
|
||||||
|
@ -1135,6 +1136,7 @@ impl LocalWorktree {
|
||||||
this.snapshot.git_repositories = Default::default();
|
this.snapshot.git_repositories = Default::default();
|
||||||
this.snapshot.ignores_by_parent_abs_path = Default::default();
|
this.snapshot.ignores_by_parent_abs_path = Default::default();
|
||||||
let root_name = new_path
|
let root_name = new_path
|
||||||
|
.as_path()
|
||||||
.file_name()
|
.file_name()
|
||||||
.map_or(String::new(), |f| f.to_string_lossy().to_string());
|
.map_or(String::new(), |f| f.to_string_lossy().to_string());
|
||||||
this.snapshot.update_abs_path(new_path, root_name);
|
this.snapshot.update_abs_path(new_path, root_name);
|
||||||
|
@ -2075,7 +2077,7 @@ impl Snapshot {
|
||||||
pub fn new(id: u64, root_name: String, abs_path: Arc<Path>) -> Self {
|
pub fn new(id: u64, root_name: String, abs_path: Arc<Path>) -> Self {
|
||||||
Snapshot {
|
Snapshot {
|
||||||
id: WorktreeId::from_usize(id as usize),
|
id: WorktreeId::from_usize(id as usize),
|
||||||
abs_path,
|
abs_path: abs_path.into(),
|
||||||
root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(),
|
root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(),
|
||||||
root_name,
|
root_name,
|
||||||
always_included_entries: Default::default(),
|
always_included_entries: Default::default(),
|
||||||
|
@ -2091,8 +2093,20 @@ impl Snapshot {
|
||||||
self.id
|
self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// Consider the following:
|
||||||
|
//
|
||||||
|
// ```rust
|
||||||
|
// let abs_path: Arc<Path> = snapshot.abs_path(); // e.g. "C:\Users\user\Desktop\project"
|
||||||
|
// let some_non_trimmed_path = Path::new("\\\\?\\C:\\Users\\user\\Desktop\\project\\main.rs");
|
||||||
|
// // The caller perform some actions here:
|
||||||
|
// some_non_trimmed_path.strip_prefix(abs_path); // This fails
|
||||||
|
// some_non_trimmed_path.starts_with(abs_path); // This fails too
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// This is definitely a bug, but it's not clear if we should handle it here or not.
|
||||||
pub fn abs_path(&self) -> &Arc<Path> {
|
pub fn abs_path(&self) -> &Arc<Path> {
|
||||||
&self.abs_path
|
self.abs_path.as_path()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_initial_update(&self, project_id: u64, worktree_id: u64) -> proto::UpdateWorktree {
|
fn build_initial_update(&self, project_id: u64, worktree_id: u64) -> proto::UpdateWorktree {
|
||||||
|
@ -2132,9 +2146,9 @@ impl Snapshot {
|
||||||
return Err(anyhow!("invalid path"));
|
return Err(anyhow!("invalid path"));
|
||||||
}
|
}
|
||||||
if path.file_name().is_some() {
|
if path.file_name().is_some() {
|
||||||
Ok(self.abs_path.join(path))
|
Ok(self.abs_path.as_path().join(path))
|
||||||
} else {
|
} else {
|
||||||
Ok(self.abs_path.to_path_buf())
|
Ok(self.abs_path.as_path().to_path_buf())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2193,7 +2207,7 @@ impl Snapshot {
|
||||||
.and_then(|entry| entry.git_status)
|
.and_then(|entry| entry.git_status)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_abs_path(&mut self, abs_path: Arc<Path>, root_name: String) {
|
fn update_abs_path(&mut self, abs_path: SanitizedPath, root_name: String) {
|
||||||
self.abs_path = abs_path;
|
self.abs_path = abs_path;
|
||||||
if root_name != self.root_name {
|
if root_name != self.root_name {
|
||||||
self.root_char_bag = root_name.chars().map(|c| c.to_ascii_lowercase()).collect();
|
self.root_char_bag = root_name.chars().map(|c| c.to_ascii_lowercase()).collect();
|
||||||
|
@ -2212,7 +2226,7 @@ impl Snapshot {
|
||||||
update.removed_entries.len()
|
update.removed_entries.len()
|
||||||
);
|
);
|
||||||
self.update_abs_path(
|
self.update_abs_path(
|
||||||
Arc::from(PathBuf::from(update.abs_path).as_path()),
|
SanitizedPath::from(PathBuf::from(update.abs_path)),
|
||||||
update.root_name,
|
update.root_name,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -2632,7 +2646,7 @@ impl LocalSnapshot {
|
||||||
|
|
||||||
fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry {
|
fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry {
|
||||||
if entry.is_file() && entry.path.file_name() == Some(&GITIGNORE) {
|
if entry.is_file() && entry.path.file_name() == Some(&GITIGNORE) {
|
||||||
let abs_path = self.abs_path.join(&entry.path);
|
let abs_path = self.abs_path.as_path().join(&entry.path);
|
||||||
match smol::block_on(build_gitignore(&abs_path, fs)) {
|
match smol::block_on(build_gitignore(&abs_path, fs)) {
|
||||||
Ok(ignore) => {
|
Ok(ignore) => {
|
||||||
self.ignores_by_parent_abs_path
|
self.ignores_by_parent_abs_path
|
||||||
|
@ -2786,8 +2800,9 @@ impl LocalSnapshot {
|
||||||
|
|
||||||
if git_state {
|
if git_state {
|
||||||
for ignore_parent_abs_path in self.ignores_by_parent_abs_path.keys() {
|
for ignore_parent_abs_path in self.ignores_by_parent_abs_path.keys() {
|
||||||
let ignore_parent_path =
|
let ignore_parent_path = ignore_parent_abs_path
|
||||||
ignore_parent_abs_path.strip_prefix(&self.abs_path).unwrap();
|
.strip_prefix(self.abs_path.as_path())
|
||||||
|
.unwrap();
|
||||||
assert!(self.entry_for_path(ignore_parent_path).is_some());
|
assert!(self.entry_for_path(ignore_parent_path).is_some());
|
||||||
assert!(self
|
assert!(self
|
||||||
.entry_for_path(ignore_parent_path.join(*GITIGNORE))
|
.entry_for_path(ignore_parent_path.join(*GITIGNORE))
|
||||||
|
@ -2941,7 +2956,7 @@ impl BackgroundScannerState {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ignore) = ignore {
|
if let Some(ignore) = ignore {
|
||||||
let abs_parent_path = self.snapshot.abs_path.join(parent_path).into();
|
let abs_parent_path = self.snapshot.abs_path.as_path().join(parent_path).into();
|
||||||
self.snapshot
|
self.snapshot
|
||||||
.ignores_by_parent_abs_path
|
.ignores_by_parent_abs_path
|
||||||
.insert(abs_parent_path, (ignore, false));
|
.insert(abs_parent_path, (ignore, false));
|
||||||
|
@ -3004,7 +3019,11 @@ impl BackgroundScannerState {
|
||||||
}
|
}
|
||||||
|
|
||||||
if entry.path.file_name() == Some(&GITIGNORE) {
|
if entry.path.file_name() == Some(&GITIGNORE) {
|
||||||
let abs_parent_path = self.snapshot.abs_path.join(entry.path.parent().unwrap());
|
let abs_parent_path = self
|
||||||
|
.snapshot
|
||||||
|
.abs_path
|
||||||
|
.as_path()
|
||||||
|
.join(entry.path.parent().unwrap());
|
||||||
if let Some((_, needs_update)) = self
|
if let Some((_, needs_update)) = self
|
||||||
.snapshot
|
.snapshot
|
||||||
.ignores_by_parent_abs_path
|
.ignores_by_parent_abs_path
|
||||||
|
@ -3085,7 +3104,7 @@ impl BackgroundScannerState {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let dot_git_abs_path = self.snapshot.abs_path.join(&dot_git_path);
|
let dot_git_abs_path = self.snapshot.abs_path.as_path().join(&dot_git_path);
|
||||||
|
|
||||||
let t0 = Instant::now();
|
let t0 = Instant::now();
|
||||||
let repository = fs.open_repo(&dot_git_abs_path)?;
|
let repository = fs.open_repo(&dot_git_abs_path)?;
|
||||||
|
@ -3299,9 +3318,9 @@ impl language::LocalFile for File {
|
||||||
fn abs_path(&self, cx: &AppContext) -> PathBuf {
|
fn abs_path(&self, cx: &AppContext) -> PathBuf {
|
||||||
let worktree_path = &self.worktree.read(cx).as_local().unwrap().abs_path;
|
let worktree_path = &self.worktree.read(cx).as_local().unwrap().abs_path;
|
||||||
if self.path.as_ref() == Path::new("") {
|
if self.path.as_ref() == Path::new("") {
|
||||||
worktree_path.to_path_buf()
|
worktree_path.as_path().to_path_buf()
|
||||||
} else {
|
} else {
|
||||||
worktree_path.join(&self.path)
|
worktree_path.as_path().join(&self.path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3712,7 +3731,7 @@ impl BackgroundScanner {
|
||||||
// the git repository in an ancestor directory. Find any gitignore files
|
// the git repository in an ancestor directory. Find any gitignore files
|
||||||
// in ancestor directories.
|
// in ancestor directories.
|
||||||
let root_abs_path = self.state.lock().snapshot.abs_path.clone();
|
let root_abs_path = self.state.lock().snapshot.abs_path.clone();
|
||||||
for (index, ancestor) in root_abs_path.ancestors().enumerate() {
|
for (index, ancestor) in root_abs_path.as_path().ancestors().enumerate() {
|
||||||
if index != 0 {
|
if index != 0 {
|
||||||
if let Ok(ignore) =
|
if let Ok(ignore) =
|
||||||
build_gitignore(&ancestor.join(*GITIGNORE), self.fs.as_ref()).await
|
build_gitignore(&ancestor.join(*GITIGNORE), self.fs.as_ref()).await
|
||||||
|
@ -3744,7 +3763,13 @@ impl BackgroundScanner {
|
||||||
self.state.lock().insert_git_repository_for_path(
|
self.state.lock().insert_git_repository_for_path(
|
||||||
Path::new("").into(),
|
Path::new("").into(),
|
||||||
ancestor_dot_git.into(),
|
ancestor_dot_git.into(),
|
||||||
Some(root_abs_path.strip_prefix(ancestor).unwrap().into()),
|
Some(
|
||||||
|
root_abs_path
|
||||||
|
.as_path()
|
||||||
|
.strip_prefix(ancestor)
|
||||||
|
.unwrap()
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
self.fs.as_ref(),
|
self.fs.as_ref(),
|
||||||
self.watcher.as_ref(),
|
self.watcher.as_ref(),
|
||||||
);
|
);
|
||||||
|
@ -3763,12 +3788,12 @@ impl BackgroundScanner {
|
||||||
if let Some(mut root_entry) = state.snapshot.root_entry().cloned() {
|
if let Some(mut root_entry) = state.snapshot.root_entry().cloned() {
|
||||||
let ignore_stack = state
|
let ignore_stack = state
|
||||||
.snapshot
|
.snapshot
|
||||||
.ignore_stack_for_abs_path(&root_abs_path, true);
|
.ignore_stack_for_abs_path(root_abs_path.as_path(), true);
|
||||||
if ignore_stack.is_abs_path_ignored(&root_abs_path, true) {
|
if ignore_stack.is_abs_path_ignored(root_abs_path.as_path(), true) {
|
||||||
root_entry.is_ignored = true;
|
root_entry.is_ignored = true;
|
||||||
state.insert_entry(root_entry.clone(), self.fs.as_ref(), self.watcher.as_ref());
|
state.insert_entry(root_entry.clone(), self.fs.as_ref(), self.watcher.as_ref());
|
||||||
}
|
}
|
||||||
state.enqueue_scan_dir(root_abs_path, &root_entry, &scan_job_tx);
|
state.enqueue_scan_dir(root_abs_path.into(), &root_entry, &scan_job_tx);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3818,7 +3843,7 @@ impl BackgroundScanner {
|
||||||
{
|
{
|
||||||
let mut state = self.state.lock();
|
let mut state = self.state.lock();
|
||||||
state.path_prefixes_to_scan.insert(path_prefix.clone());
|
state.path_prefixes_to_scan.insert(path_prefix.clone());
|
||||||
state.snapshot.abs_path.join(&path_prefix)
|
state.snapshot.abs_path.as_path().join(&path_prefix)
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(abs_path) = self.fs.canonicalize(&abs_path).await.log_err() {
|
if let Some(abs_path) = self.fs.canonicalize(&abs_path).await.log_err() {
|
||||||
|
@ -3845,7 +3870,7 @@ impl BackgroundScanner {
|
||||||
self.forcibly_load_paths(&request.relative_paths).await;
|
self.forcibly_load_paths(&request.relative_paths).await;
|
||||||
|
|
||||||
let root_path = self.state.lock().snapshot.abs_path.clone();
|
let root_path = self.state.lock().snapshot.abs_path.clone();
|
||||||
let root_canonical_path = match self.fs.canonicalize(&root_path).await {
|
let root_canonical_path = match self.fs.canonicalize(root_path.as_path()).await {
|
||||||
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);
|
||||||
|
@ -3874,7 +3899,7 @@ impl BackgroundScanner {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.reload_entries_for_paths(
|
self.reload_entries_for_paths(
|
||||||
root_path,
|
root_path.into(),
|
||||||
root_canonical_path,
|
root_canonical_path,
|
||||||
&request.relative_paths,
|
&request.relative_paths,
|
||||||
abs_paths,
|
abs_paths,
|
||||||
|
@ -3887,7 +3912,7 @@ impl BackgroundScanner {
|
||||||
|
|
||||||
async fn process_events(&self, mut abs_paths: Vec<PathBuf>) {
|
async fn process_events(&self, mut abs_paths: Vec<PathBuf>) {
|
||||||
let root_path = self.state.lock().snapshot.abs_path.clone();
|
let root_path = self.state.lock().snapshot.abs_path.clone();
|
||||||
let root_canonical_path = match self.fs.canonicalize(&root_path).await {
|
let root_canonical_path = match self.fs.canonicalize(root_path.as_path()).await {
|
||||||
Ok(path) => path,
|
Ok(path) => path,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let new_path = self
|
let new_path = self
|
||||||
|
@ -3897,21 +3922,20 @@ impl BackgroundScanner {
|
||||||
.root_file_handle
|
.root_file_handle
|
||||||
.clone()
|
.clone()
|
||||||
.and_then(|handle| handle.current_path(&self.fs).log_err())
|
.and_then(|handle| handle.current_path(&self.fs).log_err())
|
||||||
.filter(|new_path| **new_path != *root_path);
|
.map(SanitizedPath::from)
|
||||||
|
.filter(|new_path| *new_path != root_path);
|
||||||
|
|
||||||
if let Some(new_path) = new_path.as_ref() {
|
if let Some(new_path) = new_path.as_ref() {
|
||||||
log::info!(
|
log::info!(
|
||||||
"root renamed from {} to {}",
|
"root renamed from {} to {}",
|
||||||
root_path.display(),
|
root_path.as_path().display(),
|
||||||
new_path.display()
|
new_path.as_path().display()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
log::warn!("root path could not be canonicalized: {}", err);
|
log::warn!("root path could not be canonicalized: {}", err);
|
||||||
}
|
}
|
||||||
self.status_updates_tx
|
self.status_updates_tx
|
||||||
.unbounded_send(ScanState::RootUpdated {
|
.unbounded_send(ScanState::RootUpdated { new_path })
|
||||||
new_path: new_path.map(|p| p.into()),
|
|
||||||
})
|
|
||||||
.ok();
|
.ok();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -4006,7 +4030,7 @@ impl BackgroundScanner {
|
||||||
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
||||||
log::debug!("received fs events {:?}", relative_paths);
|
log::debug!("received fs events {:?}", relative_paths);
|
||||||
self.reload_entries_for_paths(
|
self.reload_entries_for_paths(
|
||||||
root_path,
|
root_path.into(),
|
||||||
root_canonical_path,
|
root_canonical_path,
|
||||||
&relative_paths,
|
&relative_paths,
|
||||||
abs_paths,
|
abs_paths,
|
||||||
|
@ -4044,7 +4068,7 @@ impl BackgroundScanner {
|
||||||
for ancestor in path.ancestors() {
|
for ancestor in path.ancestors() {
|
||||||
if let Some(entry) = state.snapshot.entry_for_path(ancestor) {
|
if let Some(entry) = state.snapshot.entry_for_path(ancestor) {
|
||||||
if entry.kind == EntryKind::UnloadedDir {
|
if entry.kind == EntryKind::UnloadedDir {
|
||||||
let abs_path = root_path.join(ancestor);
|
let abs_path = root_path.as_path().join(ancestor);
|
||||||
state.enqueue_scan_dir(abs_path.into(), entry, &scan_job_tx);
|
state.enqueue_scan_dir(abs_path.into(), entry, &scan_job_tx);
|
||||||
state.paths_to_scan.insert(path.clone());
|
state.paths_to_scan.insert(path.clone());
|
||||||
break;
|
break;
|
||||||
|
@ -4548,7 +4572,7 @@ impl BackgroundScanner {
|
||||||
snapshot
|
snapshot
|
||||||
.ignores_by_parent_abs_path
|
.ignores_by_parent_abs_path
|
||||||
.retain(|parent_abs_path, (_, needs_update)| {
|
.retain(|parent_abs_path, (_, needs_update)| {
|
||||||
if let Ok(parent_path) = parent_abs_path.strip_prefix(&abs_path) {
|
if let Ok(parent_path) = parent_abs_path.strip_prefix(abs_path.as_path()) {
|
||||||
if *needs_update {
|
if *needs_update {
|
||||||
*needs_update = false;
|
*needs_update = false;
|
||||||
if snapshot.snapshot.entry_for_path(parent_path).is_some() {
|
if snapshot.snapshot.entry_for_path(parent_path).is_some() {
|
||||||
|
@ -4627,7 +4651,10 @@ impl BackgroundScanner {
|
||||||
|
|
||||||
let mut entries_by_id_edits = Vec::new();
|
let mut entries_by_id_edits = Vec::new();
|
||||||
let mut entries_by_path_edits = Vec::new();
|
let mut entries_by_path_edits = Vec::new();
|
||||||
let path = job.abs_path.strip_prefix(&snapshot.abs_path).unwrap();
|
let path = job
|
||||||
|
.abs_path
|
||||||
|
.strip_prefix(snapshot.abs_path.as_path())
|
||||||
|
.unwrap();
|
||||||
let repo = snapshot.repo_for_path(path);
|
let repo = snapshot.repo_for_path(path);
|
||||||
for mut entry in snapshot.child_entries(path).cloned() {
|
for mut entry in snapshot.child_entries(path).cloned() {
|
||||||
let was_ignored = entry.is_ignored;
|
let was_ignored = entry.is_ignored;
|
||||||
|
|
|
@ -1124,10 +1124,7 @@ impl ToString for IdType {
|
||||||
|
|
||||||
fn parse_url_arg(arg: &str, cx: &AppContext) -> Result<String> {
|
fn parse_url_arg(arg: &str, cx: &AppContext) -> Result<String> {
|
||||||
match std::fs::canonicalize(Path::new(&arg)) {
|
match std::fs::canonicalize(Path::new(&arg)) {
|
||||||
Ok(path) => Ok(format!(
|
Ok(path) => Ok(format!("file://{}", path.display())),
|
||||||
"file://{}",
|
|
||||||
path.to_string_lossy().trim_start_matches(r#"\\?\"#)
|
|
||||||
)),
|
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
if arg.starts_with("file://")
|
if arg.starts_with("file://")
|
||||||
|| arg.starts_with("zed-cli://")
|
|| arg.starts_with("zed-cli://")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue