Represent relative paths using a dedicated, separator-agnostic type (#38744)

Closes https://github.com/zed-industries/zed/issues/38690
Closes #37353

### Background

On Windows, paths are normally separated by `\`, unlike mac and linux
where they are separated by `/`. When editing code in a project that
uses a different path style than your local system (e.g. remoting from
Windows to Linux, using WSL, and collaboration between windows and unix
users), the correct separator for a path may differ from the "native"
separator.

Previously, to work around this, Zed converted paths' separators in
numerous places. This was applied to both absolute and relative paths,
leading to incorrect conversions in some cases.

### Solution

Many code paths in Zed use paths that are *relative* to either a
worktree root or a git repository. This PR introduces a dedicated type
for these paths called `RelPath`, which stores the path in the same way
regardless of host platform, and offers `Path`-like manipulation APIs.
RelPath supports *displaying* the path using either separator, so that
we can display paths in a style that is determined at runtime based on
the current project.

The representation of absolute paths is left untouched, for now.
Absolute paths are different from relative paths because (except in
contexts where we know that the path refers to the local filesystem)
they should generally be treated as opaque strings. Currently we use a
mix of types for these paths (std::path::Path, String, SanitizedPath).

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Peter Tripp <petertripp@gmail.com>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
This commit is contained in:
Max Brunsfeld
2025-09-24 18:57:33 -04:00
committed by GitHub
co-authored by Cole Miller Piotr Osiewicz Peter Tripp Smit Barmase Lukas Wirth
parent 3c626f3758
commit 03f9cf4414
216 changed files with 5873 additions and 5257 deletions
+6 -9
View File
@@ -16,10 +16,7 @@ use gpui::{
};
use node_runtime::NodeRuntime;
use remote::RemoteClient;
use rpc::{
AnyProtoClient, TypedEnvelope,
proto::{self, ToProto},
};
use rpc::{AnyProtoClient, TypedEnvelope, proto};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{SettingsContent, SettingsStore};
@@ -845,7 +842,7 @@ impl ExternalAgentServer for LocalGemini {
// Gemini CLI doesn't seem to have a dedicated invocation for logging in--we just run it normally without any arguments.
let login = task::SpawnInTerminal {
command: Some(command.path.clone().to_proto()),
command: Some(command.path.to_string_lossy().to_string()),
args: command.args.clone(),
env: command.env.clone().unwrap_or_default(),
label: "gemini /auth".into(),
@@ -854,7 +851,7 @@ impl ExternalAgentServer for LocalGemini {
command.env.get_or_insert_default().extend(extra_env);
command.args.push("--experimental-acp".into());
Ok((command, root_dir.to_proto(), Some(login)))
Ok((command, root_dir.to_string_lossy().to_string(), Some(login)))
})
}
@@ -922,7 +919,7 @@ impl ExternalAgentServer for LocalClaudeCode {
path.strip_suffix("/@zed-industries/claude-code-acp/dist/index.js")
})
.map(|path_prefix| task::SpawnInTerminal {
command: Some(command.path.clone().to_proto()),
command: Some(command.path.to_string_lossy().to_string()),
args: vec![
Path::new(path_prefix)
.join("@anthropic-ai/claude-code/cli.js")
@@ -938,7 +935,7 @@ impl ExternalAgentServer for LocalClaudeCode {
};
command.env.get_or_insert_default().extend(extra_env);
Ok((command, root_dir.to_proto(), login))
Ok((command, root_dir.to_string_lossy().to_string(), login))
})
}
@@ -977,7 +974,7 @@ impl ExternalAgentServer for LocalCustomAgent {
env.extend(command.env.unwrap_or_default());
env.extend(extra_env);
command.env = Some(env);
Ok((command, root_dir.to_proto(), None))
Ok((command, root_dir.to_string_lossy().to_string(), None))
})
}
+12 -11
View File
@@ -21,12 +21,12 @@ use language::{
};
use rpc::{
AnyProtoClient, ErrorCode, ErrorExt as _, TypedEnvelope,
proto::{self, ToProto},
proto::{self},
};
use smol::channel::Receiver;
use std::{io, path::Path, pin::pin, sync::Arc, time::Instant};
use std::{io, pin::pin, sync::Arc, time::Instant};
use text::BufferId;
use util::{ResultExt as _, TryFutureExt, debug_panic, maybe};
use util::{ResultExt as _, TryFutureExt, debug_panic, maybe, rel_path::RelPath};
use worktree::{File, PathChange, ProjectEntryId, Worktree, WorktreeId};
/// A set of open buffers.
@@ -292,7 +292,7 @@ impl RemoteBufferStore {
fn open_buffer(
&self,
path: Arc<Path>,
path: Arc<RelPath>,
worktree: Entity<Worktree>,
cx: &mut Context<BufferStore>,
) -> Task<Result<Entity<Buffer>>> {
@@ -370,7 +370,7 @@ impl LocalBufferStore {
&self,
buffer_handle: Entity<Buffer>,
worktree: Entity<Worktree>,
path: Arc<Path>,
path: Arc<RelPath>,
mut has_changed_file: bool,
cx: &mut Context<BufferStore>,
) -> Task<Result<()>> {
@@ -389,7 +389,7 @@ impl LocalBufferStore {
}
let save = worktree.update(cx, |worktree, cx| {
worktree.write_file(path.as_ref(), text, line_ending, cx)
worktree.write_file(path, text, line_ending, cx)
});
cx.spawn(async move |this, cx| {
@@ -443,7 +443,7 @@ impl LocalBufferStore {
fn local_worktree_entries_changed(
this: &mut BufferStore,
worktree_handle: &Entity<Worktree>,
changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
changes: &[(Arc<RelPath>, ProjectEntryId, PathChange)],
cx: &mut Context<BufferStore>,
) {
let snapshot = worktree_handle.read(cx).snapshot();
@@ -462,7 +462,7 @@ impl LocalBufferStore {
fn local_worktree_entry_changed(
this: &mut BufferStore,
entry_id: ProjectEntryId,
path: &Arc<Path>,
path: &Arc<RelPath>,
worktree: &Entity<worktree::Worktree>,
snapshot: &worktree::Snapshot,
cx: &mut Context<BufferStore>,
@@ -615,7 +615,7 @@ impl LocalBufferStore {
fn open_buffer(
&self,
path: Arc<Path>,
path: Arc<RelPath>,
worktree: Entity<Worktree>,
cx: &mut Context<BufferStore>,
) -> Task<Result<Entity<Buffer>>> {
@@ -1402,8 +1402,9 @@ impl BufferStore {
.await?;
let buffer_id = buffer.read_with(&cx, |buffer, _| buffer.remote_id())?;
if let Some(new_path) = envelope.payload.new_path {
let new_path = ProjectPath::from_proto(new_path);
if let Some(new_path) = envelope.payload.new_path
&& let Some(new_path) = ProjectPath::from_proto(new_path)
{
this.update(&mut cx, |this, cx| {
this.save_buffer_as(buffer.clone(), new_path, cx)
})?
+3 -3
View File
@@ -1,7 +1,7 @@
pub mod extension;
pub mod registry;
use std::{path::Path, sync::Arc};
use std::sync::Arc;
use anyhow::{Context as _, Result};
use collections::{HashMap, HashSet};
@@ -10,7 +10,7 @@ use futures::{FutureExt as _, future::join_all};
use gpui::{App, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity, actions};
use registry::ContextServerDescriptorRegistry;
use settings::{Settings as _, SettingsStore};
use util::ResultExt as _;
use util::{ResultExt as _, rel_path::RelPath};
use crate::{
Project,
@@ -510,7 +510,7 @@ impl ContextServerStore {
.next()
.map(|worktree| settings::SettingsLocation {
worktree_id: worktree.read(cx).id(),
path: Path::new(""),
path: RelPath::empty(),
});
&ProjectSettings::get(location, cx).context_servers
}
@@ -387,7 +387,7 @@ impl BreakpointStore {
pub fn abs_path_from_buffer(buffer: &Entity<Buffer>, cx: &App) -> Option<Arc<Path>> {
worktree::File::from_dyn(buffer.read(cx).file())
.and_then(|file| file.worktree.read(cx).absolutize(&file.path).ok())
.map(|file| file.worktree.read(cx).absolutize(&file.path))
.map(Arc::<Path>::from)
}
@@ -794,7 +794,7 @@ impl BreakpointStore {
.update(cx, |this, cx| {
let path = ProjectPath {
worktree_id: worktree.read(cx).id(),
path: relative_path.into(),
path: relative_path,
};
this.open_buffer(path, cx)
})?
+6 -8
View File
@@ -50,7 +50,7 @@ use std::{
sync::{Arc, Once},
};
use task::{DebugScenario, SpawnInTerminal, TaskContext, TaskTemplate};
use util::ResultExt as _;
use util::{ResultExt as _, rel_path::RelPath};
use worktree::Worktree;
#[derive(Debug)]
@@ -206,7 +206,7 @@ impl DapStore {
let settings_location = SettingsLocation {
worktree_id: worktree.read(cx).id(),
path: Path::new(""),
path: RelPath::empty(),
};
let dap_settings = ProjectSettings::get(Some(settings_location), cx)
.dap
@@ -943,15 +943,13 @@ impl dap::adapters::DapDelegate for DapAdapterDelegate {
fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore> {
self.toolchain_store.clone()
}
async fn read_text_file(&self, path: PathBuf) -> Result<String> {
async fn read_text_file(&self, path: &RelPath) -> Result<String> {
let entry = self
.worktree
.entry_for_path(&path)
.entry_for_path(path)
.with_context(|| format!("no worktree entry for path {path:?}"))?;
let abs_path = self
.worktree
.absolutize(&entry.path)
.with_context(|| format!("cannot absolutize path {path:?}"))?;
let abs_path = self.worktree.absolutize(&entry.path);
self.fs.load(&abs_path).await
}
+75 -64
View File
@@ -20,7 +20,7 @@ use futures::{
stream::FuturesOrdered,
};
use git::{
BuildPermalinkParams, GitHostingProviderRegistry, Oid, WORK_DIRECTORY_REPO_PATH,
BuildPermalinkParams, GitHostingProviderRegistry, Oid,
blame::Blame,
parse_git_remote_url,
repository::{
@@ -45,7 +45,7 @@ use parking_lot::Mutex;
use postage::stream::Stream as _;
use rpc::{
AnyProtoClient, TypedEnvelope,
proto::{self, FromProto, ToProto, git_reset, split_repository_update},
proto::{self, git_reset, split_repository_update},
};
use serde::Deserialize;
use std::{
@@ -63,7 +63,12 @@ use std::{
};
use sum_tree::{Edit, SumTree, TreeSet};
use text::{Bias, BufferId};
use util::{ResultExt, debug_panic, paths::SanitizedPath, post_inc};
use util::{
ResultExt, debug_panic,
paths::{PathStyle, SanitizedPath},
post_inc,
rel_path::RelPath,
};
use worktree::{
File, PathChange, PathKey, PathProgress, PathSummary, PathTarget, ProjectEntryId,
UpdatedGitRepositoriesSet, UpdatedGitRepository, Worktree,
@@ -189,7 +194,7 @@ impl StatusEntry {
};
proto::StatusEntry {
repo_path: self.repo_path.as_ref().to_proto(),
repo_path: self.repo_path.to_proto(),
simple_status,
status: Some(status_to_proto(self.status)),
}
@@ -200,7 +205,7 @@ impl TryFrom<proto::StatusEntry> for StatusEntry {
type Error = anyhow::Error;
fn try_from(value: proto::StatusEntry) -> Result<Self, Self::Error> {
let repo_path = RepoPath(Arc::<Path>::from_proto(value.repo_path));
let repo_path = RepoPath::from_proto(&value.repo_path).context("invalid repo path")?;
let status = status_from_proto(value.simple_status, value.status)?;
Ok(Self { repo_path, status })
}
@@ -240,6 +245,7 @@ pub struct RepositorySnapshot {
pub id: RepositoryId,
pub statuses_by_path: SumTree<StatusEntry>,
pub work_directory_abs_path: Arc<Path>,
pub path_style: PathStyle,
pub branch: Option<Branch>,
pub head_commit: Option<CommitDetails>,
pub scan_id: u64,
@@ -947,9 +953,7 @@ impl GitStore {
{
return Task::ready(Err(anyhow!("no permalink available")));
}
let Some(file_path) = file.worktree.read(cx).absolutize(&file.path).ok() else {
return Task::ready(Err(anyhow!("no permalink available")));
};
let file_path = file.worktree.read(cx).absolutize(&file.path);
return cx.spawn(async move |cx| {
let provider_registry = cx.update(GitHostingProviderRegistry::default_global)?;
get_permalink_in_rust_registry_src(provider_registry, file_path, selection)
@@ -985,9 +989,7 @@ impl GitStore {
parse_git_remote_url(provider_registry, &origin_url)
.context("parsing Git remote URL")?;
let path = repo_path.to_str().with_context(|| {
format!("converting repo path {repo_path:?} to string")
})?;
let path = repo_path.as_str();
Ok(provider.build_permalink(
remote,
@@ -1313,7 +1315,7 @@ impl GitStore {
});
if let Some((repo, path)) = self.repository_and_path_for_buffer_id(buffer_id, cx) {
let recv = repo.update(cx, |repo, cx| {
log::debug!("hunks changed for {}", path.display());
log::debug!("hunks changed for {}", path.as_str());
repo.spawn_set_index_text_job(
path,
new_index_text.as_ref().map(|rope| rope.to_string()),
@@ -1475,6 +1477,7 @@ impl GitStore {
mut cx: AsyncApp,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
let path_style = this.worktree_store.read(cx).path_style();
let mut update = envelope.payload;
let id = RepositoryId::from_proto(update.id);
@@ -1488,6 +1491,7 @@ impl GitStore {
Repository::remote(
id,
Path::new(&update.abs_path).into(),
path_style,
ProjectId(update.project_id),
client,
git_store,
@@ -1681,9 +1685,8 @@ impl GitStore {
.payload
.paths
.into_iter()
.map(PathBuf::from)
.map(RepoPath::new)
.collect();
.map(|path| RepoPath::new(&path))
.collect::<Result<Vec<_>>>()?;
repository_handle
.update(&mut cx, |repository_handle, cx| {
@@ -1705,9 +1708,8 @@ impl GitStore {
.payload
.paths
.into_iter()
.map(PathBuf::from)
.map(RepoPath::new)
.collect();
.map(|path| RepoPath::new(&path))
.collect::<Result<Vec<_>>>()?;
repository_handle
.update(&mut cx, |repository_handle, cx| {
@@ -1730,9 +1732,8 @@ impl GitStore {
.payload
.paths
.into_iter()
.map(PathBuf::from)
.map(RepoPath::new)
.collect();
.map(|path| RepoPath::new(&path))
.collect::<Result<Vec<_>>>()?;
repository_handle
.update(&mut cx, |repository_handle, cx| {
@@ -1804,7 +1805,7 @@ impl GitStore {
) -> Result<proto::Ack> {
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
let repo_path = RepoPath::from_str(&envelope.payload.path);
let repo_path = RepoPath::from_proto(&envelope.payload.path)?;
repository_handle
.update(&mut cx, |repository_handle, cx| {
@@ -2005,7 +2006,7 @@ impl GitStore {
.files
.into_iter()
.map(|file| proto::CommitFile {
path: file.path.to_string(),
path: file.path.to_proto(),
old_text: file.old_text,
new_text: file.new_text,
})
@@ -2045,8 +2046,8 @@ impl GitStore {
.payload
.paths
.iter()
.map(|s| RepoPath::from_str(s))
.collect();
.map(|s| RepoPath::from_proto(s))
.collect::<Result<Vec<_>>>()?;
repository_handle
.update(&mut cx, |repository_handle, cx| {
@@ -2332,9 +2333,10 @@ impl GitStore {
fn process_updated_entries(
&self,
worktree: &Entity<Worktree>,
updated_entries: &[(Arc<Path>, ProjectEntryId, PathChange)],
updated_entries: &[(Arc<RelPath>, ProjectEntryId, PathChange)],
cx: &mut App,
) -> Task<HashMap<Entity<Repository>, Vec<RepoPath>>> {
let path_style = worktree.read(cx).path_style();
let mut repo_paths = self
.repositories
.values()
@@ -2349,7 +2351,7 @@ impl GitStore {
let entries = entries
.into_iter()
.filter_map(|path| worktree.absolutize(&path).ok())
.map(|path| worktree.absolutize(&path))
.collect::<Arc<[_]>>();
let executor = cx.background_executor().clone();
@@ -2369,8 +2371,9 @@ impl GitStore {
let mut paths = Vec::new();
// All paths prefixed by a given repo will constitute a continuous range.
while let Some(path) = entries.get(ix)
&& let Some(repo_path) =
RepositorySnapshot::abs_path_to_repo_path_inner(&repo_path, path)
&& let Some(repo_path) = RepositorySnapshot::abs_path_to_repo_path_inner(
&repo_path, path, path_style,
)
{
paths.push((repo_path, ix));
ix += 1;
@@ -2764,7 +2767,7 @@ impl RepositoryId {
}
impl RepositorySnapshot {
fn empty(id: RepositoryId, work_directory_abs_path: Arc<Path>) -> Self {
fn empty(id: RepositoryId, work_directory_abs_path: Arc<Path>, path_style: PathStyle) -> Self {
Self {
id,
statuses_by_path: Default::default(),
@@ -2776,6 +2779,7 @@ impl RepositorySnapshot {
remote_origin_url: None,
remote_upstream_url: None,
stash_entries: Default::default(),
path_style,
}
}
@@ -2798,7 +2802,7 @@ impl RepositorySnapshot {
merge_message: self.merge.message.as_ref().map(|msg| msg.to_string()),
project_id,
id: self.id.to_proto(),
abs_path: self.work_directory_abs_path.to_proto(),
abs_path: self.work_directory_abs_path.to_string_lossy().to_string(),
entry_ids: vec![self.id.to_proto()],
scan_id: self.scan_id,
is_last_update: true,
@@ -2836,13 +2840,13 @@ impl RepositorySnapshot {
current_new_entry = new_statuses.next();
}
Ordering::Greater => {
removed_statuses.push(old_entry.repo_path.as_ref().to_proto());
removed_statuses.push(old_entry.repo_path.to_proto());
current_old_entry = old_statuses.next();
}
}
}
(None, Some(old_entry)) => {
removed_statuses.push(old_entry.repo_path.as_ref().to_proto());
removed_statuses.push(old_entry.repo_path.to_proto());
current_old_entry = old_statuses.next();
}
(Some(new_entry), None) => {
@@ -2862,12 +2866,12 @@ impl RepositorySnapshot {
.merge
.conflicted_paths
.iter()
.map(|path| path.as_ref().to_proto())
.map(|path| path.to_proto())
.collect(),
merge_message: self.merge.message.as_ref().map(|msg| msg.to_string()),
project_id,
id: self.id.to_proto(),
abs_path: self.work_directory_abs_path.to_proto(),
abs_path: self.work_directory_abs_path.to_string_lossy().to_string(),
entry_ids: vec![],
scan_id: self.scan_id,
is_last_update: true,
@@ -2895,18 +2899,19 @@ impl RepositorySnapshot {
}
pub fn abs_path_to_repo_path(&self, abs_path: &Path) -> Option<RepoPath> {
Self::abs_path_to_repo_path_inner(&self.work_directory_abs_path, abs_path)
Self::abs_path_to_repo_path_inner(&self.work_directory_abs_path, abs_path, self.path_style)
}
#[inline]
fn abs_path_to_repo_path_inner(
work_directory_abs_path: &Path,
abs_path: &Path,
path_style: PathStyle,
) -> Option<RepoPath> {
abs_path
.strip_prefix(&work_directory_abs_path)
.map(RepoPath::from)
.ok()
.and_then(|path| RepoPath::from_std_path(path, path_style).ok())
}
pub fn had_conflict_on_last_merge_head_change(&self, repo_path: &RepoPath) -> bool {
@@ -3032,7 +3037,8 @@ impl Repository {
git_store: WeakEntity<GitStore>,
cx: &mut Context<Self>,
) -> Self {
let snapshot = RepositorySnapshot::empty(id, work_directory_abs_path.clone());
let snapshot =
RepositorySnapshot::empty(id, work_directory_abs_path.clone(), PathStyle::local());
Repository {
this: cx.weak_entity(),
git_store,
@@ -3058,12 +3064,13 @@ impl Repository {
fn remote(
id: RepositoryId,
work_directory_abs_path: Arc<Path>,
path_style: PathStyle,
project_id: ProjectId,
client: AnyProtoClient,
git_store: WeakEntity<GitStore>,
cx: &mut Context<Self>,
) -> Self {
let snapshot = RepositorySnapshot::empty(id, work_directory_abs_path);
let snapshot = RepositorySnapshot::empty(id, work_directory_abs_path, path_style);
Self {
this: cx.weak_entity(),
snapshot,
@@ -3107,12 +3114,11 @@ impl Repository {
let buffer_store = git_store.buffer_store.read(cx);
let buffer = buffer_store.get(*buffer_id)?;
let file = File::from_dyn(buffer.read(cx).file())?;
let abs_path =
file.worktree.read(cx).absolutize(&file.path).ok()?;
let abs_path = file.worktree.read(cx).absolutize(&file.path);
let repo_path = this.abs_path_to_repo_path(&abs_path)?;
log::debug!(
"start reload diff bases for repo path {}",
repo_path.0.display()
repo_path.as_str()
);
diff_state.update(cx, |diff_state, _| {
let has_unstaged_diff = diff_state
@@ -3335,12 +3341,15 @@ impl Repository {
pub fn repo_path_to_project_path(&self, path: &RepoPath, cx: &App) -> Option<ProjectPath> {
let git_store = self.git_store.upgrade()?;
let worktree_store = git_store.read(cx).worktree_store.read(cx);
let abs_path = self.snapshot.work_directory_abs_path.join(&path.0);
let abs_path = self
.snapshot
.work_directory_abs_path
.join(path.as_std_path());
let abs_path = SanitizedPath::new(&abs_path);
let (worktree, relative_path) = worktree_store.find_worktree(abs_path, cx)?;
Some(ProjectPath {
worktree_id: worktree.read(cx).id(),
path: relative_path.into(),
path: relative_path,
})
}
@@ -3464,10 +3473,7 @@ impl Repository {
project_id: project_id.0,
repository_id: id.to_proto(),
commit,
paths: paths
.into_iter()
.map(|p| p.to_string_lossy().to_string())
.collect(),
paths: paths.into_iter().map(|p| p.to_proto()).collect(),
})
.await?;
@@ -3557,12 +3563,14 @@ impl Repository {
files: response
.files
.into_iter()
.map(|file| CommitFile {
path: Path::new(&file.path).into(),
old_text: file.old_text,
new_text: file.new_text,
.map(|file| {
Ok(CommitFile {
path: RepoPath::from_proto(&file.path)?,
old_text: file.old_text,
new_text: file.new_text,
})
})
.collect(),
.collect::<Result<Vec<_>>>()?,
})
}
}
@@ -3622,7 +3630,7 @@ impl Repository {
repository_id: id.to_proto(),
paths: entries
.into_iter()
.map(|repo_path| repo_path.as_ref().to_proto())
.map(|repo_path| repo_path.to_proto())
.collect(),
})
.await
@@ -3688,7 +3696,7 @@ impl Repository {
repository_id: id.to_proto(),
paths: entries
.into_iter()
.map(|repo_path| repo_path.as_ref().to_proto())
.map(|repo_path| repo_path.to_proto())
.collect(),
})
.await
@@ -3752,7 +3760,7 @@ impl Repository {
repository_id: id.to_proto(),
paths: entries
.into_iter()
.map(|repo_path| repo_path.as_ref().to_proto())
.map(|repo_path| repo_path.to_proto())
.collect(),
})
.await
@@ -4154,7 +4162,7 @@ impl Repository {
Some(GitJobKey::WriteIndex(path.clone())),
None,
move |git_repo, mut cx| async move {
log::debug!("start updating index text for buffer {}", path.display());
log::debug!("start updating index text for buffer {}", path.as_str());
match git_repo {
RepositoryState::Local {
backend,
@@ -4170,13 +4178,13 @@ impl Repository {
.request(proto::SetIndexText {
project_id: project_id.0,
repository_id: id.to_proto(),
path: path.as_ref().to_proto(),
path: path.to_proto(),
text: content,
})
.await?;
}
}
log::debug!("finish updating index text for buffer {}", path.display());
log::debug!("finish updating index text for buffer {}", path.as_str());
if let Some(hunk_staging_operation_count) = hunk_staging_operation_count {
let project_path = this
@@ -4439,7 +4447,7 @@ impl Repository {
update
.current_merge_conflicts
.into_iter()
.map(|path| RepoPath(Path::new(&path).into())),
.filter_map(|path| RepoPath::from_proto(&path).log_err()),
);
self.snapshot.branch = update.branch_summary.as_ref().map(proto_to_branch);
self.snapshot.head_commit = update
@@ -4460,7 +4468,11 @@ impl Repository {
let edits = update
.removed_statuses
.into_iter()
.map(|path| sum_tree::Edit::Remove(PathKey(FromProto::from_proto(path))))
.filter_map(|path| {
Some(sum_tree::Edit::Remove(PathKey(
RelPath::from_proto(&path).log_err()?,
)))
})
.chain(
update
.updated_statuses
@@ -5060,9 +5072,7 @@ async fn compute_snapshot(
let mut events = Vec::new();
let branches = backend.branches().await?;
let branch = branches.into_iter().find(|branch| branch.is_head);
let statuses = backend
.status(std::slice::from_ref(&WORK_DIRECTORY_REPO_PATH))
.await?;
let statuses = backend.status(&[RelPath::empty().into()]).await?;
let stash_entries = backend.stash_entries().await?;
let statuses_by_path = SumTree::from_iter(
statuses
@@ -5108,6 +5118,7 @@ async fn compute_snapshot(
id,
statuses_by_path,
work_directory_abs_path,
path_style: prev_snapshot.path_style,
scan_id: prev_snapshot.scan_id + 1,
branch,
head_commit,
+10 -7
View File
@@ -255,20 +255,23 @@ impl EventEmitter<ConflictSetUpdate> for ConflictSet {}
#[cfg(test)]
mod tests {
use std::{path::Path, sync::mpsc};
use std::sync::mpsc;
use crate::Project;
use super::*;
use fs::FakeFs;
use git::status::{UnmergedStatus, UnmergedStatusCode};
use git::{
repository::repo_path,
status::{UnmergedStatus, UnmergedStatusCode},
};
use gpui::{BackgroundExecutor, TestAppContext};
use language::language_settings::AllLanguageSettings;
use serde_json::json;
use settings::Settings as _;
use text::{Buffer, BufferId, Point, ToOffset as _};
use unindent::Unindent as _;
use util::path;
use util::{path, rel_path::rel_path};
use worktree::WorktreeSettings;
#[test]
@@ -543,7 +546,7 @@ mod tests {
fs.with_git_state(path!("/project/.git").as_ref(), true, |state| {
state.unmerged_paths.insert(
"a.txt".into(),
repo_path("a.txt"),
UnmergedStatus {
first_head: UnmergedStatusCode::Updated,
second_head: UnmergedStatusCode::Updated,
@@ -621,7 +624,7 @@ mod tests {
cx.run_until_parked();
fs.with_git_state(path!("/project/.git").as_ref(), true, |state| {
state.unmerged_paths.insert(
"a.txt".into(),
rel_path("a.txt").into(),
UnmergedStatus {
first_head: UnmergedStatusCode::Updated,
second_head: UnmergedStatusCode::Updated,
@@ -647,7 +650,7 @@ mod tests {
// Simulate the conflict being removed by e.g. staging the file.
fs.with_git_state(path!("/project/.git").as_ref(), true, |state| {
state.unmerged_paths.remove(Path::new("a.txt"))
state.unmerged_paths.remove(&repo_path("a.txt"))
})
.unwrap();
@@ -660,7 +663,7 @@ mod tests {
// Simulate the conflict being re-added.
fs.with_git_state(path!("/project/.git").as_ref(), true, |state| {
state.unmerged_paths.insert(
"a.txt".into(),
repo_path("a.txt"),
UnmergedStatus {
first_head: UnmergedStatusCode::Updated,
second_head: UnmergedStatusCode::Updated,
+112 -126
View File
@@ -3,6 +3,7 @@ use git::{repository::RepoPath, status::GitSummary};
use std::{collections::BTreeMap, ops::Deref, path::Path};
use sum_tree::Cursor;
use text::Bias;
use util::rel_path::RelPath;
use worktree::{Entry, PathProgress, PathTarget, Traversal};
use super::{RepositoryId, RepositorySnapshot, StatusEntry};
@@ -70,10 +71,7 @@ impl<'a> GitTraversal<'a> {
return;
};
let Ok(abs_path) = self.traversal.snapshot().absolutize(&entry.path) else {
self.repo_location = None;
return;
};
let abs_path = self.traversal.snapshot().absolutize(&entry.path);
let Some((repo, repo_path)) = self.repo_root_for_path(&abs_path) else {
self.repo_location = None;
@@ -97,13 +95,13 @@ impl<'a> GitTraversal<'a> {
if entry.is_dir() {
let mut statuses = statuses.clone();
statuses.seek_forward(&PathTarget::Path(repo_path.as_ref()), Bias::Left);
let summary = statuses.summary(&PathTarget::Successor(repo_path.as_ref()), Bias::Left);
statuses.seek_forward(&PathTarget::Path(&repo_path), Bias::Left);
let summary = statuses.summary(&PathTarget::Successor(&repo_path), Bias::Left);
self.current_entry_summary = Some(summary);
} else if entry.is_file() {
// For a file entry, park the cursor on the corresponding status
if statuses.seek_forward(&PathTarget::Path(repo_path.as_ref()), Bias::Left) {
if statuses.seek_forward(&PathTarget::Path(&repo_path), Bias::Left) {
// TODO: Investigate statuses.item() being None here.
self.current_entry_summary = statuses.item().map(|item| item.status.into());
} else {
@@ -159,7 +157,7 @@ impl<'a> Iterator for GitTraversal<'a> {
}
pub struct ChildEntriesGitIter<'a> {
parent_path: &'a Path,
parent_path: &'a RelPath,
traversal: GitTraversal<'a>,
}
@@ -167,7 +165,7 @@ impl<'a> ChildEntriesGitIter<'a> {
pub fn new(
repo_snapshots: &'a HashMap<RepositoryId, RepositorySnapshot>,
worktree_snapshot: &'a worktree::Snapshot,
parent_path: &'a Path,
parent_path: &'a RelPath,
) -> Self {
let mut traversal = GitTraversal::new(
repo_snapshots,
@@ -265,7 +263,7 @@ mod tests {
use gpui::TestAppContext;
use serde_json::json;
use settings::SettingsStore;
use util::path;
use util::{path, rel_path::rel_path};
const CONFLICT: FileStatus = FileStatus::Unmerged(UnmergedStatus {
first_head: UnmergedStatusCode::Updated,
@@ -312,17 +310,14 @@ mod tests {
fs.set_status_for_repo(
Path::new(path!("/root/x/.git")),
&[
(Path::new("x2.txt"), StatusCode::Modified.index()),
(Path::new("z.txt"), StatusCode::Added.index()),
("x2.txt", StatusCode::Modified.index()),
("z.txt", StatusCode::Added.index()),
],
);
fs.set_status_for_repo(
Path::new(path!("/root/x/y/.git")),
&[(Path::new("y1.txt"), CONFLICT)],
);
fs.set_status_for_repo(Path::new(path!("/root/x/y/.git")), &[("y1.txt", CONFLICT)]);
fs.set_status_for_repo(
Path::new(path!("/root/z/.git")),
&[(Path::new("z2.txt"), StatusCode::Added.index())],
&[("z2.txt", StatusCode::Added.index())],
);
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
@@ -337,7 +332,7 @@ mod tests {
let traversal = GitTraversal::new(
&repo_snapshots,
worktree_snapshot.traverse_from_path(true, false, true, Path::new("x")),
worktree_snapshot.traverse_from_path(true, false, true, RelPath::new("x").unwrap()),
);
let entries = traversal
.map(|entry| (entry.path.clone(), entry.git_summary))
@@ -345,13 +340,13 @@ mod tests {
pretty_assertions::assert_eq!(
entries,
[
(Path::new("x/x1.txt").into(), GitSummary::UNCHANGED),
(Path::new("x/x2.txt").into(), MODIFIED),
(Path::new("x/y/y1.txt").into(), GitSummary::CONFLICT),
(Path::new("x/y/y2.txt").into(), GitSummary::UNCHANGED),
(Path::new("x/z.txt").into(), ADDED),
(Path::new("z/z1.txt").into(), GitSummary::UNCHANGED),
(Path::new("z/z2.txt").into(), ADDED),
(rel_path("x/x1.txt").into(), GitSummary::UNCHANGED),
(rel_path("x/x2.txt").into(), MODIFIED),
(rel_path("x/y/y1.txt").into(), GitSummary::CONFLICT),
(rel_path("x/y/y2.txt").into(), GitSummary::UNCHANGED),
(rel_path("x/z.txt").into(), ADDED),
(rel_path("z/z1.txt").into(), GitSummary::UNCHANGED),
(rel_path("z/z2.txt").into(), ADDED),
]
)
}
@@ -386,18 +381,15 @@ mod tests {
fs.set_status_for_repo(
Path::new(path!("/root/x/.git")),
&[
(Path::new("x2.txt"), StatusCode::Modified.index()),
(Path::new("z.txt"), StatusCode::Added.index()),
("x2.txt", StatusCode::Modified.index()),
("z.txt", StatusCode::Added.index()),
],
);
fs.set_status_for_repo(
Path::new(path!("/root/x/y/.git")),
&[(Path::new("y1.txt"), CONFLICT)],
);
fs.set_status_for_repo(Path::new(path!("/root/x/y/.git")), &[("y1.txt", CONFLICT)]);
fs.set_status_for_repo(
Path::new(path!("/root/z/.git")),
&[(Path::new("z2.txt"), StatusCode::Added.index())],
&[("z2.txt", StatusCode::Added.index())],
);
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
@@ -415,18 +407,18 @@ mod tests {
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("x/y"), GitSummary::CONFLICT),
(Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
(Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
("x/y", GitSummary::CONFLICT),
("x/y/y1.txt", GitSummary::CONFLICT),
("x/y/y2.txt", GitSummary::UNCHANGED),
],
);
check_git_statuses(
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("z"), ADDED),
(Path::new("z/z1.txt"), GitSummary::UNCHANGED),
(Path::new("z/z2.txt"), ADDED),
("z", ADDED),
("z/z1.txt", GitSummary::UNCHANGED),
("z/z2.txt", ADDED),
],
);
@@ -435,9 +427,9 @@ mod tests {
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("x"), MODIFIED + ADDED),
(Path::new("x/y"), GitSummary::CONFLICT),
(Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
("x", MODIFIED + ADDED),
("x/y", GitSummary::CONFLICT),
("x/y/y1.txt", GitSummary::CONFLICT),
],
);
@@ -446,13 +438,13 @@ mod tests {
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("x"), MODIFIED + ADDED),
(Path::new("x/x1.txt"), GitSummary::UNCHANGED),
(Path::new("x/x2.txt"), MODIFIED),
(Path::new("x/y"), GitSummary::CONFLICT),
(Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
(Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
(Path::new("x/z.txt"), ADDED),
("x", MODIFIED + ADDED),
("x/x1.txt", GitSummary::UNCHANGED),
("x/x2.txt", MODIFIED),
("x/y", GitSummary::CONFLICT),
("x/y/y1.txt", GitSummary::CONFLICT),
("x/y/y2.txt", GitSummary::UNCHANGED),
("x/z.txt", ADDED),
],
);
@@ -461,9 +453,9 @@ mod tests {
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new(""), GitSummary::UNCHANGED),
(Path::new("x"), MODIFIED + ADDED),
(Path::new("x/x1.txt"), GitSummary::UNCHANGED),
("", GitSummary::UNCHANGED),
("x", MODIFIED + ADDED),
("x/x1.txt", GitSummary::UNCHANGED),
],
);
@@ -472,17 +464,17 @@ mod tests {
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new(""), GitSummary::UNCHANGED),
(Path::new("x"), MODIFIED + ADDED),
(Path::new("x/x1.txt"), GitSummary::UNCHANGED),
(Path::new("x/x2.txt"), MODIFIED),
(Path::new("x/y"), GitSummary::CONFLICT),
(Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
(Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
(Path::new("x/z.txt"), ADDED),
(Path::new("z"), ADDED),
(Path::new("z/z1.txt"), GitSummary::UNCHANGED),
(Path::new("z/z2.txt"), ADDED),
("", GitSummary::UNCHANGED),
("x", MODIFIED + ADDED),
("x/x1.txt", GitSummary::UNCHANGED),
("x/x2.txt", MODIFIED),
("x/y", GitSummary::CONFLICT),
("x/y/y1.txt", GitSummary::CONFLICT),
("x/y/y2.txt", GitSummary::UNCHANGED),
("x/z.txt", ADDED),
("z", ADDED),
("z/z1.txt", GitSummary::UNCHANGED),
("z/z2.txt", ADDED),
],
);
}
@@ -520,9 +512,9 @@ mod tests {
fs.set_status_for_repo(
Path::new(path!("/root/.git")),
&[
(Path::new("a/b/c1.txt"), StatusCode::Added.index()),
(Path::new("a/d/e2.txt"), StatusCode::Modified.index()),
(Path::new("g/h2.txt"), CONFLICT),
("a/b/c1.txt", StatusCode::Added.index()),
("a/d/e2.txt", StatusCode::Modified.index()),
("g/h2.txt", CONFLICT),
],
);
@@ -540,9 +532,9 @@ mod tests {
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new(""), GitSummary::CONFLICT + MODIFIED + ADDED),
(Path::new("g"), GitSummary::CONFLICT),
(Path::new("g/h2.txt"), GitSummary::CONFLICT),
("", GitSummary::CONFLICT + MODIFIED + ADDED),
("g", GitSummary::CONFLICT),
("g/h2.txt", GitSummary::CONFLICT),
],
);
@@ -550,17 +542,17 @@ mod tests {
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new(""), GitSummary::CONFLICT + ADDED + MODIFIED),
(Path::new("a"), ADDED + MODIFIED),
(Path::new("a/b"), ADDED),
(Path::new("a/b/c1.txt"), ADDED),
(Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
(Path::new("a/d"), MODIFIED),
(Path::new("a/d/e2.txt"), MODIFIED),
(Path::new("f"), GitSummary::UNCHANGED),
(Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
(Path::new("g"), GitSummary::CONFLICT),
(Path::new("g/h2.txt"), GitSummary::CONFLICT),
("", GitSummary::CONFLICT + ADDED + MODIFIED),
("a", ADDED + MODIFIED),
("a/b", ADDED),
("a/b/c1.txt", ADDED),
("a/b/c2.txt", GitSummary::UNCHANGED),
("a/d", MODIFIED),
("a/d/e2.txt", MODIFIED),
("f", GitSummary::UNCHANGED),
("f/no-status.txt", GitSummary::UNCHANGED),
("g", GitSummary::CONFLICT),
("g/h2.txt", GitSummary::CONFLICT),
],
);
@@ -568,15 +560,15 @@ mod tests {
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("a/b"), ADDED),
(Path::new("a/b/c1.txt"), ADDED),
(Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
(Path::new("a/d"), MODIFIED),
(Path::new("a/d/e1.txt"), GitSummary::UNCHANGED),
(Path::new("a/d/e2.txt"), MODIFIED),
(Path::new("f"), GitSummary::UNCHANGED),
(Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
(Path::new("g"), GitSummary::CONFLICT),
("a/b", ADDED),
("a/b/c1.txt", ADDED),
("a/b/c2.txt", GitSummary::UNCHANGED),
("a/d", MODIFIED),
("a/d/e1.txt", GitSummary::UNCHANGED),
("a/d/e2.txt", MODIFIED),
("f", GitSummary::UNCHANGED),
("f/no-status.txt", GitSummary::UNCHANGED),
("g", GitSummary::CONFLICT),
],
);
@@ -584,11 +576,11 @@ mod tests {
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("a/b/c1.txt"), ADDED),
(Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
(Path::new("a/d/e1.txt"), GitSummary::UNCHANGED),
(Path::new("a/d/e2.txt"), MODIFIED),
(Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
("a/b/c1.txt", ADDED),
("a/b/c2.txt", GitSummary::UNCHANGED),
("a/d/e1.txt", GitSummary::UNCHANGED),
("a/d/e2.txt", MODIFIED),
("f/no-status.txt", GitSummary::UNCHANGED),
],
);
}
@@ -621,18 +613,18 @@ mod tests {
fs.set_status_for_repo(
Path::new(path!("/root/x/.git")),
&[(Path::new("x1.txt"), StatusCode::Added.index())],
&[("x1.txt", StatusCode::Added.index())],
);
fs.set_status_for_repo(
Path::new(path!("/root/y/.git")),
&[
(Path::new("y1.txt"), CONFLICT),
(Path::new("y2.txt"), StatusCode::Modified.index()),
("y1.txt", CONFLICT),
("y2.txt", StatusCode::Modified.index()),
],
);
fs.set_status_for_repo(
Path::new(path!("/root/z/.git")),
&[(Path::new("z2.txt"), StatusCode::Modified.index())],
&[("z2.txt", StatusCode::Modified.index())],
);
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
@@ -648,47 +640,44 @@ mod tests {
check_git_statuses(
&repo_snapshots,
&worktree_snapshot,
&[(Path::new("x"), ADDED), (Path::new("x/x1.txt"), ADDED)],
&[("x", ADDED), ("x/x1.txt", ADDED)],
);
check_git_statuses(
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("y"), GitSummary::CONFLICT + MODIFIED),
(Path::new("y/y1.txt"), GitSummary::CONFLICT),
(Path::new("y/y2.txt"), MODIFIED),
("y", GitSummary::CONFLICT + MODIFIED),
("y/y1.txt", GitSummary::CONFLICT),
("y/y2.txt", MODIFIED),
],
);
check_git_statuses(
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("z"), MODIFIED),
(Path::new("z/z2.txt"), MODIFIED),
],
&[("z", MODIFIED), ("z/z2.txt", MODIFIED)],
);
check_git_statuses(
&repo_snapshots,
&worktree_snapshot,
&[(Path::new("x"), ADDED), (Path::new("x/x1.txt"), ADDED)],
&[("x", ADDED), ("x/x1.txt", ADDED)],
);
check_git_statuses(
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("x"), ADDED),
(Path::new("x/x1.txt"), ADDED),
(Path::new("x/x2.txt"), GitSummary::UNCHANGED),
(Path::new("y"), GitSummary::CONFLICT + MODIFIED),
(Path::new("y/y1.txt"), GitSummary::CONFLICT),
(Path::new("y/y2.txt"), MODIFIED),
(Path::new("z"), MODIFIED),
(Path::new("z/z1.txt"), GitSummary::UNCHANGED),
(Path::new("z/z2.txt"), MODIFIED),
("x", ADDED),
("x/x1.txt", ADDED),
("x/x2.txt", GitSummary::UNCHANGED),
("y", GitSummary::CONFLICT + MODIFIED),
("y/y1.txt", GitSummary::CONFLICT),
("y/y2.txt", MODIFIED),
("z", MODIFIED),
("z/z1.txt", GitSummary::UNCHANGED),
("z/z2.txt", MODIFIED),
],
);
}
@@ -722,7 +711,7 @@ mod tests {
.await;
fs.set_head_and_index_for_repo(
path!("/root/.git").as_ref(),
&[("a.txt".into(), "".into()), ("b/c.txt".into(), "".into())],
&[("a.txt", "".into()), ("b/c.txt", "".into())],
);
cx.run_until_parked();
@@ -757,10 +746,7 @@ mod tests {
// detected.
fs.set_head_for_repo(
path!("/root/.git").as_ref(),
&[
("a.txt".into(), "".into()),
("b/c.txt".into(), "something-else".into()),
],
&[("a.txt", "".into()), ("b/c.txt", "something-else".into())],
"deadbeef",
);
cx.executor().run_until_parked();
@@ -777,9 +763,9 @@ mod tests {
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new(""), MODIFIED),
(Path::new("a.txt"), GitSummary::UNCHANGED),
(Path::new("b/c.txt"), MODIFIED),
("", MODIFIED),
("a.txt", GitSummary::UNCHANGED),
("b/c.txt", MODIFIED),
],
);
}
@@ -788,17 +774,17 @@ mod tests {
fn check_git_statuses(
repo_snapshots: &HashMap<RepositoryId, RepositorySnapshot>,
worktree_snapshot: &worktree::Snapshot,
expected_statuses: &[(&Path, GitSummary)],
expected_statuses: &[(&str, GitSummary)],
) {
let mut traversal = GitTraversal::new(
repo_snapshots,
worktree_snapshot.traverse_from_path(true, true, false, "".as_ref()),
worktree_snapshot.traverse_from_path(true, true, false, RelPath::empty()),
);
let found_statuses = expected_statuses
.iter()
.map(|&(path, _)| {
let git_entry = traversal
.find(|git_entry| &*git_entry.path == path)
.find(|git_entry| git_entry.path.as_ref() == rel_path(path))
.unwrap_or_else(|| panic!("Traversal has no entry for {path:?}"));
(path, git_entry.git_summary)
})
+10 -12
View File
@@ -13,10 +13,9 @@ use image::{ExtendedColorType, GenericImageView, ImageReader};
use language::{DiskState, File};
use rpc::{AnyProtoClient, ErrorExt as _};
use std::num::NonZeroU64;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::{ffi::OsStr, path::PathBuf};
use util::ResultExt;
use util::{ResultExt, rel_path::RelPath};
use worktree::{LoadedBinaryFile, PathChange, Worktree};
#[derive(Clone, Copy, Debug, Hash, PartialEq, PartialOrd, Ord, Eq)]
@@ -207,8 +206,7 @@ pub fn is_image_file(project: &Entity<Project>, path: &ProjectPath, cx: &App) ->
.abs_path();
path.path
.extension()
.or_else(|| worktree_abs_path.extension())
.and_then(OsStr::to_str)
.or_else(|| worktree_abs_path.extension()?.to_str())
.map(str::to_lowercase)
});
@@ -255,7 +253,7 @@ impl ProjectItem for ImageItem {
trait ImageStoreImpl {
fn open_image(
&self,
path: Arc<Path>,
path: Arc<RelPath>,
worktree: Entity<Worktree>,
cx: &mut Context<ImageStore>,
) -> Task<Result<Entity<ImageItem>>>;
@@ -458,7 +456,7 @@ impl ImageStore {
impl ImageStoreImpl for Entity<LocalImageStore> {
fn open_image(
&self,
path: Arc<Path>,
path: Arc<RelPath>,
worktree: Entity<Worktree>,
cx: &mut Context<ImageStore>,
) -> Task<Result<Entity<ImageItem>>> {
@@ -539,7 +537,7 @@ impl LocalImageStore {
fn local_worktree_entries_changed(
&mut self,
worktree_handle: &Entity<Worktree>,
changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
changes: &[(Arc<RelPath>, ProjectEntryId, PathChange)],
cx: &mut Context<Self>,
) {
let snapshot = worktree_handle.read(cx).snapshot();
@@ -551,7 +549,7 @@ impl LocalImageStore {
fn local_worktree_entry_changed(
&mut self,
entry_id: ProjectEntryId,
path: &Arc<Path>,
path: &Arc<RelPath>,
worktree: &Entity<worktree::Worktree>,
snapshot: &worktree::Snapshot,
cx: &mut Context<Self>,
@@ -698,7 +696,7 @@ fn create_gpui_image(content: Vec<u8>) -> anyhow::Result<Arc<gpui::Image>> {
impl ImageStoreImpl for Entity<RemoteImageStore> {
fn open_image(
&self,
_path: Arc<Path>,
_path: Arc<RelPath>,
_worktree: Entity<Worktree>,
_cx: &mut Context<ImageStore>,
) -> Task<Result<Entity<ImageItem>>> {
@@ -729,7 +727,7 @@ mod tests {
use gpui::TestAppContext;
use serde_json::json;
use settings::SettingsStore;
use std::path::PathBuf;
use util::rel_path::rel_path;
pub fn init_test(cx: &mut TestAppContext) {
zlog::init_test();
@@ -768,7 +766,7 @@ mod tests {
let project_path = ProjectPath {
worktree_id,
path: PathBuf::from("image_1.png").into(),
path: rel_path("image_1.png").into(),
};
let (task1, task2) = project.update(cx, |project, cx| {
+191 -131
View File
@@ -33,7 +33,6 @@ use crate::{
},
prettier_store::{self, PrettierStore, PrettierStoreEvent},
project_settings::{LspSettings, ProjectSettings},
relativize_path, resolve_path,
toolchain_store::{LocalToolchainStore, ToolchainStoreEvent},
worktree_store::{WorktreeStore, WorktreeStoreEvent},
yarn::YarnPathStore,
@@ -88,7 +87,7 @@ use postage::{mpsc, sink::Sink, stream::Stream, watch};
use rand::prelude::*;
use rpc::{
AnyProtoClient,
proto::{FromProto, LspRequestId, LspRequestMessage as _, ToProto},
proto::{LspRequestId, LspRequestMessage as _},
};
use serde::Serialize;
use settings::{Settings, SettingsLocation, SettingsStore};
@@ -116,8 +115,9 @@ use text::{Anchor, BufferId, LineEnding, OffsetRangeExt, ToPoint as _};
use util::{
ConnectionResult, ResultExt as _, debug_panic, defer, maybe, merge_json_value_into,
paths::{PathExt, SanitizedPath},
paths::{PathStyle, SanitizedPath},
post_inc,
rel_path::RelPath,
};
pub use fs::*;
@@ -158,7 +158,7 @@ impl FormatTrigger {
#[derive(Clone)]
struct UnifiedLanguageServer {
id: LanguageServerId,
project_roots: HashSet<Arc<Path>>,
project_roots: HashSet<Arc<RelPath>>,
}
#[derive(Clone, Hash, PartialEq, Eq)]
@@ -209,7 +209,7 @@ pub struct LocalLspStore {
diagnostics: HashMap<
WorktreeId,
HashMap<
Arc<Path>,
Arc<RelPath>,
Vec<(
LanguageServerId,
Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
@@ -1086,7 +1086,7 @@ impl LocalLspStore {
if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) {
let worktree_id = file.worktree_id(cx);
let path: Arc<Path> = file
let path: Arc<RelPath> = file
.path()
.parent()
.map(Arc::from)
@@ -1842,17 +1842,19 @@ impl LocalLspStore {
}
if !project_transaction_command.0.is_empty() {
let extra_buffers = project_transaction_command
.0
.keys()
.filter_map(|buffer_handle| {
buffer_handle
.read_with(cx, |b, cx| b.project_path(cx))
.ok()
.flatten()
})
.map(|p| p.path.to_sanitized_string())
.join(", ");
let mut extra_buffers = String::new();
for buffer in project_transaction_command.0.keys() {
buffer
.read_with(cx, |b, cx| {
if let Some(path) = b.project_path(cx) {
if !extra_buffers.is_empty() {
extra_buffers.push_str(", ");
}
extra_buffers.push_str(path.path.as_str());
}
})
.ok();
}
zlog::warn!(
logger =>
"Unexpected edits to buffers other than the buffer actively being formatted due to command {}. Impacted buffers: [{}].",
@@ -2347,7 +2349,7 @@ impl LocalLspStore {
let Some(language) = buffer.language().cloned() else {
return;
};
let path: Arc<Path> = file
let path: Arc<RelPath> = file
.path()
.parent()
.map(Arc::from)
@@ -2403,8 +2405,7 @@ impl LocalLspStore {
let path = &disposition.path;
{
let uri =
Uri::from_file_path(worktree.read(cx).abs_path().join(&path.path));
let uri = Uri::from_file_path(worktree.read(cx).absolutize(&path.path));
let server_id = self.get_or_insert_language_server(
&worktree,
@@ -3172,7 +3173,7 @@ impl LocalLspStore {
if let Some((tree, glob)) =
worktree.as_local_mut().zip(Glob::new(&pattern).log_err())
{
tree.add_path_prefix_to_scan(literal_prefix.into());
tree.add_path_prefix_to_scan(literal_prefix);
worktree_globs
.entry(tree.id())
.or_insert_with(GlobSetBuilder::new)
@@ -3268,10 +3269,11 @@ impl LocalLspStore {
worktrees: &[Entity<Worktree>],
watcher: &FileSystemWatcher,
cx: &App,
) -> Option<(Entity<Worktree>, PathBuf, String)> {
) -> Option<(Entity<Worktree>, Arc<RelPath>, String)> {
worktrees.iter().find_map(|worktree| {
let tree = worktree.read(cx);
let worktree_root_path = tree.abs_path();
let path_style = tree.path_style();
match &watcher.glob_pattern {
lsp::GlobPattern::String(s) => {
let watcher_path = SanitizedPath::new(s);
@@ -3282,7 +3284,7 @@ impl LocalLspStore {
let literal_prefix = glob_literal_prefix(relative);
Some((
worktree.clone(),
literal_prefix,
RelPath::from_std_path(&literal_prefix, path_style).ok()?,
relative.to_string_lossy().to_string(),
))
}
@@ -3296,7 +3298,11 @@ impl LocalLspStore {
let relative = base_uri.strip_prefix(&worktree_root_path).ok()?;
let mut literal_prefix = relative.to_owned();
literal_prefix.push(glob_literal_prefix(Path::new(&rp.pattern)));
Some((worktree.clone(), literal_prefix, rp.pattern.clone()))
Some((
worktree.clone(),
RelPath::from_std_path(&literal_prefix, path_style).ok()?,
rp.pattern.clone(),
))
}
}
})
@@ -3483,7 +3489,7 @@ pub struct LspStore {
_maintain_workspace_config: (Task<Result<()>>, watch::Sender<()>),
_maintain_buffer_languages: Task<()>,
diagnostic_summaries:
HashMap<WorktreeId, HashMap<Arc<Path>, HashMap<LanguageServerId, DiagnosticSummary>>>,
HashMap<WorktreeId, HashMap<Arc<RelPath>, HashMap<LanguageServerId, DiagnosticSummary>>>,
pub lsp_server_capabilities: HashMap<LanguageServerId, lsp::ServerCapabilities>,
lsp_document_colors: HashMap<BufferId, DocumentColorData>,
lsp_code_lens: HashMap<BufferId, CodeLensData>,
@@ -3569,11 +3575,28 @@ struct CoreSymbol {
pub language_server_name: LanguageServerName,
pub source_worktree_id: WorktreeId,
pub source_language_server_id: LanguageServerId,
pub path: ProjectPath,
pub path: SymbolLocation,
pub name: String,
pub kind: lsp::SymbolKind,
pub range: Range<Unclipped<PointUtf16>>,
pub signature: [u8; 32],
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SymbolLocation {
InProject(ProjectPath),
OutsideProject {
abs_path: Arc<Path>,
signature: [u8; 32],
},
}
impl SymbolLocation {
fn file_name(&self) -> Option<&str> {
match self {
Self::InProject(path) => path.path.file_name(),
Self::OutsideProject { abs_path, .. } => abs_path.file_name()?.to_str(),
}
}
}
impl LspStore {
@@ -4353,7 +4376,7 @@ impl LspStore {
let mut summaries = diangostic_summaries.iter().flat_map(|(path, summaries)| {
summaries
.iter()
.map(|(server_id, summary)| summary.to_proto(*server_id, path))
.map(|(server_id, summary)| summary.to_proto(*server_id, path.as_ref()))
});
if let Some(summary) = summaries.next() {
client
@@ -4655,7 +4678,6 @@ impl LspStore {
.unwrap_or_else(|| file.path().clone());
let worktree_path = ProjectPath { worktree_id, path };
let abs_path = file.abs_path(cx);
let worktree_root = worktree.read(cx).abs_path();
let nodes = rebase
.walk(
worktree_path,
@@ -4668,7 +4690,7 @@ impl LspStore {
for node in nodes {
let server_id = node.server_id_or_init(|disposition| {
let path = &disposition.path;
let uri = Uri::from_file_path(worktree_root.join(&path.path));
let uri = Uri::from_file_path(worktree.read(cx).absolutize(&path.path));
let key = LanguageServerSeed {
worktree_id,
name: disposition.server_name.clone(),
@@ -6965,7 +6987,6 @@ impl LspStore {
server_id: LanguageServerId,
lsp_adapter: Arc<CachedLspAdapter>,
worktree: WeakEntity<Worktree>,
worktree_abs_path: Arc<Path>,
lsp_symbols: Vec<(String, SymbolKind, lsp::Location)>,
}
@@ -7004,7 +7025,6 @@ impl LspStore {
if !supports_workspace_symbol_request {
continue;
}
let worktree_abs_path = worktree.abs_path().clone();
let worktree_handle = worktree_handle.clone();
let server_id = server.server_id();
requests.push(
@@ -7044,7 +7064,6 @@ impl LspStore {
server_id,
lsp_adapter,
worktree: worktree_handle.downgrade(),
worktree_abs_path,
lsp_symbols,
}
}),
@@ -7069,33 +7088,29 @@ impl LspStore {
let source_worktree = result.worktree.upgrade()?;
let source_worktree_id = source_worktree.read(cx).id();
let path;
let worktree;
if let Some((tree, rel_path)) =
let path = if let Some((tree, rel_path)) =
this.worktree_store.read(cx).find_worktree(&abs_path, cx)
{
worktree = tree;
path = rel_path;
let worktree_id = tree.read(cx).id();
SymbolLocation::InProject(ProjectPath {
worktree_id,
path: rel_path,
})
} else {
worktree = source_worktree;
path = relativize_path(&result.worktree_abs_path, &abs_path);
}
let worktree_id = worktree.read(cx).id();
let project_path = ProjectPath {
worktree_id,
path: path.into(),
SymbolLocation::OutsideProject {
signature: this.symbol_signature(&abs_path),
abs_path: abs_path.into(),
}
};
let signature = this.symbol_signature(&project_path);
Some(CoreSymbol {
source_language_server_id: result.server_id,
language_server_name: result.lsp_adapter.name.clone(),
source_worktree_id,
path: project_path,
path,
kind: symbol_kind,
name: symbol_name,
range: range_from_lsp(symbol_location.range),
signature,
})
})
.collect()
@@ -7638,7 +7653,7 @@ impl LspStore {
let worktree_id = worktree.read(cx).id();
let project_path = ProjectPath {
worktree_id,
path: relative_path.into(),
path: relative_path,
};
if let Some(buffer_handle) = self.buffer_store.read(cx).get_by_path(&project_path) {
@@ -7735,7 +7750,7 @@ impl LspStore {
&mut self,
worktree_id: WorktreeId,
server_id: LanguageServerId,
path_in_worktree: Arc<Path>,
path_in_worktree: Arc<RelPath>,
diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
_: &mut Context<Worktree>,
) -> Result<ControlFlow<(), Option<(u64, proto::DiagnosticSummary)>>> {
@@ -7827,18 +7842,21 @@ impl LspStore {
)));
};
let worktree_abs_path = if let Some(worktree_abs_path) = self
.worktree_store
.read(cx)
.worktree_for_id(symbol.path.worktree_id, cx)
.map(|worktree| worktree.read(cx).abs_path())
{
worktree_abs_path
} else {
return Task::ready(Err(anyhow!("worktree not found for symbol")));
let symbol_abs_path = match &symbol.path {
SymbolLocation::InProject(project_path) => self
.worktree_store
.read(cx)
.absolutize(&project_path, cx)
.context("no such worktree"),
SymbolLocation::OutsideProject {
abs_path,
signature: _,
} => Ok(abs_path.to_path_buf()),
};
let symbol_abs_path = match symbol_abs_path {
Ok(abs_path) => abs_path,
Err(err) => return Task::ready(Err(err)),
};
let symbol_abs_path = resolve_path(&worktree_abs_path, &symbol.path.path);
let symbol_uri = if let Ok(uri) = lsp::Uri::from_file_path(symbol_abs_path) {
uri
} else {
@@ -7891,8 +7909,7 @@ impl LspStore {
worktree_store.find_worktree(&worktree_root_target, cx)
})
})? {
let relative_path =
known_relative_path.unwrap_or_else(|| Arc::<Path>::from(result.1));
let relative_path = known_relative_path.unwrap_or_else(|| result.1.clone());
(result.0, relative_path)
} else {
let worktree = lsp_store
@@ -7919,7 +7936,11 @@ impl LspStore {
let relative_path = if let Some(known_path) = known_relative_path {
known_path
} else {
abs_path.strip_prefix(worktree_root)?.into()
RelPath::from_std_path(
abs_path.strip_prefix(worktree_root)?,
PathStyle::local(),
)
.context("failed to create relative path")?
};
(worktree, relative_path)
};
@@ -8326,39 +8347,56 @@ impl LspStore {
mut cx: AsyncApp,
) -> Result<proto::ProjectEntryResponse> {
let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
let (worktree_id, worktree, old_path, is_dir) = this
let new_worktree_id = WorktreeId::from_proto(envelope.payload.new_worktree_id);
let new_path =
RelPath::from_proto(&envelope.payload.new_path).context("invalid relative path")?;
let (worktree_store, old_worktree, new_worktree, old_entry) = this
.update(&mut cx, |this, cx| {
this.worktree_store
let (worktree, entry) = this
.worktree_store
.read(cx)
.worktree_and_entry_for_id(entry_id, cx)
.map(|(worktree, entry)| {
(
worktree.read(cx).id(),
worktree,
entry.path.clone(),
entry.is_dir(),
)
})
.worktree_and_entry_for_id(entry_id, cx)?;
let new_worktree = this
.worktree_store
.read(cx)
.worktree_for_id(new_worktree_id, cx)?;
Some((
this.worktree_store.clone(),
worktree,
new_worktree,
entry.clone(),
))
})?
.context("worktree not found")?;
let (old_abs_path, new_abs_path) = {
let root_path = worktree.read_with(&cx, |this, _| this.abs_path())?;
let new_path = PathBuf::from_proto(envelope.payload.new_path.clone());
(root_path.join(&old_path), root_path.join(&new_path))
};
let (old_abs_path, old_worktree_id) = old_worktree.read_with(&cx, |worktree, _| {
(worktree.absolutize(&old_entry.path), worktree.id())
})?;
let new_abs_path =
new_worktree.read_with(&cx, |worktree, _| worktree.absolutize(&new_path))?;
let _transaction = Self::will_rename_entry(
this.downgrade(),
worktree_id,
old_worktree_id,
&old_abs_path,
&new_abs_path,
is_dir,
old_entry.is_dir(),
cx.clone(),
)
.await;
let response = WorktreeStore::handle_rename_project_entry(
worktree_store,
envelope.payload,
cx.clone(),
)
.await;
let response = Worktree::handle_rename_entry(worktree, envelope.payload, cx.clone()).await;
this.read_with(&cx, |this, _| {
this.did_rename_entry(worktree_id, &old_abs_path, &new_abs_path, is_dir);
this.did_rename_entry(
old_worktree_id,
&old_abs_path,
&new_abs_path,
old_entry.is_dir(),
);
})
.ok();
response
@@ -8381,7 +8419,7 @@ impl LspStore {
{
let project_path = ProjectPath {
worktree_id,
path: Arc::<Path>::from_proto(message_summary.path),
path: RelPath::from_proto(&message_summary.path).context("invalid path")?,
};
let path = project_path.path.clone();
let server_id = LanguageServerId(message_summary.language_server_id as usize);
@@ -9436,10 +9474,16 @@ impl LspStore {
let peer_id = envelope.original_sender_id().unwrap_or_default();
let symbol = envelope.payload.symbol.context("invalid symbol")?;
let symbol = Self::deserialize_symbol(symbol)?;
let symbol = this.read_with(&cx, |this, _| {
let signature = this.symbol_signature(&symbol.path);
anyhow::ensure!(signature == symbol.signature, "invalid symbol signature");
Ok(symbol)
this.read_with(&cx, |this, _| {
if let SymbolLocation::OutsideProject {
abs_path,
signature,
} = &symbol.path
{
let new_signature = this.symbol_signature(&abs_path);
anyhow::ensure!(&new_signature == signature, "invalid symbol signature");
}
Ok(())
})??;
let buffer = this
.update(&mut cx, |this, cx| {
@@ -9452,7 +9496,6 @@ impl LspStore {
name: symbol.name,
kind: symbol.kind,
range: symbol.range,
signature: symbol.signature,
label: CodeLabel {
text: Default::default(),
runs: Default::default(),
@@ -9484,10 +9527,9 @@ impl LspStore {
})?
}
fn symbol_signature(&self, project_path: &ProjectPath) -> [u8; 32] {
fn symbol_signature(&self, abs_path: &Path) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(project_path.worktree_id.to_proto().to_be_bytes());
hasher.update(project_path.path.to_string_lossy().as_bytes());
hasher.update(abs_path.to_string_lossy().as_bytes());
hasher.update(self.nonce.to_be_bytes());
hasher.finalize().as_slice().try_into().unwrap()
}
@@ -10233,7 +10275,7 @@ impl LspStore {
let project_path = ProjectPath {
worktree_id: worktree.read(cx).id(),
path: relative_path.into(),
path: relative_path,
};
Some(
@@ -10799,7 +10841,7 @@ impl LspStore {
pub(super) fn update_local_worktree_language_servers(
&mut self,
worktree_handle: &Entity<Worktree>,
changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
changes: &[(Arc<RelPath>, ProjectEntryId, PathChange)],
cx: &mut Context<Self>,
) {
if changes.is_empty() {
@@ -10821,7 +10863,7 @@ impl LspStore {
language_server_ids.sort();
language_server_ids.dedup();
let abs_path = worktree_handle.read(cx).abs_path();
// let abs_path = worktree_handle.read(cx).abs_path();
for server_id in &language_server_ids {
if let Some(LanguageServerState::Running { server, .. }) =
local.language_servers.get(server_id)
@@ -10834,7 +10876,7 @@ impl LspStore {
changes: changes
.iter()
.filter_map(|(path, _, change)| {
if !watched_paths.is_match(path) {
if !watched_paths.is_match(path.as_std_path()) {
return None;
}
let typ = match change {
@@ -10844,10 +10886,11 @@ impl LspStore {
PathChange::Updated => lsp::FileChangeType::CHANGED,
PathChange::AddedOrUpdated => lsp::FileChangeType::CHANGED,
};
Some(lsp::FileEvent {
uri: lsp::Uri::from_file_path(abs_path.join(path)).unwrap(),
typ,
})
let uri = lsp::Uri::from_file_path(
worktree_handle.read(cx).absolutize(&path),
)
.ok()?;
Some(lsp::FileEvent { uri, typ })
})
.collect(),
};
@@ -10859,7 +10902,7 @@ impl LspStore {
}
}
for (path, _, _) in changes {
if let Some(file_name) = path.file_name().and_then(|file_name| file_name.to_str())
if let Some(file_name) = path.file_name()
&& local.watched_manifest_filenames.contains(file_name)
{
self.request_workspace_config_refresh();
@@ -10879,12 +10922,10 @@ impl LspStore {
}
fn serialize_symbol(symbol: &Symbol) -> proto::Symbol {
proto::Symbol {
let mut result = proto::Symbol {
language_server_name: symbol.language_server_name.0.to_string(),
source_worktree_id: symbol.source_worktree_id.to_proto(),
language_server_id: symbol.source_language_server_id.to_proto(),
worktree_id: symbol.path.worktree_id.to_proto(),
path: symbol.path.path.as_ref().to_proto(),
name: symbol.name.clone(),
kind: unsafe { mem::transmute::<lsp::SymbolKind, i32>(symbol.kind) },
start: Some(proto::PointUtf16 {
@@ -10895,17 +10936,45 @@ impl LspStore {
row: symbol.range.end.0.row,
column: symbol.range.end.0.column,
}),
signature: symbol.signature.to_vec(),
worktree_id: Default::default(),
path: Default::default(),
signature: Default::default(),
};
match &symbol.path {
SymbolLocation::InProject(path) => {
result.worktree_id = path.worktree_id.to_proto();
result.path = path.path.to_proto();
}
SymbolLocation::OutsideProject {
abs_path,
signature,
} => {
result.path = abs_path.to_string_lossy().to_string();
result.signature = signature.to_vec();
}
}
result
}
fn deserialize_symbol(serialized_symbol: proto::Symbol) -> Result<CoreSymbol> {
let source_worktree_id = WorktreeId::from_proto(serialized_symbol.source_worktree_id);
let worktree_id = WorktreeId::from_proto(serialized_symbol.worktree_id);
let kind = unsafe { mem::transmute::<i32, lsp::SymbolKind>(serialized_symbol.kind) };
let path = ProjectPath {
worktree_id,
path: Arc::<Path>::from_proto(serialized_symbol.path),
let path = if serialized_symbol.signature.is_empty() {
SymbolLocation::InProject(ProjectPath {
worktree_id,
path: RelPath::from_proto(&serialized_symbol.path)
.context("invalid symbol path")?,
})
} else {
SymbolLocation::OutsideProject {
abs_path: Path::new(&serialized_symbol.path).into(),
signature: serialized_symbol
.signature
.try_into()
.map_err(|_| anyhow!("invalid signature"))?,
}
};
let start = serialized_symbol.start.context("invalid start")?;
@@ -10921,10 +10990,6 @@ impl LspStore {
range: Unclipped(PointUtf16::new(start.row, start.column))
..Unclipped(PointUtf16::new(end.row, end.column)),
kind,
signature: serialized_symbol
.signature
.try_into()
.map_err(|_| anyhow!("invalid signature"))?,
})
}
@@ -12484,7 +12549,7 @@ impl DiagnosticSummary {
pub fn to_proto(
self,
language_server_id: LanguageServerId,
path: &Path,
path: &RelPath,
) -> proto::DiagnosticSummary {
proto::DiagnosticSummary {
path: path.to_proto(),
@@ -12657,7 +12722,7 @@ pub fn language_server_settings<'a>(
language_server_settings_for(
SettingsLocation {
worktree_id: delegate.worktree_id(),
path: delegate.worktree_root_path(),
path: RelPath::empty(),
},
language,
cx,
@@ -12847,16 +12912,12 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate {
Some(dir)
}
async fn read_text_file(&self, path: PathBuf) -> Result<String> {
async fn read_text_file(&self, path: &RelPath) -> Result<String> {
let entry = self
.worktree
.entry_for_path(&path)
.entry_for_path(path)
.with_context(|| format!("no worktree entry for path {path:?}"))?;
let abs_path = self
.worktree
.absolutize(&entry.path)
.with_context(|| format!("cannot absolutize path {path:?}"))?;
let abs_path = self.worktree.absolutize(&entry.path);
self.fs.load(&abs_path).await
}
}
@@ -12870,14 +12931,17 @@ async fn populate_labels_for_symbols(
#[allow(clippy::mutable_key_type)]
let mut symbols_by_language = HashMap::<Option<Arc<Language>>, Vec<CoreSymbol>>::default();
let mut unknown_paths = BTreeSet::new();
let mut unknown_paths = BTreeSet::<Arc<str>>::new();
for symbol in symbols {
let Some(file_name) = symbol.path.file_name() else {
continue;
};
let language = language_registry
.language_for_file_path(&symbol.path.path)
.language_for_file_path(Path::new(file_name))
.await
.ok()
.or_else(|| {
unknown_paths.insert(symbol.path.path.clone());
unknown_paths.insert(file_name.into());
None
});
symbols_by_language
@@ -12887,10 +12951,7 @@ async fn populate_labels_for_symbols(
}
for unknown_path in unknown_paths {
log::info!(
"no language found for symbol path {}",
unknown_path.display()
);
log::info!("no language found for symbol in file {unknown_path:?}");
}
let mut label_params = Vec::new();
@@ -12933,7 +12994,6 @@ async fn populate_labels_for_symbols(
name,
kind: symbol.kind,
range: symbol.range,
signature: symbol.signature,
});
}
}
+4 -3
View File
@@ -7,7 +7,7 @@ mod manifest_store;
mod path_trie;
mod server_tree;
use std::{borrow::Borrow, collections::hash_map::Entry, ops::ControlFlow, path::Path, sync::Arc};
use std::{borrow::Borrow, collections::hash_map::Entry, ops::ControlFlow, sync::Arc};
use collections::HashMap;
use gpui::{App, AppContext as _, Context, Entity, Subscription};
@@ -15,6 +15,7 @@ use language::{ManifestDelegate, ManifestName, ManifestQuery};
pub use manifest_store::ManifestProvidersStore;
use path_trie::{LabelPresence, RootPathTrie, TriePath};
use settings::{SettingsStore, WorktreeId};
use util::rel_path::RelPath;
use worktree::{Event as WorktreeEvent, Snapshot, Worktree};
use crate::{
@@ -184,7 +185,7 @@ impl ManifestTree {
.and_then(|manifest_name| self.root_for_path(project_path, manifest_name, delegate, cx))
.unwrap_or_else(|| ProjectPath {
worktree_id,
path: Arc::from(Path::new("")),
path: RelPath::empty().into(),
})
}
@@ -211,7 +212,7 @@ impl ManifestQueryDelegate {
}
impl ManifestDelegate for ManifestQueryDelegate {
fn exists(&self, path: &Path, is_dir: Option<bool>) -> bool {
fn exists(&self, path: &RelPath, is_dir: Option<bool>) -> bool {
self.worktree.entry_for_path(path).is_some_and(|entry| {
is_dir.is_none_or(|is_required_to_be_dir| is_required_to_be_dir == entry.is_dir())
})
+57 -50
View File
@@ -1,11 +1,11 @@
use std::{
collections::{BTreeMap, btree_map::Entry},
ffi::OsStr,
ops::ControlFlow,
path::{Path, PathBuf},
sync::Arc,
};
use util::rel_path::RelPath;
/// [RootPathTrie] is a workhorse of [super::ManifestTree]. It is responsible for determining the closest known entry for a given path.
/// It also determines how much of a given path is unexplored, thus letting callers fill in that gap if needed.
/// Conceptually, it allows one to annotate Worktree entries with arbitrary extra metadata and run closest-ancestor searches.
@@ -14,9 +14,9 @@ use std::{
/// For example, if there's a project root at path `python/project` and we query for a path `python/project/subdir/another_subdir/file.py`, there is
/// a known root at `python/project` and the unexplored part is `subdir/another_subdir` - we need to run a scan on these 2 directories.
pub(super) struct RootPathTrie<Label> {
worktree_relative_path: Arc<Path>,
worktree_relative_path: Arc<RelPath>,
labels: BTreeMap<Label, LabelPresence>,
children: BTreeMap<Arc<OsStr>, RootPathTrie<Label>>,
children: BTreeMap<Arc<str>, RootPathTrie<Label>>,
}
/// Label presence is a marker that allows to optimize searches within [RootPathTrie]; node label can be:
@@ -39,15 +39,17 @@ pub(super) enum LabelPresence {
impl<Label: Ord + Clone> RootPathTrie<Label> {
pub(super) fn new() -> Self {
Self::new_with_key(Arc::from(Path::new("")))
Self::new_with_key(Arc::from(RelPath::empty()))
}
fn new_with_key(worktree_relative_path: Arc<Path>) -> Self {
fn new_with_key(worktree_relative_path: Arc<RelPath>) -> Self {
RootPathTrie {
worktree_relative_path,
labels: Default::default(),
children: Default::default(),
}
}
// Internal implementation of inner that allows one to visit descendants of insertion point for a node.
fn insert_inner(
&mut self,
@@ -57,12 +59,13 @@ impl<Label: Ord + Clone> RootPathTrie<Label> {
) -> &mut Self {
let mut current = self;
let mut path_so_far = PathBuf::new();
let mut path_so_far = <Arc<RelPath>>::from(RelPath::empty());
for key in path.0.iter() {
path_so_far.push(Path::new(key));
path_so_far = path_so_far.join(RelPath::new(key).unwrap());
current = match current.children.entry(key.clone()) {
Entry::Vacant(vacant_entry) => vacant_entry
.insert(RootPathTrie::new_with_key(Arc::from(path_so_far.as_path()))),
Entry::Vacant(vacant_entry) => {
vacant_entry.insert(RootPathTrie::new_with_key(path_so_far.clone()))
}
Entry::Occupied(occupied_entry) => occupied_entry.into_mut(),
};
}
@@ -70,6 +73,7 @@ impl<Label: Ord + Clone> RootPathTrie<Label> {
debug_assert_eq!(_previous_value, None);
current
}
pub(super) fn insert(&mut self, path: &TriePath, value: Label, presence: LabelPresence) {
self.insert_inner(path, value, presence);
}
@@ -78,7 +82,7 @@ impl<Label: Ord + Clone> RootPathTrie<Label> {
&'a self,
path: &TriePath,
callback: &mut dyn for<'b> FnMut(
&'b Arc<Path>,
&'b Arc<RelPath>,
&'a BTreeMap<Label, LabelPresence>,
) -> ControlFlow<()>,
) {
@@ -115,11 +119,22 @@ impl<Label: Ord + Clone> RootPathTrie<Label> {
/// [TriePath] is a [Path] preprocessed for amortizing the cost of doing multiple lookups in distinct [RootPathTrie]s.
#[derive(Clone)]
pub(super) struct TriePath(Arc<[Arc<OsStr>]>);
pub(super) struct TriePath(Arc<[Arc<str>]>);
impl From<&Path> for TriePath {
fn from(value: &Path) -> Self {
TriePath(value.components().map(|c| c.as_os_str().into()).collect())
impl TriePath {
fn new(value: &RelPath) -> Self {
TriePath(
value
.components()
.map(|component| component.into())
.collect(),
)
}
}
impl From<&RelPath> for TriePath {
fn from(value: &RelPath) -> Self {
Self::new(value)
}
}
@@ -127,39 +142,38 @@ impl From<&Path> for TriePath {
mod tests {
use std::collections::BTreeSet;
use util::rel_path::rel_path;
use super::*;
#[test]
fn test_insert_and_lookup() {
let mut trie = RootPathTrie::<()>::new();
trie.insert(
&TriePath::from(Path::new("a/b/c")),
&TriePath::new(rel_path("a/b/c")),
(),
LabelPresence::Present,
);
trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, nodes| {
trie.walk(&TriePath::new(rel_path("a/b/c")), &mut |path, nodes| {
assert_eq!(nodes.get(&()), Some(&LabelPresence::Present));
assert_eq!(path.as_ref(), Path::new("a/b/c"));
assert_eq!(path.as_str(), "a/b/c");
ControlFlow::Continue(())
});
// Now let's annotate a parent with "Known missing" node.
trie.insert(
&TriePath::from(Path::new("a")),
&TriePath::new(rel_path("a")),
(),
LabelPresence::KnownAbsent,
);
// Ensure that we walk from the root to the leaf.
let mut visited_paths = BTreeSet::new();
trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, nodes| {
if path.as_ref() == Path::new("a/b/c") {
assert_eq!(
visited_paths,
BTreeSet::from_iter([Arc::from(Path::new("a/"))])
);
trie.walk(&TriePath::new(rel_path("a/b/c")), &mut |path, nodes| {
if path.as_str() == "a/b/c" {
assert_eq!(visited_paths, BTreeSet::from_iter([rel_path("a").into()]));
assert_eq!(nodes.get(&()), Some(&LabelPresence::Present));
} else if path.as_ref() == Path::new("a/") {
} else if path.as_str() == "a" {
assert!(visited_paths.is_empty());
assert_eq!(nodes.get(&()), Some(&LabelPresence::KnownAbsent));
} else {
@@ -173,15 +187,12 @@ mod tests {
// One can also pass a path whose prefix is in the tree, but not that path itself.
let mut visited_paths = BTreeSet::new();
trie.walk(
&TriePath::from(Path::new("a/b/c/d/e/f/g")),
&TriePath::new(rel_path("a/b/c/d/e/f/g")),
&mut |path, nodes| {
if path.as_ref() == Path::new("a/b/c") {
assert_eq!(
visited_paths,
BTreeSet::from_iter([Arc::from(Path::new("a/"))])
);
if path.as_str() == "a/b/c" {
assert_eq!(visited_paths, BTreeSet::from_iter([rel_path("a").into()]));
assert_eq!(nodes.get(&()), Some(&LabelPresence::Present));
} else if path.as_ref() == Path::new("a/") {
} else if path.as_str() == "a" {
assert!(visited_paths.is_empty());
assert_eq!(nodes.get(&()), Some(&LabelPresence::KnownAbsent));
} else {
@@ -195,8 +206,8 @@ mod tests {
// Test breaking from the tree-walk.
let mut visited_paths = BTreeSet::new();
trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, nodes| {
if path.as_ref() == Path::new("a/") {
trie.walk(&TriePath::new(rel_path("a/b/c")), &mut |path, nodes| {
if path.as_str() == "a" {
assert!(visited_paths.is_empty());
assert_eq!(nodes.get(&()), Some(&LabelPresence::KnownAbsent));
} else {
@@ -210,45 +221,41 @@ mod tests {
// Entry removal.
trie.insert(
&TriePath::from(Path::new("a/b")),
&TriePath::new(rel_path("a/b")),
(),
LabelPresence::KnownAbsent,
);
let mut visited_paths = BTreeSet::new();
trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, _nodes| {
trie.walk(&TriePath::new(rel_path("a/b/c")), &mut |path, _nodes| {
// Assert that we only ever visit a path once.
assert!(visited_paths.insert(path.clone()));
ControlFlow::Continue(())
});
assert_eq!(visited_paths.len(), 3);
trie.remove(&TriePath::from(Path::new("a/b/")));
trie.remove(&TriePath::new(rel_path("a/b")));
let mut visited_paths = BTreeSet::new();
trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, _nodes| {
trie.walk(&TriePath::new(rel_path("a/b/c")), &mut |path, _nodes| {
// Assert that we only ever visit a path once.
assert!(visited_paths.insert(path.clone()));
ControlFlow::Continue(())
});
assert_eq!(visited_paths.len(), 1);
assert_eq!(
visited_paths.into_iter().next().unwrap().as_ref(),
Path::new("a/")
visited_paths.into_iter().next().unwrap(),
rel_path("a").into()
);
}
#[test]
fn path_to_a_root_can_contain_multiple_known_nodes() {
let mut trie = RootPathTrie::<()>::new();
trie.insert(
&TriePath::from(Path::new("a/b")),
(),
LabelPresence::Present,
);
trie.insert(&TriePath::from(Path::new("a")), (), LabelPresence::Present);
trie.insert(&TriePath::new(rel_path("a/b")), (), LabelPresence::Present);
trie.insert(&TriePath::new(rel_path("a")), (), LabelPresence::Present);
let mut visited_paths = BTreeSet::new();
trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, nodes| {
trie.walk(&TriePath::new(rel_path("a/b/c")), &mut |path, nodes| {
assert_eq!(nodes.get(&()), Some(&LabelPresence::Present));
if path.as_ref() != Path::new("a") && path.as_ref() != Path::new("a/b") {
panic!("Unexpected path: {}", path.as_ref().display());
if path.as_str() != "a" && path.as_str() != "a/b" {
panic!("Unexpected path: {}", path.as_str());
}
assert!(visited_paths.insert(path.clone()));
ControlFlow::Continue(())
@@ -8,7 +8,6 @@
use std::{
collections::{BTreeMap, BTreeSet},
path::Path,
sync::{Arc, Weak},
};
@@ -21,6 +20,7 @@ use language::{
use lsp::LanguageServerName;
use settings::{Settings, SettingsLocation, WorktreeId};
use std::sync::OnceLock;
use util::rel_path::RelPath;
use crate::{
LanguageServerId, ProjectPath, project_settings::LspSettings,
@@ -32,7 +32,7 @@ use super::ManifestTree;
#[derive(Clone, Debug, Default)]
pub(crate) struct ServersForWorktree {
pub(crate) roots: BTreeMap<
Arc<Path>,
Arc<RelPath>,
BTreeMap<LanguageServerName, (Arc<InnerTreeNode>, BTreeSet<LanguageName>)>,
>,
}
@@ -338,7 +338,7 @@ impl LanguageServerTree {
.entry(worktree_id)
.or_default()
.roots
.entry(Arc::from(Path::new("")))
.entry(RelPath::empty().into())
.or_default()
.entry(node.disposition.server_name.clone())
.or_insert_with(|| (node, BTreeSet::new()))
+4 -4
View File
@@ -23,7 +23,7 @@ use node_runtime::NodeRuntime;
use paths::default_prettier_dir;
use prettier::Prettier;
use smol::stream::StreamExt;
use util::{ResultExt, TryFutureExt};
use util::{ResultExt, TryFutureExt, rel_path::RelPath};
use crate::{
File, PathChange, ProjectEntryId, Worktree, lsp_store::WorktreeId,
@@ -442,12 +442,12 @@ impl PrettierStore {
pub fn update_prettier_settings(
&self,
worktree: &Entity<Worktree>,
changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
changes: &[(Arc<RelPath>, ProjectEntryId, PathChange)],
cx: &mut Context<Self>,
) {
let prettier_config_files = Prettier::CONFIG_FILE_NAMES
.iter()
.map(Path::new)
.map(|name| RelPath::new(name).unwrap())
.collect::<HashSet<_>>();
let prettier_config_file_changed = changes
@@ -456,7 +456,7 @@ impl PrettierStore {
.filter(|(path, _, _)| {
!path
.components()
.any(|component| component.as_os_str().to_string_lossy() == "node_modules")
.any(|component| component == "node_modules")
})
.find(|(path, _, _)| prettier_config_files.contains(path.as_ref()));
let current_worktree_id = worktree.read(cx).id();
+160 -166
View File
@@ -37,7 +37,7 @@ use dap::inline_value::{InlineValueLocation, VariableLookupKind, VariableScope};
use crate::{
agent_server_store::{AgentServerStore, AllAgentServersSettings},
git_store::GitStore,
lsp_store::log_store::LogKind,
lsp_store::{SymbolLocation, log_store::LogKind},
};
pub use git_store::{
ConflictRegion, ConflictSet, ConflictSetSnapshot, ConflictSetUpdate,
@@ -96,7 +96,7 @@ use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent}
use remote::{RemoteClient, RemoteConnectionOptions};
use rpc::{
AnyProtoClient, ErrorCode,
proto::{FromProto, LanguageServerPromptResponse, REMOTE_SERVER_PROJECT_ID, ToProto},
proto::{LanguageServerPromptResponse, REMOTE_SERVER_PROJECT_ID},
};
use search::{SearchInputKind, SearchQuery, SearchResult};
use search_history::SearchHistory;
@@ -108,7 +108,7 @@ use std::{
borrow::Cow,
collections::BTreeMap,
ops::Range,
path::{Component, Path, PathBuf},
path::{Path, PathBuf},
pin::pin,
str,
sync::Arc,
@@ -121,7 +121,8 @@ use text::{Anchor, BufferId, OffsetRangeExt, Point, Rope};
use toolchain_store::EmptyToolchainStore;
use util::{
ResultExt as _, maybe,
paths::{PathStyle, RemotePathBuf, SanitizedPath, compare_paths},
paths::{PathStyle, SanitizedPath, compare_paths, is_absolute},
rel_path::RelPath,
};
use worktree::{CreatedEntry, Snapshot, Traversal};
pub use worktree::{
@@ -353,7 +354,7 @@ pub enum DebugAdapterClientState {
#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
pub struct ProjectPath {
pub worktree_id: WorktreeId,
pub path: Arc<Path>,
pub path: Arc<RelPath>,
}
impl ProjectPath {
@@ -364,11 +365,11 @@ impl ProjectPath {
}
}
pub fn from_proto(p: proto::ProjectPath) -> Self {
Self {
pub fn from_proto(p: proto::ProjectPath) -> Option<Self> {
Some(Self {
worktree_id: WorktreeId::from_proto(p.worktree_id),
path: Arc::<Path>::from_proto(p.path),
}
path: RelPath::from_proto(&p.path).log_err()?,
})
}
pub fn to_proto(&self) -> proto::ProjectPath {
@@ -381,7 +382,7 @@ impl ProjectPath {
pub fn root_path(worktree_id: WorktreeId) -> Self {
Self {
worktree_id,
path: Path::new("").into(),
path: RelPath::empty().into(),
}
}
@@ -743,12 +744,11 @@ pub struct Symbol {
pub language_server_name: LanguageServerName,
pub source_worktree_id: WorktreeId,
pub source_language_server_id: LanguageServerId,
pub path: ProjectPath,
pub path: SymbolLocation,
pub label: CodeLabel,
pub name: String,
pub kind: lsp::SymbolKind,
pub range: Range<Unclipped<PointUtf16>>,
pub signature: [u8; 32],
}
#[derive(Clone, Debug)]
@@ -882,28 +882,29 @@ impl DirectoryLister {
}
pub fn default_query(&self, cx: &mut App) -> String {
let separator = std::path::MAIN_SEPARATOR_STR;
match self {
let project = match self {
DirectoryLister::Project(project) => project,
DirectoryLister::Local(project, _) => project,
}
.read(cx)
.visible_worktrees(cx)
.next()
.map(|worktree| worktree.read(cx).abs_path())
.map(|dir| dir.to_string_lossy().to_string())
.or_else(|| std::env::home_dir().map(|dir| dir.to_string_lossy().to_string()))
.map(|mut s| {
s.push_str(separator);
s
})
.unwrap_or_else(|| {
if cfg!(target_os = "windows") {
format!("C:{separator}")
} else {
format!("~{separator}")
}
})
.read(cx);
let path_style = project.path_style(cx);
project
.visible_worktrees(cx)
.next()
.map(|worktree| worktree.read(cx).abs_path().to_string_lossy().to_string())
.or_else(|| std::env::home_dir().map(|dir| dir.to_string_lossy().to_string()))
.map(|mut s| {
s.push_str(path_style.separator());
s
})
.unwrap_or_else(|| {
if path_style.is_windows() {
"C:\\"
} else {
"~/"
}
.to_string()
})
}
pub fn list_directory(&self, path: String, cx: &mut App) -> Task<Result<Vec<DirectoryItem>>> {
@@ -1469,14 +1470,18 @@ impl Project {
let remote_id = response.payload.project_id;
let role = response.payload.role();
// todo(zjk)
// Set the proper path style based on the remote
let path_style = if response.payload.windows_paths {
PathStyle::Windows
} else {
PathStyle::Posix
};
let worktree_store = cx.new(|_| {
WorktreeStore::remote(
true,
client.clone().into(),
response.payload.project_id,
PathStyle::Posix,
path_style,
)
})?;
let buffer_store = cx.new(|cx| {
@@ -1548,10 +1553,9 @@ impl Project {
})?;
let agent_server_store = cx.new(|cx| AgentServerStore::collab(cx))?;
let replica_id = response.payload.replica_id as ReplicaId;
let project = cx.new(|cx| {
let replica_id = response.payload.replica_id as ReplicaId;
let snippets = SnippetProvider::new(fs.clone(), BTreeSet::from_iter([]), cx);
let weak_self = cx.weak_entity();
@@ -1560,8 +1564,14 @@ impl Project {
let mut worktrees = Vec::new();
for worktree in response.payload.worktrees {
let worktree =
Worktree::remote(remote_id, replica_id, worktree, client.clone().into(), cx);
let worktree = Worktree::remote(
remote_id,
replica_id,
worktree,
client.clone().into(),
path_style,
cx,
);
worktrees.push(worktree);
}
@@ -2022,7 +2032,7 @@ impl Project {
pub fn worktree_root_names<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = &'a str> {
self.visible_worktrees(cx)
.map(|tree| tree.read(cx).root_name())
.map(|tree| tree.read(cx).root_name().as_str())
}
pub fn worktree_for_id(&self, id: WorktreeId, cx: &App) -> Option<Entity<Worktree>> {
@@ -2120,15 +2130,11 @@ impl Project {
pub fn copy_entry(
&mut self,
entry_id: ProjectEntryId,
relative_worktree_source_path: Option<PathBuf>,
new_path: impl Into<Arc<Path>>,
new_project_path: ProjectPath,
cx: &mut Context<Self>,
) -> Task<Result<Option<Entry>>> {
let Some(worktree) = self.worktree_for_entry(entry_id, cx) else {
return Task::ready(Ok(None));
};
worktree.update(cx, |worktree, cx| {
worktree.copy_entry(entry_id, relative_worktree_source_path, new_path, cx)
self.worktree_store.update(cx, |worktree_store, cx| {
worktree_store.copy_entry(entry_id, new_project_path, cx)
})
}
@@ -2139,12 +2145,12 @@ impl Project {
pub fn rename_entry(
&mut self,
entry_id: ProjectEntryId,
new_path: impl Into<Arc<Path>>,
new_path: ProjectPath,
cx: &mut Context<Self>,
) -> Task<Result<CreatedEntry>> {
let worktree_store = self.worktree_store.read(cx);
let new_path = new_path.into();
let worktree_store = self.worktree_store.clone();
let Some((worktree, old_path, is_dir)) = worktree_store
.read(cx)
.worktree_and_entry_for_id(entry_id, cx)
.map(|(worktree, entry)| (worktree, entry.path.clone(), entry.is_dir()))
else {
@@ -2159,11 +2165,14 @@ impl Project {
let (old_abs_path, new_abs_path) = {
let root_path = worktree.read_with(cx, |this, _| this.abs_path())?;
let new_abs_path = if is_root_entry {
root_path.parent().unwrap().join(&new_path)
root_path
.parent()
.unwrap()
.join(new_path.path.as_std_path())
} else {
root_path.join(&new_path)
root_path.join(&new_path.path.as_std_path())
};
(root_path.join(&old_path), new_abs_path)
(root_path.join(old_path.as_std_path()), new_abs_path)
};
let transaction = LspStore::will_rename_entry(
lsp_store.clone(),
@@ -2175,9 +2184,9 @@ impl Project {
)
.await;
let entry = worktree
.update(cx, |worktree, cx| {
worktree.rename_entry(entry_id, new_path.clone(), cx)
let entry = worktree_store
.update(cx, |worktree_store, cx| {
worktree_store.rename_entry(entry_id, new_path.clone(), cx)
})?
.await?;
@@ -4012,7 +4021,7 @@ impl Project {
.filter(|buffer| {
let b = buffer.read(cx);
if let Some(file) = b.file() {
if !search_query.match_path(file.path()) {
if !search_query.match_path(file.path().as_std_path()) {
return false;
}
if let Some(entry) = b
@@ -4032,7 +4041,10 @@ impl Project {
(None, None) => a.read(cx).remote_id().cmp(&b.read(cx).remote_id()),
(None, Some(_)) => std::cmp::Ordering::Less,
(Some(_), None) => std::cmp::Ordering::Greater,
(Some(a), Some(b)) => compare_paths((a.path(), true), (b.path(), true)),
(Some(a), Some(b)) => compare_paths(
(a.path().as_std_path(), true),
(b.path().as_std_path(), true),
),
});
for buffer in buffers {
tx.send_blocking(buffer.clone()).unwrap()
@@ -4139,13 +4151,17 @@ impl Project {
abs_path: impl AsRef<Path>,
visible: bool,
cx: &mut Context<Self>,
) -> Task<Result<(Entity<Worktree>, PathBuf)>> {
) -> Task<Result<(Entity<Worktree>, Arc<RelPath>)>> {
self.worktree_store.update(cx, |worktree_store, cx| {
worktree_store.find_or_create_worktree(abs_path, visible, cx)
})
}
pub fn find_worktree(&self, abs_path: &Path, cx: &App) -> Option<(Entity<Worktree>, PathBuf)> {
pub fn find_worktree(
&self,
abs_path: &Path,
cx: &App,
) -> Option<(Entity<Worktree>, Arc<RelPath>)> {
self.worktree_store.read(cx).find_worktree(abs_path, cx)
}
@@ -4164,11 +4180,10 @@ impl Project {
buffer: &Entity<Buffer>,
cx: &mut Context<Self>,
) -> Task<Option<ResolvedPath>> {
let path_buf = PathBuf::from(path);
if path_buf.is_absolute() || path.starts_with("~") {
if util::paths::is_absolute(path, self.path_style(cx)) || path.starts_with("~") {
self.resolve_abs_path(path, cx)
} else {
self.resolve_path_in_worktrees(path_buf, buffer, cx)
self.resolve_path_in_worktrees(path, buffer, cx)
}
}
@@ -4189,29 +4204,26 @@ impl Project {
let expanded = PathBuf::from(shellexpand::tilde(&path).into_owned());
let fs = self.fs.clone();
cx.background_spawn(async move {
let path = expanded.as_path();
let metadata = fs.metadata(path).await.ok().flatten();
let metadata = fs.metadata(&expanded).await.ok().flatten();
metadata.map(|metadata| ResolvedPath::AbsPath {
path: expanded,
path: expanded.to_string_lossy().to_string(),
is_dir: metadata.is_dir,
})
})
} else if let Some(ssh_client) = self.remote_client.as_ref() {
let path_style = ssh_client.read(cx).path_style();
let request_path = RemotePathBuf::from_str(path, path_style);
let request = ssh_client
.read(cx)
.proto_client()
.request(proto::GetPathMetadata {
project_id: REMOTE_SERVER_PROJECT_ID,
path: request_path.to_proto(),
path: path.into(),
});
cx.background_spawn(async move {
let response = request.await.log_err()?;
if response.exists {
Some(ResolvedPath::AbsPath {
path: PathBuf::from_proto(response.path),
path: response.path,
is_dir: response.is_dir,
})
} else {
@@ -4225,17 +4237,26 @@ impl Project {
fn resolve_path_in_worktrees(
&self,
path: PathBuf,
path: &str,
buffer: &Entity<Buffer>,
cx: &mut Context<Self>,
) -> Task<Option<ResolvedPath>> {
let mut candidates = vec![path.clone()];
let mut candidates = vec![];
if let Ok(path) = RelPath::from_std_path(Path::new(path), self.path_style(cx)) {
candidates.push(path);
}
if let Some(file) = buffer.read(cx).file()
&& let Some(dir) = file.path().parent()
{
let joined = dir.to_path_buf().join(path);
candidates.push(joined);
if let Some(joined) = self
.path_style(cx)
.join(&*dir.display(self.path_style(cx)), path)
&& let Some(joined) =
RelPath::from_std_path(Path::new(&joined), self.path_style(cx)).ok()
{
candidates.push(joined);
}
}
let buffer_worktree_id = buffer.read(cx).file().map(|file| file.worktree_id(cx));
@@ -4275,15 +4296,12 @@ impl Project {
fn resolve_path_in_worktree(
worktree: &Entity<Worktree>,
path: &PathBuf,
path: &RelPath,
cx: &mut AsyncApp,
) -> Option<ResolvedPath> {
worktree
.read_with(cx, |worktree, _| {
let root_entry_path = &worktree.root_entry()?.path;
let resolved = resolve_path(root_entry_path, path);
let stripped = resolved.strip_prefix(root_entry_path).unwrap_or(&resolved);
worktree.entry_for_path(stripped).map(|entry| {
worktree.entry_for_path(path).map(|entry| {
let project_path = ProjectPath {
worktree_id: worktree.id(),
path: entry.path.clone(),
@@ -4305,10 +4323,9 @@ impl Project {
if self.is_local() {
DirectoryLister::Local(cx.entity(), self.fs.clone()).list_directory(query, cx)
} else if let Some(session) = self.remote_client.as_ref() {
let path_buf = PathBuf::from(query);
let request = proto::ListRemoteDirectory {
dev_server_id: REMOTE_SERVER_PROJECT_ID,
path: path_buf.to_proto(),
path: query,
config: Some(proto::ListRemoteDirectoryConfig { is_dir: true }),
};
@@ -4358,7 +4375,7 @@ impl Project {
pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut Context<Self>) {
let new_active_entry = entry.and_then(|project_path| {
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
let entry = worktree.read(cx).entry_for_path(project_path.path)?;
let entry = worktree.read(cx).entry_for_path(&project_path.path)?;
Some(entry.id)
});
if new_active_entry != self.active_entry {
@@ -4419,10 +4436,11 @@ impl Project {
}
pub fn absolute_path(&self, project_path: &ProjectPath, cx: &App) -> Option<PathBuf> {
self.worktree_for_id(project_path.worktree_id, cx)?
.read(cx)
.absolutize(&project_path.path)
.ok()
Some(
self.worktree_for_id(project_path.worktree_id, cx)?
.read(cx)
.absolutize(&project_path.path),
)
}
/// Attempts to find a `ProjectPath` corresponding to the given path. If the path
@@ -4435,7 +4453,7 @@ impl Project {
///
/// # Arguments
///
/// * `path` - A full path that starts with a worktree root name, or alternatively a
/// * `path` - An absolute path, or a full path that starts with a worktree root name, or a
/// relative path within a visible worktree.
/// * `cx` - A reference to the `AppContext`.
///
@@ -4443,34 +4461,41 @@ impl Project {
///
/// Returns `Some(ProjectPath)` if a matching worktree is found, otherwise `None`.
pub fn find_project_path(&self, path: impl AsRef<Path>, cx: &App) -> Option<ProjectPath> {
let path_style = self.path_style(cx);
let path = path.as_ref();
let worktree_store = self.worktree_store.read(cx);
if path.is_absolute() {
if is_absolute(&path.to_string_lossy(), path_style) {
for worktree in worktree_store.visible_worktrees(cx) {
let worktree_abs_path = worktree.read(cx).abs_path();
if let Ok(relative_path) = path.strip_prefix(worktree_abs_path) {
if let Ok(relative_path) = path.strip_prefix(worktree_abs_path)
&& let Ok(path) = RelPath::from_std_path(relative_path, path_style)
{
return Some(ProjectPath {
worktree_id: worktree.read(cx).id(),
path: relative_path.into(),
path,
});
}
}
} else {
for worktree in worktree_store.visible_worktrees(cx) {
let worktree_root_name = worktree.read(cx).root_name();
if let Ok(relative_path) = path.strip_prefix(worktree_root_name) {
if let Ok(relative_path) = path.strip_prefix(worktree_root_name)
&& let Ok(path) = RelPath::from_std_path(relative_path, path_style)
{
return Some(ProjectPath {
worktree_id: worktree.read(cx).id(),
path: relative_path.into(),
path,
});
}
}
for worktree in worktree_store.visible_worktrees(cx) {
let worktree = worktree.read(cx);
if let Some(entry) = worktree.entry_for_path(path) {
if let Ok(path) = RelPath::from_std_path(path, path_style)
&& let Some(entry) = worktree.entry_for_path(&path)
{
return Some(ProjectPath {
worktree_id: worktree.id(),
path: entry.path.clone(),
@@ -4489,13 +4514,18 @@ impl Project {
&self,
project_path: &ProjectPath,
cx: &App,
) -> Option<PathBuf> {
) -> Option<String> {
let path_style = self.path_style(cx);
if self.visible_worktrees(cx).take(2).count() < 2 {
return Some(project_path.path.to_path_buf());
return Some(project_path.path.display(path_style).to_string());
}
self.worktree_for_id(project_path.worktree_id, cx)
.and_then(|worktree| {
Some(Path::new(worktree.read(cx).abs_path().file_name()?).join(&project_path.path))
.map(|worktree| {
let worktree_name = worktree.read(cx).root_name();
worktree_name
.join(&project_path.path)
.display(path_style)
.to_string()
})
}
@@ -4503,7 +4533,7 @@ impl Project {
self.find_worktree(abs_path, cx)
.map(|(worktree, relative_path)| ProjectPath {
worktree_id: worktree.read(cx).id(),
path: relative_path.into(),
path: relative_path,
})
}
@@ -4869,7 +4899,9 @@ impl Project {
) -> Result<proto::FindSearchCandidatesResponse> {
let peer_id = envelope.original_sender_id()?;
let message = envelope.payload;
let query = SearchQuery::from_proto(message.query.context("missing query field")?)?;
let path_style = this.read_with(&cx, |this, cx| this.path_style(cx))?;
let query =
SearchQuery::from_proto(message.query.context("missing query field")?, path_style)?;
let results = this.update(&mut cx, |this, cx| {
this.find_search_candidate_buffers(&query, message.limit as _, cx)
})?;
@@ -4908,18 +4940,13 @@ impl Project {
) -> Result<proto::OpenBufferResponse> {
let peer_id = envelope.original_sender_id()?;
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let open_buffer = this.update(&mut cx, |this, cx| {
this.open_buffer(
ProjectPath {
worktree_id,
path: Arc::<Path>::from_proto(envelope.payload.path),
},
cx,
)
})?;
let buffer = open_buffer.await?;
Project::respond_to_open_buffer_request(this, buffer, peer_id, &mut cx)
let path = RelPath::from_proto(&envelope.payload.path)?;
let open_buffer = this
.update(&mut cx, |this, cx| {
this.open_buffer(ProjectPath { worktree_id, path }, cx)
})?
.await?;
Project::respond_to_open_buffer_request(this, open_buffer, peer_id, &mut cx)
}
async fn handle_open_new_buffer(
@@ -5263,6 +5290,10 @@ impl Project {
pub fn agent_location(&self) -> Option<AgentLocation> {
self.agent_location.clone()
}
pub fn path_style(&self, cx: &App) -> PathStyle {
self.worktree_store.read(cx).path_style()
}
}
pub struct PathMatchCandidateSet {
@@ -5316,16 +5347,22 @@ impl<'a> fuzzy::PathMatchCandidateSet<'a> for PathMatchCandidateSet {
}
}
fn prefix(&self) -> Arc<str> {
if self.snapshot.root_entry().is_some_and(|e| e.is_file()) {
fn prefix(&self) -> Arc<RelPath> {
if self.snapshot.root_entry().is_some_and(|e| e.is_file()) || self.include_root_name {
self.snapshot.root_name().into()
} else if self.include_root_name {
format!("{}{}", self.snapshot.root_name(), std::path::MAIN_SEPARATOR).into()
} else {
Arc::default()
RelPath::empty().into()
}
}
fn root_is_file(&self) -> bool {
self.snapshot.root_entry().is_some_and(|f| f.is_file())
}
fn path_style(&self) -> PathStyle {
self.snapshot.path_style()
}
fn candidates(&'a self, start: usize) -> Self::Candidates {
PathMatchCandidateSetIter {
traversal: match self.candidates {
@@ -5366,58 +5403,15 @@ impl<'a> From<&'a ProjectPath> for SettingsLocation<'a> {
}
}
impl<P: AsRef<Path>> From<(WorktreeId, P)> for ProjectPath {
impl<P: Into<Arc<RelPath>>> From<(WorktreeId, P)> for ProjectPath {
fn from((worktree_id, path): (WorktreeId, P)) -> Self {
Self {
worktree_id,
path: path.as_ref().into(),
path: path.into(),
}
}
}
pub fn relativize_path(base: &Path, path: &Path) -> PathBuf {
let mut path_components = path.components();
let mut base_components = base.components();
let mut components: Vec<Component> = Vec::new();
loop {
match (path_components.next(), base_components.next()) {
(None, None) => break,
(Some(a), None) => {
components.push(a);
components.extend(path_components.by_ref());
break;
}
(None, _) => components.push(Component::ParentDir),
(Some(a), Some(b)) if components.is_empty() && a == b => (),
(Some(a), Some(Component::CurDir)) => components.push(a),
(Some(a), Some(_)) => {
components.push(Component::ParentDir);
for _ in base_components {
components.push(Component::ParentDir);
}
components.push(a);
components.extend(path_components.by_ref());
break;
}
}
}
components.iter().map(|c| c.as_os_str()).collect()
}
fn resolve_path(base: &Path, path: &Path) -> PathBuf {
let mut result = base.to_path_buf();
for component in path.components() {
match component {
Component::ParentDir => {
result.pop();
}
Component::CurDir => (),
_ => result.push(component),
}
}
result
}
/// ResolvedPath is a path that has been resolved to either a ProjectPath
/// or an AbsPath and that *exists*.
#[derive(Debug, Clone)]
@@ -5427,20 +5421,20 @@ pub enum ResolvedPath {
is_dir: bool,
},
AbsPath {
path: PathBuf,
path: String,
is_dir: bool,
},
}
impl ResolvedPath {
pub fn abs_path(&self) -> Option<&Path> {
pub fn abs_path(&self) -> Option<&str> {
match self {
Self::AbsPath { path, .. } => Some(path.as_path()),
Self::AbsPath { path, .. } => Some(path),
_ => None,
}
}
pub fn into_abs_path(self) -> Option<PathBuf> {
pub fn into_abs_path(self) -> Option<String> {
match self {
Self::AbsPath { path, .. } => Some(path),
_ => None,
@@ -5550,8 +5544,8 @@ pub fn sort_worktree_entries(entries: &mut [impl AsRef<Entry>]) {
let entry_a = entry_a.as_ref();
let entry_b = entry_b.as_ref();
compare_paths(
(&entry_a.path, entry_a.is_file()),
(&entry_b.path, entry_b.is_file()),
(entry_a.path.as_std_path(), entry_a.is_file()),
(entry_b.path.as_std_path(), entry_b.is_file()),
)
});
}
+63 -72
View File
@@ -13,7 +13,7 @@ use paths::{
};
use rpc::{
AnyProtoClient, TypedEnvelope,
proto::{self, FromProto, REMOTE_SERVER_PROJECT_ID, ToProto},
proto::{self, REMOTE_SERVER_PROJECT_ID},
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -23,13 +23,9 @@ use settings::{
DapSettingsContent, InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation,
SettingsStore, parse_json_with_comments, watch_config_file,
};
use std::{
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use std::{path::PathBuf, sync::Arc, time::Duration};
use task::{DebugTaskFile, TaskTemplates, VsCodeDebugTaskFile, VsCodeTaskFile};
use util::{ResultExt, serde::default_true};
use util::{ResultExt, rel_path::RelPath, serde::default_true};
use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
use crate::{
@@ -742,6 +738,7 @@ impl SettingsObserver {
.with_context(|| format!("unknown kind {kind}"))?,
None => proto::LocalSettingsKind::Settings,
};
let path = RelPath::from_proto(&envelope.payload.path)?;
this.update(&mut cx, |this, cx| {
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let Some(worktree) = this
@@ -755,7 +752,7 @@ impl SettingsObserver {
this.update_settings(
worktree,
[(
Arc::<Path>::from_proto(envelope.payload.path.clone()),
path,
local_settings_kind_from_proto(kind),
envelope.payload.content,
)],
@@ -808,61 +805,61 @@ impl SettingsObserver {
let mut settings_contents = Vec::new();
for (path, _, change) in changes.iter() {
let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
let settings_dir = Arc::<Path>::from(
path.ancestors()
.nth(local_settings_file_relative_path().components().count())
.unwrap(),
);
let settings_dir = path
.ancestors()
.nth(local_settings_file_relative_path().components().count())
.unwrap()
.into();
(settings_dir, LocalSettingsKind::Settings)
} else if path.ends_with(local_tasks_file_relative_path()) {
let settings_dir = Arc::<Path>::from(
path.ancestors()
.nth(
local_tasks_file_relative_path()
.components()
.count()
.saturating_sub(1),
)
.unwrap(),
);
let settings_dir = path
.ancestors()
.nth(
local_tasks_file_relative_path()
.components()
.count()
.saturating_sub(1),
)
.unwrap()
.into();
(settings_dir, LocalSettingsKind::Tasks)
} else if path.ends_with(local_vscode_tasks_file_relative_path()) {
let settings_dir = Arc::<Path>::from(
path.ancestors()
.nth(
local_vscode_tasks_file_relative_path()
.components()
.count()
.saturating_sub(1),
)
.unwrap(),
);
let settings_dir = path
.ancestors()
.nth(
local_vscode_tasks_file_relative_path()
.components()
.count()
.saturating_sub(1),
)
.unwrap()
.into();
(settings_dir, LocalSettingsKind::Tasks)
} else if path.ends_with(local_debug_file_relative_path()) {
let settings_dir = Arc::<Path>::from(
path.ancestors()
.nth(
local_debug_file_relative_path()
.components()
.count()
.saturating_sub(1),
)
.unwrap(),
);
let settings_dir = path
.ancestors()
.nth(
local_debug_file_relative_path()
.components()
.count()
.saturating_sub(1),
)
.unwrap()
.into();
(settings_dir, LocalSettingsKind::Debug)
} else if path.ends_with(local_vscode_launch_file_relative_path()) {
let settings_dir = Arc::<Path>::from(
path.ancestors()
.nth(
local_vscode_tasks_file_relative_path()
.components()
.count()
.saturating_sub(1),
)
.unwrap(),
);
let settings_dir = path
.ancestors()
.nth(
local_vscode_tasks_file_relative_path()
.components()
.count()
.saturating_sub(1),
)
.unwrap()
.into();
(settings_dir, LocalSettingsKind::Debug)
} else if path.ends_with(EDITORCONFIG_NAME) {
} else if path.ends_with(RelPath::new(EDITORCONFIG_NAME).unwrap()) {
let Some(settings_dir) = path.parent().map(Arc::from) else {
continue;
};
@@ -873,13 +870,7 @@ impl SettingsObserver {
let removed = change == &PathChange::Removed;
let fs = fs.clone();
let abs_path = match worktree.read(cx).absolutize(path) {
Ok(abs_path) => abs_path,
Err(e) => {
log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
continue;
}
};
let abs_path = worktree.read(cx).absolutize(path);
settings_contents.push(async move {
(
settings_dir,
@@ -941,7 +932,7 @@ impl SettingsObserver {
let worktree = worktree.clone();
cx.spawn(async move |this, cx| {
let settings_contents: Vec<(Arc<Path>, _, _)> =
let settings_contents: Vec<(Arc<RelPath>, _, _)> =
futures::future::join_all(settings_contents).await;
cx.update(|cx| {
this.update(cx, |this, cx| {
@@ -961,7 +952,7 @@ impl SettingsObserver {
fn update_settings(
&mut self,
worktree: Entity<Worktree>,
settings_contents: impl IntoIterator<Item = (Arc<Path>, LocalSettingsKind, Option<String>)>,
settings_contents: impl IntoIterator<Item = (Arc<RelPath>, LocalSettingsKind, Option<String>)>,
cx: &mut Context<Self>,
) {
let worktree_id = worktree.read(cx).id();
@@ -991,9 +982,9 @@ impl SettingsObserver {
log::error!("Failed to set local settings: {e}");
}
Ok(()) => {
cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(
directory.join(local_settings_file_relative_path())
)));
cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(directory
.as_std_path()
.join(local_settings_file_relative_path()))));
}
}
}),
@@ -1020,9 +1011,9 @@ impl SettingsObserver {
log::error!("Failed to set local tasks: {e}");
}
Ok(()) => {
cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
directory.join(task_file_name())
)));
cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(directory
.as_std_path()
.join(RelPath::new(task_file_name()).unwrap()))));
}
}
}
@@ -1051,9 +1042,9 @@ impl SettingsObserver {
log::error!("Failed to set local tasks: {e}");
}
Ok(()) => {
cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
directory.join(task_file_name())
)));
cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(directory
.as_std_path()
.join(RelPath::new(task_file_name()).unwrap()))));
}
}
}
File diff suppressed because it is too large Load Diff
+12 -11
View File
@@ -13,7 +13,7 @@ use std::{
sync::{Arc, LazyLock},
};
use text::Anchor;
use util::paths::PathMatcher;
use util::paths::{PathMatcher, PathStyle};
#[derive(Debug)]
pub enum SearchResult {
@@ -238,7 +238,7 @@ impl SearchQuery {
is_case_sensitive.map(|c| (c, new_query))
}
pub fn from_proto(message: proto::SearchQuery) -> Result<Self> {
pub fn from_proto(message: proto::SearchQuery, path_style: PathStyle) -> Result<Self> {
let files_to_include = if message.files_to_include.is_empty() {
message
.files_to_include_legacy
@@ -270,8 +270,8 @@ impl SearchQuery {
message.case_sensitive,
message.include_ignored,
false,
PathMatcher::new(files_to_include)?,
PathMatcher::new(files_to_exclude)?,
PathMatcher::new(files_to_include, path_style)?,
PathMatcher::new(files_to_exclude, path_style)?,
message.match_full_paths,
None, // search opened only don't need search remote
)
@@ -281,8 +281,8 @@ impl SearchQuery {
message.whole_word,
message.case_sensitive,
message.include_ignored,
PathMatcher::new(files_to_include)?,
PathMatcher::new(files_to_exclude)?,
PathMatcher::new(files_to_include, path_style)?,
PathMatcher::new(files_to_exclude, path_style)?,
false,
None, // search opened only don't need search remote
)
@@ -610,9 +610,10 @@ mod tests {
"dir/[a-z].txt",
"../dir/filé",
] {
let path_matcher = PathMatcher::new(&[valid_path.to_owned()]).unwrap_or_else(|e| {
panic!("Valid path {valid_path} should be accepted, but got: {e}")
});
let path_matcher = PathMatcher::new(&[valid_path.to_owned()], PathStyle::local())
.unwrap_or_else(|e| {
panic!("Valid path {valid_path} should be accepted, but got: {e}")
});
assert!(
path_matcher.is_match(valid_path),
"Path matcher for valid path {valid_path} should match itself"
@@ -623,7 +624,7 @@ mod tests {
#[test]
fn path_matcher_creation_for_globs() {
for invalid_glob in ["dir/[].txt", "dir/[a-z.txt", "dir/{file"] {
match PathMatcher::new(&[invalid_glob.to_owned()]) {
match PathMatcher::new(&[invalid_glob.to_owned()], PathStyle::local()) {
Ok(_) => panic!("Invalid glob {invalid_glob} should not be accepted"),
Err(_expected) => {}
}
@@ -636,7 +637,7 @@ mod tests {
"dir/[a-z].txt",
"{dir,file}",
] {
match PathMatcher::new(&[valid_glob.to_owned()]) {
match PathMatcher::new(&[valid_glob.to_owned()], PathStyle::local()) {
Ok(_expected) => {}
Err(e) => panic!("Valid glob should be accepted, but got: {e}"),
}
+53 -52
View File
@@ -4,7 +4,7 @@ use std::{
borrow::Cow,
cmp::{self, Reverse},
collections::hash_map,
path::{Path, PathBuf},
path::PathBuf,
sync::Arc,
};
@@ -25,7 +25,7 @@ use task::{
VariableName,
};
use text::{BufferId, Point, ToPoint};
use util::{NumericPrefixWithSuffix, ResultExt as _, paths::PathExt as _, post_inc};
use util::{NumericPrefixWithSuffix, ResultExt as _, post_inc, rel_path::RelPath};
use worktree::WorktreeId;
use crate::{task_store::TaskSettingsLocation, worktree_store::WorktreeStore};
@@ -76,7 +76,7 @@ impl InventoryContents for DebugScenario {
#[derive(Debug)]
struct InventoryFor<T> {
global: HashMap<PathBuf, Vec<T>>,
worktree: HashMap<WorktreeId, HashMap<Arc<Path>, Vec<T>>>,
worktree: HashMap<WorktreeId, HashMap<Arc<RelPath>, Vec<T>>>,
}
impl<T: InventoryContents> InventoryFor<T> {
@@ -95,7 +95,7 @@ impl<T: InventoryContents> InventoryFor<T> {
(
TaskSourceKind::Worktree {
id: worktree,
directory_in_worktree: directory.to_path_buf(),
directory_in_worktree: directory.clone(),
id_base: Cow::Owned(format!(
"local worktree {} from directory {directory:?}",
T::LABEL
@@ -138,7 +138,7 @@ pub enum TaskSourceKind {
/// Tasks from the worktree's .zed/task.json
Worktree {
id: WorktreeId,
directory_in_worktree: PathBuf,
directory_in_worktree: Arc<RelPath>,
id_base: Cow<'static, str>,
},
/// ~/.config/zed/task.json - like global files with task definitions, applicable to any path
@@ -228,7 +228,7 @@ impl TaskSourceKind {
id_base,
directory_in_worktree,
} => {
format!("{id_base}_{id}_{}", directory_in_worktree.display())
format!("{id_base}_{id}_{}", directory_in_worktree.as_str())
}
Self::Language { name } => format!("language_{name}"),
Self::Lsp {
@@ -653,7 +653,7 @@ impl Inventory {
path: match location {
TaskSettingsLocation::Global(path) => path.to_owned(),
TaskSettingsLocation::Worktree(settings_location) => {
settings_location.path.join(task_file_name())
settings_location.path.as_std_path().join(task_file_name())
}
},
message: format!("Failed to parse tasks file content as a JSON array: {e}"),
@@ -701,7 +701,8 @@ impl Inventory {
..
} = kind
{
*id != location.worktree_id || directory_in_worktree != location.path
*id != location.worktree_id
|| directory_in_worktree.as_ref() != location.path
} else {
true
}
@@ -729,9 +730,10 @@ impl Inventory {
return Err(InvalidSettingsError::Debug {
path: match location {
TaskSettingsLocation::Global(path) => path.to_owned(),
TaskSettingsLocation::Worktree(settings_location) => {
settings_location.path.join(debug_task_file_name())
}
TaskSettingsLocation::Worktree(settings_location) => settings_location
.path
.as_std_path()
.join(debug_task_file_name()),
},
message: format!("Failed to parse tasks file content as a JSON array: {e}"),
});
@@ -969,6 +971,7 @@ impl BasicContextProvider {
Self { worktree_store }
}
}
impl ContextProvider for BasicContextProvider {
fn build_context(
&self,
@@ -991,10 +994,7 @@ impl ContextProvider for BasicContextProvider {
symbol.text[range].to_string()
});
let current_file = buffer
.file()
.and_then(|file| file.as_local())
.map(|file| file.abs_path(cx).to_sanitized_string());
let current_file = buffer.file().and_then(|file| file.as_local());
let Point { row, column } = location.range.start.to_point(&buffer_snapshot);
let row = row + 1;
let column = column + 1;
@@ -1013,44 +1013,43 @@ impl ContextProvider for BasicContextProvider {
if !selected_text.trim().is_empty() {
task_variables.insert(VariableName::SelectedText, selected_text);
}
let worktree_root_dir =
buffer
.file()
.map(|file| file.worktree_id(cx))
.and_then(|worktree_id| {
self.worktree_store
.read(cx)
.worktree_for_id(worktree_id, cx)
.and_then(|worktree| worktree.read(cx).root_dir())
});
if let Some(worktree_path) = worktree_root_dir {
let worktree = buffer
.file()
.map(|file| file.worktree_id(cx))
.and_then(|worktree_id| {
self.worktree_store
.read(cx)
.worktree_for_id(worktree_id, cx)
});
if let Some(worktree) = worktree {
let worktree = worktree.read(cx);
let path_style = worktree.path_style();
task_variables.insert(
VariableName::WorktreeRoot,
worktree_path.to_sanitized_string(),
worktree.abs_path().to_string_lossy().to_string(),
);
if let Some(full_path) = current_file.as_ref() {
let relative_path = pathdiff::diff_paths(full_path, worktree_path);
if let Some(relative_file) = relative_path {
if let Some(current_file) = current_file.as_ref() {
let relative_path = current_file.path();
task_variables.insert(
VariableName::RelativeFile,
relative_path.display(path_style).to_string(),
);
if let Some(relative_dir) = relative_path.parent() {
task_variables.insert(
VariableName::RelativeFile,
relative_file.to_sanitized_string(),
VariableName::RelativeDir,
if relative_dir.is_empty() {
String::from(".")
} else {
relative_dir.display(path_style).to_string()
},
);
if let Some(relative_dir) = relative_file.parent() {
task_variables.insert(
VariableName::RelativeDir,
if relative_dir.as_os_str().is_empty() {
String::from(".")
} else {
relative_dir.to_sanitized_string()
},
);
}
}
}
}
if let Some(path_as_string) = current_file {
let path = Path::new(&path_as_string);
if let Some(current_file) = current_file {
let path = current_file.abs_path(cx);
if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
task_variables.insert(VariableName::Filename, String::from(filename));
}
@@ -1063,7 +1062,7 @@ impl ContextProvider for BasicContextProvider {
task_variables.insert(VariableName::Dirname, dirname.into());
}
task_variables.insert(VariableName::File, path_as_string);
task_variables.insert(VariableName::File, path.to_string_lossy().to_string());
}
Task::ready(Ok(task_variables))
@@ -1096,6 +1095,8 @@ mod tests {
use pretty_assertions::assert_eq;
use serde_json::json;
use settings::SettingsLocation;
use std::path::Path;
use util::rel_path::rel_path;
use crate::task_store::TaskStore;
@@ -1181,7 +1182,7 @@ mod tests {
let worktree_id = WorktreeId::from_usize(0);
let local_worktree_location = SettingsLocation {
worktree_id,
path: Path::new("foo"),
path: RelPath::new("foo").unwrap(),
};
inventory.update(cx, |inventory, _| {
inventory
@@ -1427,7 +1428,7 @@ mod tests {
(
TaskSourceKind::Worktree {
id: worktree_1,
directory_in_worktree: PathBuf::from(".zed"),
directory_in_worktree: rel_path(".zed").into(),
id_base: "local worktree tasks from directory \".zed\"".into(),
},
common_name.to_string(),
@@ -1435,7 +1436,7 @@ mod tests {
(
TaskSourceKind::Worktree {
id: worktree_1,
directory_in_worktree: PathBuf::from(".zed"),
directory_in_worktree: rel_path(".zed").into(),
id_base: "local worktree tasks from directory \".zed\"".into(),
},
"worktree_1".to_string(),
@@ -1445,7 +1446,7 @@ mod tests {
(
TaskSourceKind::Worktree {
id: worktree_2,
directory_in_worktree: PathBuf::from(".zed"),
directory_in_worktree: rel_path(".zed").into(),
id_base: "local worktree tasks from directory \".zed\"".into(),
},
common_name.to_string(),
@@ -1453,7 +1454,7 @@ mod tests {
(
TaskSourceKind::Worktree {
id: worktree_2,
directory_in_worktree: PathBuf::from(".zed"),
directory_in_worktree: rel_path(".zed").into(),
id_base: "local worktree tasks from directory \".zed\"".into(),
},
"worktree_2".to_string(),
@@ -1475,7 +1476,7 @@ mod tests {
.update_file_based_tasks(
TaskSettingsLocation::Worktree(SettingsLocation {
worktree_id: worktree_1,
path: Path::new(".zed"),
path: RelPath::new(".zed").unwrap(),
}),
Some(&mock_tasks_from_names(
worktree_1_tasks.iter().map(|(_, name)| name.as_str()),
@@ -1486,7 +1487,7 @@ mod tests {
.update_file_based_tasks(
TaskSettingsLocation::Worktree(SettingsLocation {
worktree_id: worktree_2,
path: Path::new(".zed"),
path: RelPath::new(".zed").unwrap(),
}),
Some(&mock_tasks_from_names(
worktree_2_tasks.iter().map(|(_, name)| name.as_str()),
+1 -1
View File
@@ -438,7 +438,7 @@ fn worktree_root(
if !root_entry.is_dir() {
return None;
}
worktree.absolutize(&root_entry.path).ok()
Some(worktree.absolutize(&root_entry.path))
})
}
+6 -6
View File
@@ -16,7 +16,7 @@ use task::{Shell, ShellBuilder, ShellKind, SpawnInTerminal};
use terminal::{
TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::TerminalSettings,
};
use util::{get_default_system_shell, get_system_shell, maybe};
use util::{get_default_system_shell, get_system_shell, maybe, rel_path::RelPath};
use crate::{Project, ProjectPath};
@@ -68,7 +68,7 @@ impl Project {
{
settings_location = Some(SettingsLocation {
worktree_id: worktree.read(cx).id(),
path,
path: RelPath::empty(),
});
}
let settings = TerminalSettings::get(settings_location, cx).clone();
@@ -118,7 +118,7 @@ impl Project {
.map(|wt| wt.read(cx).id())
.map(|worktree_id| ProjectPath {
worktree_id,
path: Arc::from(Path::new("")),
path: Arc::from(RelPath::empty()),
}),
);
let toolchains = project_path_contexts
@@ -298,7 +298,7 @@ impl Project {
{
settings_location = Some(SettingsLocation {
worktree_id: worktree.read(cx).id(),
path,
path: RelPath::empty(),
});
}
let settings = TerminalSettings::get(settings_location, cx).clone();
@@ -325,7 +325,7 @@ impl Project {
.map(|wt| wt.read(cx).id())
.map(|worktree_id| ProjectPath {
worktree_id,
path: Arc::from(Path::new("")),
path: RelPath::empty().into(),
}),
);
let toolchains = project_path_contexts
@@ -464,7 +464,7 @@ impl Project {
{
settings_location = Some(SettingsLocation {
worktree_id: worktree.read(cx).id(),
path,
path: RelPath::empty(),
});
}
TerminalSettings::get(settings_location, cx)
+47 -63
View File
@@ -1,8 +1,4 @@
use std::{
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
};
use std::{path::PathBuf, str::FromStr, sync::Arc};
use anyhow::{Context as _, Result, bail};
@@ -18,12 +14,12 @@ use language::{
use rpc::{
AnyProtoClient, TypedEnvelope,
proto::{
self, FromProto, ResolveToolchainResponse, ToProto,
self, ResolveToolchainResponse,
resolve_toolchain_response::Response as ResolveResponsePayload,
},
};
use settings::WorktreeId;
use util::ResultExt as _;
use util::{ResultExt as _, rel_path::RelPath};
use crate::{
ProjectEnvironment, ProjectPath,
@@ -46,7 +42,7 @@ pub struct Toolchains {
/// Auto-detected toolchains.
pub toolchains: ToolchainList,
/// Path of the project root at which we ran the automatic toolchain detection.
pub root_path: Arc<Path>,
pub root_path: Arc<RelPath>,
pub user_toolchains: BTreeMap<ToolchainScope, IndexSet<Toolchain>>,
}
impl EventEmitter<ToolchainStoreEvent> for ToolchainStore {}
@@ -241,15 +237,15 @@ impl ToolchainStore {
name: toolchain.name.into(),
// todo(windows)
// Do we need to convert path to native string?
path: PathBuf::from(toolchain.path).to_proto().into(),
path: toolchain.path.into(),
as_json: serde_json::Value::from_str(&toolchain.raw_json)?,
language_name,
};
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let path: Arc<Path> = if let Some(path) = envelope.payload.path {
Arc::from(path.as_ref())
let path = if let Some(path) = envelope.payload.path {
RelPath::from_proto(&path)?
} else {
Arc::from("".as_ref())
RelPath::empty().into()
};
Ok(this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx))
})??
@@ -261,6 +257,7 @@ impl ToolchainStore {
envelope: TypedEnvelope<proto::ActiveToolchain>,
mut cx: AsyncApp,
) -> Result<proto::ActiveToolchainResponse> {
let path = RelPath::new(envelope.payload.path.as_deref().unwrap_or(""))?;
let toolchain = this
.update(&mut cx, |this, cx| {
let language_name = LanguageName::from_proto(envelope.payload.language_name);
@@ -268,7 +265,7 @@ impl ToolchainStore {
this.active_toolchain(
ProjectPath {
worktree_id,
path: Arc::from(envelope.payload.path.as_deref().unwrap_or("").as_ref()),
path: Arc::from(path),
},
language_name,
cx,
@@ -281,7 +278,7 @@ impl ToolchainStore {
let path = PathBuf::from(toolchain.path.to_string());
proto::Toolchain {
name: toolchain.name.into(),
path: path.to_proto(),
path: path.to_string_lossy().to_string(),
raw_json: toolchain.as_json.to_string(),
}
}),
@@ -297,9 +294,13 @@ impl ToolchainStore {
.update(&mut cx, |this, cx| {
let language_name = LanguageName::from_proto(envelope.payload.language_name);
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let path = Arc::from(envelope.payload.path.as_deref().unwrap_or("").as_ref());
this.list_toolchains(ProjectPath { worktree_id, path }, language_name, cx)
})?
let path = RelPath::from_proto(envelope.payload.path.as_deref().unwrap_or(""))?;
anyhow::Ok(this.list_toolchains(
ProjectPath { worktree_id, path },
language_name,
cx,
))
})??
.await;
let has_values = toolchains.is_some();
let groups = if let Some(Toolchains { toolchains, .. }) = &toolchains {
@@ -329,21 +330,21 @@ impl ToolchainStore {
let path = PathBuf::from(toolchain.path.to_string());
proto::Toolchain {
name: toolchain.name.to_string(),
path: path.to_proto(),
path: path.to_string_lossy().to_string(),
raw_json: toolchain.as_json.to_string(),
}
})
.collect::<Vec<_>>();
(toolchains, relative_path)
} else {
(vec![], Arc::from(Path::new("")))
(vec![], Arc::from(RelPath::empty()))
};
Ok(proto::ListToolchainsResponse {
has_values,
toolchains,
groups,
relative_worktree_path: Some(relative_path.to_string_lossy().into_owned()),
relative_worktree_path: Some(relative_path.to_proto()),
})
}
@@ -393,7 +394,7 @@ pub struct LocalToolchainStore {
languages: Arc<LanguageRegistry>,
worktree_store: Entity<WorktreeStore>,
project_environment: Entity<ProjectEnvironment>,
active_toolchains: BTreeMap<(WorktreeId, LanguageName), BTreeMap<Arc<Path>, Toolchain>>,
active_toolchains: BTreeMap<(WorktreeId, LanguageName), BTreeMap<Arc<RelPath>, Toolchain>>,
manifest_tree: Entity<ManifestTree>,
}
@@ -402,7 +403,7 @@ impl language::LocalLanguageToolchainStore for LocalStore {
fn active_toolchain(
self: Arc<Self>,
worktree_id: WorktreeId,
path: &Arc<Path>,
path: &Arc<RelPath>,
language_name: LanguageName,
cx: &mut AsyncApp,
) -> Option<Toolchain> {
@@ -419,7 +420,7 @@ impl language::LanguageToolchainStore for RemoteStore {
async fn active_toolchain(
self: Arc<Self>,
worktree_id: WorktreeId,
path: Arc<Path>,
path: Arc<RelPath>,
language_name: LanguageName,
cx: &mut AsyncApp,
) -> Option<Toolchain> {
@@ -437,7 +438,7 @@ impl language::LocalLanguageToolchainStore for EmptyToolchainStore {
fn active_toolchain(
self: Arc<Self>,
_: WorktreeId,
_: &Arc<Path>,
_: &Arc<RelPath>,
_: LanguageName,
_: &mut AsyncApp,
) -> Option<Toolchain> {
@@ -479,7 +480,7 @@ impl LocalToolchainStore {
path: ProjectPath,
language_name: LanguageName,
cx: &mut Context<Self>,
) -> Task<Option<(ToolchainList, Arc<Path>)>> {
) -> Task<Option<(ToolchainList, Arc<RelPath>)>> {
let registry = self.languages.clone();
let manifest_tree = self.manifest_tree.downgrade();
@@ -511,13 +512,12 @@ impl LocalToolchainStore {
})
.ok()?
.unwrap_or_else(|| ProjectPath {
path: Arc::from(Path::new("")),
path: Arc::from(RelPath::empty()),
worktree_id,
});
let abs_path = worktree
.update(cx, |this, _| this.absolutize(&relative_path.path).ok())
.ok()
.flatten()?;
.update(cx, |this, _| this.absolutize(&relative_path.path))
.ok()?;
let project_env = environment
.update(cx, |environment, cx| {
@@ -540,7 +540,7 @@ impl LocalToolchainStore {
pub(crate) fn active_toolchain(
&self,
worktree_id: WorktreeId,
relative_path: &Arc<Path>,
relative_path: &Arc<RelPath>,
language_name: LanguageName,
) -> Option<Toolchain> {
let ancestors = relative_path.ancestors();
@@ -609,10 +609,10 @@ impl RemoteToolchainStore {
language_name: toolchain.language_name.into(),
toolchain: Some(proto::Toolchain {
name: toolchain.name.into(),
path: path.to_proto(),
path: path.to_string_lossy().to_string(),
raw_json: toolchain.as_json.to_string(),
}),
path: Some(project_path.path.to_string_lossy().into_owned()),
path: Some(project_path.path.to_proto()),
})
.await
.log_err()?;
@@ -633,7 +633,7 @@ impl RemoteToolchainStore {
path: ProjectPath,
language_name: LanguageName,
cx: &App,
) -> Task<Option<(ToolchainList, Arc<Path>)>> {
) -> Task<Option<(ToolchainList, Arc<RelPath>)>> {
let project_id = self.project_id;
let client = self.client.clone();
cx.background_spawn(async move {
@@ -642,7 +642,7 @@ impl RemoteToolchainStore {
project_id,
worktree_id: path.worktree_id.to_proto(),
language_name: language_name.clone().into(),
path: Some(path.path.to_string_lossy().into_owned()),
path: Some(path.path.to_proto()),
})
.await
.log_err()?;
@@ -656,12 +656,7 @@ impl RemoteToolchainStore {
Some(Toolchain {
language_name: language_name.clone(),
name: toolchain.name.into(),
// todo(windows)
// Do we need to convert path to native string?
path: PathBuf::from_proto(toolchain.path)
.to_string_lossy()
.to_string()
.into(),
path: toolchain.path.into(),
as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?,
})
})
@@ -673,12 +668,13 @@ impl RemoteToolchainStore {
Some((usize::try_from(group.start_index).ok()?, group.name.into()))
})
.collect();
let relative_path = Arc::from(Path::new(
let relative_path = RelPath::from_proto(
response
.relative_worktree_path
.as_deref()
.unwrap_or_default(),
));
)
.log_err()?;
Some((
ToolchainList {
toolchains,
@@ -703,7 +699,7 @@ impl RemoteToolchainStore {
project_id,
worktree_id: path.worktree_id.to_proto(),
language_name: language_name.clone().into(),
path: Some(path.path.to_string_lossy().into_owned()),
path: Some(path.path.to_proto()),
})
.await
.log_err()?;
@@ -712,12 +708,7 @@ impl RemoteToolchainStore {
Some(Toolchain {
language_name: language_name.clone(),
name: toolchain.name.into(),
// todo(windows)
// Do we need to convert path to native string?
path: PathBuf::from_proto(toolchain.path)
.to_string_lossy()
.to_string()
.into(),
path: toolchain.path.into(),
as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?,
})
})
@@ -746,20 +737,13 @@ impl RemoteToolchainStore {
.context("Failed to resolve toolchain via RPC")?;
use proto::resolve_toolchain_response::Response;
match response {
Response::Toolchain(toolchain) => {
Ok(Toolchain {
language_name: language_name.clone(),
name: toolchain.name.into(),
// todo(windows)
// Do we need to convert path to native string?
path: PathBuf::from_proto(toolchain.path)
.to_string_lossy()
.to_string()
.into(),
as_json: serde_json::Value::from_str(&toolchain.raw_json)
.context("Deserializing ResolveToolchain LSP response")?,
})
}
Response::Toolchain(toolchain) => Ok(Toolchain {
language_name: language_name.clone(),
name: toolchain.name.into(),
path: toolchain.path.into(),
as_json: serde_json::Value::from_str(&toolchain.raw_json)
.context("Deserializing ResolveToolchain LSP response")?,
}),
Response::Error(error) => {
anyhow::bail!("{error}");
}
+335 -38
View File
@@ -7,7 +7,7 @@ use std::{
use anyhow::{Context as _, Result, anyhow};
use collections::{HashMap, HashSet};
use fs::Fs;
use fs::{Fs, copy_recursive};
use futures::{
FutureExt, SinkExt,
future::{BoxFuture, Shared},
@@ -18,7 +18,7 @@ use gpui::{
use postage::oneshot;
use rpc::{
AnyProtoClient, ErrorExt, TypedEnvelope,
proto::{self, FromProto, REMOTE_SERVER_PROJECT_ID, ToProto},
proto::{self, REMOTE_SERVER_PROJECT_ID},
};
use smol::{
channel::{Receiver, Sender},
@@ -28,16 +28,17 @@ use text::ReplicaId;
use util::{
ResultExt,
paths::{PathStyle, RemotePathBuf, SanitizedPath},
rel_path::RelPath,
};
use worktree::{
Entry, ProjectEntryId, UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId,
WorktreeSettings,
CreatedEntry, Entry, ProjectEntryId, UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree,
WorktreeId, WorktreeSettings,
};
use crate::{ProjectPath, search::SearchQuery};
struct MatchingEntry {
worktree_path: Arc<Path>,
worktree_root: Arc<Path>,
path: ProjectPath,
respond: oneshot::Sender<ProjectPath>,
}
@@ -155,11 +156,14 @@ impl WorktreeStore {
&self,
abs_path: impl AsRef<Path>,
cx: &App,
) -> Option<(Entity<Worktree>, PathBuf)> {
let abs_path = SanitizedPath::new(&abs_path);
) -> Option<(Entity<Worktree>, Arc<RelPath>)> {
let abs_path = SanitizedPath::new(abs_path.as_ref());
for tree in self.worktrees() {
if let Ok(relative_path) = abs_path.as_path().strip_prefix(tree.read(cx).abs_path()) {
return Some((tree.clone(), relative_path.into()));
let path_style = tree.read(cx).path_style();
if let Ok(relative_path) = abs_path.as_ref().strip_prefix(tree.read(cx).abs_path())
&& let Ok(relative_path) = RelPath::from_std_path(relative_path, path_style)
{
return Some((tree.clone(), relative_path));
}
}
None
@@ -167,7 +171,14 @@ impl WorktreeStore {
pub fn absolutize(&self, project_path: &ProjectPath, cx: &App) -> Option<PathBuf> {
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
worktree.read(cx).absolutize(&project_path.path).ok()
Some(worktree.read(cx).absolutize(&project_path.path))
}
pub fn path_style(&self) -> PathStyle {
match &self.state {
WorktreeStoreState::Local { .. } => PathStyle::local(),
WorktreeStoreState::Remote { path_style, .. } => *path_style,
}
}
pub fn find_or_create_worktree(
@@ -175,13 +186,13 @@ impl WorktreeStore {
abs_path: impl AsRef<Path>,
visible: bool,
cx: &mut Context<Self>,
) -> Task<Result<(Entity<Worktree>, PathBuf)>> {
) -> Task<Result<(Entity<Worktree>, Arc<RelPath>)>> {
let abs_path = abs_path.as_ref();
if let Some((tree, relative_path)) = self.find_worktree(abs_path, cx) {
Task::ready(Ok((tree, relative_path)))
} else {
let worktree = self.create_worktree(abs_path, visible, cx);
cx.background_spawn(async move { Ok((worktree.await?, PathBuf::new())) })
cx.background_spawn(async move { Ok((worktree.await?, RelPath::empty().into())) })
}
}
@@ -209,6 +220,240 @@ impl WorktreeStore {
.entry_for_path(&path.path)
}
pub fn copy_entry(
&mut self,
entry_id: ProjectEntryId,
new_project_path: ProjectPath,
cx: &mut Context<Self>,
) -> Task<Result<Option<Entry>>> {
let Some(old_worktree) = self.worktree_for_entry(entry_id, cx) else {
return Task::ready(Err(anyhow!("no such worktree")));
};
let Some(old_entry) = old_worktree.read(cx).entry_for_id(entry_id) else {
return Task::ready(Err(anyhow!("no such entry")));
};
let Some(new_worktree) = self.worktree_for_id(new_project_path.worktree_id, cx) else {
return Task::ready(Err(anyhow!("no such worktree")));
};
match &self.state {
WorktreeStoreState::Local { fs } => {
let old_abs_path = old_worktree.read(cx).absolutize(&old_entry.path);
let new_abs_path = new_worktree.read(cx).absolutize(&new_project_path.path);
let fs = fs.clone();
let copy = cx.background_spawn(async move {
copy_recursive(
fs.as_ref(),
&old_abs_path,
&new_abs_path,
Default::default(),
)
.await
});
cx.spawn(async move |_, cx| {
copy.await?;
new_worktree
.update(cx, |this, cx| {
this.as_local_mut().unwrap().refresh_entry(
new_project_path.path,
None,
cx,
)
})?
.await
})
}
WorktreeStoreState::Remote {
upstream_client,
upstream_project_id,
..
} => {
let response = upstream_client.request(proto::CopyProjectEntry {
project_id: *upstream_project_id,
entry_id: entry_id.to_proto(),
new_path: new_project_path.path.to_proto(),
new_worktree_id: new_project_path.worktree_id.to_proto(),
});
cx.spawn(async move |_, cx| {
let response = response.await?;
match response.entry {
Some(entry) => new_worktree
.update(cx, |worktree, cx| {
worktree.as_remote_mut().unwrap().insert_entry(
entry,
response.worktree_scan_id as usize,
cx,
)
})?
.await
.map(Some),
None => Ok(None),
}
})
}
}
}
pub fn rename_entry(
&mut self,
entry_id: ProjectEntryId,
new_project_path: ProjectPath,
cx: &mut Context<Self>,
) -> Task<Result<CreatedEntry>> {
let Some(old_worktree) = self.worktree_for_entry(entry_id, cx) else {
return Task::ready(Err(anyhow!("no such worktree")));
};
let Some(old_entry) = old_worktree.read(cx).entry_for_id(entry_id).cloned() else {
return Task::ready(Err(anyhow!("no such entry")));
};
let Some(new_worktree) = self.worktree_for_id(new_project_path.worktree_id, cx) else {
return Task::ready(Err(anyhow!("no such worktree")));
};
match &self.state {
WorktreeStoreState::Local { fs } => {
let abs_old_path = old_worktree.read(cx).absolutize(&old_entry.path);
let new_worktree_ref = new_worktree.read(cx);
let is_root_entry = new_worktree_ref
.root_entry()
.is_some_and(|e| e.id == entry_id);
let abs_new_path = if is_root_entry {
let abs_path = new_worktree_ref.abs_path();
let Some(root_parent_path) = abs_path.parent() else {
return Task::ready(Err(anyhow!("no parent for path {:?}", abs_path)));
};
root_parent_path.join(new_project_path.path.as_std_path())
} else {
new_worktree_ref.absolutize(&new_project_path.path)
};
let fs = fs.clone();
let case_sensitive = new_worktree
.read(cx)
.as_local()
.unwrap()
.fs_is_case_sensitive();
let do_rename =
async move |fs: &dyn Fs, old_path: &Path, new_path: &Path, overwrite| {
fs.rename(
&old_path,
&new_path,
fs::RenameOptions {
overwrite,
..fs::RenameOptions::default()
},
)
.await
.with_context(|| format!("renaming {old_path:?} into {new_path:?}"))
};
let rename = cx.background_spawn({
let abs_new_path = abs_new_path.clone();
async move {
// If we're on a case-insensitive FS and we're doing a case-only rename (i.e. `foobar` to `FOOBAR`)
// we want to overwrite, because otherwise we run into a file-already-exists error.
let overwrite = !case_sensitive
&& abs_old_path != abs_new_path
&& abs_old_path.to_str().map(|p| p.to_lowercase())
== abs_new_path.to_str().map(|p| p.to_lowercase());
// The directory we're renaming into might not exist yet
if let Err(e) =
do_rename(fs.as_ref(), &abs_old_path, &abs_new_path, overwrite).await
{
if let Some(err) = e.downcast_ref::<std::io::Error>()
&& err.kind() == std::io::ErrorKind::NotFound
{
if let Some(parent) = abs_new_path.parent() {
fs.create_dir(parent).await.with_context(|| {
format!("creating parent directory {parent:?}")
})?;
return do_rename(
fs.as_ref(),
&abs_old_path,
&abs_new_path,
overwrite,
)
.await;
}
}
return Err(e);
}
Ok(())
}
});
cx.spawn(async move |_, cx| {
rename.await?;
Ok(new_worktree
.update(cx, |this, cx| {
let local = this.as_local_mut().unwrap();
if is_root_entry {
// We eagerly update `abs_path` and refresh this worktree.
// Otherwise, the FS watcher would do it on the `RootUpdated` event,
// but with a noticeable delay, so we handle it proactively.
local.update_abs_path_and_refresh(
Some(SanitizedPath::new_arc(&abs_new_path)),
cx,
);
Task::ready(Ok(this.root_entry().cloned()))
} else {
// First refresh the parent directory (in case it was newly created)
if let Some(parent) = new_project_path.path.parent() {
let _ = local.refresh_entries_for_paths(vec![parent.into()]);
}
// Then refresh the new path
local.refresh_entry(
new_project_path.path.clone(),
Some(old_entry.path),
cx,
)
}
})?
.await?
.map(CreatedEntry::Included)
.unwrap_or_else(|| CreatedEntry::Excluded {
abs_path: abs_new_path,
}))
})
}
WorktreeStoreState::Remote {
upstream_client,
upstream_project_id,
..
} => {
let response = upstream_client.request(proto::RenameProjectEntry {
project_id: *upstream_project_id,
entry_id: entry_id.to_proto(),
new_path: new_project_path.path.to_proto(),
new_worktree_id: new_project_path.worktree_id.to_proto(),
});
cx.spawn(async move |_, cx| {
let response = response.await?;
match response.entry {
Some(entry) => new_worktree
.update(cx, |worktree, cx| {
worktree.as_remote_mut().unwrap().insert_entry(
entry,
response.worktree_scan_id as usize,
cx,
)
})?
.await
.map(CreatedEntry::Included),
None => {
let abs_path = new_worktree.read_with(cx, |worktree, _| {
worktree.absolutize(&new_project_path.path)
})?;
Ok(CreatedEntry::Excluded { abs_path })
}
}
})
}
}
}
pub fn create_worktree(
&mut self,
abs_path: impl AsRef<Path>,
@@ -226,7 +471,7 @@ impl WorktreeStore {
if upstream_client.is_via_collab() {
Task::ready(Err(Arc::new(anyhow!("cannot create worktrees via collab"))))
} else {
let abs_path = RemotePathBuf::new(abs_path.to_path_buf(), *path_style);
let abs_path = RemotePathBuf::new(abs_path.to_string(), *path_style);
self.create_remote_worktree(upstream_client.clone(), abs_path, visible, cx)
}
}
@@ -273,7 +518,7 @@ impl WorktreeStore {
cx.spawn(async move |this, cx| {
let this = this.upgrade().context("Dropped worktree store")?;
let path = RemotePathBuf::new(abs_path.into(), path_style);
let path = RemotePathBuf::new(abs_path, path_style);
let response = client
.request(proto::AddWorktree {
project_id: REMOTE_SERVER_PROJECT_ID,
@@ -288,7 +533,7 @@ impl WorktreeStore {
return Ok(existing_worktree);
}
let root_path_buf = PathBuf::from_proto(response.canonicalized_path.clone());
let root_path_buf = PathBuf::from(response.canonicalized_path.clone());
let root_name = root_path_buf
.file_name()
.map(|n| n.to_string_lossy().to_string())
@@ -305,6 +550,7 @@ impl WorktreeStore {
abs_path: response.canonicalized_path,
},
client,
path_style,
cx,
)
})?;
@@ -477,7 +723,14 @@ impl WorktreeStore {
self.worktrees.push(handle);
} else {
self.add(
&Worktree::remote(project_id, replica_id, worktree, client.clone(), cx),
&Worktree::remote(
project_id,
replica_id,
worktree,
client.clone(),
self.path_style(),
cx,
),
cx,
);
}
@@ -605,9 +858,9 @@ impl WorktreeStore {
let worktree = worktree.read(cx);
proto::WorktreeMetadata {
id: worktree.id().to_proto(),
root_name: worktree.root_name().into(),
root_name: worktree.root_name_str().to_owned(),
visible: worktree.is_visible(),
abs_path: worktree.abs_path().to_proto(),
abs_path: worktree.abs_path().to_string_lossy().to_string(),
}
})
.collect()
@@ -740,13 +993,13 @@ impl WorktreeStore {
fn scan_ignored_dir<'a>(
fs: &'a Arc<dyn Fs>,
snapshot: &'a worktree::Snapshot,
path: &'a Path,
path: &'a RelPath,
query: &'a SearchQuery,
filter_tx: &'a Sender<MatchingEntry>,
output_tx: &'a Sender<oneshot::Receiver<ProjectPath>>,
) -> BoxFuture<'a, Result<()>> {
async move {
let abs_path = snapshot.abs_path().join(path);
let abs_path = snapshot.absolutize(path);
let Some(mut files) = fs
.read_dir(&abs_path)
.await
@@ -771,21 +1024,21 @@ impl WorktreeStore {
if metadata.is_symlink || metadata.is_fifo {
continue;
}
results.push((
file.strip_prefix(snapshot.abs_path())?.to_path_buf(),
!metadata.is_dir,
))
let relative_path = file.strip_prefix(snapshot.abs_path())?;
let relative_path = RelPath::from_std_path(&relative_path, snapshot.path_style())
.context("getting relative path")?;
results.push((relative_path, !metadata.is_dir))
}
results.sort_by(|(a_path, _), (b_path, _)| a_path.cmp(b_path));
for (path, is_file) in results {
if is_file {
if query.filters_path() {
let matched_path = if query.match_full_paths() {
let mut full_path = PathBuf::from(snapshot.root_name());
full_path.push(&path);
let mut full_path = snapshot.root_name().as_std_path().to_owned();
full_path.push(path.as_std_path());
query.match_path(&full_path)
} else {
query.match_path(&path)
query.match_path(&path.as_std_path())
};
if !matched_path {
continue;
@@ -796,10 +1049,10 @@ impl WorktreeStore {
filter_tx
.send(MatchingEntry {
respond: tx,
worktree_path: snapshot.abs_path().clone(),
worktree_root: snapshot.abs_path().clone(),
path: ProjectPath {
worktree_id: snapshot.id(),
path: Arc::from(path),
path,
},
})
.await?;
@@ -844,11 +1097,11 @@ impl WorktreeStore {
if query.filters_path() {
let matched_path = if query.match_full_paths() {
let mut full_path = PathBuf::from(snapshot.root_name());
full_path.push(&entry.path);
let mut full_path = snapshot.root_name().as_std_path().to_owned();
full_path.push(entry.path.as_std_path());
query.match_path(&full_path)
} else {
query.match_path(&entry.path)
query.match_path(entry.path.as_std_path())
};
if !matched_path {
continue;
@@ -867,7 +1120,7 @@ impl WorktreeStore {
filter_tx
.send(MatchingEntry {
respond: tx,
worktree_path: snapshot.abs_path().clone(),
worktree_root: snapshot.abs_path().clone(),
path: ProjectPath {
worktree_id: snapshot.id(),
path: entry.path.clone(),
@@ -889,7 +1142,7 @@ impl WorktreeStore {
) -> Result<()> {
let mut input = pin!(input);
while let Some(mut entry) = input.next().await {
let abs_path = entry.worktree_path.join(&entry.path.path);
let abs_path = entry.worktree_root.join(entry.path.path.as_std_path());
let Some(file) = fs.open_sync(&abs_path).await.log_err() else {
continue;
};
@@ -935,11 +1188,26 @@ impl WorktreeStore {
mut cx: AsyncApp,
) -> Result<proto::ProjectEntryResponse> {
let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
let worktree = this.update(&mut cx, |this, cx| {
this.worktree_for_entry(entry_id, cx)
.context("worktree not found")
let new_worktree_id = WorktreeId::from_proto(envelope.payload.new_worktree_id);
let new_project_path = (
new_worktree_id,
RelPath::from_proto(&envelope.payload.new_path)?,
);
let (scan_id, entry) = this.update(&mut cx, |this, cx| {
let new_worktree = this
.worktree_for_id(new_worktree_id, cx)
.context("no such worktree")?;
let scan_id = new_worktree.read(cx).scan_id();
anyhow::Ok((
scan_id,
this.copy_entry(entry_id, new_project_path.into(), cx),
))
})??;
Worktree::handle_copy_entry(worktree, envelope.payload, cx).await
let entry = entry.await?;
Ok(proto::ProjectEntryResponse {
entry: entry.as_ref().map(|entry| entry.into()),
worktree_scan_id: scan_id as u64,
})
}
pub async fn handle_delete_project_entry(
@@ -955,6 +1223,35 @@ impl WorktreeStore {
Worktree::handle_delete_entry(worktree, envelope.payload, cx).await
}
pub async fn handle_rename_project_entry(
this: Entity<Self>,
request: proto::RenameProjectEntry,
mut cx: AsyncApp,
) -> Result<proto::ProjectEntryResponse> {
let entry_id = ProjectEntryId::from_proto(request.entry_id);
let new_worktree_id = WorktreeId::from_proto(request.new_worktree_id);
let rel_path = RelPath::from_proto(&request.new_path)
.with_context(|| format!("received invalid relative path {:?}", &request.new_path))?;
let (scan_id, task) = this.update(&mut cx, |this, cx| {
let worktree = this
.worktree_for_entry(entry_id, cx)
.context("no such worktree")?;
let scan_id = worktree.read(cx).scan_id();
anyhow::Ok((
scan_id,
this.rename_entry(entry_id, (new_worktree_id, rel_path).into(), cx),
))
})??;
Ok(proto::ProjectEntryResponse {
entry: match &task.await? {
CreatedEntry::Included(entry) => Some(entry.into()),
CreatedEntry::Excluded { .. } => None,
},
worktree_scan_id: scan_id as u64,
})
}
pub async fn handle_expand_project_entry(
this: Entity<Self>,
envelope: TypedEnvelope<proto::ExpandProjectEntry>,
+6 -3
View File
@@ -15,7 +15,7 @@ use anyhow::Result;
use collections::HashMap;
use fs::Fs;
use gpui::{App, AppContext as _, Context, Entity, Task};
use util::{ResultExt, archive::extract_zip};
use util::{ResultExt, archive::extract_zip, paths::PathStyle, rel_path::RelPath};
pub(crate) struct YarnPathStore {
temp_dirs: HashMap<Arc<Path>, tempfile::TempDir>,
@@ -63,12 +63,13 @@ impl YarnPathStore {
fs,
})
}
pub(crate) fn process_path(
&mut self,
path: &Path,
protocol: &str,
cx: &Context<Self>,
) -> Task<Option<(Arc<Path>, Arc<Path>)>> {
) -> Task<Option<(Arc<Path>, Arc<RelPath>)>> {
let mut is_zip = protocol.eq("zip");
let path: &Path = if let Some(non_zip_part) = path
@@ -112,7 +113,9 @@ impl YarnPathStore {
new_path
};
// Rebase zip-path onto new temp path.
let as_relative = path.strip_prefix(zip_file).ok()?.into();
let as_relative =
RelPath::from_std_path(path.strip_prefix(zip_file).ok()?, PathStyle::local())
.ok()?;
Some((zip_root.into(), as_relative))
})
} else {