Detailed changes
@@ -892,6 +892,7 @@ dependencies = [
"serde",
"serde_json",
"ui",
+ "util",
"workspace",
"workspace-hack",
]
@@ -12094,7 +12095,6 @@ dependencies = [
"markdown",
"node_runtime",
"parking_lot",
- "pathdiff",
"paths",
"postage",
"prettier",
@@ -12147,7 +12147,6 @@ dependencies = [
"git",
"git_ui",
"gpui",
- "indexmap 2.9.0",
"language",
"menu",
"pretty_assertions",
@@ -573,7 +573,7 @@ impl ToolCallContent {
))),
acp::ToolCallContent::Diff { diff } => Ok(Self::Diff(cx.new(|cx| {
Diff::finalized(
- diff.path,
+ diff.path.to_string_lossy().to_string(),
diff.old_text,
diff.new_text,
language_registry,
@@ -6,12 +6,7 @@ use itertools::Itertools;
use language::{
Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _, Point, Rope, TextBuffer,
};
-use std::{
- cmp::Reverse,
- ops::Range,
- path::{Path, PathBuf},
- sync::Arc,
-};
+use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
use util::ResultExt;
pub enum Diff {
@@ -21,7 +16,7 @@ pub enum Diff {
impl Diff {
pub fn finalized(
- path: PathBuf,
+ path: String,
old_text: Option<String>,
new_text: String,
language_registry: Arc<LanguageRegistry>,
@@ -36,7 +31,7 @@ impl Diff {
let buffer = new_buffer.clone();
async move |_, cx| {
let language = language_registry
- .language_for_file_path(&path)
+ .language_for_file_path(Path::new(&path))
.await
.log_err();
@@ -152,12 +147,15 @@ impl Diff {
let path = match self {
Diff::Pending(PendingDiff {
new_buffer: buffer, ..
- }) => buffer.read(cx).file().map(|file| file.path().as_ref()),
- Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_path()),
+ }) => buffer
+ .read(cx)
+ .file()
+ .map(|file| file.path().display(file.path_style(cx))),
+ Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_str().into()),
};
format!(
"Diff: {}\n```\n{}\n```\n",
- path.unwrap_or(Path::new("untitled")).display(),
+ path.unwrap_or("untitled".into()),
buffer_text
)
}
@@ -244,8 +242,8 @@ impl PendingDiff {
.new_buffer
.read(cx)
.file()
- .map(|file| file.path().as_ref())
- .unwrap_or(Path::new("untitled"))
+ .map(|file| file.path().display(file.path_style(cx)))
+ .unwrap_or("untitled".into())
.into();
// Replace the buffer in the multibuffer with the snapshot
@@ -348,7 +346,7 @@ impl PendingDiff {
}
pub struct FinalizedDiff {
- path: PathBuf,
+ path: String,
base_text: Arc<String>,
new_buffer: Entity<Buffer>,
multibuffer: Entity<MultiBuffer>,
@@ -8,10 +8,7 @@ use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};
use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
use std::{cmp, ops::Range, sync::Arc};
use text::{Edit, Patch, Rope};
-use util::{
- RangeExt, ResultExt as _,
- paths::{PathStyle, RemotePathBuf},
-};
+use util::{RangeExt, ResultExt as _};
/// Tracks actions performed by tools in a thread
pub struct ActionLog {
@@ -62,7 +59,13 @@ impl ActionLog {
let file_path = buffer
.read(cx)
.file()
- .map(|file| RemotePathBuf::new(file.full_path(cx), PathStyle::Posix).to_proto())
+ .map(|file| {
+ let mut path = file.full_path(cx).to_string_lossy().into_owned();
+ if file.path_style(cx).is_windows() {
+ path = path.replace('\\', "/");
+ }
+ path
+ })
.unwrap_or_else(|| format!("buffer_{}", buffer.entity_id()));
let mut result = String::new();
@@ -2301,7 +2304,7 @@ mod tests {
.await;
fs.set_head_for_repo(
path!("/project/.git").as_ref(),
- &[("file.txt".into(), "a\nb\nc\nd\ne\nf\ng\nh\ni\nj".into())],
+ &[("file.txt", "a\nb\nc\nd\ne\nf\ng\nh\ni\nj".into())],
"0000000",
);
cx.run_until_parked();
@@ -2384,7 +2387,7 @@ mod tests {
// - Ignores the last line edit (j stays as j)
fs.set_head_for_repo(
path!("/project/.git").as_ref(),
- &[("file.txt".into(), "A\nb\nc\nf\nG\nh\ni\nj".into())],
+ &[("file.txt", "A\nb\nc\nf\nG\nh\ni\nj".into())],
"0000001",
);
cx.run_until_parked();
@@ -2415,10 +2418,7 @@ mod tests {
// Make another commit that accepts the NEW line but with different content
fs.set_head_for_repo(
path!("/project/.git").as_ref(),
- &[(
- "file.txt".into(),
- "A\nb\nc\nf\nGGG\nh\nDIFFERENT\ni\nj".into(),
- )],
+ &[("file.txt", "A\nb\nc\nf\nGGG\nh\nDIFFERENT\ni\nj".into())],
"0000002",
);
cx.run_until_parked();
@@ -2444,7 +2444,7 @@ mod tests {
// Final commit that accepts all remaining edits
fs.set_head_for_repo(
path!("/project/.git").as_ref(),
- &[("file.txt".into(), "A\nb\nc\nf\nGGG\nh\nNEW\ni\nJ".into())],
+ &[("file.txt", "A\nb\nc\nf\nGGG\nh\nNEW\ni\nJ".into())],
"0000003",
);
cx.run_until_parked();
@@ -9,12 +9,14 @@ pub mod tool_use;
pub use context::{AgentContext, ContextId, ContextLoadResult};
pub use context_store::ContextStore;
+use fs::Fs;
+use std::sync::Arc;
pub use thread::{
LastRestoreCheckpoint, Message, MessageCrease, MessageId, MessageSegment, Thread, ThreadError,
ThreadEvent, ThreadFeedback, ThreadId, ThreadSummary, TokenUsageRatio,
};
pub use thread_store::{SerializedThread, TextThreadStore, ThreadStore};
-pub fn init(cx: &mut gpui::App) {
- thread_store::init(cx);
+pub fn init(fs: Arc<dyn Fs>, cx: &mut gpui::App) {
+ thread_store::init(fs, cx);
}
@@ -18,6 +18,7 @@ use std::path::PathBuf;
use std::{ops::Range, path::Path, sync::Arc};
use text::{Anchor, OffsetRangeExt as _};
use util::markdown::MarkdownCodeBlock;
+use util::rel_path::RelPath;
use util::{ResultExt as _, post_inc};
pub const RULES_ICON: IconName = IconName::Reader;
@@ -242,7 +243,7 @@ pub struct DirectoryContext {
#[derive(Debug, Clone)]
pub struct DirectoryContextDescendant {
/// Path within the directory.
- pub rel_path: Arc<Path>,
+ pub rel_path: Arc<RelPath>,
pub fenced_codeblock: SharedString,
}
@@ -968,7 +969,7 @@ pub fn load_context(
})
}
-fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<Arc<Path>> {
+fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<Arc<RelPath>> {
let mut files = Vec::new();
for entry in worktree.child_entries(path) {
@@ -14,7 +14,10 @@ use futures::{self, FutureExt};
use gpui::{App, Context, Entity, EventEmitter, Image, SharedString, Task, WeakEntity};
use language::{Buffer, File as _};
use language_model::LanguageModelImage;
-use project::{Project, ProjectItem, ProjectPath, Symbol, image_store::is_image_file};
+use project::{
+ Project, ProjectItem, ProjectPath, Symbol, image_store::is_image_file,
+ lsp_store::SymbolLocation,
+};
use prompt_store::UserPromptId;
use ref_cast::RefCast as _;
use std::{
@@ -500,7 +503,7 @@ impl ContextStore {
let Some(context_path) = buffer.project_path(cx) else {
return false;
};
- if context_path != symbol.path {
+ if symbol.path != SymbolLocation::InProject(context_path) {
return false;
}
let context_range = context.range.to_point_utf16(&buffer.snapshot());
@@ -234,7 +234,6 @@ impl MessageSegment {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ProjectSnapshot {
pub worktree_snapshots: Vec<WorktreeSnapshot>,
- pub unsaved_buffer_paths: Vec<String>,
pub timestamp: DateTime<Utc>,
}
@@ -2857,27 +2856,11 @@ impl Thread {
.map(|worktree| Self::worktree_snapshot(worktree, git_store.clone(), cx))
.collect();
- cx.spawn(async move |_, cx| {
+ cx.spawn(async move |_, _| {
let worktree_snapshots = futures::future::join_all(worktree_snapshots).await;
- let mut unsaved_buffers = Vec::new();
- cx.update(|app_cx| {
- let buffer_store = project.read(app_cx).buffer_store();
- for buffer_handle in buffer_store.read(app_cx).buffers() {
- let buffer = buffer_handle.read(app_cx);
- if buffer.is_dirty()
- && let Some(file) = buffer.file()
- {
- let path = file.path().to_string_lossy().to_string();
- unsaved_buffers.push(path);
- }
- }
- })
- .ok();
-
Arc::new(ProjectSnapshot {
worktree_snapshots,
- unsaved_buffer_paths: unsaved_buffers,
timestamp: Utc::now(),
})
})
@@ -3275,6 +3258,7 @@ mod tests {
use agent_settings::{AgentProfileId, AgentSettings};
use assistant_tool::ToolRegistry;
use assistant_tools;
+ use fs::Fs;
use futures::StreamExt;
use futures::future::BoxFuture;
use futures::stream::BoxStream;
@@ -3298,9 +3282,10 @@ mod tests {
#[gpui::test]
async fn test_message_with_context(cx: &mut TestAppContext) {
- init_test_settings(cx);
+ let fs = init_test_settings(cx);
let project = create_test_project(
+ &fs,
cx,
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
)
@@ -3375,9 +3360,10 @@ fn main() {{
#[gpui::test]
async fn test_only_include_new_contexts(cx: &mut TestAppContext) {
- init_test_settings(cx);
+ let fs = init_test_settings(cx);
let project = create_test_project(
+ &fs,
cx,
json!({
"file1.rs": "fn function1() {}\n",
@@ -3531,9 +3517,10 @@ fn main() {{
#[gpui::test]
async fn test_message_without_files(cx: &mut TestAppContext) {
- init_test_settings(cx);
+ let fs = init_test_settings(cx);
let project = create_test_project(
+ &fs,
cx,
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
)
@@ -3610,9 +3597,10 @@ fn main() {{
#[gpui::test]
#[ignore] // turn this test on when project_notifications tool is re-enabled
async fn test_stale_buffer_notification(cx: &mut TestAppContext) {
- init_test_settings(cx);
+ let fs = init_test_settings(cx);
let project = create_test_project(
+ &fs,
cx,
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
)
@@ -3738,9 +3726,10 @@ fn main() {{
#[gpui::test]
async fn test_storing_profile_setting_per_thread(cx: &mut TestAppContext) {
- init_test_settings(cx);
+ let fs = init_test_settings(cx);
let project = create_test_project(
+ &fs,
cx,
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
)
@@ -3760,9 +3749,10 @@ fn main() {{
#[gpui::test]
async fn test_serializing_thread_profile(cx: &mut TestAppContext) {
- init_test_settings(cx);
+ let fs = init_test_settings(cx);
let project = create_test_project(
+ &fs,
cx,
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
)
@@ -3803,9 +3793,10 @@ fn main() {{
#[gpui::test]
async fn test_temperature_setting(cx: &mut TestAppContext) {
- init_test_settings(cx);
+ let fs = init_test_settings(cx);
let project = create_test_project(
+ &fs,
cx,
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
)
@@ -3897,9 +3888,9 @@ fn main() {{
#[gpui::test]
async fn test_thread_summary(cx: &mut TestAppContext) {
- init_test_settings(cx);
+ let fs = init_test_settings(cx);
- let project = create_test_project(cx, json!({})).await;
+ let project = create_test_project(&fs, cx, json!({})).await;
let (_, _thread_store, thread, _context_store, model) =
setup_test_environment(cx, project.clone()).await;
@@ -3982,9 +3973,9 @@ fn main() {{
#[gpui::test]
async fn test_thread_summary_error_set_manually(cx: &mut TestAppContext) {
- init_test_settings(cx);
+ let fs = init_test_settings(cx);
- let project = create_test_project(cx, json!({})).await;
+ let project = create_test_project(&fs, cx, json!({})).await;
let (_, _thread_store, thread, _context_store, model) =
setup_test_environment(cx, project.clone()).await;
@@ -4004,9 +3995,9 @@ fn main() {{
#[gpui::test]
async fn test_thread_summary_error_retry(cx: &mut TestAppContext) {
- init_test_settings(cx);
+ let fs = init_test_settings(cx);
- let project = create_test_project(cx, json!({})).await;
+ let project = create_test_project(&fs, cx, json!({})).await;
let (_, _thread_store, thread, _context_store, model) =
setup_test_environment(cx, project.clone()).await;
@@ -4158,9 +4149,9 @@ fn main() {{
#[gpui::test]
async fn test_retry_on_overloaded_error(cx: &mut TestAppContext) {
- init_test_settings(cx);
+ let fs = init_test_settings(cx);
- let project = create_test_project(cx, json!({})).await;
+ let project = create_test_project(&fs, cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
@@ -4236,9 +4227,9 @@ fn main() {{
#[gpui::test]
async fn test_retry_on_internal_server_error(cx: &mut TestAppContext) {
- init_test_settings(cx);
+ let fs = init_test_settings(cx);
- let project = create_test_project(cx, json!({})).await;
+ let project = create_test_project(&fs, cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
@@ -4318,9 +4309,9 @@ fn main() {{
#[gpui::test]
async fn test_exponential_backoff_on_retries(cx: &mut TestAppContext) {
- init_test_settings(cx);
+ let fs = init_test_settings(cx);
- let project = create_test_project(cx, json!({})).await;
+ let project = create_test_project(&fs, cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
@@ -4438,9 +4429,9 @@ fn main() {{
#[gpui::test]
async fn test_max_retries_exceeded(cx: &mut TestAppContext) {
- init_test_settings(cx);
+ let fs = init_test_settings(cx);
- let project = create_test_project(cx, json!({})).await;
+ let project = create_test_project(&fs, cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
@@ -4529,9 +4520,9 @@ fn main() {{
#[gpui::test]
async fn test_retry_message_removed_on_retry(cx: &mut TestAppContext) {
- init_test_settings(cx);
+ let fs = init_test_settings(cx);
- let project = create_test_project(cx, json!({})).await;
+ let project = create_test_project(&fs, cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
@@ -4702,9 +4693,9 @@ fn main() {{
#[gpui::test]
async fn test_successful_completion_clears_retry_state(cx: &mut TestAppContext) {
- init_test_settings(cx);
+ let fs = init_test_settings(cx);
- let project = create_test_project(cx, json!({})).await;
+ let project = create_test_project(&fs, cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
@@ -4868,9 +4859,9 @@ fn main() {{
#[gpui::test]
async fn test_rate_limit_retry_single_attempt(cx: &mut TestAppContext) {
- init_test_settings(cx);
+ let fs = init_test_settings(cx);
- let project = create_test_project(cx, json!({})).await;
+ let project = create_test_project(&fs, cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
@@ -5053,9 +5044,9 @@ fn main() {{
#[gpui::test]
async fn test_ui_only_messages_not_sent_to_model(cx: &mut TestAppContext) {
- init_test_settings(cx);
+ let fs = init_test_settings(cx);
- let project = create_test_project(cx, json!({})).await;
+ let project = create_test_project(&fs, cx, json!({})).await;
let (_, _, thread, _, model) = setup_test_environment(cx, project.clone()).await;
// Insert a regular user message
@@ -5153,9 +5144,9 @@ fn main() {{
#[gpui::test]
async fn test_no_retry_without_burn_mode(cx: &mut TestAppContext) {
- init_test_settings(cx);
+ let fs = init_test_settings(cx);
- let project = create_test_project(cx, json!({})).await;
+ let project = create_test_project(&fs, cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Ensure we're in Normal mode (not Burn mode)
@@ -5226,9 +5217,9 @@ fn main() {{
#[gpui::test]
async fn test_retry_canceled_on_stop(cx: &mut TestAppContext) {
- init_test_settings(cx);
+ let fs = init_test_settings(cx);
- let project = create_test_project(cx, json!({})).await;
+ let project = create_test_project(&fs, cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
@@ -5334,7 +5325,8 @@ fn main() {{
cx.run_until_parked();
}
- fn init_test_settings(cx: &mut TestAppContext) {
+ fn init_test_settings(cx: &mut TestAppContext) -> Arc<dyn Fs> {
+ let fs = FakeFs::new(cx.executor());
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
@@ -5342,7 +5334,7 @@ fn main() {{
Project::init_settings(cx);
AgentSettings::register(cx);
prompt_store::init(cx);
- thread_store::init(cx);
+ thread_store::init(fs.clone(), cx);
workspace::init_settings(cx);
language_model::init_settings(cx);
ThemeSettings::register(cx);
@@ -5356,16 +5348,17 @@ fn main() {{
));
assistant_tools::init(http_client, cx);
});
+ fs
}
// Helper to create a test project with test files
async fn create_test_project(
+ fs: &Arc<dyn Fs>,
cx: &mut TestAppContext,
files: serde_json::Value,
) -> Entity<Project> {
- let fs = FakeFs::new(cx.executor());
- fs.insert_tree(path!("/test"), files).await;
- Project::test(fs, [path!("/test").as_ref()], cx).await
+ fs.as_fake().insert_tree(path!("/test"), files).await;
+ Project::test(fs.clone(), [path!("/test").as_ref()], cx).await
}
async fn setup_test_environment(
@@ -10,6 +10,7 @@ use assistant_tool::{Tool, ToolId, ToolWorkingSet};
use chrono::{DateTime, Utc};
use collections::HashMap;
use context_server::ContextServerId;
+use fs::{Fs, RemoveOptions};
use futures::{
FutureExt as _, StreamExt as _,
channel::{mpsc, oneshot},
@@ -39,7 +40,7 @@ use std::{
rc::Rc,
sync::{Arc, Mutex},
};
-use util::ResultExt as _;
+use util::{ResultExt as _, rel_path::RelPath};
use zed_env_vars::ZED_STATELESS;
@@ -85,8 +86,8 @@ const RULES_FILE_NAMES: [&str; 9] = [
"GEMINI.md",
];
-pub fn init(cx: &mut App) {
- ThreadsDatabase::init(cx);
+pub fn init(fs: Arc<dyn Fs>, cx: &mut App) {
+ ThreadsDatabase::init(fs, cx);
}
/// A system prompt shared by all threads created by this ThreadStore
@@ -234,7 +235,7 @@ impl ThreadStore {
if items.iter().any(|(path, _, _)| {
RULES_FILE_NAMES
.iter()
- .any(|name| path.as_ref() == Path::new(name))
+ .any(|name| path.as_ref() == RelPath::new(name).unwrap())
}) {
self.enqueue_system_prompt_reload();
}
@@ -327,7 +328,7 @@ impl ThreadStore {
cx: &mut App,
) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {
let tree = worktree.read(cx);
- let root_name = tree.root_name().into();
+ let root_name = tree.root_name_str().into();
let abs_path = tree.abs_path();
let mut context = WorktreeContext {
@@ -367,7 +368,7 @@ impl ThreadStore {
.into_iter()
.filter_map(|name| {
worktree
- .entry_for_path(name)
+ .entry_for_path(RelPath::new(name).unwrap())
.filter(|entry| entry.is_file())
.map(|entry| entry.path.clone())
})
@@ -869,13 +870,13 @@ impl ThreadsDatabase {
GlobalThreadsDatabase::global(cx).0.clone()
}
- fn init(cx: &mut App) {
+ fn init(fs: Arc<dyn Fs>, cx: &mut App) {
let executor = cx.background_executor().clone();
let database_future = executor
.spawn({
let executor = executor.clone();
let threads_dir = paths::data_dir().join("threads");
- async move { ThreadsDatabase::new(threads_dir, executor) }
+ async move { ThreadsDatabase::new(fs, threads_dir, executor).await }
})
.then(|result| future::ready(result.map(Arc::new).map_err(Arc::new)))
.boxed()
@@ -884,13 +885,17 @@ impl ThreadsDatabase {
cx.set_global(GlobalThreadsDatabase(database_future));
}
- pub fn new(threads_dir: PathBuf, executor: BackgroundExecutor) -> Result<Self> {
- std::fs::create_dir_all(&threads_dir)?;
+ pub async fn new(
+ fs: Arc<dyn Fs>,
+ threads_dir: PathBuf,
+ executor: BackgroundExecutor,
+ ) -> Result<Self> {
+ fs.create_dir(&threads_dir).await?;
let sqlite_path = threads_dir.join("threads.db");
let mdb_path = threads_dir.join("threads-db.1.mdb");
- let needs_migration_from_heed = mdb_path.exists();
+ let needs_migration_from_heed = fs.is_file(&mdb_path).await;
let connection = if *ZED_STATELESS {
Connection::open_memory(Some("THREAD_FALLBACK_DB"))
@@ -932,7 +937,14 @@ impl ThreadsDatabase {
.spawn(async move {
log::info!("Starting threads.db migration");
Self::migrate_from_heed(&mdb_path, db_connection, executor_clone)?;
- std::fs::remove_dir_all(mdb_path)?;
+ fs.remove_dir(
+ &mdb_path,
+ RemoveOptions {
+ recursive: true,
+ ignore_if_not_exists: true,
+ },
+ )
+ .await?;
log::info!("threads.db migrated to sqlite");
Ok::<(), anyhow::Error>(())
})
@@ -27,6 +27,7 @@ use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::Arc;
use util::ResultExt;
+use util::rel_path::RelPath;
const RULES_FILE_NAMES: [&str; 9] = [
".rules",
@@ -434,7 +435,7 @@ impl NativeAgent {
cx: &mut App,
) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {
let tree = worktree.read(cx);
- let root_name = tree.root_name().into();
+ let root_name = tree.root_name_str().into();
let abs_path = tree.abs_path();
let mut context = WorktreeContext {
@@ -474,7 +475,7 @@ impl NativeAgent {
.into_iter()
.filter_map(|name| {
worktree
- .entry_for_path(name)
+ .entry_for_path(RelPath::new(name).unwrap())
.filter(|entry| entry.is_file())
.map(|entry| entry.path.clone())
})
@@ -558,7 +559,7 @@ impl NativeAgent {
if items.iter().any(|(path, _, _)| {
RULES_FILE_NAMES
.iter()
- .any(|name| path.as_ref() == Path::new(name))
+ .any(|name| path.as_ref() == RelPath::new(name).unwrap())
}) {
self.project_context_needs_refresh.send(()).ok();
}
@@ -1208,7 +1209,7 @@ mod tests {
use language_model::fake_provider::FakeLanguageModel;
use serde_json::json;
use settings::SettingsStore;
- use util::path;
+ use util::{path, rel_path::rel_path};
#[gpui::test]
async fn test_maintaining_project_context(cx: &mut TestAppContext) {
@@ -1258,14 +1259,17 @@ mod tests {
fs.insert_file("/a/.rules", Vec::new()).await;
cx.run_until_parked();
agent.read_with(cx, |agent, cx| {
- let rules_entry = worktree.read(cx).entry_for_path(".rules").unwrap();
+ let rules_entry = worktree
+ .read(cx)
+ .entry_for_path(rel_path(".rules"))
+ .unwrap();
assert_eq!(
agent.project_context.read(cx).worktrees,
vec![WorktreeContext {
root_name: "a".into(),
abs_path: Path::new("/a").into(),
rules_file: Some(RulesFileContext {
- path_in_worktree: Path::new(".rules").into(),
+ path_in_worktree: rel_path(".rules").into(),
text: "".into(),
project_entry_id: rules_entry.id.to_usize()
})
@@ -422,17 +422,15 @@ mod tests {
use agent::MessageSegment;
use agent::context::LoadedContext;
use client::Client;
- use fs::FakeFs;
+ use fs::{FakeFs, Fs};
use gpui::AppContext;
use gpui::TestAppContext;
use http_client::FakeHttpClient;
use language_model::Role;
use project::Project;
- use serde_json::json;
use settings::SettingsStore;
- use util::test::TempTree;
- fn init_test(cx: &mut TestAppContext) {
+ fn init_test(fs: Arc<dyn Fs>, cx: &mut TestAppContext) {
env_logger::try_init().ok();
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
@@ -443,7 +441,7 @@ mod tests {
let http_client = FakeHttpClient::with_404_response();
let clock = Arc::new(clock::FakeSystemClock::new());
let client = Client::new(clock, http_client, cx);
- agent::init(cx);
+ agent::init(fs, cx);
agent_settings::init(cx);
language_model::init(client, cx);
});
@@ -451,10 +449,8 @@ mod tests {
#[gpui::test]
async fn test_retrieving_old_thread(cx: &mut TestAppContext) {
- let tree = TempTree::new(json!({}));
- util::paths::set_home_dir(tree.path().into());
- init_test(cx);
let fs = FakeFs::new(cx.executor());
+ init_test(fs.clone(), cx);
let project = Project::test(fs, [], cx).await;
// Save a thread using the old agent.
@@ -879,27 +879,11 @@ impl Thread {
.map(|worktree| Self::worktree_snapshot(worktree, git_store.clone(), cx))
.collect();
- cx.spawn(async move |_, cx| {
+ cx.spawn(async move |_, _| {
let worktree_snapshots = futures::future::join_all(worktree_snapshots).await;
- let mut unsaved_buffers = Vec::new();
- cx.update(|app_cx| {
- let buffer_store = project.read(app_cx).buffer_store();
- for buffer_handle in buffer_store.read(app_cx).buffers() {
- let buffer = buffer_handle.read(app_cx);
- if buffer.is_dirty()
- && let Some(file) = buffer.file()
- {
- let path = file.path().to_string_lossy().to_string();
- unsaved_buffers.push(path);
- }
- }
- })
- .ok();
-
Arc::new(ProjectSnapshot {
worktree_snapshots,
- unsaved_buffer_paths: unsaved_buffers,
timestamp: Utc::now(),
})
})
@@ -84,9 +84,7 @@ impl AgentTool for CopyPathTool {
.and_then(|project_path| project.entry_for_path(&project_path, cx))
{
Some(entity) => match project.find_project_path(&input.destination_path, cx) {
- Some(project_path) => {
- project.copy_entry(entity.id, None, project_path.path, cx)
- }
+ Some(project_path) => project.copy_entry(entity.id, project_path, cx),
None => Task::ready(Err(anyhow!(
"Destination path {} was outside the project.",
input.destination_path
@@ -6,7 +6,7 @@ use language::{DiagnosticSeverity, OffsetRangeExt};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use std::{fmt::Write, path::Path, sync::Arc};
+use std::{fmt::Write, sync::Arc};
use ui::SharedString;
use util::markdown::MarkdownInlineCode;
@@ -147,9 +147,7 @@ impl AgentTool for DiagnosticsTool {
has_diagnostics = true;
output.push_str(&format!(
"{}: {} error(s), {} warning(s)\n",
- Path::new(worktree.read(cx).root_name())
- .join(project_path.path)
- .display(),
+ worktree.read(cx).absolutize(&project_path.path).display(),
summary.error_count,
summary.warning_count
));
@@ -17,10 +17,12 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use smol::stream::StreamExt as _;
+use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use ui::SharedString;
use util::ResultExt;
+use util::rel_path::RelPath;
const DEFAULT_UI_TEXT: &str = "Editing file";
@@ -148,12 +150,11 @@ impl EditFileTool {
// If any path component matches the local settings folder, then this could affect
// the editor in ways beyond the project source, so prompt.
- let local_settings_folder = paths::local_settings_folder_relative_path();
+ let local_settings_folder = paths::local_settings_folder_name();
let path = Path::new(&input.path);
- if path
- .components()
- .any(|component| component.as_os_str() == local_settings_folder.as_os_str())
- {
+ if path.components().any(|component| {
+ component.as_os_str() == <_ as AsRef<OsStr>>::as_ref(&local_settings_folder)
+ }) {
return event_stream.authorize(
format!("{} (local settings)", input.display_description),
cx,
@@ -162,6 +163,7 @@ impl EditFileTool {
// It's also possible that the global config dir is configured to be inside the project,
// so check for that edge case too.
+ // TODO this is broken when remoting
if let Ok(canonical_path) = std::fs::canonicalize(&input.path)
&& canonical_path.starts_with(paths::config_dir())
{
@@ -216,9 +218,7 @@ impl AgentTool for EditFileTool {
.read(cx)
.short_full_path_for_project_path(&project_path, cx)
})
- .unwrap_or(Path::new(&input.path).into())
- .to_string_lossy()
- .to_string()
+ .unwrap_or(input.path.to_string_lossy().to_string())
.into(),
Err(raw_input) => {
if let Some(input) =
@@ -235,9 +235,7 @@ impl AgentTool for EditFileTool {
.read(cx)
.short_full_path_for_project_path(&project_path, cx)
})
- .unwrap_or(Path::new(&input.path).into())
- .to_string_lossy()
- .to_string()
+ .unwrap_or(input.path)
.into();
}
@@ -478,7 +476,7 @@ impl AgentTool for EditFileTool {
) -> Result<()> {
event_stream.update_diff(cx.new(|cx| {
Diff::finalized(
- output.input_path,
+ output.input_path.to_string_lossy().to_string(),
Some(output.old_text.to_string()),
output.new_text,
self.language_registry.clone(),
@@ -542,10 +540,12 @@ fn resolve_path(
let file_name = input
.path
.file_name()
+ .and_then(|file_name| file_name.to_str())
+ .and_then(|file_name| RelPath::new(file_name).ok())
.context("Can't create file: invalid filename")?;
let new_file_path = parent_project_path.map(|parent| ProjectPath {
- path: Arc::from(parent.path.join(file_name)),
+ path: parent.path.join(file_name),
..parent
});
@@ -690,13 +690,10 @@ mod tests {
cx.update(|cx| resolve_path(&input, project, cx))
}
+ #[track_caller]
fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) {
- let actual = path
- .expect("Should return valid path")
- .path
- .to_str()
- .unwrap()
- .replace("\\", "/"); // Naive Windows paths normalization
+ let actual = path.expect("Should return valid path").path;
+ let actual = actual.as_str();
assert_eq!(actual, expected);
}
@@ -1408,8 +1405,8 @@ mod tests {
// Parent directory references - find_project_path resolves these
(
"project/../other",
- false,
- "Path with .. is resolved by find_project_path",
+ true,
+ "Path with .. that goes outside of root directory",
),
(
"project/./src/file.rs",
@@ -1437,16 +1434,18 @@ mod tests {
)
});
+ cx.run_until_parked();
+
if should_confirm {
stream_rx.expect_authorization().await;
} else {
- auth.await.unwrap();
assert!(
stream_rx.try_next().is_err(),
"Failed for case: {} - path: {} - expected no confirmation but got one",
description,
path
);
+ auth.await.unwrap();
}
}
}
@@ -156,10 +156,14 @@ impl AgentTool for FindPathTool {
}
fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Result<Vec<PathBuf>>> {
- let path_matcher = match PathMatcher::new([
- // Sometimes models try to search for "". In this case, return all paths in the project.
- if glob.is_empty() { "*" } else { glob },
- ]) {
+ let path_style = project.read(cx).path_style(cx);
+ let path_matcher = match PathMatcher::new(
+ [
+ // Sometimes models try to search for "". In this case, return all paths in the project.
+ if glob.is_empty() { "*" } else { glob },
+ ],
+ path_style,
+ ) {
Ok(matcher) => matcher,
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))),
};
@@ -173,9 +177,8 @@ fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Resu
let mut results = Vec::new();
for snapshot in snapshots {
for entry in snapshot.entries(false, 0) {
- let root_name = PathBuf::from(snapshot.root_name());
- if path_matcher.is_match(root_name.join(&entry.path)) {
- results.push(snapshot.abs_path().join(entry.path.as_ref()));
+ if path_matcher.is_match(snapshot.root_name().join(&entry.path).as_std_path()) {
+ results.push(snapshot.absolutize(&entry.path));
}
}
}
@@ -110,12 +110,15 @@ impl AgentTool for GrepTool {
const CONTEXT_LINES: u32 = 2;
const MAX_ANCESTOR_LINES: u32 = 10;
+ let path_style = self.project.read(cx).path_style(cx);
+
let include_matcher = match PathMatcher::new(
input
.include_pattern
.as_ref()
.into_iter()
.collect::<Vec<_>>(),
+ path_style,
) {
Ok(matcher) => matcher,
Err(error) => {
@@ -132,7 +135,7 @@ impl AgentTool for GrepTool {
.iter()
.chain(global_settings.private_files.sources().iter());
- match PathMatcher::new(exclude_patterns) {
+ match PathMatcher::new(exclude_patterns, path_style) {
Ok(matcher) => matcher,
Err(error) => {
return Task::ready(Err(anyhow!("invalid exclude pattern: {error}")));
@@ -2,12 +2,12 @@ use crate::{AgentTool, ToolCallEventStream};
use agent_client_protocol::ToolKind;
use anyhow::{Result, anyhow};
use gpui::{App, Entity, SharedString, Task};
-use project::{Project, WorktreeSettings};
+use project::{Project, ProjectPath, WorktreeSettings};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use std::fmt::Write;
-use std::{path::Path, sync::Arc};
+use std::sync::Arc;
use util::markdown::MarkdownInlineCode;
/// Lists files and directories in a given path. Prefer the `grep` or `find_path` tools when searching the codebase.
@@ -86,13 +86,13 @@ impl AgentTool for ListDirectoryTool {
.read(cx)
.worktrees(cx)
.filter_map(|worktree| {
- worktree.read(cx).root_entry().and_then(|entry| {
- if entry.is_dir() {
- entry.path.to_str()
- } else {
- None
- }
- })
+ let worktree = worktree.read(cx);
+ let root_entry = worktree.root_entry()?;
+ if root_entry.is_dir() {
+ Some(root_entry.path.display(worktree.path_style()))
+ } else {
+ None
+ }
})
.collect::<Vec<_>>()
.join("\n");
@@ -143,7 +143,7 @@ impl AgentTool for ListDirectoryTool {
}
let worktree_snapshot = worktree.read(cx).snapshot();
- let worktree_root_name = worktree.read(cx).root_name().to_string();
+ let worktree_root_name = worktree.read(cx).root_name();
let Some(entry) = worktree_snapshot.entry_for_path(&project_path.path) else {
return Task::ready(Err(anyhow!("Path not found: {}", input.path)));
@@ -165,25 +165,17 @@ impl AgentTool for ListDirectoryTool {
continue;
}
- if self
- .project
- .read(cx)
- .find_project_path(&entry.path, cx)
- .map(|project_path| {
- let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
-
- worktree_settings.is_path_excluded(&project_path.path)
- || worktree_settings.is_path_private(&project_path.path)
- })
- .unwrap_or(false)
+ let project_path: ProjectPath = (worktree_snapshot.id(), entry.path.clone()).into();
+ if worktree_settings.is_path_excluded(&project_path.path)
+ || worktree_settings.is_path_private(&project_path.path)
{
continue;
}
- let full_path = Path::new(&worktree_root_name)
+ let full_path = worktree_root_name
.join(&entry.path)
- .display()
- .to_string();
+ .display(worktree_snapshot.path_style())
+ .into_owned();
if entry.is_dir() {
folders.push(full_path);
} else {
@@ -98,7 +98,7 @@ impl AgentTool for MovePathTool {
.and_then(|project_path| project.entry_for_path(&project_path, cx))
{
Some(entity) => match project.find_project_path(&input.destination_path, cx) {
- Some(project_path) => project.rename_entry(entity.id, project_path.path, cx),
+ Some(project_path) => project.rename_entry(entity.id, project_path, cx),
None => Task::ready(Err(anyhow!(
"Destination path {} was outside the project.",
input.destination_path
@@ -82,12 +82,12 @@ impl AgentTool for ReadFileTool {
{
match (input.start_line, input.end_line) {
(Some(start), Some(end)) => {
- format!("Read file `{}` (lines {}-{})", path.display(), start, end,)
+ format!("Read file `{path}` (lines {}-{})", start, end,)
}
(Some(start), None) => {
- format!("Read file `{}` (from line {})", path.display(), start)
+ format!("Read file `{path}` (from line {})", start)
}
- _ => format!("Read file `{}`", path.display()),
+ _ => format!("Read file `{path}`"),
}
.into()
} else {
@@ -1,5 +1,6 @@
use std::cell::RefCell;
use std::ops::Range;
+use std::path::PathBuf;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
@@ -13,7 +14,7 @@ use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{App, Entity, Task, WeakEntity};
use language::{Buffer, CodeLabel, HighlightId};
use lsp::CompletionContext;
-use project::lsp_store::CompletionDocumentation;
+use project::lsp_store::{CompletionDocumentation, SymbolLocation};
use project::{
Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, Project,
ProjectPath, Symbol, WorktreeId,
@@ -22,6 +23,7 @@ use prompt_store::PromptStore;
use rope::Point;
use text::{Anchor, ToPoint as _};
use ui::prelude::*;
+use util::rel_path::RelPath;
use workspace::Workspace;
use crate::AgentPanel;
@@ -187,7 +189,7 @@ impl ContextPickerCompletionProvider {
pub(crate) fn completion_for_path(
project_path: ProjectPath,
- path_prefix: &str,
+ path_prefix: &RelPath,
is_recent: bool,
is_directory: bool,
source_range: Range<Anchor>,
@@ -195,10 +197,12 @@ impl ContextPickerCompletionProvider {
project: Entity<Project>,
cx: &mut App,
) -> Option<Completion> {
+ let path_style = project.read(cx).path_style(cx);
let (file_name, directory) =
crate::context_picker::file_context_picker::extract_file_name_and_directory(
&project_path.path,
path_prefix,
+ path_style,
);
let label =
@@ -250,7 +254,15 @@ impl ContextPickerCompletionProvider {
let label = CodeLabel::plain(symbol.name.clone(), None);
- let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?;
+ let abs_path = match &symbol.path {
+ SymbolLocation::InProject(project_path) => {
+ project.read(cx).absolute_path(&project_path, cx)?
+ }
+ SymbolLocation::OutsideProject {
+ abs_path,
+ signature: _,
+ } => PathBuf::from(abs_path.as_ref()),
+ };
let uri = MentionUri::Symbol {
abs_path,
name: symbol.name.clone(),
@@ -48,7 +48,7 @@ use std::{
use text::OffsetRangeExt;
use theme::ThemeSettings;
use ui::{ButtonLike, TintColor, Toggleable, prelude::*};
-use util::{ResultExt, debug_panic};
+use util::{ResultExt, debug_panic, paths::PathStyle, rel_path::RelPath};
use workspace::{Workspace, notifications::NotifyResultExt as _};
use zed_actions::agent::Chat;
@@ -108,6 +108,11 @@ impl MessageEditor {
available_commands.clone(),
));
let mention_set = MentionSet::default();
+ // TODO: fix mentions when remoting with mixed path styles.
+ let host_and_guest_paths_differ = project
+ .read(cx)
+ .remote_client()
+ .is_some_and(|client| client.read(cx).path_style() != PathStyle::local());
let editor = cx.new(|cx| {
let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
@@ -117,7 +122,9 @@ impl MessageEditor {
editor.set_show_indent_guides(false, cx);
editor.set_soft_wrap();
editor.set_use_modal_editing(true);
- editor.set_completion_provider(Some(completion_provider.clone()));
+ if !host_and_guest_paths_differ {
+ editor.set_completion_provider(Some(completion_provider.clone()));
+ }
editor.set_context_menu_options(ContextMenuOptions {
min_entries_visible: 12,
max_entries_visible: 12,
@@ -947,6 +954,7 @@ impl MessageEditor {
window: &mut Window,
cx: &mut Context<Self>,
) {
+ let path_style = self.project.read(cx).path_style(cx);
let buffer = self.editor.read(cx).buffer().clone();
let Some(buffer) = buffer.read(cx).as_singleton() else {
return;
@@ -956,18 +964,15 @@ impl MessageEditor {
let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else {
continue;
};
- let Some(abs_path) = self.project.read(cx).absolute_path(&path, cx) else {
+ let Some(worktree) = self.project.read(cx).worktree_for_id(path.worktree_id, cx) else {
continue;
};
- let path_prefix = abs_path
- .file_name()
- .unwrap_or(path.path.as_os_str())
- .display()
- .to_string();
+ let abs_path = worktree.read(cx).absolutize(&path.path);
let (file_name, _) =
crate::context_picker::file_context_picker::extract_file_name_and_directory(
&path.path,
- &path_prefix,
+ worktree.read(cx).root_name(),
+ path_style,
);
let uri = if entry.is_dir() {
@@ -1176,7 +1181,7 @@ fn full_mention_for_directory(
abs_path: &Path,
cx: &mut App,
) -> Task<Result<Mention>> {
- fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<(Arc<Path>, PathBuf)> {
+ fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<(Arc<RelPath>, PathBuf)> {
let mut files = Vec::new();
for entry in worktree.child_entries(path) {
@@ -1261,7 +1266,7 @@ fn full_mention_for_directory(
})
}
-fn render_directory_contents(entries: Vec<(Arc<Path>, PathBuf, String)>) -> String {
+fn render_directory_contents(entries: Vec<(Arc<RelPath>, PathBuf, String)>) -> String {
let mut output = String::new();
for (_relative_path, full_path, content) in entries {
let fence = codeblock_fence_for_path(Some(&full_path), None);
@@ -1595,7 +1600,7 @@ mod tests {
use serde_json::json;
use text::Point;
use ui::{App, Context, IntoElement, Render, SharedString, Window};
- use util::{path, uri};
+ use util::{path, paths::PathStyle, rel_path::rel_path, uri};
use workspace::{AppState, Item, Workspace};
use crate::acp::{
@@ -2105,16 +2110,18 @@ mod tests {
let mut cx = VisualTestContext::from_window(*window, cx);
let paths = vec![
- path!("a/one.txt"),
- path!("a/two.txt"),
- path!("a/three.txt"),
- path!("a/four.txt"),
- path!("b/five.txt"),
- path!("b/six.txt"),
- path!("b/seven.txt"),
- path!("b/eight.txt"),
+ rel_path("a/one.txt"),
+ rel_path("a/two.txt"),
+ rel_path("a/three.txt"),
+ rel_path("a/four.txt"),
+ rel_path("b/five.txt"),
+ rel_path("b/six.txt"),
+ rel_path("b/seven.txt"),
+ rel_path("b/eight.txt"),
];
+ let slash = PathStyle::local().separator();
+
let mut opened_editors = Vec::new();
for path in paths {
let buffer = workspace
@@ -2122,7 +2129,7 @@ mod tests {
workspace.open_path(
ProjectPath {
worktree_id,
- path: Path::new(path).into(),
+ path: path.into(),
},
None,
false,
@@ -2183,10 +2190,10 @@ mod tests {
assert_eq!(
current_completion_labels(editor),
&[
- "eight.txt dir/b/",
- "seven.txt dir/b/",
- "six.txt dir/b/",
- "five.txt dir/b/",
+ format!("eight.txt dir{slash}b{slash}"),
+ format!("seven.txt dir{slash}b{slash}"),
+ format!("six.txt dir{slash}b{slash}"),
+ format!("five.txt dir{slash}b{slash}"),
]
);
editor.set_text("", window, cx);
@@ -2214,14 +2221,14 @@ mod tests {
assert_eq!(
current_completion_labels(editor),
&[
- "eight.txt dir/b/",
- "seven.txt dir/b/",
- "six.txt dir/b/",
- "five.txt dir/b/",
- "Files & Directories",
- "Symbols",
- "Threads",
- "Fetch"
+ format!("eight.txt dir{slash}b{slash}"),
+ format!("seven.txt dir{slash}b{slash}"),
+ format!("six.txt dir{slash}b{slash}"),
+ format!("five.txt dir{slash}b{slash}"),
+ "Files & Directories".into(),
+ "Symbols".into(),
+ "Threads".into(),
+ "Fetch".into()
]
);
});
@@ -2248,7 +2255,10 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(editor.text(cx), "Lorem @file one");
assert!(editor.has_visible_completions_menu());
- assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]);
+ assert_eq!(
+ current_completion_labels(editor),
+ vec![format!("one.txt dir{slash}a{slash}")]
+ );
});
editor.update_in(&mut cx, |editor, window, cx| {
@@ -2505,7 +2515,7 @@ mod tests {
format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) @file x.png")
);
assert!(editor.has_visible_completions_menu());
- assert_eq!(current_completion_labels(editor), &["x.png dir/"]);
+ assert_eq!(current_completion_labels(editor), &[format!("x.png dir{slash}")]);
});
editor.update_in(&mut cx, |editor, window, cx| {
@@ -2544,7 +2554,7 @@ mod tests {
format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) @file x.png")
);
assert!(editor.has_visible_completions_menu());
- assert_eq!(current_completion_labels(editor), &["x.png dir/"]);
+ assert_eq!(current_completion_labels(editor), &[format!("x.png dir{slash}")]);
});
editor.update_in(&mut cx, |editor, window, cx| {
@@ -3704,29 +3704,32 @@ impl AcpThreadView {
|(index, (buffer, _diff))| {
let file = buffer.read(cx).file()?;
let path = file.path();
+ let path_style = file.path_style(cx);
+ let separator = file.path_style(cx).separator();
let file_path = path.parent().and_then(|parent| {
- let parent_str = parent.to_string_lossy();
-
- if parent_str.is_empty() {
+ if parent.is_empty() {
None
} else {
Some(
- Label::new(format!("/{}{}", parent_str, std::path::MAIN_SEPARATOR_STR))
- .color(Color::Muted)
- .size(LabelSize::XSmall)
- .buffer_font(cx),
+ Label::new(format!(
+ "{separator}{}{separator}",
+ parent.display(path_style)
+ ))
+ .color(Color::Muted)
+ .size(LabelSize::XSmall)
+ .buffer_font(cx),
)
}
});
let file_name = path.file_name().map(|name| {
- Label::new(name.to_string_lossy().to_string())
+ Label::new(name.to_string())
.size(LabelSize::XSmall)
.buffer_font(cx)
});
- let file_icon = FileIcons::get_icon(path, cx)
+ let file_icon = FileIcons::get_icon(path.as_std_path(), cx)
.map(Icon::from_path)
.map(|icon| icon.color(Color::Muted).size(IconSize::Small))
.unwrap_or_else(|| {
@@ -4569,7 +4572,7 @@ impl AcpThreadView {
.read(cx)
.visible_worktrees(cx)
.next()
- .map(|worktree| worktree.read(cx).root_name().to_string())
+ .map(|worktree| worktree.read(cx).root_name_str().to_string())
});
if let Some(screen_window) = cx
@@ -264,7 +264,7 @@ pub fn init(
init_language_model_settings(cx);
}
assistant_slash_command::init(cx);
- agent::init(cx);
+ agent::init(fs.clone(), cx);
agent_panel::init(cx);
context_server_configuration::init(language_registry.clone(), fs.clone(), cx);
TextThreadEditor::init(cx);
@@ -33,6 +33,8 @@ use thread_context_picker::{
use ui::{
ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
};
+use util::paths::PathStyle;
+use util::rel_path::RelPath;
use workspace::{Workspace, notifications::NotifyResultExt};
use agent::{
@@ -228,12 +230,19 @@ impl ContextPicker {
let context_picker = cx.entity();
let menu = ContextMenu::build(window, cx, move |menu, _window, cx| {
+ let Some(workspace) = self.workspace.upgrade() else {
+ return menu;
+ };
+ let path_style = workspace.read(cx).path_style(cx);
let recent = self.recent_entries(cx);
let has_recent = !recent.is_empty();
let recent_entries = recent
.into_iter()
.enumerate()
- .map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry));
+ .map(|(ix, entry)| {
+ self.recent_menu_item(context_picker.clone(), ix, entry, path_style)
+ })
+ .collect::<Vec<_>>();
let entries = self
.workspace
@@ -395,6 +404,7 @@ impl ContextPicker {
context_picker: Entity<ContextPicker>,
ix: usize,
entry: RecentEntry,
+ path_style: PathStyle,
) -> ContextMenuItem {
match entry {
RecentEntry::File {
@@ -413,6 +423,7 @@ impl ContextPicker {
&path,
&path_prefix,
false,
+ path_style,
context_store.clone(),
cx,
)
@@ -586,7 +597,7 @@ impl Render for ContextPicker {
pub(crate) enum RecentEntry {
File {
project_path: ProjectPath,
- path_prefix: Arc<str>,
+ path_prefix: Arc<RelPath>,
},
Thread(ThreadContextEntry),
}
@@ -13,6 +13,7 @@ use http_client::HttpClientWithUrl;
use itertools::Itertools;
use language::{Buffer, CodeLabel, HighlightId};
use lsp::CompletionContext;
+use project::lsp_store::SymbolLocation;
use project::{
Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, ProjectPath,
Symbol, WorktreeId,
@@ -22,6 +23,8 @@ use rope::Point;
use text::{Anchor, OffsetRangeExt, ToPoint};
use ui::prelude::*;
use util::ResultExt as _;
+use util::paths::PathStyle;
+use util::rel_path::RelPath;
use workspace::Workspace;
use agent::{
@@ -574,11 +577,12 @@ impl ContextPickerCompletionProvider {
fn completion_for_path(
project_path: ProjectPath,
- path_prefix: &str,
+ path_prefix: &RelPath,
is_recent: bool,
is_directory: bool,
excerpt_id: ExcerptId,
source_range: Range<Anchor>,
+ path_style: PathStyle,
editor: Entity<Editor>,
context_store: Entity<ContextStore>,
cx: &App,
@@ -586,6 +590,7 @@ impl ContextPickerCompletionProvider {
let (file_name, directory) = super::file_context_picker::extract_file_name_and_directory(
&project_path.path,
path_prefix,
+ path_style,
);
let label =
@@ -657,17 +662,22 @@ impl ContextPickerCompletionProvider {
workspace: Entity<Workspace>,
cx: &mut App,
) -> Option<Completion> {
+ let path_style = workspace.read(cx).path_style(cx);
+ let SymbolLocation::InProject(symbol_path) = &symbol.path else {
+ return None;
+ };
let path_prefix = workspace
.read(cx)
.project()
.read(cx)
- .worktree_for_id(symbol.path.worktree_id, cx)?
+ .worktree_for_id(symbol_path.worktree_id, cx)?
.read(cx)
.root_name();
let (file_name, directory) = super::file_context_picker::extract_file_name_and_directory(
- &symbol.path.path,
+ &symbol_path.path,
path_prefix,
+ path_style,
);
let full_path = if let Some(directory) = directory {
format!("{}{}", directory, file_name)
@@ -768,6 +778,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
let text_thread_store = self.text_thread_store.clone();
let editor = self.editor.clone();
let http_client = workspace.read(cx).client().http_client();
+ let path_style = workspace.read(cx).path_style(cx);
let MentionCompletion { mode, argument, .. } = state;
let query = argument.unwrap_or_else(|| "".to_string());
@@ -834,6 +845,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
mat.is_dir,
excerpt_id,
source_range.clone(),
+ path_style,
editor.clone(),
context_store.clone(),
cx,
@@ -1064,7 +1076,7 @@ mod tests {
use serde_json::json;
use settings::SettingsStore;
use std::{ops::Deref, rc::Rc};
- use util::path;
+ use util::{path, rel_path::rel_path};
use workspace::{AppState, Item};
#[test]
@@ -1215,16 +1227,18 @@ mod tests {
let mut cx = VisualTestContext::from_window(*window.deref(), cx);
let paths = vec![
- path!("a/one.txt"),
- path!("a/two.txt"),
- path!("a/three.txt"),
- path!("a/four.txt"),
- path!("b/five.txt"),
- path!("b/six.txt"),
- path!("b/seven.txt"),
- path!("b/eight.txt"),
+ rel_path("a/one.txt"),
+ rel_path("a/two.txt"),
+ rel_path("a/three.txt"),
+ rel_path("a/four.txt"),
+ rel_path("b/five.txt"),
+ rel_path("b/six.txt"),
+ rel_path("b/seven.txt"),
+ rel_path("b/eight.txt"),
];
+ let slash = PathStyle::local().separator();
+
let mut opened_editors = Vec::new();
for path in paths {
let buffer = workspace
@@ -1232,7 +1246,7 @@ mod tests {
workspace.open_path(
ProjectPath {
worktree_id,
- path: Path::new(path).into(),
+ path: path.into(),
},
None,
false,
@@ -1308,13 +1322,13 @@ mod tests {
assert_eq!(
current_completion_labels(editor),
&[
- "seven.txt dir/b/",
- "six.txt dir/b/",
- "five.txt dir/b/",
- "four.txt dir/a/",
- "Files & Directories",
- "Symbols",
- "Fetch"
+ format!("seven.txt dir{slash}b{slash}"),
+ format!("six.txt dir{slash}b{slash}"),
+ format!("five.txt dir{slash}b{slash}"),
+ format!("four.txt dir{slash}a{slash}"),
+ "Files & Directories".into(),
+ "Symbols".into(),
+ "Fetch".into()
]
);
});
@@ -1341,7 +1355,10 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(editor.text(cx), "Lorem @file one");
assert!(editor.has_visible_completions_menu());
- assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]);
+ assert_eq!(
+ current_completion_labels(editor),
+ vec![format!("one.txt dir{slash}a{slash}")]
+ );
});
editor.update_in(&mut cx, |editor, window, cx| {
@@ -1350,7 +1367,10 @@ mod tests {
});
editor.update(&mut cx, |editor, cx| {
- assert_eq!(editor.text(cx), "Lorem [@one.txt](@file:dir/a/one.txt) ");
+ assert_eq!(
+ editor.text(cx),
+ format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) ")
+ );
assert!(!editor.has_visible_completions_menu());
assert_eq!(
fold_ranges(editor, cx),
@@ -1361,7 +1381,10 @@ mod tests {
cx.simulate_input(" ");
editor.update(&mut cx, |editor, cx| {
- assert_eq!(editor.text(cx), "Lorem [@one.txt](@file:dir/a/one.txt) ");
+ assert_eq!(
+ editor.text(cx),
+ format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) ")
+ );
assert!(!editor.has_visible_completions_menu());
assert_eq!(
fold_ranges(editor, cx),
@@ -1374,7 +1397,7 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
- "Lorem [@one.txt](@file:dir/a/one.txt) Ipsum ",
+ format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) Ipsum "),
);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
@@ -1388,7 +1411,7 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
- "Lorem [@one.txt](@file:dir/a/one.txt) Ipsum @file ",
+ format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) Ipsum @file "),
);
assert!(editor.has_visible_completions_menu());
assert_eq!(
@@ -1406,7 +1429,7 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
- "Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt) "
+ format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) Ipsum [@seven.txt](@file:dir{slash}b{slash}seven.txt) ")
);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
@@ -1423,7 +1446,7 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
- "Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt) \n@"
+ format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) Ipsum [@seven.txt](@file:dir{slash}b{slash}seven.txt) \n@")
);
assert!(editor.has_visible_completions_menu());
assert_eq!(
@@ -1444,7 +1467,7 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
- "Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt) \n[@six.txt](@file:dir/b/six.txt) "
+ format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) Ipsum [@seven.txt](@file:dir{slash}b{slash}seven.txt) \n[@six.txt](@file:dir{slash}b{slash}six.txt) ")
);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
@@ -1,4 +1,3 @@
-use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
@@ -10,7 +9,7 @@ use gpui::{
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
use ui::{ListItem, Tooltip, prelude::*};
-use util::ResultExt as _;
+use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
use workspace::Workspace;
use crate::context_picker::ContextPicker;
@@ -161,6 +160,8 @@ impl PickerDelegate for FileContextPickerDelegate {
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let FileMatch { mat, .. } = &self.matches.get(ix)?;
+ let workspace = self.workspace.upgrade()?;
+ let path_style = workspace.read(cx).path_style(cx);
Some(
ListItem::new(ix)
@@ -172,6 +173,7 @@ impl PickerDelegate for FileContextPickerDelegate {
&mat.path,
&mat.path_prefix,
mat.is_dir,
+ path_style,
self.context_store.clone(),
cx,
)),
@@ -214,14 +216,13 @@ pub(crate) fn search_files(
let file_matches = project.worktrees(cx).flat_map(|worktree| {
let worktree = worktree.read(cx);
- let path_prefix: Arc<str> = worktree.root_name().into();
worktree.entries(false, 0).map(move |entry| FileMatch {
mat: PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: worktree.id().to_usize(),
path: entry.path.clone(),
- path_prefix: path_prefix.clone(),
+ path_prefix: worktree.root_name().into(),
distance_to_relative_ancestor: 0,
is_dir: entry.is_dir(),
},
@@ -269,51 +270,31 @@ pub(crate) fn search_files(
}
pub fn extract_file_name_and_directory(
- path: &Path,
- path_prefix: &str,
+ path: &RelPath,
+ path_prefix: &RelPath,
+ path_style: PathStyle,
) -> (SharedString, Option<SharedString>) {
- if path == Path::new("") {
- (
- SharedString::from(
- path_prefix
- .trim_end_matches(std::path::MAIN_SEPARATOR)
- .to_string(),
- ),
- None,
- )
- } else {
- let file_name = path
- .file_name()
- .unwrap_or_default()
- .to_string_lossy()
- .to_string()
- .into();
-
- let mut directory = path_prefix
- .trim_end_matches(std::path::MAIN_SEPARATOR)
- .to_string();
- if !directory.ends_with('/') {
- directory.push('/');
- }
- if let Some(parent) = path.parent().filter(|parent| parent != &Path::new("")) {
- directory.push_str(&parent.to_string_lossy());
- directory.push('/');
- }
-
- (file_name, Some(directory.into()))
- }
+ let full_path = path_prefix.join(path);
+ let file_name = full_path.file_name().unwrap_or_default();
+ let display_path = full_path.display(path_style);
+ let (directory, file_name) = display_path.split_at(display_path.len() - file_name.len());
+ (
+ file_name.to_string().into(),
+ Some(SharedString::new(directory)).filter(|dir| !dir.is_empty()),
+ )
}
pub fn render_file_context_entry(
id: ElementId,
worktree_id: WorktreeId,
- path: &Arc<Path>,
- path_prefix: &Arc<str>,
+ path: &Arc<RelPath>,
+ path_prefix: &Arc<RelPath>,
is_directory: bool,
+ path_style: PathStyle,
context_store: WeakEntity<ContextStore>,
cx: &App,
) -> Stateful<Div> {
- let (file_name, directory) = extract_file_name_and_directory(path, path_prefix);
+ let (file_name, directory) = extract_file_name_and_directory(path, path_prefix, path_style);
let added = context_store.upgrade().and_then(|context_store| {
let project_path = ProjectPath {
@@ -330,9 +311,9 @@ pub fn render_file_context_entry(
});
let file_icon = if is_directory {
- FileIcons::get_folder_icon(false, path, cx)
+ FileIcons::get_folder_icon(false, path.as_std_path(), cx)
} else {
- FileIcons::get_icon(path, cx)
+ FileIcons::get_icon(path.as_std_path(), cx)
}
.map(Icon::from_path)
.unwrap_or_else(|| Icon::new(IconName::File));
@@ -2,13 +2,14 @@ use std::cmp::Reverse;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
-use anyhow::Result;
+use anyhow::{Result, anyhow};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
+use project::lsp_store::SymbolLocation;
use project::{DocumentSymbol, Symbol};
use ui::{ListItem, prelude::*};
use util::ResultExt as _;
@@ -191,7 +192,10 @@ pub(crate) fn add_symbol(
) -> Task<Result<(Option<AgentContextHandle>, bool)>> {
let project = workspace.read(cx).project().clone();
let open_buffer_task = project.update(cx, |project, cx| {
- project.open_buffer(symbol.path.clone(), cx)
+ let SymbolLocation::InProject(symbol_path) = &symbol.path else {
+ return Task::ready(Err(anyhow!("can't add symbol from outside of project")));
+ };
+ project.open_buffer(symbol_path.clone(), cx)
});
cx.spawn(async move |cx| {
let buffer = open_buffer_task.await?;
@@ -291,10 +295,11 @@ pub(crate) fn search_symbols(
.map(|(id, symbol)| {
StringMatchCandidate::new(id, symbol.label.filter_text())
})
- .partition(|candidate| {
- project
- .entry_for_path(&symbols[candidate.id].path, cx)
- .is_some_and(|e| !e.is_ignored)
+ .partition(|candidate| match &symbols[candidate.id].path {
+ SymbolLocation::InProject(project_path) => project
+ .entry_for_path(project_path, cx)
+ .is_some_and(|e| !e.is_ignored),
+ SymbolLocation::OutsideProject { .. } => false,
})
})
.log_err()
@@ -360,13 +365,18 @@ fn compute_symbol_entries(
}
pub fn render_symbol_context_entry(id: ElementId, entry: &SymbolEntry) -> Stateful<Div> {
- let path = entry
- .symbol
- .path
- .path
- .file_name()
- .map(|s| s.to_string_lossy())
- .unwrap_or_default();
+ let path = match &entry.symbol.path {
+ SymbolLocation::InProject(project_path) => {
+ project_path.path.file_name().unwrap_or_default().into()
+ }
+ SymbolLocation::OutsideProject {
+ abs_path,
+ signature: _,
+ } => abs_path
+ .file_name()
+ .map(|f| f.to_string_lossy())
+ .unwrap_or_default(),
+ };
let symbol_location = format!("{} L{}", path, entry.symbol.range.start.0.row + 1);
h_flex()
@@ -1431,10 +1431,14 @@ impl TextThreadEditor {
else {
continue;
};
- let worktree_root_name = worktree.read(cx).root_name().to_string();
- let mut full_path = PathBuf::from(worktree_root_name.clone());
- full_path.push(&project_path.path);
- file_slash_command_args.push(full_path.to_string_lossy().to_string());
+ let path_style = worktree.read(cx).path_style();
+ let full_path = worktree
+ .read(cx)
+ .root_name()
+ .join(&project_path.path)
+ .display(path_style)
+ .into_owned();
+ file_slash_command_args.push(full_path);
}
let cmd_name = FileSlashCommand.name();
@@ -25,6 +25,7 @@ parking_lot.workspace = true
serde.workspace = true
serde_json.workspace = true
ui.workspace = true
+util.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
@@ -1,12 +1,11 @@
-use std::path::PathBuf;
-use std::sync::{Arc, atomic::AtomicBool};
-
use anyhow::Result;
use async_trait::async_trait;
use extension::{Extension, ExtensionHostProxy, ExtensionSlashCommandProxy, WorktreeDelegate};
use gpui::{App, Task, WeakEntity, Window};
use language::{BufferSnapshot, LspAdapterDelegate};
+use std::sync::{Arc, atomic::AtomicBool};
use ui::prelude::*;
+use util::rel_path::RelPath;
use workspace::Workspace;
use crate::{
@@ -54,7 +53,7 @@ impl WorktreeDelegate for WorktreeDelegateAdapter {
self.0.worktree_root_path().to_string_lossy().to_string()
}
- async fn read_text_file(&self, path: PathBuf) -> Result<String> {
+ async fn read_text_file(&self, path: &RelPath) -> Result<String> {
self.0.read_text_file(path).await
}
@@ -41,6 +41,9 @@ worktree.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
+fs = { workspace = true, features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true
-settings.workspace = true
+project = { workspace = true, features = ["test-support"] }
+settings = { workspace = true, features = ["test-support"] }
zlog.workspace = true
@@ -0,0 +1,159 @@
+use anyhow::{Context as _, Result, anyhow};
+use assistant_slash_command::{
+ ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
+ SlashCommandResult,
+};
+use fs::Fs;
+use gpui::{App, Entity, Task, WeakEntity};
+use language::{BufferSnapshot, LspAdapterDelegate};
+use project::{Project, ProjectPath};
+use std::{
+ fmt::Write,
+ path::Path,
+ sync::{Arc, atomic::AtomicBool},
+};
+use ui::prelude::*;
+use util::rel_path::RelPath;
+use workspace::Workspace;
+
+pub struct CargoWorkspaceSlashCommand;
+
+impl CargoWorkspaceSlashCommand {
+ async fn build_message(fs: Arc<dyn Fs>, path_to_cargo_toml: &Path) -> Result<String> {
+ let buffer = fs.load(path_to_cargo_toml).await?;
+ let cargo_toml: cargo_toml::Manifest = toml::from_str(&buffer)?;
+
+ let mut message = String::new();
+ writeln!(message, "You are in a Rust project.")?;
+
+ if let Some(workspace) = cargo_toml.workspace {
+ writeln!(
+ message,
+ "The project is a Cargo workspace with the following members:"
+ )?;
+ for member in workspace.members {
+ writeln!(message, "- {member}")?;
+ }
+
+ if !workspace.default_members.is_empty() {
+ writeln!(message, "The default members are:")?;
+ for member in workspace.default_members {
+ writeln!(message, "- {member}")?;
+ }
+ }
+
+ if !workspace.dependencies.is_empty() {
+ writeln!(
+ message,
+ "The following workspace dependencies are installed:"
+ )?;
+ for dependency in workspace.dependencies.keys() {
+ writeln!(message, "- {dependency}")?;
+ }
+ }
+ } else if let Some(package) = cargo_toml.package {
+ writeln!(
+ message,
+ "The project name is \"{name}\".",
+ name = package.name
+ )?;
+
+ let description = package
+ .description
+ .as_ref()
+ .and_then(|description| description.get().ok().cloned());
+ if let Some(description) = description.as_ref() {
+ writeln!(message, "It describes itself as \"{description}\".")?;
+ }
+
+ if !cargo_toml.dependencies.is_empty() {
+ writeln!(message, "The following dependencies are installed:")?;
+ for dependency in cargo_toml.dependencies.keys() {
+ writeln!(message, "- {dependency}")?;
+ }
+ }
+ }
+
+ Ok(message)
+ }
+
+ fn path_to_cargo_toml(project: Entity<Project>, cx: &mut App) -> Option<Arc<Path>> {
+ let worktree = project.read(cx).worktrees(cx).next()?;
+ let worktree = worktree.read(cx);
+ let entry = worktree.entry_for_path(RelPath::new("Cargo.toml").unwrap())?;
+ let path = ProjectPath {
+ worktree_id: worktree.id(),
+ path: entry.path.clone(),
+ };
+ Some(Arc::from(
+ project.read(cx).absolute_path(&path, cx)?.as_path(),
+ ))
+ }
+}
+
+impl SlashCommand for CargoWorkspaceSlashCommand {
+ fn name(&self) -> String {
+ "cargo-workspace".into()
+ }
+
+ fn description(&self) -> String {
+ "insert project workspace metadata".into()
+ }
+
+ fn menu_text(&self) -> String {
+ "Insert Project Workspace Metadata".into()
+ }
+
+ fn complete_argument(
+ self: Arc<Self>,
+ _arguments: &[String],
+ _cancel: Arc<AtomicBool>,
+ _workspace: Option<WeakEntity<Workspace>>,
+ _window: &mut Window,
+ _cx: &mut App,
+ ) -> Task<Result<Vec<ArgumentCompletion>>> {
+ Task::ready(Err(anyhow!("this command does not require argument")))
+ }
+
+ fn requires_argument(&self) -> bool {
+ false
+ }
+
+ fn run(
+ self: Arc<Self>,
+ _arguments: &[String],
+ _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
+ _context_buffer: BufferSnapshot,
+ workspace: WeakEntity<Workspace>,
+ _delegate: Option<Arc<dyn LspAdapterDelegate>>,
+ _window: &mut Window,
+ cx: &mut App,
+ ) -> Task<SlashCommandResult> {
+ let output = workspace.update(cx, |workspace, cx| {
+ let project = workspace.project().clone();
+ let fs = workspace.project().read(cx).fs().clone();
+ let path = Self::path_to_cargo_toml(project, cx);
+ let output = cx.background_spawn(async move {
+ let path = path.with_context(|| "Cargo.toml not found")?;
+ Self::build_message(fs, &path).await
+ });
+
+ cx.foreground_executor().spawn(async move {
+ let text = output.await?;
+ let range = 0..text.len();
+ Ok(SlashCommandOutput {
+ text,
+ sections: vec![SlashCommandOutputSection {
+ range,
+ icon: IconName::FileTree,
+ label: "Project".into(),
+ metadata: None,
+ }],
+ run_commands_in_text: false,
+ }
+ .into_event_stream())
+ })
+ });
+ output.unwrap_or_else(|error| Task::ready(Err(error)))
+ }
+}
@@ -13,12 +13,12 @@ use project::{DiagnosticSummary, PathMatchCandidateSet, Project};
use rope::Point;
use std::{
fmt::Write,
- path::{Path, PathBuf},
+ path::Path,
sync::{Arc, atomic::AtomicBool},
};
use ui::prelude::*;
-use util::ResultExt;
-use util::paths::PathMatcher;
+use util::paths::{PathMatcher, PathStyle};
+use util::{ResultExt, rel_path::RelPath};
use workspace::Workspace;
use crate::create_label_for_command;
@@ -36,7 +36,7 @@ impl DiagnosticsSlashCommand {
if query.is_empty() {
let workspace = workspace.read(cx);
let entries = workspace.recent_navigation_history(Some(10), cx);
- let path_prefix: Arc<str> = Arc::default();
+ let path_prefix: Arc<RelPath> = RelPath::empty().into();
Task::ready(
entries
.into_iter()
@@ -125,6 +125,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
+ let path_style = workspace.read(cx).project().read(cx).path_style(cx);
let query = arguments.last().cloned().unwrap_or_default();
let paths = self.search_paths(query.clone(), cancellation_flag.clone(), &workspace, cx);
@@ -134,11 +135,11 @@ impl SlashCommand for DiagnosticsSlashCommand {
.await
.into_iter()
.map(|path_match| {
- format!(
- "{}{}",
- path_match.path_prefix,
- path_match.path.to_string_lossy()
- )
+ path_match
+ .path_prefix
+ .join(&path_match.path)
+ .display(path_style)
+ .to_string()
})
.collect();
@@ -183,9 +184,11 @@ impl SlashCommand for DiagnosticsSlashCommand {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
- let options = Options::parse(arguments);
+ let project = workspace.read(cx).project();
+ let path_style = project.read(cx).path_style(cx);
+ let options = Options::parse(arguments, path_style);
- let task = collect_diagnostics(workspace.read(cx).project().clone(), options, cx);
+ let task = collect_diagnostics(project.clone(), options, cx);
window.spawn(cx, async move |_| {
task.await?
@@ -204,14 +207,14 @@ struct Options {
const INCLUDE_WARNINGS_ARGUMENT: &str = "--include-warnings";
impl Options {
- fn parse(arguments: &[String]) -> Self {
+ fn parse(arguments: &[String], path_style: PathStyle) -> Self {
let mut include_warnings = false;
let mut path_matcher = None;
for arg in arguments {
if arg == INCLUDE_WARNINGS_ARGUMENT {
include_warnings = true;
} else {
- path_matcher = PathMatcher::new(&[arg.to_owned()]).log_err();
+ path_matcher = PathMatcher::new(&[arg.to_owned()], path_style).log_err();
}
}
Self {
@@ -237,21 +240,15 @@ fn collect_diagnostics(
None
};
+ let path_style = project.read(cx).path_style(cx);
let glob_is_exact_file_match = if let Some(path) = options
.path_matcher
.as_ref()
.and_then(|pm| pm.sources().first())
{
- PathBuf::try_from(path)
- .ok()
- .and_then(|path| {
- project.read(cx).worktrees(cx).find_map(|worktree| {
- let worktree = worktree.read(cx);
- let worktree_root_path = Path::new(worktree.root_name());
- let relative_path = path.strip_prefix(worktree_root_path).ok()?;
- worktree.absolutize(relative_path).ok()
- })
- })
+ project
+ .read(cx)
+ .find_project_path(Path::new(path), cx)
.is_some()
} else {
false
@@ -263,9 +260,8 @@ fn collect_diagnostics(
.diagnostic_summaries(false, cx)
.flat_map(|(path, _, summary)| {
let worktree = project.read(cx).worktree_for_id(path.worktree_id, cx)?;
- let mut path_buf = PathBuf::from(worktree.read(cx).root_name());
- path_buf.push(&path.path);
- Some((path, path_buf, summary))
+ let full_path = worktree.read(cx).root_name().join(&path.path);
+ Some((path, full_path, summary))
})
.collect();
@@ -281,7 +277,7 @@ fn collect_diagnostics(
let mut project_summary = DiagnosticSummary::default();
for (project_path, path, summary) in diagnostic_summaries {
if let Some(path_matcher) = &options.path_matcher
- && !path_matcher.is_match(&path)
+ && !path_matcher.is_match(&path.as_std_path())
{
continue;
}
@@ -294,7 +290,7 @@ fn collect_diagnostics(
}
let last_end = output.text.len();
- let file_path = path.to_string_lossy().to_string();
+ let file_path = path.display(path_style).to_string();
if !glob_is_exact_file_match {
writeln!(&mut output.text, "{file_path}").unwrap();
}
@@ -14,11 +14,11 @@ use smol::stream::StreamExt;
use std::{
fmt::Write,
ops::{Range, RangeInclusive},
- path::{Path, PathBuf},
+ path::Path,
sync::{Arc, atomic::AtomicBool},
};
use ui::prelude::*;
-use util::ResultExt;
+use util::{ResultExt, rel_path::RelPath};
use workspace::Workspace;
use worktree::ChildEntriesOptions;
@@ -48,7 +48,7 @@ impl FileSlashCommand {
include_dirs: true,
include_ignored: false,
};
- let entries = worktree.child_entries_with_options(Path::new(""), options);
+ let entries = worktree.child_entries_with_options(RelPath::empty(), options);
entries.map(move |entry| {
(
project::ProjectPath {
@@ -61,19 +61,18 @@ impl FileSlashCommand {
}))
.collect::<Vec<_>>();
- let path_prefix: Arc<str> = Arc::default();
+ let path_prefix: Arc<RelPath> = RelPath::empty().into();
Task::ready(
entries
.into_iter()
.filter_map(|(entry, is_dir)| {
let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
- let mut full_path = PathBuf::from(worktree.read(cx).root_name());
- full_path.push(&entry.path);
+ let full_path = worktree.read(cx).root_name().join(&entry.path);
Some(PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: entry.worktree_id.to_usize(),
- path: full_path.into(),
+ path: full_path,
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
is_dir,
@@ -149,6 +148,8 @@ impl SlashCommand for FileSlashCommand {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
+ let path_style = workspace.read(cx).path_style(cx);
+
let paths = self.search_paths(
arguments.last().cloned().unwrap_or_default(),
cancellation_flag,
@@ -161,14 +162,14 @@ impl SlashCommand for FileSlashCommand {
.await
.into_iter()
.filter_map(|path_match| {
- let text = format!(
- "{}{}",
- path_match.path_prefix,
- path_match.path.to_string_lossy()
- );
+ let text = path_match
+ .path_prefix
+ .join(&path_match.path)
+ .display(path_style)
+ .to_string();
let mut label = CodeLabel::default();
- let file_name = path_match.path.file_name()?.to_string_lossy();
+ let file_name = path_match.path.file_name()?;
let label_text = if path_match.is_dir {
format!("{}/ ", file_name)
} else {
@@ -247,14 +248,13 @@ fn collect_files(
cx.spawn(async move |cx| {
for snapshot in snapshots {
let worktree_id = snapshot.id();
- let mut directory_stack: Vec<Arc<Path>> = Vec::new();
- let mut folded_directory_names_stack = Vec::new();
+ let path_style = snapshot.path_style();
+ let mut directory_stack: Vec<Arc<RelPath>> = Vec::new();
+ let mut folded_directory_names: Arc<RelPath> = RelPath::empty().into();
let mut is_top_level_directory = true;
for entry in snapshot.entries(false, 0) {
- let mut path_including_worktree_name = PathBuf::new();
- path_including_worktree_name.push(snapshot.root_name());
- path_including_worktree_name.push(&entry.path);
+ let path_including_worktree_name = snapshot.root_name().join(&entry.path);
if !matchers
.iter()
@@ -277,13 +277,7 @@ fn collect_files(
)))?;
}
- let filename = entry
- .path
- .file_name()
- .unwrap_or_default()
- .to_str()
- .unwrap_or_default()
- .to_string();
+ let filename = entry.path.file_name().unwrap_or_default().to_string();
if entry.is_dir() {
// Auto-fold directories that contain no files
@@ -292,24 +286,23 @@ fn collect_files(
if child_entries.next().is_none() && child.kind.is_dir() {
if is_top_level_directory {
is_top_level_directory = false;
- folded_directory_names_stack.push(
- path_including_worktree_name.to_string_lossy().to_string(),
- );
+ folded_directory_names =
+ folded_directory_names.join(&path_including_worktree_name);
} else {
- folded_directory_names_stack.push(filename.to_string());
+ folded_directory_names =
+ folded_directory_names.join(RelPath::new(&filename).unwrap());
}
continue;
}
} else {
// Skip empty directories
- folded_directory_names_stack.clear();
+ folded_directory_names = RelPath::empty().into();
continue;
}
- let prefix_paths = folded_directory_names_stack.drain(..).as_slice().join("/");
- if prefix_paths.is_empty() {
+ if folded_directory_names.is_empty() {
let label = if is_top_level_directory {
is_top_level_directory = false;
- path_including_worktree_name.to_string_lossy().to_string()
+ path_including_worktree_name.display(path_style).to_string()
} else {
filename
};
@@ -320,28 +313,23 @@ fn collect_files(
}))?;
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
SlashCommandContent::Text {
- text: label,
+ text: label.to_string(),
run_commands_in_text: false,
},
)))?;
directory_stack.push(entry.path.clone());
} else {
- // todo(windows)
- // Potential bug: this assumes that the path separator is always `\` on Windows
- let entry_name = format!(
- "{}{}{}",
- prefix_paths,
- std::path::MAIN_SEPARATOR_STR,
- &filename
- );
+ let entry_name =
+ folded_directory_names.join(RelPath::new(&filename).unwrap());
+ let entry_name = entry_name.display(path_style);
events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection {
icon: IconName::Folder,
- label: entry_name.clone().into(),
+ label: entry_name.to_string().into(),
metadata: None,
}))?;
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
SlashCommandContent::Text {
- text: entry_name,
+ text: entry_name.to_string(),
run_commands_in_text: false,
},
)))?;
@@ -356,7 +344,7 @@ fn collect_files(
} else if entry.is_file() {
let Some(open_buffer_task) = project_handle
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, &entry.path), cx)
+ project.open_buffer((worktree_id, entry.path.clone()), cx)
})
.ok()
else {
@@ -367,7 +355,9 @@ fn collect_files(
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
append_buffer_to_output(
&snapshot,
- Some(&path_including_worktree_name),
+ Some(Path::new(
+ path_including_worktree_name.display(path_style).as_ref(),
+ )),
&mut output,
)
.log_err();
@@ -462,10 +452,9 @@ pub fn build_entry_output_section(
/// This contains a small fork of the util::paths::PathMatcher, that is stricter about the prefix
/// check. Only subpaths pass the prefix check, rather than any prefix.
mod custom_path_matcher {
- use std::{fmt::Debug as _, path::Path};
-
use globset::{Glob, GlobSet, GlobSetBuilder};
- use util::paths::SanitizedPath;
+ use std::fmt::Debug as _;
+ use util::{paths::SanitizedPath, rel_path::RelPath};
#[derive(Clone, Debug, Default)]
pub struct PathMatcher {
@@ -492,12 +481,12 @@ mod custom_path_matcher {
pub fn new(globs: &[String]) -> Result<Self, globset::Error> {
let globs = globs
.iter()
- .map(|glob| Glob::new(&SanitizedPath::new(glob).to_glob_string()))
+ .map(|glob| Glob::new(&SanitizedPath::new(glob).to_string()))
.collect::<Result<Vec<_>, _>>()?;
let sources = globs.iter().map(|glob| glob.glob().to_owned()).collect();
let sources_with_trailing_slash = globs
.iter()
- .map(|glob| glob.glob().to_string() + std::path::MAIN_SEPARATOR_STR)
+ .map(|glob| glob.glob().to_string() + "/")
.collect();
let mut glob_builder = GlobSetBuilder::new();
for single_glob in globs {
@@ -511,16 +500,13 @@ mod custom_path_matcher {
})
}
- pub fn is_match<P: AsRef<Path>>(&self, other: P) -> bool {
- let other_path = other.as_ref();
+ pub fn is_match(&self, other: &RelPath) -> bool {
self.sources
.iter()
.zip(self.sources_with_trailing_slash.iter())
.any(|(source, with_slash)| {
- let as_bytes = other_path.as_os_str().as_encoded_bytes();
- // todo(windows)
- // Potential bug: this assumes that the path separator is always `\` on Windows
- let with_slash = if source.ends_with(std::path::MAIN_SEPARATOR_STR) {
+ let as_bytes = other.as_str().as_bytes();
+ let with_slash = if source.ends_with('/') {
source.as_bytes()
} else {
with_slash.as_bytes()
@@ -528,13 +514,13 @@ mod custom_path_matcher {
as_bytes.starts_with(with_slash) || as_bytes.ends_with(source.as_bytes())
})
- || self.glob.is_match(other_path)
- || self.check_with_end_separator(other_path)
+ || self.glob.is_match(other)
+ || self.check_with_end_separator(other)
}
- fn check_with_end_separator(&self, path: &Path) -> bool {
- let path_str = path.to_string_lossy();
- let separator = std::path::MAIN_SEPARATOR_STR;
+ fn check_with_end_separator(&self, path: &RelPath) -> bool {
+ let path_str = path.as_str();
+ let separator = "/";
if path_str.ends_with(separator) {
false
} else {
@@ -96,9 +96,7 @@ impl Tool for CopyPathTool {
.and_then(|project_path| project.entry_for_path(&project_path, cx))
{
Some(entity) => match project.find_project_path(&input.destination_path, cx) {
- Some(project_path) => {
- project.copy_entry(entity.id, None, project_path.path, cx)
- }
+ Some(project_path) => project.copy_entry(entity.id, project_path, cx),
None => Task::ready(Err(anyhow!(
"Destination path {} was outside the project.",
input.destination_path
@@ -8,7 +8,7 @@ use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchem
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use std::{fmt::Write, path::Path, sync::Arc};
+use std::{fmt::Write, sync::Arc};
use ui::IconName;
use util::markdown::MarkdownInlineCode;
@@ -150,9 +150,7 @@ impl Tool for DiagnosticsTool {
has_diagnostics = true;
output.push_str(&format!(
"{}: {} error(s), {} warning(s)\n",
- Path::new(worktree.read(cx).root_name())
- .join(project_path.path)
- .display(),
+ worktree.read(cx).absolutize(&project_path.path).display(),
summary.error_count,
summary.warning_count
));
@@ -38,6 +38,7 @@ use settings::Settings;
use std::{
cmp::Reverse,
collections::HashSet,
+ ffi::OsStr,
ops::Range,
path::{Path, PathBuf},
sync::Arc,
@@ -45,7 +46,7 @@ use std::{
};
use theme::ThemeSettings;
use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*};
-use util::ResultExt;
+use util::{ResultExt, rel_path::RelPath};
use workspace::Workspace;
pub struct EditFileTool;
@@ -146,11 +147,11 @@ impl Tool for EditFileTool {
// If any path component matches the local settings folder, then this could affect
// the editor in ways beyond the project source, so prompt.
- let local_settings_folder = paths::local_settings_folder_relative_path();
+ let local_settings_folder = paths::local_settings_folder_name();
let path = Path::new(&input.path);
if path
.components()
- .any(|component| component.as_os_str() == local_settings_folder.as_os_str())
+ .any(|c| c.as_os_str() == <str as AsRef<OsStr>>::as_ref(local_settings_folder))
{
return true;
}
@@ -195,10 +196,10 @@ impl Tool for EditFileTool {
let mut description = input.display_description.clone();
// Add context about why confirmation may be needed
- let local_settings_folder = paths::local_settings_folder_relative_path();
+ let local_settings_folder = paths::local_settings_folder_name();
if path
.components()
- .any(|c| c.as_os_str() == local_settings_folder.as_os_str())
+ .any(|c| c.as_os_str() == <str as AsRef<OsStr>>::as_ref(local_settings_folder))
{
description.push_str(" (local settings)");
} else if let Ok(canonical_path) = std::fs::canonicalize(&input.path)
@@ -377,7 +378,7 @@ impl Tool for EditFileTool {
.await;
let output = EditFileToolOutput {
- original_path: project_path.path.to_path_buf(),
+ original_path: project_path.path.as_std_path().to_owned(),
new_text,
old_text,
raw_output: Some(agent_output),
@@ -549,10 +550,11 @@ fn resolve_path(
let file_name = input
.path
.file_name()
+ .and_then(|file_name| file_name.to_str())
.context("Can't create file: invalid filename")?;
let new_file_path = parent_project_path.map(|parent| ProjectPath {
- path: Arc::from(parent.path.join(file_name)),
+ path: parent.path.join(RelPath::new(file_name).unwrap()),
..parent
});
@@ -1236,7 +1238,7 @@ mod tests {
use serde_json::json;
use settings::SettingsStore;
use std::fs;
- use util::path;
+ use util::{path, rel_path::rel_path};
#[gpui::test]
async fn test_edit_nonexistent_file(cx: &mut TestAppContext) {
@@ -1355,14 +1357,10 @@ mod tests {
cx.update(|cx| resolve_path(&input, project, cx))
}
+ #[track_caller]
fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) {
- let actual = path
- .expect("Should return valid path")
- .path
- .to_str()
- .unwrap()
- .replace("\\", "/"); // Naive Windows paths normalization
- assert_eq!(actual, expected);
+ let actual = path.expect("Should return valid path").path;
+ assert_eq!(actual.as_ref(), rel_path(expected));
}
#[test]
@@ -1976,25 +1974,22 @@ mod tests {
let project = Project::test(fs.clone(), [path!("/home/user/myproject").as_ref()], cx).await;
// Get the actual local settings folder name
- let local_settings_folder = paths::local_settings_folder_relative_path();
+ let local_settings_folder = paths::local_settings_folder_name();
// Test various config path patterns
let test_cases = vec![
(
- format!("{}/settings.json", local_settings_folder.display()),
+ format!("{local_settings_folder}/settings.json"),
true,
"Top-level local settings file".to_string(),
),
(
- format!(
- "myproject/{}/settings.json",
- local_settings_folder.display()
- ),
+ format!("myproject/{local_settings_folder}/settings.json"),
true,
"Local settings in project path".to_string(),
),
(
- format!("src/{}/config.toml", local_settings_folder.display()),
+ format!("src/{local_settings_folder}/config.toml"),
true,
"Local settings in subdirectory".to_string(),
),
@@ -2205,12 +2200,7 @@ mod tests {
("", false, "Empty path is treated as project root"),
// Root directory
("/", true, "Root directory should be outside project"),
- // Parent directory references - find_project_path resolves these
- (
- "project/../other",
- false,
- "Path with .. is resolved by find_project_path",
- ),
+ ("project/../other", true, "Path with .. is outside project"),
(
"project/./src/file.rs",
false,
@@ -161,10 +161,13 @@ impl Tool for FindPathTool {
}
fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Result<Vec<PathBuf>>> {
- let path_matcher = match PathMatcher::new([
- // Sometimes models try to search for "". In this case, return all paths in the project.
- if glob.is_empty() { "*" } else { glob },
- ]) {
+ let path_matcher = match PathMatcher::new(
+ [
+ // Sometimes models try to search for "". In this case, return all paths in the project.
+ if glob.is_empty() { "*" } else { glob },
+ ],
+ project.read(cx).path_style(cx),
+ ) {
Ok(matcher) => matcher,
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))),
};
@@ -178,10 +181,15 @@ fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Resu
Ok(snapshots
.iter()
.flat_map(|snapshot| {
- let root_name = PathBuf::from(snapshot.root_name());
snapshot
.entries(false, 0)
- .map(move |entry| root_name.join(&entry.path))
+ .map(move |entry| {
+ snapshot
+ .root_name()
+ .join(&entry.path)
+ .as_std_path()
+ .to_path_buf()
+ })
.filter(|path| path_matcher.is_match(&path))
})
.collect())
@@ -125,6 +125,7 @@ impl Tool for GrepTool {
.as_ref()
.into_iter()
.collect::<Vec<_>>(),
+ project.read(cx).path_style(cx),
) {
Ok(matcher) => matcher,
Err(error) => {
@@ -141,7 +142,7 @@ impl Tool for GrepTool {
.iter()
.chain(global_settings.private_files.sources().iter());
- match PathMatcher::new(exclude_patterns) {
+ match PathMatcher::new(exclude_patterns, project.read(cx).path_style(cx)) {
Ok(matcher) => matcher,
Err(error) => {
return Task::ready(Err(anyhow!("invalid exclude pattern: {error}"))).into();
@@ -4,11 +4,11 @@ use anyhow::{Result, anyhow};
use assistant_tool::{Tool, ToolResult};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
-use project::{Project, WorktreeSettings};
+use project::{Project, ProjectPath, WorktreeSettings};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
-use std::{fmt::Write, path::Path, sync::Arc};
+use std::{fmt::Write, sync::Arc};
use ui::IconName;
use util::markdown::MarkdownInlineCode;
@@ -100,7 +100,7 @@ impl Tool for ListDirectoryTool {
.filter_map(|worktree| {
worktree.read(cx).root_entry().and_then(|entry| {
if entry.is_dir() {
- entry.path.to_str()
+ Some(entry.path.as_str())
} else {
None
}
@@ -158,7 +158,6 @@ impl Tool for ListDirectoryTool {
}
let worktree_snapshot = worktree.read(cx).snapshot();
- let worktree_root_name = worktree.read(cx).root_name().to_string();
let Some(entry) = worktree_snapshot.entry_for_path(&project_path.path) else {
return Task::ready(Err(anyhow!("Path not found: {}", input.path))).into();
@@ -180,23 +179,22 @@ impl Tool for ListDirectoryTool {
continue;
}
- if project
- .read(cx)
- .find_project_path(&entry.path, cx)
- .map(|project_path| {
- let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
+ let project_path = ProjectPath {
+ worktree_id: worktree_snapshot.id(),
+ path: entry.path.clone(),
+ };
+ let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
- worktree_settings.is_path_excluded(&project_path.path)
- || worktree_settings.is_path_private(&project_path.path)
- })
- .unwrap_or(false)
+ if worktree_settings.is_path_excluded(&project_path.path)
+ || worktree_settings.is_path_private(&project_path.path)
{
continue;
}
- let full_path = Path::new(&worktree_root_name)
+ let full_path = worktree_snapshot
+ .root_name()
.join(&entry.path)
- .display()
+ .display(worktree_snapshot.path_style())
.to_string();
if entry.is_dir() {
folders.push(full_path);
@@ -108,7 +108,7 @@ impl Tool for MovePathTool {
.and_then(|project_path| project.entry_for_path(&project_path, cx))
{
Some(entity) => match project.find_project_path(&input.destination_path, cx) {
- Some(project_path) => project.rename_entry(entity.id, project_path.path, cx),
+ Some(project_path) => project.rename_entry(entity.id, project_path, cx),
None => Task::ready(Err(anyhow!(
"Destination path {} was outside the project.",
input.destination_path
@@ -24,7 +24,7 @@ use postage::{sink::Sink, stream::Stream, watch};
use project::Project;
use settings::Settings as _;
use std::{future::Future, mem, rc::Rc, sync::Arc, time::Duration};
-use util::{ResultExt, TryFutureExt, post_inc};
+use util::{ResultExt, TryFutureExt, paths::PathStyle, post_inc};
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
@@ -1163,6 +1163,7 @@ impl Room {
room_id: self.id(),
worktrees: project.read(cx).worktree_metadata_protos(cx),
is_ssh_project: project.read(cx).is_via_remote_server(),
+ windows_paths: Some(project.read(cx).path_style(cx) == PathStyle::Windows),
});
cx.spawn(async move |this, cx| {
@@ -405,7 +405,7 @@ impl Telemetry {
let mut project_types: HashSet<&str> = HashSet::new();
for (path, _, _) in updated_entries_set.iter() {
- let Some(file_name) = path.file_name().and_then(|f| f.to_str()) else {
+ let Some(file_name) = path.file_name() else {
continue;
};
@@ -601,6 +601,7 @@ mod tests {
use http_client::FakeHttpClient;
use std::collections::HashMap;
use telemetry_events::FlexibleEvent;
+ use util::rel_path::RelPath;
use worktree::{PathChange, ProjectEntryId, WorktreeId};
#[gpui::test]
@@ -855,12 +856,12 @@ mod tests {
let entries: Vec<_> = file_paths
.into_iter()
.enumerate()
- .map(|(i, path)| {
- (
- Arc::from(std::path::Path::new(path)),
+ .filter_map(|(i, path)| {
+ Some((
+ Arc::from(RelPath::new(path).ok()?),
ProjectEntryId::from_proto(i as u64 + 1),
PathChange::Added,
- )
+ ))
})
.collect();
let updated_entries: UpdatedEntriesSet = Arc::from(entries.as_slice());
@@ -61,7 +61,8 @@ CREATE TABLE "projects" (
"host_user_id" INTEGER REFERENCES users (id),
"host_connection_id" INTEGER,
"host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
- "unregistered" BOOLEAN NOT NULL DEFAULT FALSE
+ "unregistered" BOOLEAN NOT NULL DEFAULT FALSE,
+ "windows_paths" BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id");
@@ -0,0 +1 @@
+ALTER TABLE projects ADD COLUMN windows_paths BOOLEAN DEFAULT FALSE;
@@ -34,6 +34,7 @@ use std::{
};
use time::PrimitiveDateTime;
use tokio::sync::{Mutex, OwnedMutexGuard};
+use util::paths::PathStyle;
use worktree_settings_file::LocalSettingsKind;
#[cfg(test)]
@@ -598,6 +599,7 @@ pub struct Project {
pub worktrees: BTreeMap<u64, Worktree>,
pub repositories: Vec<proto::UpdateRepository>,
pub language_servers: Vec<LanguageServer>,
+ pub path_style: PathStyle,
}
pub struct ProjectCollaborator {
@@ -33,6 +33,7 @@ impl Database {
connection: ConnectionId,
worktrees: &[proto::WorktreeMetadata],
is_ssh_project: bool,
+ windows_paths: bool,
) -> Result<TransactionGuard<(ProjectId, proto::Room)>> {
self.room_transaction(room_id, |tx| async move {
let participant = room_participant::Entity::find()
@@ -69,6 +70,7 @@ impl Database {
connection.owner_id as i32,
))),
id: ActiveValue::NotSet,
+ windows_paths: ActiveValue::set(windows_paths),
}
.insert(&*tx)
.await?;
@@ -1046,6 +1048,12 @@ impl Database {
.all(tx)
.await?;
+ let path_style = if project.windows_paths {
+ PathStyle::Windows
+ } else {
+ PathStyle::Posix
+ };
+
let project = Project {
id: project.id,
role,
@@ -1073,6 +1081,7 @@ impl Database {
capabilities: language_server.capabilities,
})
.collect(),
+ path_style,
};
Ok((project, replica_id as ReplicaId))
}
@@ -12,6 +12,7 @@ pub struct Model {
pub host_user_id: Option<UserId>,
pub host_connection_id: Option<i32>,
pub host_connection_server_id: Option<ServerId>,
+ pub windows_paths: bool,
}
impl Model {
@@ -558,18 +558,18 @@ async fn test_project_count(db: &Arc<Database>) {
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
- db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[], false)
+ db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[], false, false)
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 1);
- db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[], false)
+ db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[], false, false)
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
// Projects shared by admins aren't counted.
- db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[], false)
+ db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[], false, false)
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
@@ -36,6 +36,7 @@ use reqwest_client::ReqwestClient;
use rpc::proto::split_repository_update;
use supermaven_api::{CreateExternalUserRequest, SupermavenAdminApi};
use tracing::Span;
+use util::paths::PathStyle;
use futures::{
FutureExt, SinkExt, StreamExt, TryStreamExt, channel::oneshot, future::BoxFuture,
@@ -1879,6 +1880,7 @@ async fn share_project(
session.connection_id,
&request.worktrees,
request.is_ssh_project,
+ request.windows_paths.unwrap_or(false),
)
.await?;
response.send(proto::ShareProjectResponse {
@@ -2012,6 +2014,7 @@ async fn join_project(
language_servers,
language_server_capabilities,
role: project.role.into(),
+ windows_paths: project.path_style == PathStyle::Windows,
})?;
for (worktree_id, worktree) in mem::take(&mut project.worktrees) {
@@ -13,6 +13,7 @@ use gpui::{BackgroundExecutor, Context, Entity, TestAppContext, Window};
use rpc::{RECEIVE_TIMEOUT, proto::PeerId};
use serde_json::json;
use std::ops::Range;
+use util::rel_path::rel_path;
use workspace::CollaboratorId;
#[gpui::test]
@@ -256,7 +257,13 @@ async fn test_channel_notes_participant_indices(
executor.start_waiting();
let editor_a = workspace_a
.update_in(cx_a, |workspace, window, cx| {
- workspace.open_path((worktree_id_a, "file.txt"), None, true, window, cx)
+ workspace.open_path(
+ (worktree_id_a, rel_path("file.txt")),
+ None,
+ true,
+ window,
+ cx,
+ )
})
.await
.unwrap()
@@ -265,7 +272,13 @@ async fn test_channel_notes_participant_indices(
executor.start_waiting();
let editor_b = workspace_b
.update_in(cx_b, |workspace, window, cx| {
- workspace.open_path((worktree_id_a, "file.txt"), None, true, window, cx)
+ workspace.open_path(
+ (worktree_id_a, rel_path("file.txt")),
+ None,
+ true,
+ window,
+ cx,
+ )
})
.await
.unwrap()
@@ -4,6 +4,7 @@ use chrono::Utc;
use editor::Editor;
use gpui::{BackgroundExecutor, TestAppContext};
use rpc::proto;
+use util::rel_path::rel_path;
#[gpui::test]
async fn test_channel_guests(
@@ -55,7 +56,7 @@ async fn test_channel_guests(
project_b
.update(cx_b, |project, cx| {
let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
- project.create_entry((worktree_id, "b.txt"), false, cx)
+ project.create_entry((worktree_id, rel_path("b.txt")), false, cx)
})
.await
.is_err()
@@ -16,6 +16,7 @@ use editor::{
};
use fs::Fs;
use futures::{SinkExt, StreamExt, channel::mpsc, lock::Mutex};
+use git::repository::repo_path;
use gpui::{App, Rgba, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
use indoc::indoc;
use language::FakeLspAdapter;
@@ -38,7 +39,7 @@ use std::{
},
};
use text::Point;
-use util::{path, uri};
+use util::{path, rel_path::rel_path, uri};
use workspace::{CloseIntent, Workspace};
#[gpui::test(iterations = 10)]
@@ -97,7 +98,7 @@ async fn test_host_disconnect(
let editor_b = workspace_b
.update(cx_b, |workspace, window, cx| {
- workspace.open_path((worktree_id, "b.txt"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("b.txt")), None, true, window, cx)
})
.unwrap()
.await
@@ -205,7 +206,9 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
// Open a buffer as client A
let buffer_a = project_a
- .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+ .update(cx_a, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("a.txt")), cx)
+ })
.await
.unwrap();
let cx_a = cx_a.add_empty_window();
@@ -222,7 +225,9 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
let cx_b = cx_b.add_empty_window();
// Open a buffer as client B
let buffer_b = project_b
- .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+ .update(cx_b, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("a.txt")), cx)
+ })
.await
.unwrap();
let editor_b = cx_b
@@ -334,7 +339,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
// Open a file in an editor as the guest.
let buffer_b = project_b
- .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+ .update(cx_b, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("main.rs")), cx)
+ })
.await
.unwrap();
let cx_b = cx_b.add_empty_window();
@@ -408,7 +415,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
// Open the buffer on the host.
let buffer_a = project_a
- .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+ .update(cx_a, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("main.rs")), cx)
+ })
.await
.unwrap();
cx_a.executor().run_until_parked();
@@ -599,7 +608,7 @@ async fn test_collaborating_with_code_actions(
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
let editor_b = workspace_b
.update_in(cx_b, |workspace, window, cx| {
- workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
})
.await
.unwrap()
@@ -825,7 +834,7 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
let editor_b = workspace_b
.update_in(cx_b, |workspace, window, cx| {
- workspace.open_path((worktree_id, "one.rs"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("one.rs")), None, true, window, cx)
})
.await
.unwrap()
@@ -1072,7 +1081,7 @@ async fn test_slow_lsp_server(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
let editor_b = workspace_b
.update_in(cx_b, |workspace, window, cx| {
- workspace.open_path((worktree_id, "one.rs"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("one.rs")), None, true, window, cx)
})
.await
.unwrap()
@@ -1412,7 +1421,10 @@ async fn test_share_project(
project_b
.update(cx_b, |project, cx| {
let worktree = project.worktrees(cx).next().unwrap();
- let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap();
+ let entry = worktree
+ .read(cx)
+ .entry_for_path(rel_path("ignored-dir"))
+ .unwrap();
project.expand_entry(worktree_id, entry.id, cx).unwrap()
})
.await
@@ -1435,17 +1447,21 @@ async fn test_share_project(
// Open the same file as client B and client A.
let buffer_b = project_b
- .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
+ .update(cx_b, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("b.txt")), cx)
+ })
.await
.unwrap();
buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
project_a.read_with(cx_a, |project, cx| {
- assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
+ assert!(project.has_open_buffer((worktree_id, rel_path("b.txt")), cx))
});
let buffer_a = project_a
- .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
+ .update(cx_a, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("b.txt")), cx)
+ })
.await
.unwrap();
@@ -1553,7 +1569,9 @@ async fn test_on_input_format_from_host_to_guest(
// Open a file in an editor as the host.
let buffer_a = project_a
- .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+ .update(cx_a, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("main.rs")), cx)
+ })
.await
.unwrap();
let cx_a = cx_a.add_empty_window();
@@ -1586,7 +1604,9 @@ async fn test_on_input_format_from_host_to_guest(
// Open the buffer on the guest and see that the formatting worked
let buffer_b = project_b
- .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+ .update(cx_b, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("main.rs")), cx)
+ })
.await
.unwrap();
@@ -1686,7 +1706,9 @@ async fn test_on_input_format_from_guest_to_host(
// Open a file in an editor as the guest.
let buffer_b = project_b
- .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+ .update(cx_b, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("main.rs")), cx)
+ })
.await
.unwrap();
let cx_b = cx_b.add_empty_window();
@@ -1732,7 +1754,9 @@ async fn test_on_input_format_from_guest_to_host(
// Open the buffer on the host and see that the formatting worked
let buffer_a = project_a
- .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+ .update(cx_a, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("main.rs")), cx)
+ })
.await
.unwrap();
executor.run_until_parked();
@@ -1881,7 +1905,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
.unwrap();
let editor_a = workspace_a
.update_in(cx_a, |workspace, window, cx| {
- workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
})
.await
.unwrap()
@@ -1931,7 +1955,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
let editor_b = workspace_b
.update_in(cx_b, |workspace, window, cx| {
- workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
})
.await
.unwrap()
@@ -2126,7 +2150,7 @@ async fn test_inlay_hint_refresh_is_forwarded(
let editor_a = workspace_a
.update_in(cx_a, |workspace, window, cx| {
- workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
})
.await
.unwrap()
@@ -2135,7 +2159,7 @@ async fn test_inlay_hint_refresh_is_forwarded(
let editor_b = workspace_b
.update_in(cx_b, |workspace, window, cx| {
- workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
})
.await
.unwrap()
@@ -2313,7 +2337,7 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo
.unwrap();
let editor_a = workspace_a
.update_in(cx_a, |workspace, window, cx| {
- workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
})
.await
.unwrap()
@@ -2373,7 +2397,7 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
let editor_b = workspace_b
.update_in(cx_b, |workspace, window, cx| {
- workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
})
.await
.unwrap()
@@ -2594,7 +2618,7 @@ async fn test_lsp_pull_diagnostics(
.unwrap();
let editor_a_main = workspace_a
.update_in(cx_a, |workspace, window, cx| {
- workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
})
.await
.unwrap()
@@ -2953,7 +2977,7 @@ async fn test_lsp_pull_diagnostics(
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
let editor_b_main = workspace_b
.update_in(cx_b, |workspace, window, cx| {
- workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
})
.await
.unwrap()
@@ -3001,7 +3025,7 @@ async fn test_lsp_pull_diagnostics(
let editor_b_lib = workspace_b
.update_in(cx_b, |workspace, window, cx| {
- workspace.open_path((worktree_id, "lib.rs"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("lib.rs")), None, true, window, cx)
})
.await
.unwrap()
@@ -3355,7 +3379,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
};
client_a.fs().set_blame_for_repo(
Path::new(path!("/my-repo/.git")),
- vec![("file.txt".into(), blame)],
+ vec![(repo_path("file.txt"), blame)],
);
let (project_a, worktree_id) = client_a.build_local_project(path!("/my-repo"), cx_a).await;
@@ -3368,7 +3392,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
let editor_a = workspace_a
.update_in(cx_a, |workspace, window, cx| {
- workspace.open_path((worktree_id, "file.txt"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("file.txt")), None, true, window, cx)
})
.await
.unwrap()
@@ -3380,7 +3404,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
let editor_b = workspace_b
.update_in(cx_b, |workspace, window, cx| {
- workspace.open_path((worktree_id, "file.txt"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("file.txt")), None, true, window, cx)
})
.await
.unwrap()
@@ -3558,13 +3582,13 @@ async fn test_collaborating_with_editorconfig(
.unwrap();
let main_buffer_a = project_a
.update(cx_a, |p, cx| {
- p.open_buffer((worktree_id, "src/main.rs"), cx)
+ p.open_buffer((worktree_id, rel_path("src/main.rs")), cx)
})
.await
.unwrap();
let other_buffer_a = project_a
.update(cx_a, |p, cx| {
- p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
+ p.open_buffer((worktree_id, rel_path("src/other_mod/other.rs")), cx)
})
.await
.unwrap();
@@ -3592,13 +3616,13 @@ async fn test_collaborating_with_editorconfig(
let project_b = client_b.join_remote_project(project_id, cx_b).await;
let main_buffer_b = project_b
.update(cx_b, |p, cx| {
- p.open_buffer((worktree_id, "src/main.rs"), cx)
+ p.open_buffer((worktree_id, rel_path("src/main.rs")), cx)
})
.await
.unwrap();
let other_buffer_b = project_b
.update(cx_b, |p, cx| {
- p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
+ p.open_buffer((worktree_id, rel_path("src/other_mod/other.rs")), cx)
})
.await
.unwrap();
@@ -3717,7 +3741,7 @@ fn main() { let foo = other::foo(); }"};
let editorconfig_buffer_b = project_b
.update(cx_b, |p, cx| {
- p.open_buffer((worktree_id, "src/other_mod/.editorconfig"), cx)
+ p.open_buffer((worktree_id, rel_path("src/other_mod/.editorconfig")), cx)
})
.await
.unwrap();
@@ -3794,7 +3818,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let project_path = ProjectPath {
worktree_id,
- path: Arc::from(Path::new(&"test.txt")),
+ path: rel_path(&"test.txt").into(),
};
let abs_path = project_a.read_with(cx_a, |project, cx| {
project
@@ -4017,7 +4041,7 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
let editor_a = workspace_a
.update_in(cx_a, |workspace, window, cx| {
- workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
})
.await
.unwrap()
@@ -4026,7 +4050,7 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
let editor_b = workspace_b
.update_in(cx_b, |workspace, window, cx| {
- workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
})
.await
.unwrap()
@@ -16,7 +16,7 @@ use rpc::proto::PeerId;
use serde_json::json;
use settings::SettingsStore;
use text::{Point, ToPoint};
-use util::{path, test::sample_text};
+use util::{path, rel_path::rel_path, test::sample_text};
use workspace::{CollaboratorId, SplitDirection, Workspace, item::ItemHandle as _};
use super::TestClient;
@@ -86,7 +86,7 @@ async fn test_basic_following(
let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
let editor_a1 = workspace_a
.update_in(cx_a, |workspace, window, cx| {
- workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
})
.await
.unwrap()
@@ -94,7 +94,7 @@ async fn test_basic_following(
.unwrap();
let editor_a2 = workspace_a
.update_in(cx_a, |workspace, window, cx| {
- workspace.open_path((worktree_id, "2.txt"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("2.txt")), None, true, window, cx)
})
.await
.unwrap()
@@ -104,7 +104,7 @@ async fn test_basic_following(
// Client B opens an editor.
let editor_b1 = workspace_b
.update_in(cx_b, |workspace, window, cx| {
- workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
})
.await
.unwrap()
@@ -146,7 +146,7 @@ async fn test_basic_following(
});
assert_eq!(
cx_b.read(|cx| editor_b2.project_path(cx)),
- Some((worktree_id, "2.txt").into())
+ Some((worktree_id, rel_path("2.txt")).into())
);
assert_eq!(
editor_b2.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
@@ -286,12 +286,12 @@ async fn test_basic_following(
let multibuffer_a = cx_a.new(|cx| {
let buffer_a1 = project_a.update(cx, |project, cx| {
project
- .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
+ .get_open_buffer(&(worktree_id, rel_path("1.txt")).into(), cx)
.unwrap()
});
let buffer_a2 = project_a.update(cx, |project, cx| {
project
- .get_open_buffer(&(worktree_id, "2.txt").into(), cx)
+ .get_open_buffer(&(worktree_id, rel_path("2.txt")).into(), cx)
.unwrap()
});
let mut result = MultiBuffer::new(Capability::ReadWrite);
@@ -618,13 +618,13 @@ async fn test_following_tab_order(
//Open 1, 3 in that order on client A
workspace_a
.update_in(cx_a, |workspace, window, cx| {
- workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
})
.await
.unwrap();
workspace_a
.update_in(cx_a, |workspace, window, cx| {
- workspace.open_path((worktree_id, "3.txt"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("3.txt")), None, true, window, cx)
})
.await
.unwrap();
@@ -632,14 +632,7 @@ async fn test_following_tab_order(
let pane_paths = |pane: &Entity<workspace::Pane>, cx: &mut VisualTestContext| {
pane.update(cx, |pane, cx| {
pane.items()
- .map(|item| {
- item.project_path(cx)
- .unwrap()
- .path
- .to_str()
- .unwrap()
- .to_owned()
- })
+ .map(|item| item.project_path(cx).unwrap().path.as_str().to_owned())
.collect::<Vec<_>>()
})
};
@@ -656,7 +649,7 @@ async fn test_following_tab_order(
//Open just 2 on client B
workspace_b
.update_in(cx_b, |workspace, window, cx| {
- workspace.open_path((worktree_id, "2.txt"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("2.txt")), None, true, window, cx)
})
.await
.unwrap();
@@ -668,7 +661,7 @@ async fn test_following_tab_order(
//Open just 1 on client B
workspace_b
.update_in(cx_b, |workspace, window, cx| {
- workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
})
.await
.unwrap();
@@ -728,7 +721,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
workspace_a
.update_in(cx_a, |workspace, window, cx| {
- workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
})
.await
.unwrap()
@@ -739,7 +732,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
workspace_b
.update_in(cx_b, |workspace, window, cx| {
- workspace.open_path((worktree_id, "2.txt"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("2.txt")), None, true, window, cx)
})
.await
.unwrap()
@@ -816,14 +809,14 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
// Clients A and B each open a new file.
workspace_a
.update_in(cx_a, |workspace, window, cx| {
- workspace.open_path((worktree_id, "3.txt"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("3.txt")), None, true, window, cx)
})
.await
.unwrap();
workspace_b
.update_in(cx_b, |workspace, window, cx| {
- workspace.open_path((worktree_id, "4.txt"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("4.txt")), None, true, window, cx)
})
.await
.unwrap();
@@ -1259,7 +1252,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
let _editor_a1 = workspace_a
.update_in(cx_a, |workspace, window, cx| {
- workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
})
.await
.unwrap()
@@ -1359,7 +1352,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
// When client B activates a different item in the original pane, it automatically stops following client A.
workspace_b
.update_in(cx_b, |workspace, window, cx| {
- workspace.open_path((worktree_id, "2.txt"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("2.txt")), None, true, window, cx)
})
.await
.unwrap();
@@ -1492,7 +1485,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
workspace_a
.update_in(cx_a, |workspace, window, cx| {
- workspace.open_path((worktree_id_a, "w.rs"), None, true, window, cx)
+ workspace.open_path((worktree_id_a, rel_path("w.rs")), None, true, window, cx)
})
.await
.unwrap();
@@ -1545,7 +1538,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
// b moves to x.rs in a's project, and a follows
workspace_b_project_a
.update_in(&mut cx_b2, |workspace, window, cx| {
- workspace.open_path((worktree_id_a, "x.rs"), None, true, window, cx)
+ workspace.open_path((worktree_id_a, rel_path("x.rs")), None, true, window, cx)
})
.await
.unwrap();
@@ -1574,7 +1567,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
// b moves to y.rs in b's project, a is still following but can't yet see
workspace_b
.update_in(cx_b, |workspace, window, cx| {
- workspace.open_path((worktree_id_b, "y.rs"), None, true, window, cx)
+ workspace.open_path((worktree_id_b, rel_path("y.rs")), None, true, window, cx)
})
.await
.unwrap();
@@ -1759,7 +1752,7 @@ async fn test_following_into_excluded_file(
// Client A opens editors for a regular file and an excluded file.
let editor_for_regular = workspace_a
.update_in(cx_a, |workspace, window, cx| {
- workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
})
.await
.unwrap()
@@ -1767,7 +1760,13 @@ async fn test_following_into_excluded_file(
.unwrap();
let editor_for_excluded_a = workspace_a
.update_in(cx_a, |workspace, window, cx| {
- workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, window, cx)
+ workspace.open_path(
+ (worktree_id, rel_path(".git/COMMIT_EDITMSG")),
+ None,
+ true,
+ window,
+ cx,
+ )
})
.await
.unwrap()
@@ -1805,7 +1804,7 @@ async fn test_following_into_excluded_file(
});
assert_eq!(
cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
- Some((worktree_id, ".git/COMMIT_EDITMSG").into())
+ Some((worktree_id, rel_path(".git/COMMIT_EDITMSG")).into())
);
assert_eq!(
editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
@@ -2051,7 +2050,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
// Client A opens a local buffer in their unshared project.
let _unshared_editor_a1 = workspace_a
.update_in(cx_a, |workspace, window, cx| {
- workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
})
.await
.unwrap()
@@ -1,7 +1,4 @@
-use std::{
- path::{Path, PathBuf},
- sync::Arc,
-};
+use std::path::Path;
use call::ActiveCall;
use git::status::{FileStatus, StatusCode, TrackedStatus};
@@ -9,7 +6,7 @@ use git_ui::project_diff::ProjectDiff;
use gpui::{TestAppContext, VisualTestContext};
use project::ProjectPath;
use serde_json::json;
-use util::path;
+use util::{path, rel_path::rel_path};
use workspace::Workspace;
//
@@ -41,13 +38,13 @@ async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext)
)
.await;
- client_a.fs().set_git_content_for_repo(
+ client_a.fs().set_head_and_index_for_repo(
Path::new(path!("/a/.git")),
&[
- ("changed.txt".into(), "before\n".to_string(), None),
- ("unchanged.txt".into(), "unchanged\n".to_string(), None),
- ("deleted.txt".into(), "deleted\n".to_string(), None),
- ("secret.pem".into(), "shh\n".to_string(), None),
+ ("changed.txt", "before\n".to_string()),
+ ("unchanged.txt", "unchanged\n".to_string()),
+ ("deleted.txt", "deleted\n".to_string()),
+ ("secret.pem", "shh\n".to_string()),
],
);
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
@@ -109,7 +106,7 @@ async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext)
project_b.update(cx_b, |project, cx| {
let project_path = ProjectPath {
worktree_id,
- path: Arc::from(PathBuf::from("unchanged.txt")),
+ path: rel_path("unchanged.txt").into(),
};
let status = project.project_path_git_status(&project_path, cx);
assert_eq!(
@@ -14,7 +14,10 @@ use client::{RECEIVE_TIMEOUT, User};
use collections::{HashMap, HashSet};
use fs::{FakeFs, Fs as _, RemoveOptions};
use futures::{StreamExt as _, channel::mpsc};
-use git::status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode};
+use git::{
+ repository::repo_path,
+ status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode},
+};
use gpui::{
App, BackgroundExecutor, Entity, Modifiers, MouseButton, MouseDownEvent, TestAppContext,
UpdateGlobal, px, size,
@@ -30,7 +33,7 @@ use parking_lot::Mutex;
use pretty_assertions::assert_eq;
use project::{
DiagnosticSummary, HoverBlockKind, Project, ProjectPath,
- lsp_store::{FormatTrigger, LspFormatTarget},
+ lsp_store::{FormatTrigger, LspFormatTarget, SymbolLocation},
search::{SearchQuery, SearchResult},
};
use prompt_store::PromptBuilder;
@@ -49,7 +52,7 @@ use std::{
time::Duration,
};
use unindent::Unindent as _;
-use util::{path, uri};
+use util::{path, rel_path::rel_path, uri};
use workspace::Pane;
#[ctor::ctor]
@@ -1418,7 +1421,9 @@ async fn test_unshare_project(
assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
project_b
- .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+ .update(cx_b, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("a.txt")), cx)
+ })
.await
.unwrap();
@@ -1454,7 +1459,9 @@ async fn test_unshare_project(
assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
project_c2
- .update(cx_c, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+ .update(cx_c, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("a.txt")), cx)
+ })
.await
.unwrap();
@@ -1584,11 +1591,15 @@ async fn test_project_reconnect(
});
let buffer_a1 = project_a1
- .update(cx_a, |p, cx| p.open_buffer((worktree1_id, "a.txt"), cx))
+ .update(cx_a, |p, cx| {
+ p.open_buffer((worktree1_id, rel_path("a.txt")), cx)
+ })
.await
.unwrap();
let buffer_b1 = project_b1
- .update(cx_b, |p, cx| p.open_buffer((worktree1_id, "a.txt"), cx))
+ .update(cx_b, |p, cx| {
+ p.open_buffer((worktree1_id, rel_path("a.txt")), cx)
+ })
.await
.unwrap();
@@ -1675,20 +1686,15 @@ async fn test_project_reconnect(
assert!(project.is_shared());
assert!(worktree_a1.read(cx).has_update_observer());
assert_eq!(
- worktree_a1
- .read(cx)
- .snapshot()
- .paths()
- .map(|p| p.to_str().unwrap())
- .collect::<Vec<_>>(),
+ worktree_a1.read(cx).snapshot().paths().collect::<Vec<_>>(),
vec![
- path!("a.txt"),
- path!("b.txt"),
- path!("subdir2"),
- path!("subdir2/f.txt"),
- path!("subdir2/g.txt"),
- path!("subdir2/h.txt"),
- path!("subdir2/i.txt")
+ rel_path("a.txt"),
+ rel_path("b.txt"),
+ rel_path("subdir2"),
+ rel_path("subdir2/f.txt"),
+ rel_path("subdir2/g.txt"),
+ rel_path("subdir2/h.txt"),
+ rel_path("subdir2/i.txt")
]
);
assert!(worktree_a3.read(cx).has_update_observer());
@@ -1697,7 +1703,7 @@ async fn test_project_reconnect(
.read(cx)
.snapshot()
.paths()
- .map(|p| p.to_str().unwrap())
+ .map(|p| p.as_str())
.collect::<Vec<_>>(),
vec!["w.txt", "x.txt", "y.txt"]
);
@@ -1712,16 +1718,15 @@ async fn test_project_reconnect(
.read(cx)
.snapshot()
.paths()
- .map(|p| p.to_str().unwrap())
.collect::<Vec<_>>(),
vec![
- path!("a.txt"),
- path!("b.txt"),
- path!("subdir2"),
- path!("subdir2/f.txt"),
- path!("subdir2/g.txt"),
- path!("subdir2/h.txt"),
- path!("subdir2/i.txt")
+ rel_path("a.txt"),
+ rel_path("b.txt"),
+ rel_path("subdir2"),
+ rel_path("subdir2/f.txt"),
+ rel_path("subdir2/g.txt"),
+ rel_path("subdir2/h.txt"),
+ rel_path("subdir2/i.txt")
]
);
assert!(project.worktree_for_id(worktree2_id, cx).is_none());
@@ -1732,7 +1737,7 @@ async fn test_project_reconnect(
.read(cx)
.snapshot()
.paths()
- .map(|p| p.to_str().unwrap())
+ .map(|p| p.as_str())
.collect::<Vec<_>>(),
vec!["w.txt", "x.txt", "y.txt"]
);
@@ -1809,16 +1814,15 @@ async fn test_project_reconnect(
.read(cx)
.snapshot()
.paths()
- .map(|p| p.to_str().unwrap())
.collect::<Vec<_>>(),
vec![
- path!("a.txt"),
- path!("b.txt"),
- path!("subdir2"),
- path!("subdir2/f.txt"),
- path!("subdir2/g.txt"),
- path!("subdir2/h.txt"),
- path!("subdir2/j.txt")
+ rel_path("a.txt"),
+ rel_path("b.txt"),
+ rel_path("subdir2"),
+ rel_path("subdir2/f.txt"),
+ rel_path("subdir2/g.txt"),
+ rel_path("subdir2/h.txt"),
+ rel_path("subdir2/j.txt")
]
);
assert!(project.worktree_for_id(worktree2_id, cx).is_none());
@@ -1829,7 +1833,7 @@ async fn test_project_reconnect(
.read(cx)
.snapshot()
.paths()
- .map(|p| p.to_str().unwrap())
+ .map(|p| p.as_str())
.collect::<Vec<_>>(),
vec!["z.txt"]
);
@@ -2370,11 +2374,15 @@ async fn test_propagate_saves_and_fs_changes(
// Open and edit a buffer as both guests B and C.
let buffer_b = project_b
- .update(cx_b, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
+ .update(cx_b, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("file1.rs")), cx)
+ })
.await
.unwrap();
let buffer_c = project_c
- .update(cx_c, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
+ .update(cx_c, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("file1.rs")), cx)
+ })
.await
.unwrap();
@@ -2390,7 +2398,9 @@ async fn test_propagate_saves_and_fs_changes(
// Open and edit that buffer as the host.
let buffer_a = project_a
- .update(cx_a, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
+ .update(cx_a, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("file1.rs")), cx)
+ })
.await
.unwrap();
@@ -2461,27 +2471,21 @@ async fn test_propagate_saves_and_fs_changes(
worktree_a.read_with(cx_a, |tree, _| {
assert_eq!(
- tree.paths()
- .map(|p| p.to_string_lossy())
- .collect::<Vec<_>>(),
+ tree.paths().map(|p| p.as_str()).collect::<Vec<_>>(),
["file1.js", "file3", "file4"]
)
});
worktree_b.read_with(cx_b, |tree, _| {
assert_eq!(
- tree.paths()
- .map(|p| p.to_string_lossy())
- .collect::<Vec<_>>(),
+ tree.paths().map(|p| p.as_str()).collect::<Vec<_>>(),
["file1.js", "file3", "file4"]
)
});
worktree_c.read_with(cx_c, |tree, _| {
assert_eq!(
- tree.paths()
- .map(|p| p.to_string_lossy())
- .collect::<Vec<_>>(),
+ tree.paths().map(|p| p.as_str()).collect::<Vec<_>>(),
["file1.js", "file3", "file4"]
)
});
@@ -2489,17 +2493,17 @@ async fn test_propagate_saves_and_fs_changes(
// Ensure buffer files are updated as well.
buffer_a.read_with(cx_a, |buffer, _| {
- assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
+ assert_eq!(buffer.file().unwrap().path().as_str(), "file1.js");
assert_eq!(buffer.language().unwrap().name(), "JavaScript".into());
});
buffer_b.read_with(cx_b, |buffer, _| {
- assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
+ assert_eq!(buffer.file().unwrap().path().as_str(), "file1.js");
assert_eq!(buffer.language().unwrap().name(), "JavaScript".into());
});
buffer_c.read_with(cx_c, |buffer, _| {
- assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
+ assert_eq!(buffer.file().unwrap().path().as_str(), "file1.js");
assert_eq!(buffer.language().unwrap().name(), "JavaScript".into());
});
@@ -2524,7 +2528,7 @@ async fn test_propagate_saves_and_fs_changes(
project_a
.update(cx_a, |project, cx| {
let path = ProjectPath {
- path: Arc::from(Path::new("file3.rs")),
+ path: rel_path("file3.rs").into(),
worktree_id: worktree_a.read(cx).id(),
};
@@ -2538,7 +2542,7 @@ async fn test_propagate_saves_and_fs_changes(
new_buffer_b.read_with(cx_b, |buffer_b, _| {
assert_eq!(
buffer_b.file().unwrap().path().as_ref(),
- Path::new("file3.rs")
+ rel_path("file3.rs")
);
new_buffer_a.read_with(cx_a, |buffer_a, _| {
@@ -2621,19 +2625,20 @@ async fn test_git_diff_base_change(
"
.unindent();
- client_a.fs().set_index_for_repo(
- Path::new("/dir/.git"),
- &[("a.txt".into(), staged_text.clone())],
- );
+ client_a
+ .fs()
+ .set_index_for_repo(Path::new("/dir/.git"), &[("a.txt", staged_text.clone())]);
client_a.fs().set_head_for_repo(
Path::new("/dir/.git"),
- &[("a.txt".into(), committed_text.clone())],
+ &[("a.txt", committed_text.clone())],
"deadbeef",
);
// Create the buffer
let buffer_local_a = project_local
- .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+ .update(cx_a, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("a.txt")), cx)
+ })
.await
.unwrap();
let local_unstaged_diff_a = project_local
@@ -2661,7 +2666,9 @@ async fn test_git_diff_base_change(
// Create remote buffer
let remote_buffer_a = project_remote
- .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+ .update(cx_b, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("a.txt")), cx)
+ })
.await
.unwrap();
let remote_unstaged_diff_a = project_remote
@@ -2717,11 +2724,11 @@ async fn test_git_diff_base_change(
// Update the index text of the open buffer
client_a.fs().set_index_for_repo(
Path::new("/dir/.git"),
- &[("a.txt".into(), new_staged_text.clone())],
+ &[("a.txt", new_staged_text.clone())],
);
client_a.fs().set_head_for_repo(
Path::new("/dir/.git"),
- &[("a.txt".into(), new_committed_text.clone())],
+ &[("a.txt", new_committed_text.clone())],
"deadbeef",
);
@@ -2790,12 +2797,14 @@ async fn test_git_diff_base_change(
client_a.fs().set_index_for_repo(
Path::new("/dir/sub/.git"),
- &[("b.txt".into(), staged_text.clone())],
+ &[("b.txt", staged_text.clone())],
);
// Create the buffer
let buffer_local_b = project_local
- .update(cx_a, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
+ .update(cx_a, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("sub/b.txt")), cx)
+ })
.await
.unwrap();
let local_unstaged_diff_b = project_local
@@ -2823,7 +2832,9 @@ async fn test_git_diff_base_change(
// Create remote buffer
let remote_buffer_b = project_remote
- .update(cx_b, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
+ .update(cx_b, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("sub/b.txt")), cx)
+ })
.await
.unwrap();
let remote_unstaged_diff_b = project_remote
@@ -2851,7 +2862,7 @@ async fn test_git_diff_base_change(
// Updatet the staged text
client_a.fs().set_index_for_repo(
Path::new("/dir/sub/.git"),
- &[("b.txt".into(), new_staged_text.clone())],
+ &[("b.txt", new_staged_text.clone())],
);
// Wait for buffer_local_b to receive it
@@ -3011,21 +3022,21 @@ async fn test_git_status_sync(
// and b.txt is unmerged.
client_a.fs().set_head_for_repo(
path!("/dir/.git").as_ref(),
- &[("b.txt".into(), "B".into()), ("c.txt".into(), "c".into())],
+ &[("b.txt", "B".into()), ("c.txt", "c".into())],
"deadbeef",
);
client_a.fs().set_index_for_repo(
path!("/dir/.git").as_ref(),
&[
- ("a.txt".into(), "".into()),
- ("b.txt".into(), "B".into()),
- ("c.txt".into(), "c".into()),
+ ("a.txt", "".into()),
+ ("b.txt", "B".into()),
+ ("c.txt", "c".into()),
],
);
client_a.fs().set_unmerged_paths_for_repo(
path!("/dir/.git").as_ref(),
&[(
- "b.txt".into(),
+ repo_path("b.txt"),
UnmergedStatus {
first_head: UnmergedStatusCode::Updated,
second_head: UnmergedStatusCode::Deleted,
@@ -3056,13 +3067,8 @@ async fn test_git_status_sync(
executor.run_until_parked();
#[track_caller]
- fn assert_status(
- file: impl AsRef<Path>,
- status: Option<FileStatus>,
- project: &Project,
- cx: &App,
- ) {
- let file = file.as_ref();
+ fn assert_status(file: &str, status: Option<FileStatus>, project: &Project, cx: &App) {
+ let file = repo_path(file);
let repos = project
.repositories(cx)
.values()
@@ -3072,7 +3078,7 @@ async fn test_git_status_sync(
let repo = repos.into_iter().next().unwrap();
assert_eq!(
repo.read(cx)
- .status_for_path(&file.into())
+ .status_for_path(&file)
.map(|entry| entry.status),
status
);
@@ -3107,7 +3113,7 @@ async fn test_git_status_sync(
// and modify c.txt in the working copy.
client_a.fs().set_index_for_repo(
path!("/dir/.git").as_ref(),
- &[("a.txt".into(), "a".into()), ("c.txt".into(), "c".into())],
+ &[("a.txt", "a".into()), ("c.txt", "c".into())],
);
client_a
.fs()
@@ -3202,7 +3208,7 @@ async fn test_fs_operations(
let entry = project_b
.update(cx_b, |project, cx| {
- project.create_entry((worktree_id, "c.txt"), false, cx)
+ project.create_entry((worktree_id, rel_path("c.txt")), false, cx)
})
.await
.unwrap()
@@ -3211,27 +3217,21 @@ async fn test_fs_operations(
worktree_a.read_with(cx_a, |worktree, _| {
assert_eq!(
- worktree
- .paths()
- .map(|p| p.to_string_lossy())
- .collect::<Vec<_>>(),
+ worktree.paths().map(|p| p.as_str()).collect::<Vec<_>>(),
["a.txt", "b.txt", "c.txt"]
);
});
worktree_b.read_with(cx_b, |worktree, _| {
assert_eq!(
- worktree
- .paths()
- .map(|p| p.to_string_lossy())
- .collect::<Vec<_>>(),
+ worktree.paths().map(|p| p.as_str()).collect::<Vec<_>>(),
["a.txt", "b.txt", "c.txt"]
);
});
project_b
.update(cx_b, |project, cx| {
- project.rename_entry(entry.id, Path::new("d.txt"), cx)
+ project.rename_entry(entry.id, (worktree_id, rel_path("d.txt")).into(), cx)
})
.await
.unwrap()
@@ -3240,27 +3240,21 @@ async fn test_fs_operations(
worktree_a.read_with(cx_a, |worktree, _| {
assert_eq!(
- worktree
- .paths()
- .map(|p| p.to_string_lossy())
- .collect::<Vec<_>>(),
+ worktree.paths().map(|p| p.as_str()).collect::<Vec<_>>(),
["a.txt", "b.txt", "d.txt"]
);
});
worktree_b.read_with(cx_b, |worktree, _| {
assert_eq!(
- worktree
- .paths()
- .map(|p| p.to_string_lossy())
- .collect::<Vec<_>>(),
+ worktree.paths().map(|p| p.as_str()).collect::<Vec<_>>(),
["a.txt", "b.txt", "d.txt"]
);
});
let dir_entry = project_b
.update(cx_b, |project, cx| {
- project.create_entry((worktree_id, "DIR"), true, cx)
+ project.create_entry((worktree_id, rel_path("DIR")), true, cx)
})
.await
.unwrap()
@@ -3269,27 +3263,21 @@ async fn test_fs_operations(
worktree_a.read_with(cx_a, |worktree, _| {
assert_eq!(
- worktree
- .paths()
- .map(|p| p.to_string_lossy())
- .collect::<Vec<_>>(),
+ worktree.paths().map(|p| p.as_str()).collect::<Vec<_>>(),
["DIR", "a.txt", "b.txt", "d.txt"]
);
});
worktree_b.read_with(cx_b, |worktree, _| {
assert_eq!(
- worktree
- .paths()
- .map(|p| p.to_string_lossy())
- .collect::<Vec<_>>(),
+ worktree.paths().map(|p| p.as_str()).collect::<Vec<_>>(),
["DIR", "a.txt", "b.txt", "d.txt"]
);
});
project_b
.update(cx_b, |project, cx| {
- project.create_entry((worktree_id, "DIR/e.txt"), false, cx)
+ project.create_entry((worktree_id, rel_path("DIR/e.txt")), false, cx)
})
.await
.unwrap()
@@ -3298,7 +3286,7 @@ async fn test_fs_operations(
project_b
.update(cx_b, |project, cx| {
- project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
+ project.create_entry((worktree_id, rel_path("DIR/SUBDIR")), true, cx)
})
.await
.unwrap()
@@ -3307,7 +3295,7 @@ async fn test_fs_operations(
project_b
.update(cx_b, |project, cx| {
- project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
+ project.create_entry((worktree_id, rel_path("DIR/SUBDIR/f.txt")), false, cx)
})
.await
.unwrap()
@@ -3316,43 +3304,41 @@ async fn test_fs_operations(
worktree_a.read_with(cx_a, |worktree, _| {
assert_eq!(
- worktree
- .paths()
- .map(|p| p.to_string_lossy())
- .collect::<Vec<_>>(),
+ worktree.paths().collect::<Vec<_>>(),
[
- path!("DIR"),
- path!("DIR/SUBDIR"),
- path!("DIR/SUBDIR/f.txt"),
- path!("DIR/e.txt"),
- path!("a.txt"),
- path!("b.txt"),
- path!("d.txt")
+ rel_path("DIR"),
+ rel_path("DIR/SUBDIR"),
+ rel_path("DIR/SUBDIR/f.txt"),
+ rel_path("DIR/e.txt"),
+ rel_path("a.txt"),
+ rel_path("b.txt"),
+ rel_path("d.txt")
]
);
});
worktree_b.read_with(cx_b, |worktree, _| {
assert_eq!(
- worktree
- .paths()
- .map(|p| p.to_string_lossy())
- .collect::<Vec<_>>(),
+ worktree.paths().collect::<Vec<_>>(),
[
- path!("DIR"),
- path!("DIR/SUBDIR"),
- path!("DIR/SUBDIR/f.txt"),
- path!("DIR/e.txt"),
- path!("a.txt"),
- path!("b.txt"),
- path!("d.txt")
+ rel_path("DIR"),
+ rel_path("DIR/SUBDIR"),
+ rel_path("DIR/SUBDIR/f.txt"),
+ rel_path("DIR/e.txt"),
+ rel_path("a.txt"),
+ rel_path("b.txt"),
+ rel_path("d.txt")
]
);
});
project_b
.update(cx_b, |project, cx| {
- project.copy_entry(entry.id, None, Path::new("f.txt"), cx)
+ project.copy_entry(
+ entry.id,
+ (worktree_b.read(cx).id(), rel_path("f.txt")).into(),
+ cx,
+ )
})
.await
.unwrap()
@@ -3360,38 +3346,32 @@ async fn test_fs_operations(
worktree_a.read_with(cx_a, |worktree, _| {
assert_eq!(
- worktree
- .paths()
- .map(|p| p.to_string_lossy())
- .collect::<Vec<_>>(),
+ worktree.paths().collect::<Vec<_>>(),
[
- path!("DIR"),
- path!("DIR/SUBDIR"),
- path!("DIR/SUBDIR/f.txt"),
- path!("DIR/e.txt"),
- path!("a.txt"),
- path!("b.txt"),
- path!("d.txt"),
- path!("f.txt")
+ rel_path("DIR"),
+ rel_path("DIR/SUBDIR"),
+ rel_path("DIR/SUBDIR/f.txt"),
+ rel_path("DIR/e.txt"),
+ rel_path("a.txt"),
+ rel_path("b.txt"),
+ rel_path("d.txt"),
+ rel_path("f.txt")
]
);
});
worktree_b.read_with(cx_b, |worktree, _| {
assert_eq!(
- worktree
- .paths()
- .map(|p| p.to_string_lossy())
- .collect::<Vec<_>>(),
+ worktree.paths().collect::<Vec<_>>(),
[
- path!("DIR"),
- path!("DIR/SUBDIR"),
- path!("DIR/SUBDIR/f.txt"),
- path!("DIR/e.txt"),
- path!("a.txt"),
- path!("b.txt"),
- path!("d.txt"),
- path!("f.txt")
+ rel_path("DIR"),
+ rel_path("DIR/SUBDIR"),
+ rel_path("DIR/SUBDIR/f.txt"),
+ rel_path("DIR/e.txt"),
+ rel_path("a.txt"),
+ rel_path("b.txt"),
+ rel_path("d.txt"),
+ rel_path("f.txt")
]
);
});
@@ -3406,20 +3386,14 @@ async fn test_fs_operations(
worktree_a.read_with(cx_a, |worktree, _| {
assert_eq!(
- worktree
- .paths()
- .map(|p| p.to_string_lossy())
- .collect::<Vec<_>>(),
+ worktree.paths().map(|p| p.as_str()).collect::<Vec<_>>(),
["a.txt", "b.txt", "d.txt", "f.txt"]
);
});
worktree_b.read_with(cx_b, |worktree, _| {
assert_eq!(
- worktree
- .paths()
- .map(|p| p.to_string_lossy())
- .collect::<Vec<_>>(),
+ worktree.paths().map(|p| p.as_str()).collect::<Vec<_>>(),
["a.txt", "b.txt", "d.txt", "f.txt"]
);
});
@@ -3433,20 +3407,14 @@ async fn test_fs_operations(
worktree_a.read_with(cx_a, |worktree, _| {
assert_eq!(
- worktree
- .paths()
- .map(|p| p.to_string_lossy())
- .collect::<Vec<_>>(),
+ worktree.paths().map(|p| p.as_str()).collect::<Vec<_>>(),
["a.txt", "b.txt", "f.txt"]
);
});
worktree_b.read_with(cx_b, |worktree, _| {
assert_eq!(
- worktree
- .paths()
- .map(|p| p.to_string_lossy())
- .collect::<Vec<_>>(),
+ worktree.paths().map(|p| p.as_str()).collect::<Vec<_>>(),
["a.txt", "b.txt", "f.txt"]
);
});
@@ -3511,8 +3479,8 @@ async fn test_local_settings(
))
.collect::<Vec<_>>(),
&[
- (Path::new("").into(), Some(2)),
- (Path::new("a").into(), Some(8)),
+ (rel_path("").into(), Some(2)),
+ (rel_path("a").into(), Some(8)),
]
)
});
@@ -3533,10 +3501,7 @@ async fn test_local_settings(
content.all_languages.defaults.tab_size.map(Into::into)
))
.collect::<Vec<_>>(),
- &[
- (Path::new("").into(), None),
- (Path::new("a").into(), Some(8)),
- ]
+ &[(rel_path("").into(), None), (rel_path("a").into(), Some(8)),]
)
});
@@ -3567,8 +3532,8 @@ async fn test_local_settings(
))
.collect::<Vec<_>>(),
&[
- (Path::new("a").into(), Some(8)),
- (Path::new("b").into(), Some(4)),
+ (rel_path("a").into(), Some(8)),
+ (rel_path("b").into(), Some(4)),
]
)
});
@@ -3599,7 +3564,7 @@ async fn test_local_settings(
.local_settings(worktree_b.read(cx).id())
.map(|(path, content)| (path, content.all_languages.defaults.hard_tabs))
.collect::<Vec<_>>(),
- &[(Path::new("a").into(), Some(true))],
+ &[(rel_path("a").into(), Some(true))],
)
});
}
@@ -3636,7 +3601,9 @@ async fn test_buffer_conflict_after_save(
// Open a buffer as client B
let buffer_b = project_b
- .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+ .update(cx_b, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("a.txt")), cx)
+ })
.await
.unwrap();
@@ -3700,7 +3667,9 @@ async fn test_buffer_reloading(
// Open a buffer as client B
let buffer_b = project_b
- .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+ .update(cx_b, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("a.txt")), cx)
+ })
.await
.unwrap();
@@ -3758,12 +3727,16 @@ async fn test_editing_while_guest_opens_buffer(
// Open a buffer as client A
let buffer_a = project_a
- .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+ .update(cx_a, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("a.txt")), cx)
+ })
.await
.unwrap();
// Start opening the same buffer as client B
- let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx));
+ let open_buffer = project_b.update(cx_b, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("a.txt")), cx)
+ });
let buffer_b = cx_b.executor().spawn(open_buffer);
// Edit the buffer as client A while client B is still opening it.
@@ -3810,7 +3783,9 @@ async fn test_leaving_worktree_while_opening_buffer(
project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1));
// Begin opening a buffer as client B, but leave the project before the open completes.
- let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx));
+ let open_buffer = project_b.update(cx_b, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("a.txt")), cx)
+ });
let buffer_b = cx_b.executor().spawn(open_buffer);
cx_b.update(|_| drop(project_b));
drop(buffer_b);
@@ -3852,7 +3827,9 @@ async fn test_canceling_buffer_opening(
let project_b = client_b.join_remote_project(project_id, cx_b).await;
let buffer_a = project_a
- .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+ .update(cx_a, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("a.txt")), cx)
+ })
.await
.unwrap();
@@ -3928,7 +3905,7 @@ async fn test_leaving_project(
let buffer_b1 = project_b1
.update(cx_b, |project, cx| {
let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
- project.open_buffer((worktree_id, "a.txt"), cx)
+ project.open_buffer((worktree_id, rel_path("a.txt")), cx)
})
.await
.unwrap();
@@ -3966,7 +3943,7 @@ async fn test_leaving_project(
let buffer_b2 = project_b2
.update(cx_b, |project, cx| {
let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
- project.open_buffer((worktree_id, "a.txt"), cx)
+ project.open_buffer((worktree_id, rel_path("a.txt")), cx)
})
.await
.unwrap();
@@ -4131,7 +4108,7 @@ async fn test_collaborating_with_diagnostics(
&[(
ProjectPath {
worktree_id,
- path: Arc::from(Path::new("a.rs")),
+ path: rel_path("a.rs").into(),
},
LanguageServerId(0),
DiagnosticSummary {
@@ -4167,7 +4144,7 @@ async fn test_collaborating_with_diagnostics(
&[(
ProjectPath {
worktree_id,
- path: Arc::from(Path::new("a.rs")),
+ path: rel_path("a.rs").into(),
},
LanguageServerId(0),
DiagnosticSummary {
@@ -4208,7 +4185,7 @@ async fn test_collaborating_with_diagnostics(
[(
ProjectPath {
worktree_id,
- path: Arc::from(Path::new("a.rs")),
+ path: rel_path("a.rs").into(),
},
LanguageServerId(0),
DiagnosticSummary {
@@ -4225,7 +4202,7 @@ async fn test_collaborating_with_diagnostics(
[(
ProjectPath {
worktree_id,
- path: Arc::from(Path::new("a.rs")),
+ path: rel_path("a.rs").into(),
},
LanguageServerId(0),
DiagnosticSummary {
@@ -4237,7 +4214,9 @@ async fn test_collaborating_with_diagnostics(
});
// Open the file with the errors on client B. They should be present.
- let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
+ let open_buffer = project_b.update(cx_b, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("a.rs")), cx)
+ });
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
buffer_b.read_with(cx_b, |buffer, _| {
@@ -4356,7 +4335,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
let project_b = client_b.join_remote_project(project_id, cx_b).await;
let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
project_b.update(cx_b, |p, cx| {
- p.open_buffer_with_lsp((worktree_id, file_name), cx)
+ p.open_buffer_with_lsp((worktree_id, rel_path(file_name)), cx)
})
}))
.await
@@ -4454,7 +4433,9 @@ async fn test_reloading_buffer_manually(
.await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
let buffer_a = project_a
- .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
+ .update(cx_a, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("a.rs")), cx)
+ })
.await
.unwrap();
let project_id = active_call_a
@@ -4464,7 +4445,9 @@ async fn test_reloading_buffer_manually(
let project_b = client_b.join_remote_project(project_id, cx_b).await;
- let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
+ let open_buffer = project_b.update(cx_b, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("a.rs")), cx)
+ });
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
buffer_b.update(cx_b, |buffer, cx| {
buffer.edit([(4..7, "six")], None, cx);
@@ -4562,7 +4545,9 @@ async fn test_formatting_buffer(
let project_b = client_b.join_remote_project(project_id, cx_b).await;
let buffer_b = project_b
- .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
+ .update(cx_b, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("a.rs")), cx)
+ })
.await
.unwrap();
@@ -4688,7 +4673,9 @@ async fn test_prettier_formatting_buffer(
.await;
let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
- let open_buffer = project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx));
+ let open_buffer = project_a.update(cx_a, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("a.ts")), cx)
+ });
let buffer_a = cx_a.executor().spawn(open_buffer).await.unwrap();
let project_id = active_call_a
@@ -4698,7 +4685,7 @@ async fn test_prettier_formatting_buffer(
let project_b = client_b.join_remote_project(project_id, cx_b).await;
let (buffer_b, _) = project_b
.update(cx_b, |p, cx| {
- p.open_buffer_with_lsp((worktree_id, "a.ts"), cx)
+ p.open_buffer_with_lsp((worktree_id, rel_path("a.ts")), cx)
})
.await
.unwrap();
@@ -4838,7 +4825,7 @@ async fn test_definition(
// Open the file on client B.
let (buffer_b, _handle) = project_b
.update(cx_b, |p, cx| {
- p.open_buffer_with_lsp((worktree_id, "a.rs"), cx)
+ p.open_buffer_with_lsp((worktree_id, rel_path("a.rs")), cx)
})
.await
.unwrap();
@@ -5016,7 +5003,7 @@ async fn test_references(
// Open the file on client B.
let (buffer_b, _handle) = project_b
.update(cx_b, |p, cx| {
- p.open_buffer_with_lsp((worktree_id, "one.rs"), cx)
+ p.open_buffer_with_lsp((worktree_id, rel_path("one.rs")), cx)
})
.await
.unwrap();
@@ -5088,7 +5075,7 @@ async fn test_references(
let three_buffer = references[2].buffer.read(cx);
assert_eq!(
two_buffer.file().unwrap().path().as_ref(),
- Path::new("two.rs")
+ rel_path("two.rs")
);
assert_eq!(references[1].buffer, references[0].buffer);
assert_eq!(
@@ -5288,7 +5275,7 @@ async fn test_document_highlights(
// Open the file on client B.
let (buffer_b, _handle) = project_b
.update(cx_b, |p, cx| {
- p.open_buffer_with_lsp((worktree_id, "main.rs"), cx)
+ p.open_buffer_with_lsp((worktree_id, rel_path("main.rs")), cx)
})
.await
.unwrap();
@@ -5431,7 +5418,7 @@ async fn test_lsp_hover(
// Open the file as the guest
let (buffer_b, _handle) = project_b
.update(cx_b, |p, cx| {
- p.open_buffer_with_lsp((worktree_id, "main.rs"), cx)
+ p.open_buffer_with_lsp((worktree_id, rel_path("main.rs")), cx)
})
.await
.unwrap();
@@ -5623,7 +5610,7 @@ async fn test_project_symbols(
// Cause the language server to start.
let _buffer = project_b
.update(cx_b, |p, cx| {
- p.open_buffer_with_lsp((worktree_id, "one.rs"), cx)
+ p.open_buffer_with_lsp((worktree_id, rel_path("one.rs")), cx)
})
.await
.unwrap();
@@ -5673,7 +5660,10 @@ async fn test_project_symbols(
// Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
let mut fake_symbol = symbols[0].clone();
- fake_symbol.path.path = Path::new(path!("/code/secrets")).into();
+ fake_symbol.path = SymbolLocation::OutsideProject {
+ abs_path: Path::new(path!("/code/secrets")).into(),
+ signature: [0x17; 32],
+ };
let error = project_b
.update(cx_b, |project, cx| {
project.open_buffer_for_symbol(&fake_symbol, cx)
@@ -27,7 +27,11 @@ use std::{
rc::Rc,
sync::Arc,
};
-use util::{ResultExt, path};
+use util::{
+ ResultExt, path,
+ paths::PathStyle,
+ rel_path::{RelPath, RelPathBuf, rel_path},
+};
#[gpui::test(
iterations = 100,
@@ -66,7 +70,7 @@ enum ClientOperation {
OpenBuffer {
project_root_name: String,
is_local: bool,
- full_path: PathBuf,
+ full_path: RelPathBuf,
},
SearchProject {
project_root_name: String,
@@ -77,24 +81,24 @@ enum ClientOperation {
EditBuffer {
project_root_name: String,
is_local: bool,
- full_path: PathBuf,
+ full_path: RelPathBuf,
edits: Vec<(Range<usize>, Arc<str>)>,
},
CloseBuffer {
project_root_name: String,
is_local: bool,
- full_path: PathBuf,
+ full_path: RelPathBuf,
},
SaveBuffer {
project_root_name: String,
is_local: bool,
- full_path: PathBuf,
+ full_path: RelPathBuf,
detach: bool,
},
RequestLspDataInBuffer {
project_root_name: String,
is_local: bool,
- full_path: PathBuf,
+ full_path: RelPathBuf,
offset: usize,
kind: LspRequestKind,
detach: bool,
@@ -102,7 +106,7 @@ enum ClientOperation {
CreateWorktreeEntry {
project_root_name: String,
is_local: bool,
- full_path: PathBuf,
+ full_path: RelPathBuf,
is_dir: bool,
},
WriteFsEntry {
@@ -119,7 +123,7 @@ enum ClientOperation {
enum GitOperation {
WriteGitIndex {
repo_path: PathBuf,
- contents: Vec<(PathBuf, String)>,
+ contents: Vec<(RelPathBuf, String)>,
},
WriteGitBranch {
repo_path: PathBuf,
@@ -127,7 +131,7 @@ enum GitOperation {
},
WriteGitStatuses {
repo_path: PathBuf,
- statuses: Vec<(PathBuf, FileStatus)>,
+ statuses: Vec<(RelPathBuf, FileStatus)>,
},
}
@@ -311,8 +315,8 @@ impl RandomizedTest for ProjectCollaborationTest {
let Some(worktree) = worktree else { continue };
let is_dir = rng.random::<bool>();
let mut full_path =
- worktree.read_with(cx, |w, _| PathBuf::from(w.root_name()));
- full_path.push(gen_file_name(rng));
+ worktree.read_with(cx, |w, _| w.root_name().to_rel_path_buf());
+ full_path.push(rel_path(&gen_file_name(rng)));
if !is_dir {
full_path.set_extension("rs");
}
@@ -346,8 +350,18 @@ impl RandomizedTest for ProjectCollaborationTest {
continue;
};
- let full_path = buffer
- .read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
+ let full_path = buffer.read_with(cx, |buffer, cx| {
+ let file = buffer.file().unwrap();
+ let worktree = project
+ .read(cx)
+ .worktree_for_id(file.worktree_id(cx), cx)
+ .unwrap();
+ worktree
+ .read(cx)
+ .root_name()
+ .join(file.path())
+ .to_rel_path_buf()
+ });
match rng.random_range(0..100_u32) {
// Close the buffer
@@ -436,16 +450,16 @@ impl RandomizedTest for ProjectCollaborationTest {
.filter(|e| e.is_file())
.choose(rng)
.unwrap();
- if entry.path.as_ref() == Path::new("") {
- Path::new(worktree.root_name()).into()
+ if entry.path.as_ref().is_empty() {
+ worktree.root_name().into()
} else {
- Path::new(worktree.root_name()).join(&entry.path)
+ worktree.root_name().join(&entry.path)
}
});
break ClientOperation::OpenBuffer {
project_root_name,
is_local,
- full_path,
+ full_path: full_path.to_rel_path_buf(),
};
}
}
@@ -940,7 +954,11 @@ impl RandomizedTest for ProjectCollaborationTest {
}
for (path, _) in contents.iter() {
- if !client.fs().files().contains(&repo_path.join(path)) {
+ if !client
+ .fs()
+ .files()
+ .contains(&repo_path.join(path.as_std_path()))
+ {
return Err(TestError::Inapplicable);
}
}
@@ -954,8 +972,8 @@ impl RandomizedTest for ProjectCollaborationTest {
let dot_git_dir = repo_path.join(".git");
let contents = contents
- .into_iter()
- .map(|(path, contents)| (path.into(), contents))
+ .iter()
+ .map(|(path, contents)| (path.as_str(), contents.clone()))
.collect::<Vec<_>>();
if client.fs().metadata(&dot_git_dir).await?.is_none() {
client.fs().create_dir(&dot_git_dir).await?;
@@ -993,7 +1011,11 @@ impl RandomizedTest for ProjectCollaborationTest {
return Err(TestError::Inapplicable);
}
for (path, _) in statuses.iter() {
- if !client.fs().files().contains(&repo_path.join(path)) {
+ if !client
+ .fs()
+ .files()
+ .contains(&repo_path.join(path.as_std_path()))
+ {
return Err(TestError::Inapplicable);
}
}
@@ -1009,7 +1031,7 @@ impl RandomizedTest for ProjectCollaborationTest {
let statuses = statuses
.iter()
- .map(|(path, val)| (path.as_path(), *val))
+ .map(|(path, val)| (path.as_str(), *val))
.collect::<Vec<_>>();
if client.fs().metadata(&dot_git_dir).await?.is_none() {
@@ -1426,7 +1448,7 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation
repo_path: &Path,
rng: &mut StdRng,
client: &TestClient,
- ) -> Vec<PathBuf> {
+ ) -> Vec<RelPathBuf> {
let mut paths = client
.fs()
.files()
@@ -1440,7 +1462,11 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation
paths
.iter()
- .map(|path| path.strip_prefix(repo_path).unwrap().to_path_buf())
+ .map(|path| {
+ RelPath::from_std_path(path.strip_prefix(repo_path).unwrap(), PathStyle::local())
+ .unwrap()
+ .to_rel_path_buf()
+ })
.collect::<Vec<_>>()
}
@@ -1487,7 +1513,7 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation
fn buffer_for_full_path(
client: &TestClient,
project: &Entity<Project>,
- full_path: &PathBuf,
+ full_path: &RelPath,
cx: &TestAppContext,
) -> Option<Entity<language::Buffer>> {
client
@@ -1495,7 +1521,12 @@ fn buffer_for_full_path(
.iter()
.find(|buffer| {
buffer.read_with(cx, |buffer, cx| {
- buffer.file().unwrap().full_path(cx) == *full_path
+ let file = buffer.file().unwrap();
+ let Some(worktree) = project.read(cx).worktree_for_id(file.worktree_id(cx), cx)
+ else {
+ return false;
+ };
+ worktree.read(cx).root_name().join(&file.path()).as_ref() == full_path
})
})
.cloned()
@@ -1536,23 +1567,23 @@ fn root_name_for_project(project: &Entity<Project>, cx: &TestAppContext) -> Stri
.next()
.unwrap()
.read(cx)
- .root_name()
+ .root_name_str()
.to_string()
})
}
fn project_path_for_full_path(
project: &Entity<Project>,
- full_path: &Path,
+ full_path: &RelPath,
cx: &TestAppContext,
) -> Option<ProjectPath> {
let mut components = full_path.components();
- let root_name = components.next().unwrap().as_os_str().to_str().unwrap();
- let path = components.as_path().into();
+ let root_name = components.next().unwrap();
+ let path = components.rest().into();
let worktree_id = project.read_with(cx, |project, cx| {
project.worktrees(cx).find_map(|worktree| {
let worktree = worktree.read(cx);
- if worktree.root_name() == root_name {
+ if worktree.root_name_str() == root_name {
Some(worktree.id())
} else {
None
@@ -33,7 +33,7 @@ use std::{
sync::{Arc, atomic::AtomicUsize},
};
use task::TcpArgumentsTemplate;
-use util::path;
+use util::{path, rel_path::rel_path};
#[gpui::test(iterations = 10)]
async fn test_sharing_an_ssh_remote_project(
@@ -124,26 +124,26 @@ async fn test_sharing_an_ssh_remote_project(
worktree_a.update(cx_a, |worktree, _cx| {
assert_eq!(
- worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
+ worktree.paths().collect::<Vec<_>>(),
vec![
- Path::new(".zed"),
- Path::new(".zed/settings.json"),
- Path::new("README.md"),
- Path::new("src"),
- Path::new("src/lib.rs"),
+ rel_path(".zed"),
+ rel_path(".zed/settings.json"),
+ rel_path("README.md"),
+ rel_path("src"),
+ rel_path("src/lib.rs"),
]
);
});
worktree_b.update(cx_b, |worktree, _cx| {
assert_eq!(
- worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
+ worktree.paths().collect::<Vec<_>>(),
vec![
- Path::new(".zed"),
- Path::new(".zed/settings.json"),
- Path::new("README.md"),
- Path::new("src"),
- Path::new("src/lib.rs"),
+ rel_path(".zed"),
+ rel_path(".zed/settings.json"),
+ rel_path("README.md"),
+ rel_path("src"),
+ rel_path("src/lib.rs"),
]
);
});
@@ -151,7 +151,7 @@ async fn test_sharing_an_ssh_remote_project(
// User B can open buffers in the remote project.
let buffer_b = project_b
.update(cx_b, |project, cx| {
- project.open_buffer((worktree_id, "src/lib.rs"), cx)
+ project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
})
.await
.unwrap();
@@ -177,7 +177,7 @@ async fn test_sharing_an_ssh_remote_project(
buffer_b.clone(),
ProjectPath {
worktree_id: worktree_id.to_owned(),
- path: Arc::from(Path::new("src/renamed.rs")),
+ path: rel_path("src/renamed.rs").into(),
},
cx,
)
@@ -194,14 +194,8 @@ async fn test_sharing_an_ssh_remote_project(
cx_b.run_until_parked();
cx_b.update(|cx| {
assert_eq!(
- buffer_b
- .read(cx)
- .file()
- .unwrap()
- .path()
- .to_string_lossy()
- .to_string(),
- path!("src/renamed.rs").to_string()
+ buffer_b.read(cx).file().unwrap().path().as_ref(),
+ rel_path("src/renamed.rs")
);
});
}
@@ -489,7 +483,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
// Opens the buffer and formats it
let (buffer_b, _handle) = project_b
.update(cx_b, |p, cx| {
- p.open_buffer_with_lsp((worktree_id, "a.ts"), cx)
+ p.open_buffer_with_lsp((worktree_id, rel_path("a.ts")), cx)
})
.await
.expect("user B opens buffer for formatting");
@@ -547,7 +541,9 @@ async fn test_ssh_collaboration_formatting_with_prettier(
// User A opens and formats the same buffer too
let buffer_a = project_a
- .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx))
+ .update(cx_a, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("a.ts")), cx)
+ })
.await
.expect("user A opens buffer for formatting");
@@ -40,6 +40,7 @@ use std::{
sync::Arc,
};
use sum_tree::Dimensions;
+use util::rel_path::RelPath;
use util::{ResultExt, fs::remove_matching};
use workspace::Workspace;
@@ -963,8 +964,7 @@ impl Copilot {
let hard_tabs = settings.hard_tabs;
let relative_path = buffer
.file()
- .map(|file| file.path().to_path_buf())
- .unwrap_or_default();
+ .map_or(RelPath::empty().into(), |file| file.path().clone());
cx.background_spawn(async move {
let (version, snapshot) = snapshot.await?;
@@ -975,7 +975,7 @@ impl Copilot {
tab_size: tab_size.into(),
indent_size: 1,
insert_spaces: !hard_tabs,
- relative_path: relative_path.to_string_lossy().into(),
+ relative_path: relative_path.to_proto(),
position: point_to_lsp(position),
version: version.try_into().unwrap(),
},
@@ -1194,7 +1194,7 @@ async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::
mod tests {
use super::*;
use gpui::TestAppContext;
- use util::path;
+ use util::{path, paths::PathStyle, rel_path::rel_path};
#[gpui::test(iterations = 10)]
async fn test_buffer_management(cx: &mut TestAppContext) {
@@ -1258,7 +1258,7 @@ mod tests {
buffer.file_updated(
Arc::new(File {
abs_path: path!("/root/child/buffer-1").into(),
- path: Path::new("child/buffer-1").into(),
+ path: rel_path("child/buffer-1").into(),
}),
cx,
)
@@ -1355,7 +1355,7 @@ mod tests {
struct File {
abs_path: PathBuf,
- path: Arc<Path>,
+ path: Arc<RelPath>,
}
impl language::File for File {
@@ -1369,15 +1369,19 @@ mod tests {
}
}
- fn path(&self) -> &Arc<Path> {
+ fn path(&self) -> &Arc<RelPath> {
&self.path
}
+ fn path_style(&self, _: &App) -> PathStyle {
+ PathStyle::local()
+ }
+
fn full_path(&self, _: &App) -> PathBuf {
unimplemented!()
}
- fn file_name<'a>(&'a self, _: &'a App) -> &'a std::ffi::OsStr {
+ fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
unimplemented!()
}
@@ -24,7 +24,7 @@ use std::{
sync::Arc,
};
use task::{DebugScenario, TcpArgumentsTemplate, ZedDebugConfig};
-use util::archive::extract_zip;
+use util::{archive::extract_zip, rel_path::RelPath};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DapStatus {
@@ -44,7 +44,7 @@ pub trait DapDelegate: Send + Sync + 'static {
fn fs(&self) -> Arc<dyn Fs>;
fn output_to_console(&self, msg: String);
async fn which(&self, command: &OsStr) -> Option<PathBuf>;
- async fn read_text_file(&self, path: PathBuf) -> Result<String>;
+ async fn read_text_file(&self, path: &RelPath) -> Result<String>;
async fn shell_env(&self) -> collections::HashMap<String, String>;
}
@@ -20,7 +20,7 @@ use std::{
ffi::OsStr,
path::{Path, PathBuf},
};
-use util::{ResultExt, maybe};
+use util::{ResultExt, maybe, paths::PathStyle, rel_path::RelPath};
#[derive(Default)]
pub(crate) struct PythonDebugAdapter {
@@ -726,13 +726,16 @@ impl DebugAdapter for PythonDebugAdapter {
.config
.get("cwd")
.and_then(|cwd| {
- cwd.as_str()
- .map(Path::new)?
- .strip_prefix(delegate.worktree_root_path())
- .ok()
+ RelPath::from_std_path(
+ cwd.as_str()
+ .map(Path::new)?
+ .strip_prefix(delegate.worktree_root_path())
+ .ok()?,
+ PathStyle::local(),
+ )
+ .ok()
})
- .unwrap_or_else(|| "".as_ref())
- .into();
+ .unwrap_or_else(|| RelPath::empty().into());
let toolchain = delegate
.toolchain_store()
.active_toolchain(
@@ -15,6 +15,7 @@ use dap::{
use extension::{Extension, WorktreeDelegate};
use gpui::AsyncApp;
use task::{DebugScenario, ZedDebugConfig};
+use util::rel_path::RelPath;
pub(crate) struct ExtensionDapAdapter {
extension: Arc<dyn Extension>,
@@ -57,7 +58,7 @@ impl WorktreeDelegate for WorktreeDelegateAdapter {
self.0.worktree_root_path().to_string_lossy().to_string()
}
- async fn read_text_file(&self, path: PathBuf) -> Result<String> {
+ async fn read_text_file(&self, path: &RelPath) -> Result<String> {
self.0.read_text_file(path).await
}
@@ -33,6 +33,7 @@ use std::sync::{Arc, LazyLock};
use task::{DebugScenario, TaskContext};
use tree_sitter::{Query, StreamingIterator as _};
use ui::{ContextMenu, Divider, PopoverMenuHandle, Tab, Tooltip, prelude::*};
+use util::rel_path::RelPath;
use util::{ResultExt, debug_panic, maybe};
use workspace::SplitDirection;
use workspace::item::SaveOptions;
@@ -1061,14 +1062,14 @@ impl DebugPanel {
directory_in_worktree: dir,
..
} => {
- let relative_path = if dir.ends_with(".vscode") {
- dir.join("launch.json")
+ let relative_path = if dir.ends_with(RelPath::new(".vscode").unwrap()) {
+ dir.join(RelPath::new("launch.json").unwrap())
} else {
- dir.join("debug.json")
+ dir.join(RelPath::new("debug.json").unwrap())
};
ProjectPath {
worktree_id: id,
- path: Arc::from(relative_path),
+ path: relative_path,
}
}
_ => return self.save_scenario(scenario, worktree_id, window, cx),
@@ -1129,7 +1130,7 @@ impl DebugPanel {
let fs =
workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
- path.push(paths::local_settings_folder_relative_path());
+ path.push(paths::local_settings_folder_name());
if !fs.is_dir(path.as_path()).await {
fs.create_dir(path.as_path()).await?;
}
@@ -32,7 +32,7 @@ use ui::{
SharedString, Styled, StyledExt, ToggleButton, ToggleState, Toggleable, Tooltip, Window, div,
h_flex, relative, rems, v_flex,
};
-use util::ResultExt;
+use util::{ResultExt, rel_path::RelPath};
use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr, pane};
use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
@@ -1026,29 +1026,27 @@ impl DebugDelegate {
let mut path = if worktrees.len() > 1
&& let Some(worktree) = project.worktree_for_id(*worktree_id, cx)
{
- let worktree_path = worktree.read(cx).abs_path();
- let full_path = worktree_path.join(directory_in_worktree);
- full_path
+ worktree
+ .read(cx)
+ .root_name()
+ .join(directory_in_worktree)
+ .to_rel_path_buf()
} else {
- directory_in_worktree.clone()
+ directory_in_worktree.to_rel_path_buf()
};
- match path
- .components()
- .next_back()
- .and_then(|component| component.as_os_str().to_str())
- {
+ match path.components().next_back() {
Some(".zed") => {
- path.push("debug.json");
+ path.push(RelPath::new("debug.json").unwrap());
}
Some(".vscode") => {
- path.push("launch.json");
+ path.push(RelPath::new("launch.json").unwrap());
}
_ => {}
}
- Some(path.display().to_string())
+ path.display(project.path_style(cx)).to_string()
})
- .unwrap_or_else(|_| Some(directory_in_worktree.display().to_string())),
+ .ok(),
Some(TaskSourceKind::AbsPath { abs_path, .. }) => {
Some(abs_path.to_string_lossy().into_owned())
}
@@ -1135,7 +1133,7 @@ impl DebugDelegate {
id: _,
directory_in_worktree: dir,
id_base: _,
- } => dir.ends_with(".zed"),
+ } => dir.ends_with(RelPath::new(".zed").unwrap()),
_ => false,
});
@@ -1154,7 +1152,10 @@ impl DebugDelegate {
id: _,
directory_in_worktree: dir,
id_base: _,
- } => !(hide_vscode && dir.ends_with(".vscode")),
+ } => {
+ !(hide_vscode
+ && dir.ends_with(RelPath::new(".vscode").unwrap()))
+ }
_ => true,
})
.filter(|(_, scenario)| valid_adapters.contains(&scenario.adapter))
@@ -26,6 +26,7 @@ use ui::{
Divider, DividerColor, FluentBuilder as _, Indicator, IntoElement, ListItem, Render,
StatefulInteractiveElement, Tooltip, WithScrollbar, prelude::*,
};
+use util::rel_path::RelPath;
use workspace::Workspace;
use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
@@ -663,6 +664,7 @@ impl Render for BreakpointList {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx);
self.breakpoints.clear();
+ let path_style = self.worktree_store.read(cx).path_style();
let weak = cx.weak_entity();
let breakpoints = breakpoints.into_iter().flat_map(|(path, mut breakpoints)| {
let relative_worktree_path = self
@@ -673,7 +675,7 @@ impl Render for BreakpointList {
worktree
.read(cx)
.is_visible()
- .then(|| Path::new(worktree.read(cx).root_name()).join(relative_path))
+ .then(|| worktree.read(cx).root_name().join(&relative_path))
});
breakpoints.sort_by_key(|breakpoint| breakpoint.row);
let weak = weak.clone();
@@ -683,14 +685,9 @@ impl Render for BreakpointList {
let dir = relative_worktree_path
.clone()
- .unwrap_or_else(|| PathBuf::from(&*breakpoint.path))
+ .or_else(|| RelPath::from_std_path(&breakpoint.path, path_style).ok())?
.parent()
- .and_then(|parent| {
- parent
- .to_str()
- .map(ToOwned::to_owned)
- .map(SharedString::from)
- });
+ .map(|parent| SharedString::from(parent.display(path_style).to_string()));
let name = file_name
.to_str()
.map(ToOwned::to_owned)
@@ -87,7 +87,7 @@ impl ModuleList {
this.open_buffer(
ProjectPath {
worktree_id,
- path: relative_path.into(),
+ path: relative_path,
},
cx,
)
@@ -401,7 +401,7 @@ impl StackFrameList {
this.open_buffer(
ProjectPath {
worktree_id,
- path: relative_path.into(),
+ path: relative_path,
},
cx,
)
@@ -181,7 +181,7 @@ impl StackTraceView {
let project_path = ProjectPath {
worktree_id: worktree.read_with(cx, |tree, _| tree.id())?,
- path: relative_path.into(),
+ path: relative_path,
};
if let Some(buffer) = this
@@ -32,7 +32,7 @@ use std::{
};
use terminal_view::terminal_panel::TerminalPanel;
use tests::{active_debug_session_panel, init_test, init_test_workspace};
-use util::path;
+use util::{path, rel_path::rel_path};
use workspace::item::SaveOptions;
use workspace::{Item, dock::Panel};
@@ -1114,7 +1114,7 @@ async fn test_send_breakpoints_when_editor_has_been_saved(
let buffer = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "main.rs"), cx)
+ project.open_buffer((worktree_id, rel_path("main.rs")), cx)
})
.await
.unwrap();
@@ -1276,14 +1276,14 @@ async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
let first = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "main.rs"), cx)
+ project.open_buffer((worktree_id, rel_path("main.rs")), cx)
})
.await
.unwrap();
let second = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "second.rs"), cx)
+ project.open_buffer((worktree_id, rel_path("second.rs")), cx)
})
.await
.unwrap();
@@ -1499,14 +1499,14 @@ async fn test_active_debug_line_setting(executor: BackgroundExecutor, cx: &mut T
let main_buffer = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "main.rs"), cx)
+ project.open_buffer((worktree_id, rel_path("main.rs")), cx)
})
.await
.unwrap();
let second_buffer = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "second.rs"), cx)
+ project.open_buffer((worktree_id, rel_path("second.rs")), cx)
})
.await
.unwrap();
@@ -7,7 +7,7 @@ use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_python, tr
use project::{FakeFs, Project};
use serde_json::json;
use unindent::Unindent as _;
-use util::path;
+use util::{path, rel_path::rel_path};
use crate::{
debugger_panel::DebugPanel,
@@ -215,7 +215,7 @@ fn main() {
let buffer = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "main.rs"), cx)
+ project.open_buffer((worktree_id, rel_path("main.rs")), cx)
})
.await
.unwrap();
@@ -1584,7 +1584,7 @@ def process_data(untyped_param, typed_param: int, another_typed: str):
let buffer = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "main.py"), cx)
+ project.open_buffer((worktree_id, rel_path("main.py")), cx)
})
.await
.unwrap();
@@ -2082,7 +2082,7 @@ async fn test_inline_values_util(
let buffer = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "main.rs"), cx)
+ project.open_buffer((worktree_id, rel_path("main.rs")), cx)
})
.await
.unwrap();
@@ -13,7 +13,7 @@ use project::{FakeFs, Project};
use serde_json::json;
use std::sync::Arc;
use unindent::Unindent as _;
-use util::path;
+use util::{path, rel_path::rel_path};
#[gpui::test]
async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
@@ -331,12 +331,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
let project_path = editors[0]
.update(cx, |editor, cx| editor.project_path(cx))
.unwrap();
- let expected = if cfg!(target_os = "windows") {
- "src\\test.js"
- } else {
- "src/test.js"
- };
- assert_eq!(expected, project_path.path.to_string_lossy());
+ assert_eq!(rel_path("src/test.js"), project_path.path.as_ref());
assert_eq!(test_file_content, editors[0].read(cx).text(cx));
assert_eq!(
vec![2..3],
@@ -399,12 +394,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
let project_path = editors[0]
.update(cx, |editor, cx| editor.project_path(cx))
.unwrap();
- let expected = if cfg!(target_os = "windows") {
- "src\\module.js"
- } else {
- "src/module.js"
- };
- assert_eq!(expected, project_path.path.to_string_lossy());
+ assert_eq!(rel_path("src/module.js"), project_path.path.as_ref());
assert_eq!(module_file_content, editors[0].read(cx).text(cx));
assert_eq!(
vec![0..1],
@@ -28,7 +28,6 @@ use std::{
};
use text::{Anchor, BufferSnapshot, OffsetRangeExt};
use ui::{Button, ButtonStyle, Icon, IconName, Label, Tooltip, h_flex, prelude::*};
-use util::paths::PathExt;
use workspace::{
ItemHandle, ItemNavHistory, ToolbarItemLocation, Workspace,
item::{BreadcrumbText, Item, ItemEvent, TabContentParams},
@@ -783,15 +782,16 @@ impl Item for BufferDiagnosticsEditor {
}
// Builds the content to be displayed in the tab.
- fn tab_content(&self, params: TabContentParams, _window: &Window, _cx: &App) -> AnyElement {
+ fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
+ let path_style = self.project.read(cx).path_style(cx);
let error_count = self.summary.error_count;
let warning_count = self.summary.warning_count;
let label = Label::new(
self.project_path
.path
.file_name()
- .map(|f| f.to_sanitized_string())
- .unwrap_or_else(|| self.project_path.path.to_sanitized_string()),
+ .map(|s| s.to_string())
+ .unwrap_or_else(|| self.project_path.path.display(path_style).to_string()),
);
h_flex()
@@ -827,11 +827,12 @@ impl Item for BufferDiagnosticsEditor {
"Buffer Diagnostics".into()
}
- fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
+ fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> {
+ let path_style = self.project.read(cx).path_style(cx);
Some(
format!(
"Buffer Diagnostics - {}",
- self.project_path.path.to_sanitized_string()
+ self.project_path.path.display(path_style)
)
.into(),
)
@@ -848,7 +849,8 @@ impl Item for BufferDiagnosticsEditor {
impl Render for BufferDiagnosticsEditor {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- let filename = self.project_path.path.to_sanitized_string();
+ let path_style = self.project.read(cx).path_style(cx);
+ let filename = self.project_path.path.display(path_style).to_string();
let error_count = self.summary.error_count;
let warning_count = match self.include_warnings {
true => self.summary.warning_count,
@@ -27,7 +27,7 @@ use std::{
str::FromStr,
};
use unindent::Unindent as _;
-use util::{RandomCharIter, path, post_inc};
+use util::{RandomCharIter, path, post_inc, rel_path::rel_path};
#[ctor::ctor]
fn init_logger() {
@@ -1609,7 +1609,7 @@ async fn test_buffer_diagnostics(cx: &mut TestAppContext) {
worktree_id: project.read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
}),
- path: Arc::from(Path::new("main.rs")),
+ path: rel_path("main.rs").into(),
};
let buffer = project
.update(cx, |project, cx| {
@@ -1763,7 +1763,7 @@ async fn test_buffer_diagnostics_without_warnings(cx: &mut TestAppContext) {
worktree_id: project.read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
}),
- path: Arc::from(Path::new("main.rs")),
+ path: rel_path("main.rs").into(),
};
let buffer = project
.update(cx, |project, cx| {
@@ -1892,7 +1892,7 @@ async fn test_buffer_diagnostics_multiple_servers(cx: &mut TestAppContext) {
worktree_id: project.read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
}),
- path: Arc::from(Path::new("main.rs")),
+ path: rel_path("main.rs").into(),
};
let buffer = project
.update(cx, |project, cx| {
@@ -9,7 +9,6 @@ use std::collections::{HashMap, HashSet};
use std::io::{self, Read};
use std::process;
use std::sync::{LazyLock, OnceLock};
-use util::paths::PathExt;
static KEYMAP_MACOS: LazyLock<KeymapFile> = LazyLock::new(|| {
load_keymap("keymaps/default-macos.json").expect("Failed to load MacOS keymap")
@@ -345,7 +344,7 @@ fn handle_postprocessing() -> Result<()> {
let mut queue = Vec::with_capacity(64);
queue.push(root_dir.clone());
while let Some(dir) = queue.pop() {
- for entry in std::fs::read_dir(&dir).context(dir.to_sanitized_string())? {
+ for entry in std::fs::read_dir(&dir).context("failed to read docs dir")? {
let Ok(entry) = entry else {
continue;
};
@@ -324,7 +324,7 @@ impl SyntaxIndex {
cx.spawn(async move |_this, cx| {
let loaded_file = load_task.await?;
let language = language_registry
- .language_for_file_path(&project_path.path)
+ .language_for_file_path(&project_path.path.as_std_path())
.await
.ok();
@@ -549,7 +549,7 @@ impl SyntaxIndexState {
#[cfg(test)]
mod tests {
use super::*;
- use std::{path::Path, sync::Arc};
+ use std::sync::Arc;
use gpui::TestAppContext;
use indoc::indoc;
@@ -558,7 +558,7 @@ mod tests {
use serde_json::json;
use settings::SettingsStore;
use text::OffsetRangeExt as _;
- use util::path;
+ use util::{path, rel_path::rel_path};
use crate::syntax_index::SyntaxIndex;
@@ -739,7 +739,7 @@ mod tests {
.read(cx)
.path_for_entry(*project_entry_id, cx)
.unwrap();
- assert_eq!(project_path.path.as_ref(), Path::new(path),);
+ assert_eq!(project_path.path.as_ref(), rel_path(path),);
declaration
} else {
panic!("Expected a buffer declaration, found {:?}", declaration);
@@ -764,7 +764,7 @@ mod tests {
.unwrap()
.path
.as_ref(),
- Path::new(path),
+ rel_path(path),
);
declaration
} else {
@@ -4,6 +4,7 @@ use language::Language;
use project::lsp_store::lsp_ext_command::SwitchSourceHeaderResult;
use rpc::proto;
use url::Url;
+use util::paths::PathStyle;
use workspace::{OpenOptions, OpenVisible};
use crate::lsp_ext::find_specific_language_server_in_selection;
@@ -38,7 +39,11 @@ pub fn switch_source_header(
let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
cx.spawn_in(window, async move |_editor, cx| {
let source_file = buffer.read_with(cx, |buffer, _| {
- buffer.file().map(|file| file.path()).map(|path| path.to_string_lossy().to_string()).unwrap_or_else(|| "Unknown".to_string())
+ buffer
+ .file()
+ .map(|file| file.path())
+ .map(|path| path.display(PathStyle::local()).to_string())
+ .unwrap_or_else(|| "Unknown".to_string())
})?;
let switch_source_header = if let Some((client, project_id)) = upstream_client {
@@ -53,18 +58,22 @@ pub fn switch_source_header(
.context("lsp ext switch source header proto request")?;
SwitchSourceHeaderResult(response.target_file)
} else {
- project.update(cx, |project, cx| {
- project.request_lsp(
- buffer,
- project::LanguageServerToQuery::Other(server_to_query),
- project::lsp_store::lsp_ext_command::SwitchSourceHeader,
- cx,
- )
- })?.await.with_context(|| format!("Switch source/header LSP request for path \"{source_file}\" failed"))?
+ project
+ .update(cx, |project, cx| {
+ project.request_lsp(
+ buffer,
+ project::LanguageServerToQuery::Other(server_to_query),
+ project::lsp_store::lsp_ext_command::SwitchSourceHeader,
+ cx,
+ )
+ })?
+ .await
+ .with_context(|| {
+ format!("Switch source/header LSP request for path \"{source_file}\" failed")
+ })?
};
if switch_source_header.0.is_empty() {
- log::info!("Clangd returned an empty string when requesting to switch source/header from \"{source_file}\"" );
return Ok(());
}
@@ -75,18 +84,24 @@ pub fn switch_source_header(
)
})?;
- let path = goto.to_file_path().map_err(|()| {
- anyhow::anyhow!("URL conversion to file path failed for \"{goto}\"")
- })?;
+ let path = goto
+ .to_file_path()
+ .map_err(|()| anyhow::anyhow!("URL conversion to file path failed for \"{goto}\""))?;
workspace
.update_in(cx, |workspace, window, cx| {
- workspace.open_abs_path(path, OpenOptions { visible: Some(OpenVisible::None), ..Default::default() }, window, cx)
+ workspace.open_abs_path(
+ path,
+ OpenOptions {
+ visible: Some(OpenVisible::None),
+ ..Default::default()
+ },
+ window,
+ cx,
+ )
})
.with_context(|| {
- format!(
- "Switch source/header could not open \"{goto}\" in workspace"
- )
+ format!("Switch source/header could not open \"{goto}\" in workspace")
})?
.await
.map(|_| ())
@@ -2494,7 +2494,7 @@ impl Editor {
if let Some(extension) = singleton_buffer
.read(cx)
.file()
- .and_then(|file| file.path().extension()?.to_str())
+ .and_then(|file| file.path().extension())
{
key_context.set("extension", extension.to_string());
}
@@ -7603,7 +7603,7 @@ impl Editor {
let extension = buffer
.read(cx)
.file()
- .and_then(|file| Some(file.path().extension()?.to_string_lossy().to_string()));
+ .and_then(|file| Some(file.path().extension()?.to_string()));
let event_type = match accepted {
true => "Edit Prediction Accepted",
@@ -19263,10 +19263,6 @@ impl Editor {
{
return Some(dir.to_owned());
}
-
- if let Some(project_path) = buffer.read(cx).project_path(cx) {
- return Some(project_path.path.to_path_buf());
- }
}
None
@@ -19294,16 +19290,6 @@ impl Editor {
})
}
- fn target_file_path(&self, cx: &mut Context<Self>) -> Option<PathBuf> {
- self.active_excerpt(cx).and_then(|(_, buffer, _)| {
- let project_path = buffer.read(cx).project_path(cx)?;
- let project = self.project()?.read(cx);
- let entry = project.entry_for_path(&project_path, cx)?;
- let path = entry.path.to_path_buf();
- Some(path)
- })
- }
-
pub fn reveal_in_finder(
&mut self,
_: &RevealInFileManager,
@@ -19336,9 +19322,12 @@ impl Editor {
_window: &mut Window,
cx: &mut Context<Self>,
) {
- if let Some(path) = self.target_file_path(cx)
- && let Some(path) = path.to_str()
- {
+ if let Some(path) = self.active_excerpt(cx).and_then(|(_, buffer, _)| {
+ let project = self.project()?.read(cx);
+ let path = buffer.read(cx).file()?.path();
+ let path = path.display(project.path_style(cx));
+ Some(path)
+ }) {
cx.write_to_clipboard(ClipboardItem::new_string(path.to_string()));
} else {
cx.propagate();
@@ -19414,16 +19403,14 @@ impl Editor {
) {
if let Some(file) = self.target_file(cx)
&& let Some(file_stem) = file.path().file_stem()
- && let Some(name) = file_stem.to_str()
{
- cx.write_to_clipboard(ClipboardItem::new_string(name.to_string()));
+ cx.write_to_clipboard(ClipboardItem::new_string(file_stem.to_string()));
}
}
pub fn copy_file_name(&mut self, _: &CopyFileName, _: &mut Window, cx: &mut Context<Self>) {
if let Some(file) = self.target_file(cx)
- && let Some(file_name) = file.path().file_name()
- && let Some(name) = file_name.to_str()
+ && let Some(name) = file.path().file_name()
{
cx.write_to_clipboard(ClipboardItem::new_string(name.to_string()));
}
@@ -19691,9 +19678,8 @@ impl Editor {
cx: &mut Context<Self>,
) {
let selection = self.selections.newest::<Point>(cx).start.row + 1;
- if let Some(file) = self.target_file(cx)
- && let Some(path) = file.path().to_str()
- {
+ if let Some(file) = self.target_file(cx) {
+ let path = file.path().display(file.path_style(cx));
cx.write_to_clipboard(ClipboardItem::new_string(format!("{path}:{selection}")));
}
}
@@ -56,6 +56,7 @@ use text::ToPoint as _;
use unindent::Unindent;
use util::{
assert_set_eq, path,
+ rel_path::rel_path,
test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
uri,
};
@@ -11142,19 +11143,19 @@ async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
let buffer_1 = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "main.rs"), cx)
+ project.open_buffer((worktree_id, rel_path("main.rs")), cx)
})
.await
.unwrap();
let buffer_2 = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "other.rs"), cx)
+ project.open_buffer((worktree_id, rel_path("other.rs")), cx)
})
.await
.unwrap();
let buffer_3 = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "lib.rs"), cx)
+ project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
})
.await
.unwrap();
@@ -11329,19 +11330,19 @@ async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
// Open three buffers
let buffer_1 = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "file1.rs"), cx)
+ project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
})
.await
.unwrap();
let buffer_2 = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "file2.rs"), cx)
+ project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
})
.await
.unwrap();
let buffer_3 = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "file3.rs"), cx)
+ project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
})
.await
.unwrap();
@@ -14677,7 +14678,7 @@ async fn test_multiline_completion(cx: &mut TestAppContext) {
.unwrap();
let editor = workspace
.update(cx, |workspace, window, cx| {
- workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
})
.unwrap()
.await
@@ -16394,7 +16395,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
leader.update(cx, |leader, cx| {
leader.buffer.update(cx, |multibuffer, cx| {
multibuffer.set_excerpts_for_path(
- PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
+ PathKey::namespaced(1, "b.txt".into()),
buffer_1.clone(),
vec![
Point::row_range(0..3),
@@ -16405,7 +16406,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
cx,
);
multibuffer.set_excerpts_for_path(
- PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
+ PathKey::namespaced(1, "a.txt".into()),
buffer_2.clone(),
vec![Point::row_range(0..6), Point::row_range(8..12)],
0,
@@ -16897,7 +16898,7 @@ async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
.unwrap();
let editor_handle = workspace
.update(cx, |workspace, window, cx| {
- workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
})
.unwrap()
.await
@@ -20878,9 +20879,9 @@ async fn test_display_diff_hunks(cx: &mut TestAppContext) {
fs.set_head_for_repo(
path!("/test/.git").as_ref(),
&[
- ("file-1".into(), "one\n".into()),
- ("file-2".into(), "two\n".into()),
- ("file-3".into(), "three\n".into()),
+ ("file-1", "one\n".into()),
+ ("file-2", "two\n".into()),
+ ("file-3", "three\n".into()),
],
"deadbeef",
);
@@ -20904,7 +20905,7 @@ async fn test_display_diff_hunks(cx: &mut TestAppContext) {
for buffer in &buffers {
let snapshot = buffer.read(cx).snapshot();
multibuffer.set_excerpts_for_path(
- PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
+ PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().as_str().into()),
buffer.clone(),
vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
2,
@@ -21657,19 +21658,19 @@ async fn test_folding_buffers(cx: &mut TestAppContext) {
let buffer_1 = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "first.rs"), cx)
+ project.open_buffer((worktree_id, rel_path("first.rs")), cx)
})
.await
.unwrap();
let buffer_2 = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "second.rs"), cx)
+ project.open_buffer((worktree_id, rel_path("second.rs")), cx)
})
.await
.unwrap();
let buffer_3 = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "third.rs"), cx)
+ project.open_buffer((worktree_id, rel_path("third.rs")), cx)
})
.await
.unwrap();
@@ -21825,19 +21826,19 @@ async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
let buffer_1 = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "first.rs"), cx)
+ project.open_buffer((worktree_id, rel_path("first.rs")), cx)
})
.await
.unwrap();
let buffer_2 = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "second.rs"), cx)
+ project.open_buffer((worktree_id, rel_path("second.rs")), cx)
})
.await
.unwrap();
let buffer_3 = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "third.rs"), cx)
+ project.open_buffer((worktree_id, rel_path("third.rs")), cx)
})
.await
.unwrap();
@@ -21960,7 +21961,7 @@ async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut Test
let buffer_1 = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "main.rs"), cx)
+ project.open_buffer((worktree_id, rel_path("main.rs")), cx)
})
.await
.unwrap();
@@ -22499,7 +22500,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
let buffer = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "main.rs"), cx)
+ project.open_buffer((worktree_id, rel_path("main.rs")), cx)
})
.await
.unwrap();
@@ -22613,7 +22614,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
let buffer = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "main.rs"), cx)
+ project.open_buffer((worktree_id, rel_path("main.rs")), cx)
})
.await
.unwrap();
@@ -22783,7 +22784,7 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
let buffer = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "main.rs"), cx)
+ project.open_buffer((worktree_id, rel_path("main.rs")), cx)
})
.await
.unwrap();
@@ -23371,7 +23372,7 @@ println!("5");
let editor_1 = workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_path(
- (worktree_id, "main.rs"),
+ (worktree_id, rel_path("main.rs")),
Some(pane_1.downgrade()),
true,
window,
@@ -23414,7 +23415,7 @@ println!("5");
let editor_2 = workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_path(
- (worktree_id, "main.rs"),
+ (worktree_id, rel_path("main.rs")),
Some(pane_2.downgrade()),
true,
window,
@@ -23453,7 +23454,7 @@ println!("5");
let _other_editor_1 = workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_path(
- (worktree_id, "lib.rs"),
+ (worktree_id, rel_path("lib.rs")),
Some(pane_1.downgrade()),
true,
window,
@@ -23489,7 +23490,7 @@ println!("5");
let _other_editor_2 = workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_path(
- (worktree_id, "lib.rs"),
+ (worktree_id, rel_path("lib.rs")),
Some(pane_2.downgrade()),
true,
window,
@@ -23526,7 +23527,7 @@ println!("5");
let _editor_1_reopened = workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_path(
- (worktree_id, "main.rs"),
+ (worktree_id, rel_path("main.rs")),
Some(pane_1.downgrade()),
true,
window,
@@ -23540,7 +23541,7 @@ println!("5");
let _editor_2_reopened = workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_path(
- (worktree_id, "main.rs"),
+ (worktree_id, rel_path("main.rs")),
Some(pane_2.downgrade()),
true,
window,
@@ -23634,7 +23635,7 @@ println!("5");
let editor = workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_path(
- (worktree_id, "main.rs"),
+ (worktree_id, rel_path("main.rs")),
Some(pane.downgrade()),
true,
window,
@@ -23693,7 +23694,7 @@ println!("5");
let _editor_reopened = workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_path(
- (worktree_id, "main.rs"),
+ (worktree_id, rel_path("main.rs")),
Some(pane.downgrade()),
true,
window,
@@ -23860,7 +23861,7 @@ async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
.unwrap();
let editor = workspace
.update(cx, |workspace, window, cx| {
- workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
})
.unwrap()
.await
@@ -24054,7 +24055,7 @@ async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
let main_editor = workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_path(
- (worktree_id, "main.rs"),
+ (worktree_id, rel_path("main.rs")),
Some(pane.downgrade()),
true,
window,
@@ -25636,7 +25637,7 @@ async fn test_document_colors(cx: &mut TestAppContext) {
.path();
assert_eq!(
editor_file.as_ref(),
- Path::new("first.rs"),
+ rel_path("first.rs"),
"Both editors should be opened for the same file"
)
}
@@ -25816,7 +25817,7 @@ async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
let handle = workspace
.update_in(cx, |workspace, window, cx| {
- let project_path = (worktree_id, "one.pdf");
+ let project_path = (worktree_id, rel_path("one.pdf"));
workspace.open_path(project_path, None, true, window, cx)
})
.await
@@ -3779,13 +3779,15 @@ impl EditorElement {
.as_ref()
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
.unwrap_or_default();
- let can_open_excerpts = Editor::can_open_excerpts_in_file(for_excerpt.buffer.file());
+ let file = for_excerpt.buffer.file();
+ let can_open_excerpts = Editor::can_open_excerpts_in_file(file);
+ let path_style = file.map(|file| file.path_style(cx));
let relative_path = for_excerpt.buffer.resolve_file_path(cx, include_root);
let filename = relative_path
.as_ref()
.and_then(|path| Some(path.file_name()?.to_string_lossy().to_string()));
let parent_path = relative_path.as_ref().and_then(|path| {
- Some(path.parent()?.to_string_lossy().to_string() + std::path::MAIN_SEPARATOR_STR)
+ Some(path.parent()?.to_string_lossy().to_string() + path_style?.separator())
});
let focus_handle = editor.focus_handle(cx);
let colors = cx.theme().colors();
@@ -3990,12 +3992,13 @@ impl EditorElement {
&& let Some(worktree) =
project.read(cx).worktree_for_id(file.worktree_id(cx), cx)
{
+ let path_style = file.path_style(cx);
let worktree = worktree.read(cx);
let relative_path = file.path();
let entry_for_path = worktree.entry_for_path(relative_path);
let abs_path = entry_for_path.map(|e| {
e.canonical_path.as_deref().map_or_else(
- || worktree.abs_path().join(relative_path),
+ || worktree.absolutize(relative_path),
Path::to_path_buf,
)
});
@@ -4031,7 +4034,7 @@ impl EditorElement {
Some(Box::new(zed_actions::workspace::CopyRelativePath)),
window.handler_for(&editor, move |_, _, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(
- relative_path.to_string_lossy().to_string(),
+ relative_path.display(path_style).to_string(),
));
}),
)
@@ -698,6 +698,7 @@ async fn parse_commit_messages(
#[cfg(test)]
mod tests {
use super::*;
+ use git::repository::repo_path;
use gpui::Context;
use language::{Point, Rope};
use project::FakeFs;
@@ -850,7 +851,7 @@ mod tests {
fs.set_blame_for_repo(
Path::new("/my-repo/.git"),
vec![(
- "file.txt".into(),
+ repo_path("file.txt"),
Blame {
entries: vec![
blame_entry("1b1b1b", 0..1),
@@ -967,7 +968,7 @@ mod tests {
fs.set_blame_for_repo(
Path::new(path!("/my-repo/.git")),
vec![(
- "file.txt".into(),
+ repo_path("file.txt"),
Blame {
entries: vec![blame_entry("1b1b1b", 0..4)],
..Default::default()
@@ -1135,7 +1136,7 @@ mod tests {
fs.set_blame_for_repo(
Path::new(path!("/my-repo/.git")),
vec![(
- "file.txt".into(),
+ repo_path("file.txt"),
Blame {
entries: blame_entries,
..Default::default()
@@ -1178,7 +1179,7 @@ mod tests {
fs.set_blame_for_repo(
Path::new(path!("/my-repo/.git")),
vec![(
- "file.txt".into(),
+ repo_path("file.txt"),
Blame {
entries: blame_entries,
..Default::default()
@@ -651,7 +651,7 @@ impl Item for Editor {
fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString {
if let Some(path) = path_for_buffer(&self.buffer, detail, true, cx) {
- path.to_string_lossy().to_string().into()
+ path.to_string().into()
} else {
// Use the same logic as the displayed title for consistency
self.buffer.read(cx).title(cx).to_string().into()
@@ -667,7 +667,7 @@ impl Item for Editor {
.file_icons
.then(|| {
path_for_buffer(&self.buffer, 0, true, cx)
- .and_then(|path| FileIcons::get_icon(path.as_ref(), cx))
+ .and_then(|path| FileIcons::get_icon(Path::new(&*path), cx))
})
.flatten()
.map(Icon::from_path)
@@ -703,8 +703,7 @@ impl Item for Editor {
let description = params.detail.and_then(|detail| {
let path = path_for_buffer(&self.buffer, detail, false, cx)?;
- let description = path.to_string_lossy();
- let description = description.trim();
+ let description = path.trim();
if description.is_empty() {
return None;
@@ -898,10 +897,7 @@ impl Item for Editor {
.as_singleton()
.expect("cannot call save_as on an excerpt list");
- let file_extension = path
- .path
- .extension()
- .map(|a| a.to_string_lossy().to_string());
+ let file_extension = path.path.extension().map(|a| a.to_string());
self.report_editor_event(
ReportEditorEvent::Saved { auto_saved: false },
file_extension,
@@ -1167,7 +1163,7 @@ impl SerializableItem for Editor {
let (worktree, path) = project.find_worktree(&abs_path, cx)?;
let project_path = ProjectPath {
worktree_id: worktree.read(cx).id(),
- path: path.into(),
+ path: path,
};
Some(project.open_path(project_path, cx))
});
@@ -1288,7 +1284,7 @@ impl SerializableItem for Editor {
project
.read(cx)
.worktree_for_id(worktree_id, cx)
- .and_then(|worktree| worktree.read(cx).absolutize(file.path()).ok())
+ .map(|worktree| worktree.read(cx).absolutize(file.path()))
.or_else(|| {
let full_path = file.full_path(cx);
let project_path = project.read(cx).find_project_path(&full_path, cx)?;
@@ -1882,7 +1878,7 @@ fn path_for_buffer<'a>(
height: usize,
include_filename: bool,
cx: &'a App,
-) -> Option<Cow<'a, Path>> {
+) -> Option<Cow<'a, str>> {
let file = buffer.read(cx).as_singleton()?.read(cx).file()?;
path_for_file(file.as_ref(), height, include_filename, cx)
}
@@ -1892,7 +1888,7 @@ fn path_for_file<'a>(
mut height: usize,
include_filename: bool,
cx: &'a App,
-) -> Option<Cow<'a, Path>> {
+) -> Option<Cow<'a, str>> {
// Ensure we always render at least the filename.
height += 1;
@@ -1906,22 +1902,21 @@ fn path_for_file<'a>(
}
}
- // Here we could have just always used `full_path`, but that is very
- // allocation-heavy and so we try to use a `Cow<Path>` if we haven't
- // traversed all the way up to the worktree's root.
+ // The full_path method allocates, so avoid calling it if height is zero.
if height > 0 {
- let full_path = file.full_path(cx);
- if include_filename {
- Some(full_path.into())
- } else {
- Some(full_path.parent()?.to_path_buf().into())
+ let mut full_path = file.full_path(cx);
+ if !include_filename {
+ if !full_path.pop() {
+ return None;
+ }
}
+ Some(full_path.to_string_lossy().to_string().into())
} else {
let mut path = file.path().strip_prefix(prefix).ok()?;
if !include_filename {
path = path.parent()?;
}
- Some(path.into())
+ Some(path.display(file.path_style(cx)))
}
}
@@ -1936,12 +1931,12 @@ mod tests {
use language::{LanguageMatcher, TestFile};
use project::FakeFs;
use std::path::{Path, PathBuf};
- use util::path;
+ use util::{path, rel_path::RelPath};
#[gpui::test]
fn test_path_for_file(cx: &mut App) {
let file = TestFile {
- path: Path::new("").into(),
+ path: RelPath::empty().into(),
root_name: String::new(),
local_root: None,
};
@@ -217,15 +217,7 @@ pub fn editor_content_with_blocks(editor: &Entity<Editor>, cx: &mut VisualTestCo
height,
} => {
lines[row.0 as usize].push_str(&cx.update(|_, cx| {
- format!(
- "§ {}",
- first_excerpt
- .buffer
- .file()
- .unwrap()
- .file_name(cx)
- .to_string_lossy()
- )
+ format!("§ {}", first_excerpt.buffer.file().unwrap().file_name(cx))
}));
for row in row.0 + 1..row.0 + height {
lines[row as usize].push_str("§ -----");
@@ -237,17 +229,11 @@ pub fn editor_content_with_blocks(editor: &Entity<Editor>, cx: &mut VisualTestCo
}
}
Block::BufferHeader { excerpt, height } => {
- lines[row.0 as usize].push_str(&cx.update(|_, cx| {
- format!(
- "§ {}",
- excerpt
- .buffer
- .file()
- .unwrap()
- .file_name(cx)
- .to_string_lossy()
- )
- }));
+ lines[row.0 as usize].push_str(
+ &cx.update(|_, cx| {
+ format!("§ {}", excerpt.buffer.file().unwrap().file_name(cx))
+ }),
+ );
for row in row.0 + 1..row.0 + height {
lines[row as usize].push_str("§ -----");
}
@@ -296,7 +296,7 @@ impl EditorTestContext {
let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
fs.set_head_for_repo(
&Self::root_path().join(".git"),
- &[(path.into(), diff_base.to_string())],
+ &[(path.as_str(), diff_base.to_string())],
"deadbeef",
);
self.cx.run_until_parked();
@@ -317,7 +317,7 @@ impl EditorTestContext {
let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
fs.set_index_for_repo(
&Self::root_path().join(".git"),
- &[(path.into(), diff_base.to_string())],
+ &[(path.as_str(), diff_base.to_string())],
);
self.cx.run_until_parked();
}
@@ -329,7 +329,7 @@ impl EditorTestContext {
let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
let mut found = None;
fs.with_git_state(&Self::root_path().join(".git"), false, |git_state| {
- found = git_state.index_contents.get(path.as_ref()).cloned();
+ found = git_state.index_contents.get(&path.into()).cloned();
})
.unwrap();
assert_eq!(expected, found.as_deref());
@@ -1,7 +1,6 @@
use std::{
error::Error,
fmt::{self, Debug},
- path::Path,
sync::{Arc, Mutex},
time::Duration,
};
@@ -20,6 +19,7 @@ use collections::HashMap;
use futures::{FutureExt as _, StreamExt, channel::mpsc, select_biased};
use gpui::{App, AppContext, AsyncApp, Entity};
use language_model::{LanguageModel, Role, StopReason};
+use util::rel_path::RelPath;
pub const THREAD_EVENT_TIMEOUT: Duration = Duration::from_secs(60 * 2);
@@ -354,7 +354,7 @@ impl ExampleContext {
Ok(response)
}
- pub fn edits(&self) -> HashMap<Arc<Path>, FileEdits> {
+ pub fn edits(&self) -> HashMap<Arc<RelPath>, FileEdits> {
self.agent_thread
.read_with(&self.app, |thread, cx| {
let action_log = thread.action_log().read(cx);
@@ -1,8 +1,7 @@
-use std::path::Path;
-
use agent_settings::AgentProfileId;
use anyhow::Result;
use async_trait::async_trait;
+use util::rel_path::RelPath;
use crate::example::{Example, ExampleContext, ExampleMetadata, JudgeAssertion, LanguageServer};
@@ -68,7 +67,7 @@ impl Example for AddArgToTraitMethod {
for tool_name in add_ignored_window_paths {
let path_str = format!("crates/assistant_tools/src/{}.rs", tool_name);
- let edits = edits.get(Path::new(&path_str));
+ let edits = edits.get(RelPath::new(&path_str).unwrap());
let ignored = edits.is_some_and(|edits| {
edits.has_added_line(" _window: Option<gpui::AnyWindowHandle>,\n")
@@ -86,7 +85,8 @@ impl Example for AddArgToTraitMethod {
// Adds unignored argument to `batch_tool`
- let batch_tool_edits = edits.get(Path::new("crates/assistant_tools/src/batch_tool.rs"));
+ let batch_tool_edits =
+ edits.get(RelPath::new("crates/assistant_tools/src/batch_tool.rs").unwrap());
cx.assert(
batch_tool_edits.is_some_and(|edits| {
@@ -65,7 +65,7 @@ impl Example for CodeBlockCitations {
thread
.project()
.read(cx)
- .find_project_path(path_range.path, cx)
+ .find_project_path(path_range.path.as_ref(), cx)
})
.ok()
.flatten();
@@ -250,7 +250,7 @@ impl ExampleInstance {
worktree
.files(false, 0)
.find_map(|e| {
- if e.path.clone().extension().and_then(|ext| ext.to_str())
+ if e.path.clone().extension()
== Some(&language_server.file_extension)
{
Some(ProjectPath {
@@ -16,6 +16,7 @@ use gpui::{App, Task};
use language::LanguageName;
use semantic_version::SemanticVersion;
use task::{SpawnInTerminal, ZedDebugConfig};
+use util::rel_path::RelPath;
pub use crate::capabilities::*;
pub use crate::extension_events::*;
@@ -33,7 +34,7 @@ pub fn init(cx: &mut App) {
pub trait WorktreeDelegate: Send + Sync + 'static {
fn id(&self) -> u64;
fn root_path(&self) -> String;
- async fn read_text_file(&self, path: PathBuf) -> Result<String>;
+ async fn read_text_file(&self, path: &RelPath) -> Result<String>;
async fn which(&self, binary_name: String) -> Option<String>;
async fn shell_env(&self) -> Vec<(String, String)>;
}
@@ -1752,7 +1752,14 @@ impl ExtensionStore {
})?
.await?;
let dest_dir = RemotePathBuf::new(
- PathBuf::from(&response.tmp_dir).join(missing_extension.clone().id),
+ path_style
+ .join(&response.tmp_dir, &missing_extension.id)
+ .with_context(|| {
+ format!(
+ "failed to construct destination path: {:?}, {:?}",
+ response.tmp_dir, missing_extension.id,
+ )
+ })?,
path_style,
);
log::info!("Uploading extension {}", missing_extension.clone().id);
@@ -1,10 +1,7 @@
use std::{path::PathBuf, sync::Arc};
use anyhow::{Context as _, Result};
-use client::{
- TypedEnvelope,
- proto::{self, FromProto},
-};
+use client::{TypedEnvelope, proto};
use collections::{HashMap, HashSet};
use extension::{
Extension, ExtensionDebugAdapterProviderProxy, ExtensionHostProxy, ExtensionLanguageProxy,
@@ -342,7 +339,7 @@ impl HeadlessExtensionStore {
version: extension.version,
dev: extension.dev,
},
- PathBuf::from_proto(envelope.payload.tmp_dir),
+ PathBuf::from(envelope.payload.tmp_dir),
cx,
)
})?
@@ -16,6 +16,7 @@ use std::{
path::{Path, PathBuf},
sync::{Arc, OnceLock},
};
+use util::rel_path::RelPath;
use util::{archive::extract_zip, fs::make_file_executable, maybe};
use wasmtime::component::{Linker, Resource};
@@ -421,12 +422,12 @@ impl ExtensionImports for WasmState {
) -> wasmtime::Result<Result<String, String>> {
self.on_main_thread(|cx| {
async move {
- let location = location
- .as_ref()
- .map(|location| ::settings::SettingsLocation {
+ let location = location.as_ref().and_then(|location| {
+ Some(::settings::SettingsLocation {
worktree_id: WorktreeId::from_proto(location.worktree_id),
- path: Path::new(&location.path),
- });
+ path: RelPath::new(&location.path).ok()?,
+ })
+ });
cx.update(|cx| match category.as_str() {
"language" => {
@@ -31,7 +31,7 @@ use std::{
};
use task::{SpawnInTerminal, ZedDebugConfig};
use url::Url;
-use util::{archive::extract_zip, fs::make_file_executable, maybe};
+use util::{archive::extract_zip, fs::make_file_executable, maybe, rel_path::RelPath};
use wasmtime::component::{Linker, Resource};
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 6, 0);
@@ -564,7 +564,7 @@ impl HostWorktree for WasmState {
) -> wasmtime::Result<Result<String, String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate
- .read_text_file(path.into())
+ .read_text_file(RelPath::new(&path)?)
.await
.map_err(|error| error.to_string()))
}
@@ -914,12 +914,12 @@ impl ExtensionImports for WasmState {
) -> wasmtime::Result<Result<String, String>> {
self.on_main_thread(|cx| {
async move {
- let location = location
- .as_ref()
- .map(|location| ::settings::SettingsLocation {
+ let location = location.as_ref().and_then(|location| {
+ Some(::settings::SettingsLocation {
worktree_id: WorktreeId::from_proto(location.worktree_id),
- path: Path::new(&location.path),
- });
+ path: RelPath::new(&location.path).ok()?,
+ })
+ });
cx.update(|cx| match category.as_str() {
"language" => {
@@ -1,5 +1,4 @@
use std::collections::HashMap;
-use std::path::Path;
use std::sync::{Arc, OnceLock};
use db::kvp::KEY_VALUE_STORE;
@@ -8,6 +7,7 @@ use extension_host::ExtensionStore;
use gpui::{AppContext as _, Context, Entity, SharedString, Window};
use language::Buffer;
use ui::prelude::*;
+use util::rel_path::RelPath;
use workspace::notifications::simple_message_notification::MessageNotification;
use workspace::{Workspace, notifications::NotificationId};
@@ -100,15 +100,9 @@ struct SuggestedExtension {
}
/// Returns the suggested extension for the given [`Path`].
-fn suggested_extension(path: impl AsRef<Path>) -> Option<SuggestedExtension> {
- let path = path.as_ref();
-
- let file_extension: Option<Arc<str>> = path
- .extension()
- .and_then(|extension| Some(extension.to_str()?.into()));
- let file_name: Option<Arc<str>> = path
- .file_name()
- .and_then(|file_name| Some(file_name.to_str()?.into()));
+fn suggested_extension(path: &RelPath) -> Option<SuggestedExtension> {
+ let file_extension: Option<Arc<str>> = path.extension().map(|extension| extension.into());
+ let file_name: Option<Arc<str>> = path.file_name().map(|name| name.into());
let (file_name_or_extension, extension_id) = None
// We suggest against file names first, as these suggestions will be more
@@ -210,39 +204,40 @@ pub(crate) fn suggest(buffer: Entity<Buffer>, window: &mut Window, cx: &mut Cont
#[cfg(test)]
mod tests {
use super::*;
+ use util::rel_path::rel_path;
#[test]
pub fn test_suggested_extension() {
assert_eq!(
- suggested_extension("Cargo.toml"),
+ suggested_extension(rel_path("Cargo.toml")),
Some(SuggestedExtension {
extension_id: "toml".into(),
file_name_or_extension: "toml".into()
})
);
assert_eq!(
- suggested_extension("Cargo.lock"),
+ suggested_extension(rel_path("Cargo.lock")),
Some(SuggestedExtension {
extension_id: "toml".into(),
file_name_or_extension: "Cargo.lock".into()
})
);
assert_eq!(
- suggested_extension("Dockerfile"),
+ suggested_extension(rel_path("Dockerfile")),
Some(SuggestedExtension {
extension_id: "dockerfile".into(),
file_name_or_extension: "Dockerfile".into()
})
);
assert_eq!(
- suggested_extension("a/b/c/d/.gitignore"),
+ suggested_extension(rel_path("a/b/c/d/.gitignore")),
Some(SuggestedExtension {
extension_id: "git-firefly".into(),
file_name_or_extension: ".gitignore".into()
})
);
assert_eq!(
- suggested_extension("a/b/c/d/test.gleam"),
+ suggested_extension(rel_path("a/b/c/d/test.gleam")),
Some(SuggestedExtension {
extension_id: "gleam".into(),
file_name_or_extension: "gleam".into()
@@ -39,7 +39,12 @@ use ui::{
ButtonLike, ContextMenu, HighlightedLabel, Indicator, KeyBinding, ListItem, ListItemSpacing,
PopoverMenu, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
};
-use util::{ResultExt, maybe, paths::PathWithPosition, post_inc};
+use util::{
+ ResultExt, maybe,
+ paths::{PathStyle, PathWithPosition},
+ post_inc,
+ rel_path::RelPath,
+};
use workspace::{
ModalView, OpenOptions, OpenVisible, SplitDirection, Workspace, item::PreviewTabsSettings,
notifications::NotifyResultExt, pane,
@@ -126,38 +131,34 @@ impl FileFinder {
let project = workspace.project().read(cx);
let fs = project.fs();
- let currently_opened_path = workspace
- .active_item(cx)
- .and_then(|item| item.project_path(cx))
- .map(|project_path| {
- let abs_path = project
- .worktree_for_id(project_path.worktree_id, cx)
- .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path));
- FoundPath::new(project_path, abs_path)
- });
+ let currently_opened_path = workspace.active_item(cx).and_then(|item| {
+ let project_path = item.project_path(cx)?;
+ let abs_path = project
+ .worktree_for_id(project_path.worktree_id, cx)?
+ .read(cx)
+ .absolutize(&project_path.path);
+ Some(FoundPath::new(project_path, abs_path))
+ });
let history_items = workspace
.recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
.into_iter()
.filter_map(|(project_path, abs_path)| {
if project.entry_for_path(&project_path, cx).is_some() {
- return Some(Task::ready(Some(FoundPath::new(project_path, abs_path))));
+ return Some(Task::ready(Some(FoundPath::new(project_path, abs_path?))));
}
let abs_path = abs_path?;
if project.is_local() {
let fs = fs.clone();
Some(cx.background_spawn(async move {
if fs.is_file(&abs_path).await {
- Some(FoundPath::new(project_path, Some(abs_path)))
+ Some(FoundPath::new(project_path, abs_path))
} else {
None
}
}))
} else {
- Some(Task::ready(Some(FoundPath::new(
- project_path,
- Some(abs_path),
- ))))
+ Some(Task::ready(Some(FoundPath::new(project_path, abs_path))))
}
})
.collect::<Vec<_>>();
@@ -465,7 +466,7 @@ enum Match {
}
impl Match {
- fn relative_path(&self) -> Option<&Arc<Path>> {
+ fn relative_path(&self) -> Option<&Arc<RelPath>> {
match self {
Match::History { path, .. } => Some(&path.project.path),
Match::Search(panel_match) => Some(&panel_match.0.path),
@@ -475,20 +476,14 @@ impl Match {
fn abs_path(&self, project: &Entity<Project>, cx: &App) -> Option<PathBuf> {
match self {
- Match::History { path, .. } => path.absolute.clone().or_else(|| {
+ Match::History { path, .. } => Some(path.absolute.clone()),
+ Match::Search(ProjectPanelOrdMatch(path_match)) => Some(
project
.read(cx)
- .worktree_for_id(path.project.worktree_id, cx)?
+ .worktree_for_id(WorktreeId::from_usize(path_match.worktree_id), cx)?
.read(cx)
- .absolutize(&path.project.path)
- .ok()
- }),
- Match::Search(ProjectPanelOrdMatch(path_match)) => project
- .read(cx)
- .worktree_for_id(WorktreeId::from_usize(path_match.worktree_id), cx)?
- .read(cx)
- .absolutize(&path_match.path)
- .ok(),
+ .absolutize(&path_match.path),
+ ),
Match::CreateNew(_) => None,
}
}
@@ -671,10 +666,9 @@ impl Matches {
}
if let Some(filename) = panel_match.0.path.file_name() {
- let path_str = panel_match.0.path.to_string_lossy();
- let filename_str = filename.to_string_lossy();
+ let path_str = panel_match.0.path.as_str();
- if let Some(filename_pos) = path_str.rfind(&*filename_str)
+ if let Some(filename_pos) = path_str.rfind(filename)
&& panel_match.0.positions[0] >= filename_pos
{
let mut prev_position = panel_match.0.positions[0];
@@ -696,7 +690,7 @@ fn matching_history_items<'a>(
history_items: impl IntoIterator<Item = &'a FoundPath>,
currently_opened: Option<&'a FoundPath>,
query: &FileSearchQuery,
-) -> HashMap<Arc<Path>, Match> {
+) -> HashMap<Arc<RelPath>, Match> {
let mut candidates_paths = HashMap::default();
let history_items_by_worktrees = history_items
@@ -714,7 +708,7 @@ fn matching_history_items<'a>(
.project
.path
.file_name()?
- .to_string_lossy()
+ .to_string()
.to_lowercase()
.chars(),
),
@@ -768,11 +762,11 @@ fn matching_history_items<'a>(
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct FoundPath {
project: ProjectPath,
- absolute: Option<PathBuf>,
+ absolute: PathBuf,
}
impl FoundPath {
- fn new(project: ProjectPath, absolute: Option<PathBuf>) -> Self {
+ fn new(project: ProjectPath, absolute: PathBuf) -> Self {
Self { project, absolute }
}
}
@@ -944,47 +938,44 @@ impl FileFinderDelegate {
extend_old_matches,
);
- let filename = &query.raw_query;
- let mut query_path = Path::new(filename);
- // add option of creating new file only if path is relative
- let available_worktree = self
- .project
- .read(cx)
- .visible_worktrees(cx)
- .filter(|worktree| !worktree.read(cx).is_single_file())
- .collect::<Vec<_>>();
- let worktree_count = available_worktree.len();
- let mut expect_worktree = available_worktree.first().cloned();
- for worktree in available_worktree {
- let worktree_root = worktree
+ let path_style = self.project.read(cx).path_style(cx);
+ let query_path = query.raw_query.as_str();
+ if let Ok(mut query_path) = RelPath::from_std_path(Path::new(query_path), path_style) {
+ let available_worktree = self
+ .project
.read(cx)
- .abs_path()
- .file_name()
- .map_or(String::new(), |f| f.to_string_lossy().to_string());
- if worktree_count > 1 && query_path.starts_with(&worktree_root) {
- query_path = query_path
- .strip_prefix(&worktree_root)
- .unwrap_or(query_path);
- expect_worktree = Some(worktree);
- break;
+ .visible_worktrees(cx)
+ .filter(|worktree| !worktree.read(cx).is_single_file())
+ .collect::<Vec<_>>();
+ let worktree_count = available_worktree.len();
+ let mut expect_worktree = available_worktree.first().cloned();
+ for worktree in available_worktree {
+ let worktree_root = worktree.read(cx).root_name();
+ if worktree_count > 1 {
+ if let Ok(suffix) = query_path.strip_prefix(worktree_root) {
+ query_path = suffix.into();
+ expect_worktree = Some(worktree);
+ break;
+ }
+ }
}
- }
- if let Some(FoundPath { ref project, .. }) = self.currently_opened_path {
- let worktree_id = project.worktree_id;
- expect_worktree = self.project.read(cx).worktree_for_id(worktree_id, cx);
- }
+ if let Some(FoundPath { ref project, .. }) = self.currently_opened_path {
+ let worktree_id = project.worktree_id;
+ expect_worktree = self.project.read(cx).worktree_for_id(worktree_id, cx);
+ }
- if let Some(worktree) = expect_worktree {
- let worktree = worktree.read(cx);
- if query_path.is_relative()
- && worktree.entry_for_path(&query_path).is_none()
- && !filename.ends_with("/")
- {
- self.matches.matches.push(Match::CreateNew(ProjectPath {
- worktree_id: worktree.id(),
- path: Arc::from(query_path),
- }));
+ if let Some(worktree) = expect_worktree {
+ let worktree = worktree.read(cx);
+ if worktree.entry_for_path(&query_path).is_none()
+ && !query.raw_query.ends_with("/")
+ && !(path_style.is_windows() && query.raw_query.ends_with("\\"))
+ {
+ self.matches.matches.push(Match::CreateNew(ProjectPath {
+ worktree_id: worktree.id(),
+ path: query_path,
+ }));
+ }
}
}
@@ -1009,8 +1000,8 @@ impl FileFinderDelegate {
path_match: &Match,
window: &mut Window,
cx: &App,
- ix: usize,
) -> (HighlightedLabel, HighlightedLabel) {
+ let path_style = self.project.read(cx).path_style(cx);
let (file_name, file_name_positions, mut full_path, mut full_path_positions) =
match &path_match {
Match::History {
@@ -1018,68 +1009,52 @@ impl FileFinderDelegate {
panel_match,
} => {
let worktree_id = entry_path.project.worktree_id;
- let project_relative_path = &entry_path.project.path;
- let has_worktree = self
+ let worktree = self
.project
.read(cx)
.worktree_for_id(worktree_id, cx)
- .is_some();
-
- if let Some(absolute_path) =
- entry_path.absolute.as_ref().filter(|_| !has_worktree)
- {
+ .filter(|worktree| worktree.read(cx).is_visible());
+
+ if let Some(panel_match) = panel_match {
+ self.labels_for_path_match(&panel_match.0, path_style)
+ } else if let Some(worktree) = worktree {
+ let full_path =
+ worktree.read(cx).root_name().join(&entry_path.project.path);
+ let mut components = full_path.components();
+ let filename = components.next_back().unwrap_or("");
+ let prefix = components.rest();
(
- absolute_path
- .file_name()
- .map_or_else(
- || project_relative_path.to_string_lossy(),
- |file_name| file_name.to_string_lossy(),
- )
- .to_string(),
+ filename.to_string(),
Vec::new(),
- absolute_path.to_string_lossy().to_string(),
+ prefix.display(path_style).to_string() + path_style.separator(),
Vec::new(),
)
} else {
- let mut path = Arc::clone(project_relative_path);
- if project_relative_path.as_ref() == Path::new("")
- && let Some(absolute_path) = &entry_path.absolute
- {
- path = Arc::from(absolute_path.as_path());
- }
-
- let mut path_match = PathMatch {
- score: ix as f64,
- positions: Vec::new(),
- worktree_id: worktree_id.to_usize(),
- path,
- is_dir: false, // File finder doesn't support directories
- path_prefix: "".into(),
- distance_to_relative_ancestor: usize::MAX,
- };
- if let Some(found_path_match) = &panel_match {
- path_match
- .positions
- .extend(found_path_match.0.positions.iter())
- }
-
- self.labels_for_path_match(&path_match)
+ (
+ entry_path
+ .absolute
+ .file_name()
+ .map_or(String::new(), |f| f.to_string_lossy().into_owned()),
+ Vec::new(),
+ entry_path.absolute.parent().map_or(String::new(), |path| {
+ path.to_string_lossy().into_owned() + path_style.separator()
+ }),
+ Vec::new(),
+ )
}
}
- Match::Search(path_match) => self.labels_for_path_match(&path_match.0),
+ Match::Search(path_match) => self.labels_for_path_match(&path_match.0, path_style),
Match::CreateNew(project_path) => (
- format!("Create file: {}", project_path.path.display()),
+ format!("Create file: {}", project_path.path.display(path_style)),
vec![],
String::from(""),
vec![],
),
};
- if file_name_positions.is_empty()
- && let Some(user_home_path) = std::env::var("HOME").ok()
- {
- let user_home_path = user_home_path.trim();
- if !user_home_path.is_empty() && full_path.starts_with(user_home_path) {
+ if file_name_positions.is_empty() {
+ let user_home_path = util::paths::home_dir().to_string_lossy();
+ if !user_home_path.is_empty() && full_path.starts_with(&*user_home_path) {
full_path.replace_range(0..user_home_path.len(), "~");
full_path_positions.retain_mut(|pos| {
if *pos >= user_home_path.len() {
@@ -1147,17 +1122,13 @@ impl FileFinderDelegate {
fn labels_for_path_match(
&self,
path_match: &PathMatch,
+ path_style: PathStyle,
) -> (String, Vec<usize>, String, Vec<usize>) {
- let path = &path_match.path;
- let path_string = path.to_string_lossy();
- let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
+ let full_path = path_match.path_prefix.join(&path_match.path);
let mut path_positions = path_match.positions.clone();
- let file_name = path.file_name().map_or_else(
- || path_match.path_prefix.to_string(),
- |file_name| file_name.to_string_lossy().to_string(),
- );
- let file_name_start = path_match.path_prefix.len() + path_string.len() - file_name.len();
+ let file_name = full_path.file_name().unwrap_or("");
+ let file_name_start = full_path.as_str().len() - file_name.len();
let file_name_positions = path_positions
.iter()
.filter_map(|pos| {
@@ -1167,12 +1138,33 @@ impl FileFinderDelegate {
None
}
})
- .collect();
+ .collect::<Vec<_>>();
- let full_path = full_path.trim_end_matches(&file_name).to_string();
+ let full_path = full_path
+ .display(path_style)
+ .trim_end_matches(&file_name)
+ .to_string();
path_positions.retain(|idx| *idx < full_path.len());
- (file_name, file_name_positions, full_path, path_positions)
+ debug_assert!(
+ file_name_positions
+ .iter()
+ .all(|ix| file_name[*ix..].chars().next().is_some()),
+ "invalid file name positions {file_name:?} {file_name_positions:?}"
+ );
+ debug_assert!(
+ path_positions
+ .iter()
+ .all(|ix| full_path[*ix..].chars().next().is_some()),
+ "invalid path positions {full_path:?} {path_positions:?}"
+ );
+
+ (
+ file_name.to_string(),
+ file_name_positions,
+ full_path,
+ path_positions,
+ )
}
fn lookup_absolute_path(
@@ -1210,8 +1202,8 @@ impl FileFinderDelegate {
score: 1.0,
positions: Vec::new(),
worktree_id: worktree.read(cx).id().to_usize(),
- path: Arc::from(relative_path),
- path_prefix: "".into(),
+ path: relative_path,
+ path_prefix: RelPath::empty().into(),
is_dir: false, // File finder doesn't support directories
distance_to_relative_ancestor: usize::MAX,
}));
@@ -1333,7 +1325,7 @@ impl PickerDelegate for FileFinderDelegate {
.all(|worktree| {
worktree
.read(cx)
- .entry_for_path(Path::new("a"))
+ .entry_for_path(RelPath::new("a").unwrap())
.is_none_or(|entry| !entry.is_dir())
})
{
@@ -1351,7 +1343,7 @@ impl PickerDelegate for FileFinderDelegate {
.all(|worktree| {
worktree
.read(cx)
- .entry_for_path(Path::new("b"))
+ .entry_for_path(RelPath::new("b").unwrap())
.is_none_or(|entry| !entry.is_dir())
})
{
@@ -1381,8 +1373,8 @@ impl PickerDelegate for FileFinderDelegate {
project
.worktree_for_id(history_item.project.worktree_id, cx)
.is_some()
- || ((project.is_local() || project.is_via_remote_server())
- && history_item.absolute.is_some())
+ || project.is_local()
+ || project.is_via_remote_server()
}),
self.currently_opened_path.as_ref(),
None,
@@ -1397,13 +1389,7 @@ impl PickerDelegate for FileFinderDelegate {
Task::ready(())
} else {
let path_position = PathWithPosition::parse_str(raw_query);
-
- #[cfg(windows)]
- let raw_query = raw_query.trim().to_owned().replace("/", "\\");
- #[cfg(not(windows))]
- let raw_query = raw_query.trim();
-
- let raw_query = raw_query.trim_end_matches(':').to_owned();
+ let raw_query = raw_query.trim().trim_end_matches(':').to_owned();
let path = path_position.path.to_str();
let path_trimmed = path.unwrap_or(&raw_query).trim_end_matches(':');
let file_query_end = if path_trimmed == raw_query {
@@ -1505,38 +1491,18 @@ impl PickerDelegate for FileFinderDelegate {
window,
cx,
)
+ } else if secondary {
+ workspace.split_abs_path(path.absolute.clone(), false, window, cx)
} else {
- match path.absolute.as_ref() {
- Some(abs_path) => {
- if secondary {
- workspace.split_abs_path(
- abs_path.to_path_buf(),
- false,
- window,
- cx,
- )
- } else {
- workspace.open_abs_path(
- abs_path.to_path_buf(),
- OpenOptions {
- visible: Some(OpenVisible::None),
- ..Default::default()
- },
- window,
- cx,
- )
- }
- }
- None => split_or_open(
- workspace,
- ProjectPath {
- worktree_id,
- path: Arc::clone(&path.project.path),
- },
- window,
- cx,
- ),
- }
+ workspace.open_abs_path(
+ path.absolute.clone(),
+ OpenOptions {
+ visible: Some(OpenVisible::None),
+ ..Default::default()
+ },
+ window,
+ cx,
+ )
}
}
Match::Search(m) => split_or_open(
@@ -1615,7 +1581,7 @@ impl PickerDelegate for FileFinderDelegate {
.size(IconSize::Small)
.into_any_element(),
};
- let (file_name_label, full_path_label) = self.labels_for_match(path_match, window, cx, ix);
+ let (file_name_label, full_path_label) = self.labels_for_match(path_match, window, cx);
let file_icon = maybe!({
if !settings.file_icons {
@@ -4,10 +4,10 @@ use super::*;
use editor::Editor;
use gpui::{Entity, TestAppContext, VisualTestContext};
use menu::{Confirm, SelectNext, SelectPrevious};
-use pretty_assertions::assert_eq;
+use pretty_assertions::{assert_eq, assert_matches};
use project::{FS_WATCH_LATENCY, RemoveOptions};
use serde_json::json;
-use util::path;
+use util::{path, rel_path::rel_path};
use workspace::{AppState, CloseActiveItem, OpenOptions, ToggleFileFinder, Workspace};
#[ctor::ctor]
@@ -77,8 +77,8 @@ fn test_custom_project_search_ordering_in_file_finder() {
score: 0.5,
positions: Vec::new(),
worktree_id: 0,
- path: Arc::from(Path::new("b0.5")),
- path_prefix: Arc::default(),
+ path: rel_path("b0.5").into(),
+ path_prefix: rel_path("").into(),
distance_to_relative_ancestor: 0,
is_dir: false,
}),
@@ -86,8 +86,8 @@ fn test_custom_project_search_ordering_in_file_finder() {
score: 1.0,
positions: Vec::new(),
worktree_id: 0,
- path: Arc::from(Path::new("c1.0")),
- path_prefix: Arc::default(),
+ path: rel_path("c1.0").into(),
+ path_prefix: rel_path("").into(),
distance_to_relative_ancestor: 0,
is_dir: false,
}),
@@ -95,8 +95,8 @@ fn test_custom_project_search_ordering_in_file_finder() {
score: 1.0,
positions: Vec::new(),
worktree_id: 0,
- path: Arc::from(Path::new("a1.0")),
- path_prefix: Arc::default(),
+ path: rel_path("a1.0").into(),
+ path_prefix: rel_path("").into(),
distance_to_relative_ancestor: 0,
is_dir: false,
}),
@@ -104,8 +104,8 @@ fn test_custom_project_search_ordering_in_file_finder() {
score: 0.5,
positions: Vec::new(),
worktree_id: 0,
- path: Arc::from(Path::new("a0.5")),
- path_prefix: Arc::default(),
+ path: rel_path("a0.5").into(),
+ path_prefix: rel_path("").into(),
distance_to_relative_ancestor: 0,
is_dir: false,
}),
@@ -113,8 +113,8 @@ fn test_custom_project_search_ordering_in_file_finder() {
score: 1.0,
positions: Vec::new(),
worktree_id: 0,
- path: Arc::from(Path::new("b1.0")),
- path_prefix: Arc::default(),
+ path: rel_path("b1.0").into(),
+ path_prefix: rel_path("").into(),
distance_to_relative_ancestor: 0,
is_dir: false,
}),
@@ -128,8 +128,8 @@ fn test_custom_project_search_ordering_in_file_finder() {
score: 1.0,
positions: Vec::new(),
worktree_id: 0,
- path: Arc::from(Path::new("a1.0")),
- path_prefix: Arc::default(),
+ path: rel_path("a1.0").into(),
+ path_prefix: rel_path("").into(),
distance_to_relative_ancestor: 0,
is_dir: false,
}),
@@ -137,8 +137,8 @@ fn test_custom_project_search_ordering_in_file_finder() {
score: 1.0,
positions: Vec::new(),
worktree_id: 0,
- path: Arc::from(Path::new("b1.0")),
- path_prefix: Arc::default(),
+ path: rel_path("b1.0").into(),
+ path_prefix: rel_path("").into(),
distance_to_relative_ancestor: 0,
is_dir: false,
}),
@@ -146,8 +146,8 @@ fn test_custom_project_search_ordering_in_file_finder() {
score: 1.0,
positions: Vec::new(),
worktree_id: 0,
- path: Arc::from(Path::new("c1.0")),
- path_prefix: Arc::default(),
+ path: rel_path("c1.0").into(),
+ path_prefix: rel_path("").into(),
distance_to_relative_ancestor: 0,
is_dir: false,
}),
@@ -155,8 +155,8 @@ fn test_custom_project_search_ordering_in_file_finder() {
score: 0.5,
positions: Vec::new(),
worktree_id: 0,
- path: Arc::from(Path::new("a0.5")),
- path_prefix: Arc::default(),
+ path: rel_path("a0.5").into(),
+ path_prefix: rel_path("").into(),
distance_to_relative_ancestor: 0,
is_dir: false,
}),
@@ -164,8 +164,8 @@ fn test_custom_project_search_ordering_in_file_finder() {
score: 0.5,
positions: Vec::new(),
worktree_id: 0,
- path: Arc::from(Path::new("b0.5")),
- path_prefix: Arc::default(),
+ path: rel_path("b0.5").into(),
+ path_prefix: rel_path("").into(),
distance_to_relative_ancestor: 0,
is_dir: false,
}),
@@ -366,7 +366,7 @@ async fn test_absolute_paths(cx: &mut TestAppContext) {
picker.update(cx, |picker, _| {
assert_eq!(
collect_search_matches(picker).search_paths_only(),
- vec![PathBuf::from("a/b/file2.txt")],
+ vec![rel_path("a/b/file2.txt").into()],
"Matching abs path should be the only match"
)
});
@@ -388,7 +388,7 @@ async fn test_absolute_paths(cx: &mut TestAppContext) {
picker.update(cx, |picker, _| {
assert_eq!(
collect_search_matches(picker).search_paths_only(),
- Vec::<PathBuf>::new(),
+ Vec::new(),
"Mismatching abs path should produce no matches"
)
});
@@ -421,7 +421,7 @@ async fn test_complex_path(cx: &mut TestAppContext) {
assert_eq!(picker.delegate.matches.len(), 2);
assert_eq!(
collect_search_matches(picker).search_paths_only(),
- vec![PathBuf::from("其他/S数据表格/task.xlsx")],
+ vec![rel_path("其他/S数据表格/task.xlsx").into()],
)
});
cx.dispatch_action(Confirm);
@@ -713,13 +713,13 @@ async fn test_ignored_root(cx: &mut TestAppContext) {
assert_eq!(
matches.search,
vec![
- PathBuf::from("ignored-root/hi"),
- PathBuf::from("tracked-root/hi"),
- PathBuf::from("ignored-root/hiccup"),
- PathBuf::from("tracked-root/hiccup"),
- PathBuf::from("ignored-root/height"),
- PathBuf::from("ignored-root/happiness"),
- PathBuf::from("tracked-root/happiness"),
+ rel_path("ignored-root/hi").into(),
+ rel_path("tracked-root/hi").into(),
+ rel_path("ignored-root/hiccup").into(),
+ rel_path("tracked-root/hiccup").into(),
+ rel_path("ignored-root/height").into(),
+ rel_path("ignored-root/happiness").into(),
+ rel_path("tracked-root/happiness").into(),
],
"All ignored files that were indexed are found for default ignored mode"
);
@@ -738,14 +738,14 @@ async fn test_ignored_root(cx: &mut TestAppContext) {
assert_eq!(
matches.search,
vec![
- PathBuf::from("ignored-root/hi"),
- PathBuf::from("tracked-root/hi"),
- PathBuf::from("ignored-root/hiccup"),
- PathBuf::from("tracked-root/hiccup"),
- PathBuf::from("ignored-root/height"),
- PathBuf::from("tracked-root/height"),
- PathBuf::from("ignored-root/happiness"),
- PathBuf::from("tracked-root/happiness"),
+ rel_path("ignored-root/hi").into(),
+ rel_path("tracked-root/hi").into(),
+ rel_path("ignored-root/hiccup").into(),
+ rel_path("tracked-root/hiccup").into(),
+ rel_path("ignored-root/height").into(),
+ rel_path("tracked-root/height").into(),
+ rel_path("ignored-root/happiness").into(),
+ rel_path("tracked-root/happiness").into(),
],
"All ignored files should be found, for the toggled on ignored mode"
);
@@ -765,9 +765,9 @@ async fn test_ignored_root(cx: &mut TestAppContext) {
assert_eq!(
matches.search,
vec![
- PathBuf::from("tracked-root/hi"),
- PathBuf::from("tracked-root/hiccup"),
- PathBuf::from("tracked-root/happiness"),
+ rel_path("tracked-root/hi").into(),
+ rel_path("tracked-root/hiccup").into(),
+ rel_path("tracked-root/happiness").into(),
],
"Only non-ignored files should be found for the turned off ignored mode"
);
@@ -812,13 +812,13 @@ async fn test_ignored_root(cx: &mut TestAppContext) {
assert_eq!(
matches.search,
vec![
- PathBuf::from("ignored-root/hi"),
- PathBuf::from("tracked-root/hi"),
- PathBuf::from("ignored-root/hiccup"),
- PathBuf::from("tracked-root/hiccup"),
- PathBuf::from("ignored-root/height"),
- PathBuf::from("ignored-root/happiness"),
- PathBuf::from("tracked-root/happiness"),
+ rel_path("ignored-root/hi").into(),
+ rel_path("tracked-root/hi").into(),
+ rel_path("ignored-root/hiccup").into(),
+ rel_path("tracked-root/hiccup").into(),
+ rel_path("ignored-root/height").into(),
+ rel_path("ignored-root/happiness").into(),
+ rel_path("tracked-root/happiness").into(),
],
"Only for the worktree with the ignored root, all indexed ignored files are found in the auto ignored mode"
);
@@ -838,16 +838,16 @@ async fn test_ignored_root(cx: &mut TestAppContext) {
assert_eq!(
matches.search,
vec![
- PathBuf::from("ignored-root/hi"),
- PathBuf::from("tracked-root/hi"),
- PathBuf::from("ignored-root/hiccup"),
- PathBuf::from("tracked-root/hiccup"),
- PathBuf::from("ignored-root/height"),
- PathBuf::from("tracked-root/height"),
- PathBuf::from("tracked-root/heights/height_1"),
- PathBuf::from("tracked-root/heights/height_2"),
- PathBuf::from("ignored-root/happiness"),
- PathBuf::from("tracked-root/happiness"),
+ rel_path("ignored-root/hi").into(),
+ rel_path("tracked-root/hi").into(),
+ rel_path("ignored-root/hiccup").into(),
+ rel_path("tracked-root/hiccup").into(),
+ rel_path("ignored-root/height").into(),
+ rel_path("tracked-root/height").into(),
+ rel_path("tracked-root/heights/height_1").into(),
+ rel_path("tracked-root/heights/height_2").into(),
+ rel_path("ignored-root/happiness").into(),
+ rel_path("tracked-root/happiness").into(),
],
"All ignored files that were indexed are found in the turned on ignored mode"
);
@@ -867,9 +867,9 @@ async fn test_ignored_root(cx: &mut TestAppContext) {
assert_eq!(
matches.search,
vec![
- PathBuf::from("tracked-root/hi"),
- PathBuf::from("tracked-root/hiccup"),
- PathBuf::from("tracked-root/happiness"),
+ rel_path("tracked-root/hi").into(),
+ rel_path("tracked-root/hiccup").into(),
+ rel_path("tracked-root/happiness").into(),
],
"Only non-ignored files should be found for the turned off ignored mode"
);
@@ -910,7 +910,7 @@ async fn test_single_file_worktrees(cx: &mut TestAppContext) {
assert_eq!(matches.len(), 1);
let (file_name, file_name_positions, full_path, full_path_positions) =
- delegate.labels_for_path_match(&matches[0]);
+ delegate.labels_for_path_match(&matches[0], PathStyle::local());
assert_eq!(file_name, "the-file");
assert_eq!(file_name_positions, &[0, 1, 4]);
assert_eq!(full_path, "");
@@ -968,7 +968,7 @@ async fn test_create_file_for_multiple_worktrees(cx: &mut TestAppContext) {
let b_path = ProjectPath {
worktree_id: worktree_id2,
- path: Arc::from(Path::new(path!("the-parent-dirb/fileb"))),
+ path: rel_path("the-parent-dirb/fileb").into(),
};
workspace
.update_in(cx, |workspace, window, cx| {
@@ -1001,7 +1001,7 @@ async fn test_create_file_for_multiple_worktrees(cx: &mut TestAppContext) {
project_path,
Some(ProjectPath {
worktree_id: worktree_id2,
- path: Arc::from(Path::new(path!("the-parent-dirb/filec")))
+ path: rel_path("the-parent-dirb/filec").into()
})
);
});
@@ -1038,10 +1038,7 @@ async fn test_create_file_no_focused_with_multiple_worktrees(cx: &mut TestAppCon
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
let (_worktree_id1, worktree_id2) = cx.read(|cx| {
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
- (
- WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize),
- WorktreeId::from_usize(worktrees[1].entity_id().as_u64() as usize),
- )
+ (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
});
let finder = open_file_picker(&workspace, cx);
@@ -1065,7 +1062,7 @@ async fn test_create_file_no_focused_with_multiple_worktrees(cx: &mut TestAppCon
project_path,
Some(ProjectPath {
worktree_id: worktree_id2,
- path: Arc::from(Path::new("filec"))
+ path: rel_path("filec").into()
})
);
});
@@ -1103,7 +1100,7 @@ async fn test_path_distance_ordering(cx: &mut TestAppContext) {
// so that one should be sorted earlier
let b_path = ProjectPath {
worktree_id,
- path: Arc::from(Path::new("dir2/b.txt")),
+ path: rel_path("dir2/b.txt").into(),
};
workspace
.update_in(cx, |workspace, window, cx| {
@@ -1121,8 +1118,8 @@ async fn test_path_distance_ordering(cx: &mut TestAppContext) {
finder.update(cx, |picker, _| {
let matches = collect_search_matches(picker).search_paths_only();
- assert_eq!(matches[0].as_path(), Path::new("dir2/a.txt"));
- assert_eq!(matches[1].as_path(), Path::new("dir1/a.txt"));
+ assert_eq!(matches[0].as_ref(), rel_path("dir2/a.txt"));
+ assert_eq!(matches[1].as_ref(), rel_path("dir1/a.txt"));
});
}
@@ -1207,9 +1204,9 @@ async fn test_query_history(cx: &mut gpui::TestAppContext) {
vec![FoundPath::new(
ProjectPath {
worktree_id,
- path: Arc::from(Path::new("test/first.rs")),
+ path: rel_path("test/first.rs").into(),
},
- Some(PathBuf::from(path!("/src/test/first.rs")))
+ PathBuf::from(path!("/src/test/first.rs"))
)],
"Should show 1st opened item in the history when opening the 2nd item"
);
@@ -1222,16 +1219,16 @@ async fn test_query_history(cx: &mut gpui::TestAppContext) {
FoundPath::new(
ProjectPath {
worktree_id,
- path: Arc::from(Path::new("test/second.rs")),
+ path: rel_path("test/second.rs").into(),
},
- Some(PathBuf::from(path!("/src/test/second.rs")))
+ PathBuf::from(path!("/src/test/second.rs"))
),
FoundPath::new(
ProjectPath {
worktree_id,
- path: Arc::from(Path::new("test/first.rs")),
+ path: rel_path("test/first.rs").into(),
},
- Some(PathBuf::from(path!("/src/test/first.rs")))
+ PathBuf::from(path!("/src/test/first.rs"))
),
],
"Should show 1st and 2nd opened items in the history when opening the 3rd item. \
@@ -1246,23 +1243,23 @@ async fn test_query_history(cx: &mut gpui::TestAppContext) {
FoundPath::new(
ProjectPath {
worktree_id,
- path: Arc::from(Path::new("test/third.rs")),
+ path: rel_path("test/third.rs").into(),
},
- Some(PathBuf::from(path!("/src/test/third.rs")))
+ PathBuf::from(path!("/src/test/third.rs"))
),
FoundPath::new(
ProjectPath {
worktree_id,
- path: Arc::from(Path::new("test/second.rs")),
+ path: rel_path("test/second.rs").into(),
},
- Some(PathBuf::from(path!("/src/test/second.rs")))
+ PathBuf::from(path!("/src/test/second.rs"))
),
FoundPath::new(
ProjectPath {
worktree_id,
- path: Arc::from(Path::new("test/first.rs")),
+ path: rel_path("test/first.rs").into(),
},
- Some(PathBuf::from(path!("/src/test/first.rs")))
+ PathBuf::from(path!("/src/test/first.rs"))
),
],
"Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
@@ -1277,23 +1274,23 @@ async fn test_query_history(cx: &mut gpui::TestAppContext) {
FoundPath::new(
ProjectPath {
worktree_id,
- path: Arc::from(Path::new("test/second.rs")),
+ path: rel_path("test/second.rs").into(),
},
- Some(PathBuf::from(path!("/src/test/second.rs")))
+ PathBuf::from(path!("/src/test/second.rs"))
),
FoundPath::new(
ProjectPath {
worktree_id,
- path: Arc::from(Path::new("test/third.rs")),
+ path: rel_path("test/third.rs").into(),
},
- Some(PathBuf::from(path!("/src/test/third.rs")))
+ PathBuf::from(path!("/src/test/third.rs"))
),
FoundPath::new(
ProjectPath {
worktree_id,
- path: Arc::from(Path::new("test/first.rs")),
+ path: rel_path("test/first.rs").into(),
},
- Some(PathBuf::from(path!("/src/test/first.rs")))
+ PathBuf::from(path!("/src/test/first.rs"))
),
],
"Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
@@ -1301,6 +1298,62 @@ async fn test_query_history(cx: &mut gpui::TestAppContext) {
);
}
+#[gpui::test]
+async fn test_history_match_positions(cx: &mut gpui::TestAppContext) {
+ let app_state = init_test(cx);
+
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ path!("/src"),
+ json!({
+ "test": {
+ "first.rs": "// First Rust file",
+ "second.rs": "// Second Rust file",
+ "third.rs": "// Third Rust file",
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
+ let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
+
+ workspace.update_in(cx, |_workspace, window, cx| window.focused(cx));
+
+ open_close_queried_buffer("efir", 1, "first.rs", &workspace, cx).await;
+ let history = open_close_queried_buffer("second", 1, "second.rs", &workspace, cx).await;
+ assert_eq!(history.len(), 1);
+
+ let picker = open_file_picker(&workspace, cx);
+ cx.simulate_input("fir");
+ picker.update_in(cx, |finder, window, cx| {
+ let matches = &finder.delegate.matches.matches;
+ assert_matches!(
+ matches.as_slice(),
+ [Match::History { .. }, Match::CreateNew { .. }]
+ );
+ assert_eq!(
+ matches[0].panel_match().unwrap().0.path.as_ref(),
+ rel_path("test/first.rs")
+ );
+ assert_eq!(matches[0].panel_match().unwrap().0.positions, &[5, 6, 7]);
+
+ let (file_label, path_label) =
+ finder
+ .delegate
+ .labels_for_match(&finder.delegate.matches.matches[0], window, cx);
+ assert_eq!(file_label.text(), "first.rs");
+ assert_eq!(file_label.highlight_indices(), &[0, 1, 2]);
+ assert_eq!(
+ path_label.text(),
+ format!("test{}", PathStyle::local().separator())
+ );
+ assert_eq!(path_label.highlight_indices(), &[] as &[usize]);
+ });
+}
+
#[gpui::test]
async fn test_external_files_history(cx: &mut gpui::TestAppContext) {
let app_state = init_test(cx);
@@ -1392,9 +1445,9 @@ async fn test_external_files_history(cx: &mut gpui::TestAppContext) {
vec![FoundPath::new(
ProjectPath {
worktree_id: external_worktree_id,
- path: Arc::from(Path::new("")),
+ path: rel_path("").into(),
},
- Some(PathBuf::from(path!("/external-src/test/third.rs")))
+ PathBuf::from(path!("/external-src/test/third.rs"))
)],
"Should show external file with its full path in the history after it was open"
);
@@ -1407,16 +1460,16 @@ async fn test_external_files_history(cx: &mut gpui::TestAppContext) {
FoundPath::new(
ProjectPath {
worktree_id,
- path: Arc::from(Path::new("test/second.rs")),
+ path: rel_path("test/second.rs").into(),
},
- Some(PathBuf::from(path!("/src/test/second.rs")))
+ PathBuf::from(path!("/src/test/second.rs"))
),
FoundPath::new(
ProjectPath {
worktree_id: external_worktree_id,
- path: Arc::from(Path::new("")),
+ path: rel_path("").into(),
},
- Some(PathBuf::from(path!("/external-src/test/third.rs")))
+ PathBuf::from(path!("/external-src/test/third.rs"))
),
],
"Should keep external file with history updates",
@@ -1529,12 +1582,12 @@ async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
assert_eq!(history_match, &FoundPath::new(
ProjectPath {
worktree_id,
- path: Arc::from(Path::new("test/first.rs")),
+ path: rel_path("test/first.rs").into(),
},
- Some(PathBuf::from(path!("/src/test/first.rs")))
+ PathBuf::from(path!("/src/test/first.rs")),
));
assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
- assert_eq!(matches.search.first().unwrap(), Path::new("test/fourth.rs"));
+ assert_eq!(matches.search.first().unwrap().as_ref(), rel_path("test/fourth.rs"));
});
let second_query = "fsdasdsa";
@@ -1572,12 +1625,12 @@ async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
assert_eq!(history_match, &FoundPath::new(
ProjectPath {
worktree_id,
- path: Arc::from(Path::new("test/first.rs")),
+ path: rel_path("test/first.rs").into(),
},
- Some(PathBuf::from(path!("/src/test/first.rs")))
+ PathBuf::from(path!("/src/test/first.rs"))
));
assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
- assert_eq!(matches.search.first().unwrap(), Path::new("test/fourth.rs"));
+ assert_eq!(matches.search.first().unwrap().as_ref(), rel_path("test/fourth.rs"));
});
}
@@ -1626,13 +1679,16 @@ async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) {
let search_matches = collect_search_matches(finder);
assert_eq!(
search_matches.history,
- vec![PathBuf::from("test/1_qw"), PathBuf::from("test/6_qwqwqw"),],
+ vec![
+ rel_path("test/1_qw").into(),
+ rel_path("test/6_qwqwqw").into()
+ ],
);
assert_eq!(
search_matches.search,
vec![
- PathBuf::from("test/5_qwqwqw"),
- PathBuf::from("test/7_qwqwqw"),
+ rel_path("test/5_qwqwqw").into(),
+ rel_path("test/7_qwqwqw").into()
],
);
});
@@ -2083,10 +2139,10 @@ async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppCo
assert_eq!(
search_entries,
vec![
- PathBuf::from("collab_ui/collab_ui.rs"),
- PathBuf::from("collab_ui/first.rs"),
- PathBuf::from("collab_ui/third.rs"),
- PathBuf::from("collab_ui/second.rs"),
+ rel_path("collab_ui/collab_ui.rs").into(),
+ rel_path("collab_ui/first.rs").into(),
+ rel_path("collab_ui/third.rs").into(),
+ rel_path("collab_ui/second.rs").into(),
],
"Despite all search results having the same directory name, the most matching one should be on top"
);
@@ -2135,8 +2191,8 @@ async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext)
assert_eq!(
collect_search_matches(picker).history,
vec![
- PathBuf::from("test/first.rs"),
- PathBuf::from("test/third.rs"),
+ rel_path("test/first.rs").into(),
+ rel_path("test/third.rs").into(),
],
"Should have all opened files in the history, except the ones that do not exist on disk"
);
@@ -2766,15 +2822,15 @@ fn active_file_picker(
#[derive(Debug, Default)]
struct SearchEntries {
- history: Vec<PathBuf>,
+ history: Vec<Arc<RelPath>>,
history_found_paths: Vec<FoundPath>,
- search: Vec<PathBuf>,
+ search: Vec<Arc<RelPath>>,
search_matches: Vec<PathMatch>,
}
impl SearchEntries {
#[track_caller]
- fn search_paths_only(self) -> Vec<PathBuf> {
+ fn search_paths_only(self) -> Vec<Arc<RelPath>> {
assert!(
self.history.is_empty(),
"Should have no history matches, but got: {:?}",
@@ -2802,20 +2858,15 @@ fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries
path: history_path,
panel_match: path_match,
} => {
- search_entries.history.push(
- path_match
- .as_ref()
- .map(|path_match| {
- Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path)
- })
- .unwrap_or_else(|| {
- history_path
- .absolute
- .as_deref()
- .unwrap_or_else(|| &history_path.project.path)
- .to_path_buf()
- }),
- );
+ if let Some(path_match) = path_match.as_ref() {
+ search_entries
+ .history
+ .push(path_match.0.path_prefix.join(&path_match.0.path));
+ } else {
+ // This occurs when the query is empty and we show history matches
+ // that are outside the project.
+ panic!("currently not exercised in tests");
+ }
search_entries
.history_found_paths
.push(history_path.clone());
@@ -2823,7 +2874,7 @@ fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries
Match::Search(path_match) => {
search_entries
.search
- .push(Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path));
+ .push(path_match.0.path_prefix.join(&path_match.0.path));
search_entries.search_matches.push(path_match.0.clone());
}
Match::CreateNew(_) => {}
@@ -2858,12 +2909,11 @@ fn assert_match_at_position(
.get(match_index)
.unwrap_or_else(|| panic!("Finder has no match for index {match_index}"));
let match_file_name = match &match_item {
- Match::History { path, .. } => path.absolute.as_deref().unwrap().file_name(),
+ Match::History { path, .. } => path.absolute.file_name().and_then(|s| s.to_str()),
Match::Search(path_match) => path_match.0.path.file_name(),
Match::CreateNew(project_path) => project_path.path.file_name(),
}
- .unwrap()
- .to_string_lossy();
+ .unwrap();
assert_eq!(match_file_name, expected_file_name);
}
@@ -2901,11 +2951,11 @@ async fn test_filename_precedence(cx: &mut TestAppContext) {
assert_eq!(
search_matches,
vec![
- PathBuf::from("routes/+layout.svelte"),
- PathBuf::from("layout/app.css"),
- PathBuf::from("layout/app.d.ts"),
- PathBuf::from("layout/app.html"),
- PathBuf::from("layout/+page.svelte"),
+ rel_path("routes/+layout.svelte").into(),
+ rel_path("layout/app.css").into(),
+ rel_path("layout/app.d.ts").into(),
+ rel_path("layout/app.html").into(),
+ rel_path("layout/+page.svelte").into(),
],
"File with 'layout' in filename should be prioritized over files in 'layout' directory"
);
@@ -7,7 +7,7 @@ use picker::{Picker, PickerDelegate};
use project::{DirectoryItem, DirectoryLister};
use settings::Settings;
use std::{
- path::{self, MAIN_SEPARATOR_STR, Path, PathBuf},
+ path::{self, Path, PathBuf},
sync::{
Arc,
atomic::{self, AtomicBool},
@@ -217,7 +217,7 @@ impl OpenPathPrompt {
) {
workspace.toggle_modal(window, cx, |window, cx| {
let delegate =
- OpenPathDelegate::new(tx, lister.clone(), creating_path, PathStyle::current());
+ OpenPathDelegate::new(tx, lister.clone(), creating_path, PathStyle::local());
let picker = Picker::uniform_list(delegate, window, cx).width(rems(34.));
let query = lister.default_query(cx);
picker.set_query(query, window, cx);
@@ -822,7 +822,7 @@ impl PickerDelegate for OpenPathDelegate {
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
- Arc::from(format!("[directory{MAIN_SEPARATOR_STR}]filename.ext"))
+ Arc::from(format!("[directory{}]filename.ext", self.path_style.separator()).as_str())
}
fn separators_after_indices(&self) -> Vec<usize> {
@@ -37,7 +37,7 @@ async fn test_open_path_prompt(cx: &mut TestAppContext) {
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
- let (picker, cx) = build_open_path_prompt(project, false, PathStyle::current(), cx);
+ let (picker, cx) = build_open_path_prompt(project, false, PathStyle::local(), cx);
insert_query(path!("sadjaoislkdjasldj"), &picker, cx).await;
assert_eq!(collect_match_candidates(&picker, cx), Vec::<String>::new());
@@ -119,7 +119,7 @@ async fn test_open_path_prompt_completion(cx: &mut TestAppContext) {
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
- let (picker, cx) = build_open_path_prompt(project, false, PathStyle::current(), cx);
+ let (picker, cx) = build_open_path_prompt(project, false, PathStyle::local(), cx);
// Confirm completion for the query "/root", since it's a directory, it should add a trailing slash.
let query = path!("/root");
@@ -227,7 +227,7 @@ async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
- let (picker, cx) = build_open_path_prompt(project, false, PathStyle::current(), cx);
+ let (picker, cx) = build_open_path_prompt(project, false, PathStyle::local(), cx);
// Support both forward and backward slashes.
let query = "C:/root/";
@@ -372,7 +372,7 @@ async fn test_new_path_prompt(cx: &mut TestAppContext) {
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
- let (picker, cx) = build_open_path_prompt(project, true, PathStyle::current(), cx);
+ let (picker, cx) = build_open_path_prompt(project, true, PathStyle::local(), cx);
insert_query(path!("/root"), &picker, cx).await;
assert_eq!(collect_match_candidates(&picker, cx), vec!["root"]);
@@ -17,6 +17,7 @@ use parking_lot::Mutex;
use rope::Rope;
use smol::future::FutureExt as _;
use std::{path::PathBuf, sync::Arc};
+use util::{paths::PathStyle, rel_path::RelPath};
#[derive(Clone)]
pub struct FakeGitRepository {
@@ -82,7 +83,7 @@ impl GitRepository for FakeGitRepository {
self.with_state_async(false, move |state| {
state
.index_contents
- .get(path.as_ref())
+ .get(&path)
.context("not present in index")
.cloned()
})
@@ -97,7 +98,7 @@ impl GitRepository for FakeGitRepository {
self.with_state_async(false, move |state| {
state
.head_contents
- .get(path.as_ref())
+ .get(&path)
.context("not present in HEAD")
.cloned()
})
@@ -225,6 +226,7 @@ impl GitRepository for FakeGitRepository {
.read_file_sync(path)
.ok()
.map(|content| String::from_utf8(content).unwrap())?;
+ let repo_path = RelPath::from_std_path(repo_path, PathStyle::local()).ok()?;
Some((repo_path.into(), (content, is_ignored)))
})
.collect();
@@ -386,7 +388,11 @@ impl GitRepository for FakeGitRepository {
let contents = paths
.into_iter()
.map(|path| {
- let abs_path = self.dot_git_path.parent().unwrap().join(&path);
+ let abs_path = self
+ .dot_git_path
+ .parent()
+ .unwrap()
+ .join(&path.as_std_path());
Box::pin(async move { (path.clone(), self.fs.load(&abs_path).await.ok()) })
})
.collect::<Vec<_>>();
@@ -47,7 +47,7 @@ use collections::{BTreeMap, btree_map};
use fake_git_repo::FakeGitRepositoryState;
#[cfg(any(test, feature = "test-support"))]
use git::{
- repository::RepoPath,
+ repository::{RepoPath, repo_path},
status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus},
};
#[cfg(any(test, feature = "test-support"))]
@@ -1608,13 +1608,13 @@ impl FakeFs {
.unwrap();
}
- pub fn set_index_for_repo(&self, dot_git: &Path, index_state: &[(RepoPath, String)]) {
+ pub fn set_index_for_repo(&self, dot_git: &Path, index_state: &[(&str, String)]) {
self.with_git_state(dot_git, true, |state| {
state.index_contents.clear();
state.index_contents.extend(
index_state
.iter()
- .map(|(path, content)| (path.clone(), content.clone())),
+ .map(|(path, content)| (repo_path(path), content.clone())),
);
})
.unwrap();
@@ -1623,7 +1623,7 @@ impl FakeFs {
pub fn set_head_for_repo(
&self,
dot_git: &Path,
- head_state: &[(RepoPath, String)],
+ head_state: &[(&str, String)],
sha: impl Into<String>,
) {
self.with_git_state(dot_git, true, |state| {
@@ -1631,50 +1631,22 @@ impl FakeFs {
state.head_contents.extend(
head_state
.iter()
- .map(|(path, content)| (path.clone(), content.clone())),
+ .map(|(path, content)| (repo_path(path), content.clone())),
);
state.refs.insert("HEAD".into(), sha.into());
})
.unwrap();
}
- pub fn set_git_content_for_repo(
- &self,
- dot_git: &Path,
- head_state: &[(RepoPath, String, Option<String>)],
- ) {
+ pub fn set_head_and_index_for_repo(&self, dot_git: &Path, contents_by_path: &[(&str, String)]) {
self.with_git_state(dot_git, true, |state| {
state.head_contents.clear();
state.head_contents.extend(
- head_state
+ contents_by_path
.iter()
- .map(|(path, head_content, _)| (path.clone(), head_content.clone())),
+ .map(|(path, contents)| (repo_path(path), contents.clone())),
);
- state.index_contents.clear();
- state.index_contents.extend(head_state.iter().map(
- |(path, head_content, index_content)| {
- (
- path.clone(),
- index_content.as_ref().unwrap_or(head_content).clone(),
- )
- },
- ));
- })
- .unwrap();
- }
-
- pub fn set_head_and_index_for_repo(
- &self,
- dot_git: &Path,
- contents_by_path: &[(RepoPath, String)],
- ) {
- self.with_git_state(dot_git, true, |state| {
- state.head_contents.clear();
- state.index_contents.clear();
- state.head_contents.extend(contents_by_path.iter().cloned());
- state
- .index_contents
- .extend(contents_by_path.iter().cloned());
+ state.index_contents = state.head_contents.clone();
})
.unwrap();
}
@@ -1689,7 +1661,7 @@ impl FakeFs {
/// Put the given git repository into a state with the given status,
/// by mutating the head, index, and unmerged state.
- pub fn set_status_for_repo(&self, dot_git: &Path, statuses: &[(&Path, FileStatus)]) {
+ pub fn set_status_for_repo(&self, dot_git: &Path, statuses: &[(&str, FileStatus)]) {
let workdir_path = dot_git.parent().unwrap();
let workdir_contents = self.files_with_contents(workdir_path);
self.with_git_state(dot_git, true, |state| {
@@ -1697,10 +1669,12 @@ impl FakeFs {
state.head_contents.clear();
state.unmerged_paths.clear();
for (path, content) in workdir_contents {
- let repo_path: RepoPath = path.strip_prefix(&workdir_path).unwrap().into();
+ use util::{paths::PathStyle, rel_path::RelPath};
+
+ let repo_path: RepoPath = RelPath::from_std_path(path.strip_prefix(&workdir_path).unwrap(), PathStyle::local()).unwrap().into();
let status = statuses
.iter()
- .find_map(|(p, status)| (**p == *repo_path.0).then_some(status));
+ .find_map(|(p, status)| (*p == repo_path.as_str()).then_some(status));
let mut content = String::from_utf8_lossy(&content).to_string();
let mut index_content = None;
@@ -17,3 +17,6 @@ gpui.workspace = true
util.workspace = true
log.workspace = true
workspace-hack.workspace = true
+
+[dev-dependencies]
+util = {workspace = true, features = ["test-support"]}
@@ -1,5 +1,5 @@
use std::{
- borrow::{Borrow, Cow},
+ borrow::Borrow,
collections::BTreeMap,
sync::atomic::{self, AtomicBool},
};
@@ -27,7 +27,7 @@ pub struct Matcher<'a> {
pub trait MatchCandidate {
fn has_chars(&self, bag: CharBag) -> bool;
- fn to_string(&self) -> Cow<'_, str>;
+ fn candidate_chars(&self) -> impl Iterator<Item = char>;
}
impl<'a> Matcher<'a> {
@@ -83,7 +83,7 @@ impl<'a> Matcher<'a> {
candidate_chars.clear();
lowercase_candidate_chars.clear();
extra_lowercase_chars.clear();
- for (i, c) in candidate.borrow().to_string().chars().enumerate() {
+ for (i, c) in candidate.borrow().candidate_chars().enumerate() {
candidate_chars.push(c);
let mut char_lowercased = c.to_lowercase().collect::<Vec<_>>();
if char_lowercased.len() > 1 {
@@ -202,8 +202,6 @@ impl<'a> Matcher<'a> {
cur_score: f64,
extra_lowercase_chars: &BTreeMap<usize, usize>,
) -> f64 {
- use std::path::MAIN_SEPARATOR;
-
if query_idx == self.query.len() {
return 1.0;
}
@@ -245,17 +243,11 @@ impl<'a> Matcher<'a> {
None => continue,
}
};
- let is_path_sep = path_char == MAIN_SEPARATOR;
+ let is_path_sep = path_char == '/';
if query_idx == 0 && is_path_sep {
last_slash = j_regular;
}
-
- #[cfg(not(target_os = "windows"))]
- let need_to_score =
- query_char == path_char || (is_path_sep && query_char == '_' || query_char == '\\');
- // `query_char == '\\'` breaks `test_match_path_entries` on Windows, `\` is only used as a path separator on Windows.
- #[cfg(target_os = "windows")]
let need_to_score = query_char == path_char || (is_path_sep && query_char == '_');
if need_to_score {
let curr = match prefix.get(j_regular) {
@@ -270,7 +262,7 @@ impl<'a> Matcher<'a> {
None => path[j_regular - 1 - prefix.len()],
};
- if last == MAIN_SEPARATOR {
+ if last == '/' {
char_score = 0.9;
} else if (last == '-' || last == '_' || last == ' ' || last.is_numeric())
|| (last.is_lowercase() && curr.is_uppercase())
@@ -291,7 +283,7 @@ impl<'a> Matcher<'a> {
// Apply a severe penalty if the case doesn't match.
// This will make the exact matches have higher score than the case-insensitive and the
// path insensitive matches.
- if (self.smart_case || curr == MAIN_SEPARATOR) && self.query[query_idx] != curr {
+ if (self.smart_case || curr == '/') && self.query[query_idx] != curr {
char_score *= 0.001;
}
@@ -348,13 +340,12 @@ impl<'a> Matcher<'a> {
#[cfg(test)]
mod tests {
+ use util::rel_path::{RelPath, rel_path};
+
use crate::{PathMatch, PathMatchCandidate};
use super::*;
- use std::{
- path::{Path, PathBuf},
- sync::Arc,
- };
+ use std::sync::Arc;
#[test]
fn test_get_last_positions() {
@@ -376,7 +367,6 @@ mod tests {
assert_eq!(matcher.last_positions, vec![0, 3, 4, 8]);
}
- #[cfg(not(target_os = "windows"))]
#[test]
fn test_match_path_entries() {
let paths = vec![
@@ -388,9 +378,9 @@ mod tests {
"alphabravocharlie",
"AlphaBravoCharlie",
"thisisatestdir",
- "/////ThisIsATestDir",
- "/this/is/a/test/dir",
- "/test/tiatd",
+ "ThisIsATestDir",
+ "this/is/a/test/dir",
+ "test/tiatd",
];
assert_eq!(
@@ -404,63 +394,15 @@ mod tests {
);
assert_eq!(
match_single_path_query("t/i/a/t/d", false, &paths),
- vec![("/this/is/a/test/dir", vec![1, 5, 6, 8, 9, 10, 11, 15, 16]),]
- );
-
- assert_eq!(
- match_single_path_query("tiatd", false, &paths),
- vec![
- ("/test/tiatd", vec![6, 7, 8, 9, 10]),
- ("/this/is/a/test/dir", vec![1, 6, 9, 11, 16]),
- ("/////ThisIsATestDir", vec![5, 9, 11, 12, 16]),
- ("thisisatestdir", vec![0, 2, 6, 7, 11]),
- ]
- );
- }
-
- /// todo(windows)
- /// Now, on Windows, users can only use the backslash as a path separator.
- /// I do want to support both the backslash and the forward slash as path separators on Windows.
- #[cfg(target_os = "windows")]
- #[test]
- fn test_match_path_entries() {
- let paths = vec![
- "",
- "a",
- "ab",
- "abC",
- "abcd",
- "alphabravocharlie",
- "AlphaBravoCharlie",
- "thisisatestdir",
- "\\\\\\\\\\ThisIsATestDir",
- "\\this\\is\\a\\test\\dir",
- "\\test\\tiatd",
- ];
-
- assert_eq!(
- match_single_path_query("abc", false, &paths),
- vec![
- ("abC", vec![0, 1, 2]),
- ("abcd", vec![0, 1, 2]),
- ("AlphaBravoCharlie", vec![0, 5, 10]),
- ("alphabravocharlie", vec![4, 5, 10]),
- ]
- );
- assert_eq!(
- match_single_path_query("t\\i\\a\\t\\d", false, &paths),
- vec![(
- "\\this\\is\\a\\test\\dir",
- vec![1, 5, 6, 8, 9, 10, 11, 15, 16]
- ),]
+ vec![("this/is/a/test/dir", vec![0, 4, 5, 7, 8, 9, 10, 14, 15]),]
);
assert_eq!(
match_single_path_query("tiatd", false, &paths),
vec![
- ("\\test\\tiatd", vec![6, 7, 8, 9, 10]),
- ("\\this\\is\\a\\test\\dir", vec![1, 6, 9, 11, 16]),
- ("\\\\\\\\\\ThisIsATestDir", vec![5, 9, 11, 12, 16]),
+ ("test/tiatd", vec![5, 6, 7, 8, 9]),
+ ("ThisIsATestDir", vec![0, 4, 6, 7, 11]),
+ ("this/is/a/test/dir", vec![0, 5, 8, 10, 15]),
("thisisatestdir", vec![0, 2, 6, 7, 11]),
]
);
@@ -491,7 +433,7 @@ mod tests {
"aαbβ/cγdδ",
"αβγδ/bcde",
"c1️⃣2️⃣3️⃣/d4️⃣5️⃣6️⃣/e7️⃣8️⃣9️⃣/f",
- "/d/🆒/h",
+ "d/🆒/h",
];
assert_eq!("1️⃣".len(), 7);
assert_eq!(
@@ -602,9 +544,9 @@ mod tests {
let query = query.chars().collect::<Vec<_>>();
let query_chars = CharBag::from(&lowercase_query[..]);
- let path_arcs: Vec<Arc<Path>> = paths
+ let path_arcs: Vec<Arc<RelPath>> = paths
.iter()
- .map(|path| Arc::from(PathBuf::from(path)))
+ .map(|path| Arc::from(rel_path(path)))
.collect::<Vec<_>>();
let mut path_entries = Vec::new();
for (i, path) in paths.iter().enumerate() {
@@ -632,8 +574,8 @@ mod tests {
score,
worktree_id: 0,
positions: positions.clone(),
- path: Arc::from(candidate.path),
- path_prefix: "".into(),
+ path: candidate.path.into(),
+ path_prefix: RelPath::empty().into(),
distance_to_relative_ancestor: usize::MAX,
is_dir: false,
},
@@ -647,7 +589,7 @@ mod tests {
paths
.iter()
.copied()
- .find(|p| result.path.as_ref() == Path::new(p))
+ .find(|p| result.path.as_ref() == rel_path(p))
.unwrap(),
result.positions,
)
@@ -1,13 +1,12 @@
use gpui::BackgroundExecutor;
use std::{
- borrow::Cow,
cmp::{self, Ordering},
- path::Path,
sync::{
Arc,
atomic::{self, AtomicBool},
},
};
+use util::{paths::PathStyle, rel_path::RelPath};
use crate::{
CharBag,
@@ -17,7 +16,7 @@ use crate::{
#[derive(Clone, Debug)]
pub struct PathMatchCandidate<'a> {
pub is_dir: bool,
- pub path: &'a Path,
+ pub path: &'a RelPath,
pub char_bag: CharBag,
}
@@ -26,8 +25,8 @@ pub struct PathMatch {
pub score: f64,
pub positions: Vec<usize>,
pub worktree_id: usize,
- pub path: Arc<Path>,
- pub path_prefix: Arc<str>,
+ pub path: Arc<RelPath>,
+ pub path_prefix: Arc<RelPath>,
pub is_dir: bool,
/// Number of steps removed from a shared parent with the relative path
/// Used to order closer paths first in the search list
@@ -41,8 +40,10 @@ pub trait PathMatchCandidateSet<'a>: Send + Sync {
fn is_empty(&self) -> bool {
self.len() == 0
}
- fn prefix(&self) -> Arc<str>;
+ fn root_is_file(&self) -> bool;
+ fn prefix(&self) -> Arc<RelPath>;
fn candidates(&'a self, start: usize) -> Self::Candidates;
+ fn path_style(&self) -> PathStyle;
}
impl<'a> MatchCandidate for PathMatchCandidate<'a> {
@@ -50,8 +51,8 @@ impl<'a> MatchCandidate for PathMatchCandidate<'a> {
self.char_bag.is_superset(bag)
}
- fn to_string(&self) -> Cow<'a, str> {
- self.path.to_string_lossy()
+ fn candidate_chars(&self) -> impl Iterator<Item = char> {
+ self.path.as_str().chars()
}
}
@@ -109,8 +110,8 @@ pub fn match_fixed_path_set(
worktree_id,
positions: positions.clone(),
is_dir: candidate.is_dir,
- path: Arc::from(candidate.path),
- path_prefix: Arc::default(),
+ path: candidate.path.into(),
+ path_prefix: RelPath::empty().into(),
distance_to_relative_ancestor: usize::MAX,
},
);
@@ -121,7 +122,7 @@ pub fn match_fixed_path_set(
pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
candidate_sets: &'a [Set],
query: &str,
- relative_to: &Option<Arc<Path>>,
+ relative_to: &Option<Arc<RelPath>>,
smart_case: bool,
max_results: usize,
cancel_flag: &AtomicBool,
@@ -132,12 +133,27 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
return Vec::new();
}
- let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
- let query = query.chars().collect::<Vec<_>>();
+ let path_style = candidate_sets[0].path_style();
+
+ let query = query
+ .chars()
+ .map(|char| {
+ if path_style.is_windows() && char == '\\' {
+ '/'
+ } else {
+ char
+ }
+ })
+ .collect::<Vec<_>>();
+
+ let lowercase_query = query
+ .iter()
+ .map(|query| query.to_ascii_lowercase())
+ .collect::<Vec<_>>();
- let lowercase_query = &lowercase_query;
let query = &query;
- let query_char_bag = CharBag::from(&lowercase_query[..]);
+ let lowercase_query = &lowercase_query;
+ let query_char_bag = CharBag::from_iter(lowercase_query.iter().copied());
let num_cpus = executor.num_cpus().min(path_count);
let segment_size = path_count.div_ceil(num_cpus);
@@ -168,7 +184,11 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
let candidates = candidate_set.candidates(start).take(end - start);
let worktree_id = candidate_set.id();
- let prefix = candidate_set.prefix().chars().collect::<Vec<_>>();
+ let mut prefix =
+ candidate_set.prefix().as_str().chars().collect::<Vec<_>>();
+ if !candidate_set.root_is_file() && !prefix.is_empty() {
+ prefix.push('/');
+ }
let lowercase_prefix = prefix
.iter()
.map(|c| c.to_ascii_lowercase())
@@ -219,7 +239,7 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
/// Compute the distance from a given path to some other path
/// If there is no shared path, returns usize::MAX
-fn distance_between_paths(path: &Path, relative_to: &Path) -> usize {
+fn distance_between_paths(path: &RelPath, relative_to: &RelPath) -> usize {
let mut path_components = path.components();
let mut relative_components = relative_to.components();
@@ -234,12 +254,12 @@ fn distance_between_paths(path: &Path, relative_to: &Path) -> usize {
#[cfg(test)]
mod tests {
- use std::path::Path;
+ use util::rel_path::RelPath;
use super::distance_between_paths;
#[test]
fn test_distance_between_paths_empty() {
- distance_between_paths(Path::new(""), Path::new(""));
+ distance_between_paths(RelPath::empty(), RelPath::empty());
}
}
@@ -4,7 +4,7 @@ use crate::{
};
use gpui::BackgroundExecutor;
use std::{
- borrow::{Borrow, Cow},
+ borrow::Borrow,
cmp::{self, Ordering},
iter,
ops::Range,
@@ -28,13 +28,13 @@ impl StringMatchCandidate {
}
}
-impl<'a> MatchCandidate for &'a StringMatchCandidate {
+impl MatchCandidate for &StringMatchCandidate {
fn has_chars(&self, bag: CharBag) -> bool {
self.char_bag.is_superset(bag)
}
- fn to_string(&self) -> Cow<'a, str> {
- self.string.as_str().into()
+ fn candidate_chars(&self) -> impl Iterator<Item = char> {
+ self.string.chars()
}
}
@@ -1,4 +1,5 @@
use crate::commit::get_messages;
+use crate::repository::RepoPath;
use crate::{GitRemote, Oid};
use anyhow::{Context as _, Result};
use collections::{HashMap, HashSet};
@@ -33,7 +34,7 @@ impl Blame {
pub async fn for_path(
git_binary: &Path,
working_directory: &Path,
- path: &Path,
+ path: &RepoPath,
content: &Rope,
remote_url: Option<String>,
) -> Result<Self> {
@@ -66,7 +67,7 @@ const GIT_BLAME_NO_PATH: &str = "fatal: no such path";
async fn run_git_blame(
git_binary: &Path,
working_directory: &Path,
- path: &Path,
+ path: &RepoPath,
contents: &Rope,
) -> Result<String> {
let mut child = util::command::new_smol_command(git_binary)
@@ -76,7 +77,7 @@ async fn run_git_blame(
.arg("-w")
.arg("--contents")
.arg("-")
- .arg(path.as_os_str())
+ .arg(path.as_str())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
@@ -39,7 +39,7 @@ pub async fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<Hash
}
/// Parse the output of `git diff --name-status -z`
-pub fn parse_git_diff_name_status(content: &str) -> impl Iterator<Item = (&Path, StatusCode)> {
+pub fn parse_git_diff_name_status(content: &str) -> impl Iterator<Item = (&str, StatusCode)> {
let mut parts = content.split('\0');
std::iter::from_fn(move || {
loop {
@@ -51,13 +51,14 @@ pub fn parse_git_diff_name_status(content: &str) -> impl Iterator<Item = (&Path,
"D" => StatusCode::Deleted,
_ => continue,
};
- return Some((Path::new(path), status));
+ return Some((path, status));
}
})
}
#[cfg(test)]
mod tests {
+
use super::*;
#[test]
@@ -78,31 +79,19 @@ mod tests {
assert_eq!(
output,
&[
- (Path::new("Cargo.lock"), StatusCode::Modified),
- (Path::new("crates/project/Cargo.toml"), StatusCode::Modified),
- (
- Path::new("crates/project/src/buffer_store.rs"),
- StatusCode::Modified
- ),
- (Path::new("crates/project/src/git.rs"), StatusCode::Deleted),
- (
- Path::new("crates/project/src/git_store.rs"),
- StatusCode::Added
- ),
+ ("Cargo.lock", StatusCode::Modified),
+ ("crates/project/Cargo.toml", StatusCode::Modified),
+ ("crates/project/src/buffer_store.rs", StatusCode::Modified),
+ ("crates/project/src/git.rs", StatusCode::Deleted),
+ ("crates/project/src/git_store.rs", StatusCode::Added),
(
- Path::new("crates/project/src/git_store/git_traversal.rs"),
+ "crates/project/src/git_store/git_traversal.rs",
StatusCode::Added,
),
+ ("crates/project/src/project.rs", StatusCode::Modified),
+ ("crates/project/src/worktree_store.rs", StatusCode::Modified),
(
- Path::new("crates/project/src/project.rs"),
- StatusCode::Modified
- ),
- (
- Path::new("crates/project/src/worktree_store.rs"),
- StatusCode::Modified
- ),
- (
- Path::new("crates/project_panel/src/project_panel.rs"),
+ "crates/project_panel/src/project_panel.rs",
StatusCode::Modified
),
]
@@ -12,22 +12,17 @@ use anyhow::{Context as _, Result};
pub use git2 as libgit;
use gpui::{Action, actions};
pub use repository::RemoteCommandOutput;
-pub use repository::WORK_DIRECTORY_REPO_PATH;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use std::ffi::OsStr;
use std::fmt;
use std::str::FromStr;
-use std::sync::LazyLock;
-
-pub static DOT_GIT: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new(".git"));
-pub static GITIGNORE: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new(".gitignore"));
-pub static FSMONITOR_DAEMON: LazyLock<&'static OsStr> =
- LazyLock::new(|| OsStr::new("fsmonitor--daemon"));
-pub static LFS_DIR: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("lfs"));
-pub static COMMIT_MESSAGE: LazyLock<&'static OsStr> =
- LazyLock::new(|| OsStr::new("COMMIT_EDITMSG"));
-pub static INDEX_LOCK: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("index.lock"));
+
+pub const DOT_GIT: &str = ".git";
+pub const GITIGNORE: &str = ".gitignore";
+pub const FSMONITOR_DAEMON: &str = "fsmonitor--daemon";
+pub const LFS_DIR: &str = "lfs";
+pub const COMMIT_MESSAGE: &str = "COMMIT_EDITMSG";
+pub const INDEX_LOCK: &str = "index.lock";
actions!(
git,
@@ -12,12 +12,9 @@ use parking_lot::Mutex;
use rope::Rope;
use schemars::JsonSchema;
use serde::Deserialize;
-use std::borrow::{Borrow, Cow};
use std::ffi::{OsStr, OsString};
use std::io::prelude::*;
-use std::path::Component;
use std::process::{ExitStatus, Stdio};
-use std::sync::LazyLock;
use std::{
cmp::Ordering,
future,
@@ -28,6 +25,8 @@ use std::{
use sum_tree::MapSeekTarget;
use thiserror::Error;
use util::command::{new_smol_command, new_std_command};
+use util::paths::PathStyle;
+use util::rel_path::RelPath;
use util::{ResultExt, paths};
use uuid::Uuid;
@@ -719,16 +718,21 @@ impl GitRepository for RealGitRepository {
let mut info_line = String::new();
let mut newline = [b'\0'];
for (path, status_code) in changes {
+ // git-show outputs `/`-delimited paths even on Windows.
+ let Ok(rel_path) = RelPath::new(path) else {
+ continue;
+ };
+
match status_code {
StatusCode::Modified => {
- writeln!(&mut stdin, "{commit}:{}", path.display())?;
- writeln!(&mut stdin, "{parent_sha}:{}", path.display())?;
+ writeln!(&mut stdin, "{commit}:{path}")?;
+ writeln!(&mut stdin, "{parent_sha}:{path}")?;
}
StatusCode::Added => {
- writeln!(&mut stdin, "{commit}:{}", path.display())?;
+ writeln!(&mut stdin, "{commit}:{path}")?;
}
StatusCode::Deleted => {
- writeln!(&mut stdin, "{parent_sha}:{}", path.display())?;
+ writeln!(&mut stdin, "{parent_sha}:{path}")?;
}
_ => continue,
}
@@ -766,7 +770,7 @@ impl GitRepository for RealGitRepository {
}
files.push(CommitFile {
- path: path.into(),
+ path: rel_path.into(),
old_text,
new_text,
})
@@ -824,7 +828,7 @@ impl GitRepository for RealGitRepository {
.current_dir(&working_directory?)
.envs(env.iter())
.args(["checkout", &commit, "--"])
- .args(paths.iter().map(|path| path.as_ref()))
+ .args(paths.iter().map(|path| path.as_str()))
.output()
.await?;
anyhow::ensure!(
@@ -846,13 +850,11 @@ impl GitRepository for RealGitRepository {
.spawn(async move {
fn logic(repo: &git2::Repository, path: &RepoPath) -> Result<Option<String>> {
// This check is required because index.get_path() unwraps internally :(
- check_path_to_repo_path_errors(path)?;
-
let mut index = repo.index()?;
index.read(false)?;
const STAGE_NORMAL: i32 = 0;
- let oid = match index.get_path(path, STAGE_NORMAL) {
+ let oid = match index.get_path(path.as_std_path(), STAGE_NORMAL) {
Some(entry) if entry.mode != GIT_MODE_SYMLINK => entry.id,
_ => return Ok(None),
};
@@ -876,7 +878,7 @@ impl GitRepository for RealGitRepository {
.spawn(async move {
let repo = repo.lock();
let head = repo.head().ok()?.peel_to_tree().log_err()?;
- let entry = head.get_path(&path).ok()?;
+ let entry = head.get_path(path.as_std_path()).ok()?;
if entry.filemode() == i32::from(git2::FileMode::Link) {
return None;
}
@@ -918,7 +920,7 @@ impl GitRepository for RealGitRepository {
.current_dir(&working_directory)
.envs(env.iter())
.args(["update-index", "--add", "--cacheinfo", "100644", sha])
- .arg(path.to_unix_style())
+ .arg(path.as_str())
.output()
.await?;
@@ -933,7 +935,7 @@ impl GitRepository for RealGitRepository {
.current_dir(&working_directory)
.envs(env.iter())
.args(["update-index", "--force-remove"])
- .arg(path.to_unix_style())
+ .arg(path.as_str())
.output()
.await?;
anyhow::ensure!(
@@ -1251,7 +1253,7 @@ impl GitRepository for RealGitRepository {
.current_dir(&working_directory?)
.envs(env.iter())
.args(["update-index", "--add", "--remove", "--"])
- .args(paths.iter().map(|p| p.to_unix_style()))
+ .args(paths.iter().map(|p| p.as_str()))
.output()
.await?;
anyhow::ensure!(
@@ -1812,7 +1814,7 @@ fn git_status_args(path_prefixes: &[RepoPath]) -> Vec<OsString> {
OsString::from("-z"),
];
args.extend(path_prefixes.iter().map(|path_prefix| {
- if path_prefix.0.as_ref() == Path::new("") {
+ if path_prefix.is_empty() {
Path::new(".").into()
} else {
path_prefix.as_os_str().into()
@@ -2066,99 +2068,65 @@ async fn run_askpass_command(
}
}
-pub static WORK_DIRECTORY_REPO_PATH: LazyLock<RepoPath> =
- LazyLock::new(|| RepoPath(Path::new("").into()));
-
#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
-pub struct RepoPath(pub Arc<Path>);
+pub struct RepoPath(pub Arc<RelPath>);
impl RepoPath {
- pub fn new(path: PathBuf) -> Self {
- debug_assert!(path.is_relative(), "Repo paths must be relative");
-
- RepoPath(path.into())
+ pub fn new<S: AsRef<str> + ?Sized>(s: &S) -> Result<Self> {
+ let rel_path = RelPath::new(s)?;
+ Ok(rel_path.into())
}
- pub fn from_str(path: &str) -> Self {
- let path = Path::new(path);
- debug_assert!(path.is_relative(), "Repo paths must be relative");
-
- RepoPath(path.into())
+ pub fn from_proto(proto: &str) -> Result<Self> {
+ let rel_path = RelPath::from_proto(proto)?;
+ Ok(rel_path.into())
}
- pub fn to_unix_style(&self) -> Cow<'_, OsStr> {
- #[cfg(target_os = "windows")]
- {
- use std::ffi::OsString;
-
- let path = self.0.as_os_str().to_string_lossy().replace("\\", "/");
- Cow::Owned(OsString::from(path))
- }
- #[cfg(not(target_os = "windows"))]
- {
- Cow::Borrowed(self.0.as_os_str())
- }
+ pub fn from_std_path(path: &Path, path_style: PathStyle) -> Result<Self> {
+ let rel_path = RelPath::from_std_path(path, path_style)?;
+ Ok(rel_path.into())
}
}
-impl std::fmt::Display for RepoPath {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- self.0.to_string_lossy().fmt(f)
- }
+#[cfg(any(test, feature = "test-support"))]
+pub fn repo_path<S: AsRef<str> + ?Sized>(s: &S) -> RepoPath {
+ RepoPath(RelPath::new(s).unwrap().into())
}
-impl From<&Path> for RepoPath {
- fn from(value: &Path) -> Self {
- RepoPath::new(value.into())
+impl From<&RelPath> for RepoPath {
+ fn from(value: &RelPath) -> Self {
+ RepoPath(value.into())
}
}
-impl From<Arc<Path>> for RepoPath {
- fn from(value: Arc<Path>) -> Self {
+impl From<Arc<RelPath>> for RepoPath {
+ fn from(value: Arc<RelPath>) -> Self {
RepoPath(value)
}
}
-impl From<PathBuf> for RepoPath {
- fn from(value: PathBuf) -> Self {
- RepoPath::new(value)
- }
-}
-
-impl From<&str> for RepoPath {
- fn from(value: &str) -> Self {
- Self::from_str(value)
- }
-}
-
impl Default for RepoPath {
fn default() -> Self {
- RepoPath(Path::new("").into())
- }
-}
-
-impl AsRef<Path> for RepoPath {
- fn as_ref(&self) -> &Path {
- self.0.as_ref()
+ RepoPath(RelPath::empty().into())
}
}
impl std::ops::Deref for RepoPath {
- type Target = Path;
+ type Target = RelPath;
fn deref(&self) -> &Self::Target {
&self.0
}
}
-impl Borrow<Path> for RepoPath {
- fn borrow(&self) -> &Path {
- self.0.as_ref()
+impl AsRef<Path> for RepoPath {
+ fn as_ref(&self) -> &Path {
+ RelPath::as_ref(&self.0)
}
}
#[derive(Debug)]
-pub struct RepoPathDescendants<'a>(pub &'a Path);
+pub struct RepoPathDescendants<'a>(pub &'a RepoPath);
impl MapSeekTarget<RepoPath> for RepoPathDescendants<'_> {
fn cmp_cursor(&self, key: &RepoPath) -> Ordering {
@@ -2244,35 +2212,6 @@ fn parse_upstream_track(upstream_track: &str) -> Result<UpstreamTracking> {
}))
}
-fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
- match relative_file_path.components().next() {
- None => anyhow::bail!("repo path should not be empty"),
- Some(Component::Prefix(_)) => anyhow::bail!(
- "repo path `{}` should be relative, not a windows prefix",
- relative_file_path.to_string_lossy()
- ),
- Some(Component::RootDir) => {
- anyhow::bail!(
- "repo path `{}` should be relative",
- relative_file_path.to_string_lossy()
- )
- }
- Some(Component::CurDir) => {
- anyhow::bail!(
- "repo path `{}` should not start with `.`",
- relative_file_path.to_string_lossy()
- )
- }
- Some(Component::ParentDir) => {
- anyhow::bail!(
- "repo path `{}` should not start with `..`",
- relative_file_path.to_string_lossy()
- )
- }
- _ => Ok(()),
- }
-}
-
fn checkpoint_author_envs() -> HashMap<String, String> {
HashMap::from_iter([
("GIT_AUTHOR_NAME".to_string(), "Zed".to_string()),
@@ -2299,12 +2238,9 @@ mod tests {
let repo =
RealGitRepository::new(&repo_dir.path().join(".git"), None, cx.executor()).unwrap();
- repo.stage_paths(
- vec![RepoPath::from_str("file")],
- Arc::new(HashMap::default()),
- )
- .await
- .unwrap();
+ repo.stage_paths(vec![repo_path("file")], Arc::new(HashMap::default()))
+ .await
+ .unwrap();
repo.commit(
"Initial commit".into(),
None,
@@ -2328,12 +2264,9 @@ mod tests {
smol::fs::write(&file_path, "modified after checkpoint")
.await
.unwrap();
- repo.stage_paths(
- vec![RepoPath::from_str("file")],
- Arc::new(HashMap::default()),
- )
- .await
- .unwrap();
+ repo.stage_paths(vec![repo_path("file")], Arc::new(HashMap::default()))
+ .await
+ .unwrap();
repo.commit(
"Commit after checkpoint".into(),
None,
@@ -2466,12 +2399,9 @@ mod tests {
RealGitRepository::new(&repo_dir.path().join(".git"), None, cx.executor()).unwrap();
// initial commit
- repo.stage_paths(
- vec![RepoPath::from_str("main.rs")],
- Arc::new(HashMap::default()),
- )
- .await
- .unwrap();
+ repo.stage_paths(vec![repo_path("main.rs")], Arc::new(HashMap::default()))
+ .await
+ .unwrap();
repo.commit(
"Initial commit".into(),
None,
@@ -1,8 +1,8 @@
use crate::repository::RepoPath;
use anyhow::Result;
use serde::{Deserialize, Serialize};
-use std::{path::Path, str::FromStr, sync::Arc};
-use util::ResultExt;
+use std::{str::FromStr, sync::Arc};
+use util::{ResultExt, rel_path::RelPath};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum FileStatus {
@@ -447,7 +447,8 @@ impl FromStr for GitStatus {
}
let status = entry.as_bytes()[0..2].try_into().unwrap();
let status = FileStatus::from_bytes(status).log_err()?;
- let path = RepoPath(Path::new(path).into());
+ // git-status outputs `/`-delimited repo paths, even on Windows.
+ let path = RepoPath(RelPath::new(path).log_err()?.into());
Some((path, status))
})
.collect::<Vec<_>>();
@@ -14,13 +14,12 @@ use multi_buffer::PathKey;
use project::{Project, WorktreeId, git_store::Repository};
use std::{
any::{Any, TypeId},
- ffi::OsStr,
fmt::Write as _,
- path::{Path, PathBuf},
+ path::PathBuf,
sync::Arc,
};
use ui::{Color, Icon, IconName, Label, LabelCommon as _, SharedString};
-use util::{ResultExt, truncate_and_trailoff};
+use util::{ResultExt, paths::PathStyle, rel_path::RelPath, truncate_and_trailoff};
use workspace::{
Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace,
item::{BreadcrumbText, ItemEvent, TabContentParams},
@@ -40,7 +39,7 @@ struct GitBlob {
}
struct CommitMetadataFile {
- title: Arc<Path>,
+ title: Arc<RelPath>,
worktree_id: WorktreeId,
}
@@ -129,7 +128,9 @@ impl CommitView {
let mut metadata_buffer_id = None;
if let Some(worktree_id) = first_worktree_id {
let file = Arc::new(CommitMetadataFile {
- title: PathBuf::from(format!("commit {}", commit.sha)).into(),
+ title: RelPath::new(&format!("commit {}", commit.sha))
+ .unwrap()
+ .into(),
worktree_id,
});
let buffer = cx.new(|cx| {
@@ -144,7 +145,7 @@ impl CommitView {
});
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.set_excerpts_for_path(
- PathKey::namespaced(COMMIT_METADATA_NAMESPACE, file.title.clone()),
+ PathKey::namespaced(COMMIT_METADATA_NAMESPACE, file.title.as_str().into()),
buffer.clone(),
vec![Point::zero()..buffer.read(cx).max_point()],
0,
@@ -192,7 +193,7 @@ impl CommitView {
.collect::<Vec<_>>();
let path = snapshot.file().unwrap().path().clone();
let _is_newly_added = multibuffer.set_excerpts_for_path(
- PathKey::namespaced(FILE_NAMESPACE, path),
+ PathKey::namespaced(FILE_NAMESPACE, path.as_str().into()),
buffer,
diff_hunk_ranges,
multibuffer_context_lines(cx),
@@ -227,15 +228,19 @@ impl language::File for GitBlob {
}
}
- fn path(&self) -> &Arc<Path> {
+ fn path_style(&self, _: &App) -> PathStyle {
+ PathStyle::Posix
+ }
+
+ fn path(&self) -> &Arc<RelPath> {
&self.path.0
}
fn full_path(&self, _: &App) -> PathBuf {
- self.path.to_path_buf()
+ self.path.as_std_path().to_path_buf()
}
- fn file_name<'a>(&'a self, _: &'a App) -> &'a OsStr {
+ fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
self.path.file_name().unwrap()
}
@@ -261,15 +266,19 @@ impl language::File for CommitMetadataFile {
DiskState::New
}
- fn path(&self) -> &Arc<Path> {
+ fn path_style(&self, _: &App) -> PathStyle {
+ PathStyle::Posix
+ }
+
+ fn path(&self) -> &Arc<RelPath> {
&self.title
}
fn full_path(&self, _: &App) -> PathBuf {
- self.title.as_ref().into()
+ PathBuf::from(self.title.as_str().to_owned())
}
- fn file_name<'a>(&'a self, _: &'a App) -> &'a OsStr {
+ fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
self.title.file_name().unwrap()
}
@@ -53,7 +53,7 @@ use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore, StatusStyle};
use std::future::Future;
use std::ops::Range;
-use std::path::{Path, PathBuf};
+use std::path::Path;
use std::{collections::HashSet, sync::Arc, time::Duration, usize};
use strum::{IntoEnumIterator, VariantNames};
use time::OffsetDateTime;
@@ -61,6 +61,7 @@ use ui::{
Checkbox, CommonAnimationExt, ContextMenu, ElevationIndex, IconPosition, Label, LabelSize,
PopoverMenu, ScrollAxes, Scrollbars, SplitButton, Tooltip, WithScrollbar, prelude::*,
};
+use util::paths::PathStyle;
use util::{ResultExt, TryFutureExt, maybe};
use workspace::SERIALIZATION_THROTTLE_TIME;
@@ -251,23 +252,22 @@ impl GitListEntry {
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct GitStatusEntry {
pub(crate) repo_path: RepoPath,
- pub(crate) abs_path: PathBuf,
pub(crate) status: FileStatus,
pub(crate) staging: StageStatus,
}
impl GitStatusEntry {
- fn display_name(&self) -> String {
+ fn display_name(&self, path_style: PathStyle) -> String {
self.repo_path
.file_name()
- .map(|name| name.to_string_lossy().into_owned())
- .unwrap_or_else(|| self.repo_path.to_string_lossy().into_owned())
+ .map(|name| name.to_owned())
+ .unwrap_or_else(|| self.repo_path.display(path_style).to_string())
}
- fn parent_dir(&self) -> Option<String> {
+ fn parent_dir(&self, path_style: PathStyle) -> Option<String> {
self.repo_path
.parent()
- .map(|parent| parent.to_string_lossy().into_owned())
+ .map(|parent| parent.display(path_style).to_string())
}
}
@@ -826,6 +826,7 @@ impl GitPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
+ let path_style = self.project.read(cx).path_style(cx);
maybe!({
let list_entry = self.entries.get(self.selected_entry?)?.clone();
let entry = list_entry.status_entry()?.to_owned();
@@ -841,8 +842,7 @@ impl GitPanel {
entry
.repo_path
.file_name()
- .unwrap_or(entry.repo_path.as_os_str())
- .to_string_lossy()
+ .unwrap_or(entry.repo_path.display(path_style).as_ref()),
),
None,
&["Restore", "Cancel"],
@@ -885,7 +885,7 @@ impl GitPanel {
if entry.status.staging().has_staged() {
self.change_file_stage(false, vec![entry.clone()], cx);
}
- let filename = path.path.file_name()?.to_string_lossy();
+ let filename = path.path.file_name()?.to_string();
if !entry.status.is_created() {
self.perform_checkout(vec![entry.clone()], window, cx);
@@ -1028,7 +1028,7 @@ impl GitPanel {
let mut details = entries
.iter()
.filter_map(|entry| entry.repo_path.0.file_name())
- .map(|filename| filename.to_string_lossy())
+ .map(|filename| filename.to_string())
.take(5)
.join("\n");
if entries.len() > 5 {
@@ -1084,7 +1084,7 @@ impl GitPanel {
.repo_path
.0
.file_name()
- .map(|f| f.to_string_lossy())
+ .map(|f| f.to_string())
.unwrap_or_default()
})
.take(5)
@@ -1721,7 +1721,7 @@ impl GitPanel {
.repo_path
.file_name()
.unwrap_or_default()
- .to_string_lossy();
+ .to_string();
Some(format!("{} {}", action_text, file_name))
}
@@ -1973,11 +1973,7 @@ impl GitPanel {
cx.spawn_in(window, async move |this, cx| {
let mut paths = path.await.ok()?.ok()??;
let mut path = paths.pop()?;
- let repo_name = repo
- .split(std::path::MAIN_SEPARATOR_STR)
- .last()?
- .strip_suffix(".git")?
- .to_owned();
+ let repo_name = repo.split("/").last()?.strip_suffix(".git")?.to_owned();
let fs = this.read_with(cx, |this, _| this.fs.clone()).ok()?;
@@ -2558,6 +2554,7 @@ impl GitPanel {
}
fn update_visible_entries(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ let path_style = self.project.read(cx).path_style(cx);
let bulk_staging = self.bulk_staging.take();
let last_staged_path_prev_index = bulk_staging
.as_ref()
@@ -2609,10 +2606,8 @@ impl GitPanel {
continue;
}
- let abs_path = repo.work_directory_abs_path.join(&entry.repo_path.0);
let entry = GitStatusEntry {
repo_path: entry.repo_path.clone(),
- abs_path,
status: entry.status,
staging,
};
@@ -2623,8 +2618,8 @@ impl GitPanel {
}
let width_estimate = Self::item_width_estimate(
- entry.parent_dir().map(|s| s.len()).unwrap_or(0),
- entry.display_name().len(),
+ entry.parent_dir(path_style).map(|s| s.len()).unwrap_or(0),
+ entry.display_name(path_style).len(),
);
match max_width_item.as_mut() {
@@ -3634,7 +3629,7 @@ impl GitPanel {
cx: &App,
) -> Option<AnyElement> {
let repo = self.active_repository.as_ref()?.read(cx);
- let project_path = (file.worktree_id(cx), file.path()).into();
+ let project_path = (file.worktree_id(cx), file.path().clone()).into();
let repo_path = repo.project_path_to_repo_path(&project_path, cx)?;
let ix = self.entry_by_path(&repo_path, cx)?;
let entry = self.entries.get(ix)?;
@@ -3887,7 +3882,8 @@ impl GitPanel {
window: &Window,
cx: &Context<Self>,
) -> AnyElement {
- let display_name = entry.display_name();
+ let path_style = self.project.read(cx).path_style(cx);
+ let display_name = entry.display_name(path_style);
let selected = self.selected_entry == Some(ix);
let marked = self.marked_entries.contains(&ix);
@@ -4060,11 +4056,14 @@ impl GitPanel {
.items_center()
.flex_1()
// .overflow_hidden()
- .when_some(entry.parent_dir(), |this, parent| {
+ .when_some(entry.parent_dir(path_style), |this, parent| {
if !parent.is_empty() {
this.child(
- self.entry_label(format!("{}/", parent), path_color)
- .when(status.is_deleted(), |this| this.strikethrough()),
+ self.entry_label(
+ format!("{parent}{}", path_style.separator()),
+ path_color,
+ )
+ .when(status.is_deleted(), |this| this.strikethrough()),
)
} else {
this
@@ -4889,7 +4888,10 @@ impl Component for PanelRepoFooter {
#[cfg(test)]
mod tests {
- use git::status::{StatusCode, UnmergedStatus, UnmergedStatusCode};
+ use git::{
+ repository::repo_path,
+ status::{StatusCode, UnmergedStatus, UnmergedStatusCode},
+ };
use gpui::{TestAppContext, VisualTestContext};
use project::{FakeFs, WorktreeSettings};
use serde_json::json;
@@ -4941,14 +4943,8 @@ mod tests {
fs.set_status_for_repo(
Path::new(path!("/root/zed/.git")),
&[
- (
- Path::new("crates/gpui/gpui.rs"),
- StatusCode::Modified.worktree(),
- ),
- (
- Path::new("crates/util/util.rs"),
- StatusCode::Modified.worktree(),
- ),
+ ("crates/gpui/gpui.rs", StatusCode::Modified.worktree()),
+ ("crates/util/util.rs", StatusCode::Modified.worktree()),
],
);
@@ -4989,14 +4985,12 @@ mod tests {
header: Section::Tracked
}),
GitListEntry::Status(GitStatusEntry {
- abs_path: path!("/root/zed/crates/gpui/gpui.rs").into(),
- repo_path: "crates/gpui/gpui.rs".into(),
+ repo_path: repo_path("crates/gpui/gpui.rs"),
status: StatusCode::Modified.worktree(),
staging: StageStatus::Unstaged,
}),
GitListEntry::Status(GitStatusEntry {
- abs_path: path!("/root/zed/crates/util/util.rs").into(),
- repo_path: "crates/util/util.rs".into(),
+ repo_path: repo_path("crates/util/util.rs"),
status: StatusCode::Modified.worktree(),
staging: StageStatus::Unstaged,
},),
@@ -5016,14 +5010,12 @@ mod tests {
header: Section::Tracked
}),
GitListEntry::Status(GitStatusEntry {
- abs_path: path!("/root/zed/crates/gpui/gpui.rs").into(),
- repo_path: "crates/gpui/gpui.rs".into(),
+ repo_path: repo_path("crates/gpui/gpui.rs"),
status: StatusCode::Modified.worktree(),
staging: StageStatus::Unstaged,
}),
GitListEntry::Status(GitStatusEntry {
- abs_path: path!("/root/zed/crates/util/util.rs").into(),
- repo_path: "crates/util/util.rs".into(),
+ repo_path: repo_path("crates/util/util.rs"),
status: StatusCode::Modified.worktree(),
staging: StageStatus::Unstaged,
},),
@@ -5061,14 +5053,14 @@ mod tests {
fs.set_status_for_repo(
Path::new(path!("/root/project/.git")),
&[
- (Path::new("src/main.rs"), StatusCode::Modified.worktree()),
- (Path::new("src/lib.rs"), StatusCode::Modified.worktree()),
- (Path::new("tests/test.rs"), StatusCode::Modified.worktree()),
- (Path::new("new_file.txt"), FileStatus::Untracked),
- (Path::new("another_new.rs"), FileStatus::Untracked),
- (Path::new("src/utils.rs"), FileStatus::Untracked),
+ ("src/main.rs", StatusCode::Modified.worktree()),
+ ("src/lib.rs", StatusCode::Modified.worktree()),
+ ("tests/test.rs", StatusCode::Modified.worktree()),
+ ("new_file.txt", FileStatus::Untracked),
+ ("another_new.rs", FileStatus::Untracked),
+ ("src/utils.rs", FileStatus::Untracked),
(
- Path::new("conflict.txt"),
+ "conflict.txt",
UnmergedStatus {
first_head: UnmergedStatusCode::Updated,
second_head: UnmergedStatusCode::Updated,
@@ -5242,7 +5234,7 @@ mod tests {
fs.set_status_for_repo(
Path::new(path!("/root/project/.git")),
- &[(Path::new("src/main.rs"), StatusCode::Modified.worktree())],
+ &[("src/main.rs", StatusCode::Modified.worktree())],
);
let project = Project::test(fs.clone(), [Path::new(path!("/root/project"))], cx).await;
@@ -243,7 +243,7 @@ impl ProjectDiff {
TRACKED_NAMESPACE
};
- let path_key = PathKey::namespaced(namespace, entry.repo_path.0);
+ let path_key = PathKey::namespaced(namespace, entry.repo_path.as_str().into());
self.move_to_path(path_key, window, cx)
}
@@ -397,7 +397,7 @@ impl ProjectDiff {
} else {
TRACKED_NAMESPACE
};
- let path_key = PathKey::namespaced(namespace, entry.repo_path.0.clone());
+ let path_key = PathKey::namespaced(namespace, entry.repo_path.as_str().into());
previous_paths.remove(&path_key);
let load_buffer = self
@@ -535,7 +535,7 @@ impl ProjectDiff {
self.multibuffer
.read(cx)
.excerpt_paths()
- .map(|key| key.path().to_string_lossy().to_string())
+ .map(|key| key.path().to_string())
.collect()
}
}
@@ -1406,12 +1406,12 @@ mod tests {
fs.set_head_for_repo(
path!("/project/.git").as_ref(),
- &[("foo.txt".into(), "foo\n".into())],
+ &[("foo.txt", "foo\n".into())],
"deadbeef",
);
fs.set_index_for_repo(
path!("/project/.git").as_ref(),
- &[("foo.txt".into(), "foo\n".into())],
+ &[("foo.txt", "foo\n".into())],
);
cx.run_until_parked();
@@ -1461,16 +1461,13 @@ mod tests {
fs.set_head_and_index_for_repo(
path!("/project/.git").as_ref(),
- &[
- ("bar".into(), "bar\n".into()),
- ("foo".into(), "foo\n".into()),
- ],
+ &[("bar", "bar\n".into()), ("foo", "foo\n".into())],
);
cx.run_until_parked();
let editor = cx.update_window_entity(&diff, |diff, window, cx| {
diff.move_to_path(
- PathKey::namespaced(TRACKED_NAMESPACE, Path::new("foo").into()),
+ PathKey::namespaced(TRACKED_NAMESPACE, "foo".into()),
window,
cx,
);
@@ -1491,7 +1488,7 @@ mod tests {
let editor = cx.update_window_entity(&diff, |diff, window, cx| {
diff.move_to_path(
- PathKey::namespaced(TRACKED_NAMESPACE, Path::new("bar").into()),
+ PathKey::namespaced(TRACKED_NAMESPACE, "bar".into()),
window,
cx,
);
@@ -1543,7 +1540,7 @@ mod tests {
fs.set_head_for_repo(
path!("/project/.git").as_ref(),
- &[("foo".into(), "original\n".into())],
+ &[("foo", "original\n".into())],
"deadbeef",
);
cx.run_until_parked();
@@ -1646,12 +1643,12 @@ mod tests {
)
.await;
- fs.set_git_content_for_repo(
+ fs.set_head_and_index_for_repo(
Path::new("/a/.git"),
&[
- ("b.txt".into(), "before\n".to_string(), None),
- ("c.txt".into(), "unchanged\n".to_string(), None),
- ("d.txt".into(), "deleted\n".to_string(), None),
+ ("b.txt", "before\n".to_string()),
+ ("c.txt", "unchanged\n".to_string()),
+ ("d.txt", "deleted\n".to_string()),
],
);
@@ -1764,9 +1761,9 @@ mod tests {
)
.await;
- fs.set_git_content_for_repo(
+ fs.set_head_and_index_for_repo(
Path::new("/a/.git"),
- &[("main.rs".into(), git_contents.to_owned(), None)],
+ &[("main.rs", git_contents.to_owned())],
);
let project = Project::test(fs, [Path::new("/a")], cx).await;
@@ -1816,7 +1813,7 @@ mod tests {
fs.set_status_for_repo(
Path::new(path!("/project/.git")),
&[(
- Path::new("foo"),
+ "foo",
UnmergedStatus {
first_head: UnmergedStatusCode::Updated,
second_head: UnmergedStatusCode::Updated,
@@ -311,7 +311,7 @@ mod tests {
use project::{FakeFs, Project};
use serde_json::json;
use std::{num::NonZeroU32, sync::Arc, time::Duration};
- use util::path;
+ use util::{path, rel_path::rel_path};
use workspace::{AppState, Workspace};
#[gpui::test]
@@ -356,7 +356,7 @@ mod tests {
.unwrap();
let editor = workspace
.update_in(cx, |workspace, window, cx| {
- workspace.open_path((worktree_id, "a.rs"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("a.rs")), None, true, window, cx)
})
.await
.unwrap()
@@ -460,7 +460,7 @@ mod tests {
.unwrap();
let editor = workspace
.update_in(cx, |workspace, window, cx| {
- workspace.open_path((worktree_id, "a.rs"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("a.rs")), None, true, window, cx)
})
.await
.unwrap()
@@ -545,7 +545,7 @@ mod tests {
.unwrap();
let editor = workspace
.update_in(cx, |workspace, window, cx| {
- workspace.open_path((worktree_id, "a.rs"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("a.rs")), None, true, window, cx)
})
.await
.unwrap()
@@ -623,7 +623,7 @@ mod tests {
.unwrap();
let editor = workspace
.update_in(cx, |workspace, window, cx| {
- workspace.open_path((worktree_id, "a.rs"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("a.rs")), None, true, window, cx)
})
.await
.unwrap()
@@ -1,8 +1,6 @@
mod image_info;
mod image_viewer_settings;
-use std::path::PathBuf;
-
use anyhow::Context as _;
use editor::{EditorSettings, items::entry_git_aware_label_color};
use file_icons::FileIcons;
@@ -144,7 +142,6 @@ impl Item for ImageView {
.read(cx)
.file
.file_name(cx)
- .to_string_lossy()
.to_string()
.into()
}
@@ -198,20 +195,14 @@ impl Item for ImageView {
}
fn breadcrumbs_text_for_image(project: &Project, image: &ImageItem, cx: &App) -> String {
- let path = image.file.file_name(cx);
- if project.visible_worktrees(cx).count() <= 1 {
- return path.to_string_lossy().to_string();
+ let mut path = image.file.path().clone();
+ if project.visible_worktrees(cx).count() > 1
+ && let Some(worktree) = project.worktree_for_id(image.project_path(cx).worktree_id, cx)
+ {
+ path = worktree.read(cx).root_name().join(&path);
}
- project
- .worktree_for_id(image.project_path(cx).worktree_id, cx)
- .map(|worktree| {
- PathBuf::from(worktree.read(cx).root_name())
- .join(path)
- .to_string_lossy()
- .to_string()
- })
- .unwrap_or_else(|| path.to_string_lossy().to_string())
+ path.display(project.path_style(cx)).to_string()
}
impl SerializableItem for ImageView {
@@ -242,7 +233,7 @@ impl SerializableItem for ImageView {
let project_path = ProjectPath {
worktree_id,
- path: relative_path.into(),
+ path: relative_path,
};
let image_item = project
@@ -24,6 +24,7 @@ use std::path::Path;
use std::rc::Rc;
use std::sync::LazyLock;
use ui::{Label, LabelSize, Tooltip, prelude::*, styled_ext_reflection, v_flex};
+use util::rel_path::RelPath;
use util::split_str_with_ranges;
/// Path used for unsaved buffer that contains style json. To support the json language server, this
@@ -466,7 +467,7 @@ impl DivInspector {
let project_path = worktree.read_with(cx, |worktree, _cx| ProjectPath {
worktree_id: worktree.id(),
- path: Path::new("").into(),
+ path: RelPath::empty().into(),
})?;
let buffer = project
@@ -94,7 +94,7 @@ pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut Ap
break;
}
for directory in worktree.read(cx).directories(true, 1) {
- let full_directory_path = worktree_root.join(&directory.path);
+ let full_directory_path = worktree_root.join(directory.path.as_std_path());
if full_directory_path.ends_with(&journal_dir_clone) {
open_new_workspace = false;
break 'outer;
@@ -41,13 +41,12 @@ use std::{
cell::Cell,
cmp::{self, Ordering, Reverse},
collections::{BTreeMap, BTreeSet},
- ffi::OsStr,
future::Future,
iter::{self, Iterator, Peekable},
mem,
num::NonZeroU32,
ops::{Deref, Range},
- path::{Path, PathBuf},
+ path::PathBuf,
rc,
sync::{Arc, LazyLock},
time::{Duration, Instant},
@@ -65,7 +64,7 @@ pub use text::{
use theme::{ActiveTheme as _, SyntaxTheme};
#[cfg(any(test, feature = "test-support"))]
use util::RandomCharIter;
-use util::{RangeExt, debug_panic, maybe};
+use util::{RangeExt, debug_panic, maybe, paths::PathStyle, rel_path::RelPath};
#[cfg(any(test, feature = "test-support"))]
pub use {tree_sitter_python, tree_sitter_rust, tree_sitter_typescript};
@@ -349,15 +348,18 @@ pub trait File: Send + Sync + Any {
fn disk_state(&self) -> DiskState;
/// Returns the path of this file relative to the worktree's root directory.
- fn path(&self) -> &Arc<Path>;
+ fn path(&self) -> &Arc<RelPath>;
/// Returns the path of this file relative to the worktree's parent directory (this means it
/// includes the name of the worktree's root folder).
fn full_path(&self, cx: &App) -> PathBuf;
+ /// Returns the path style of this file.
+ fn path_style(&self, cx: &App) -> PathStyle;
+
/// Returns the last component of this handle's absolute path. If this handle refers to the root
/// of its worktree, then this method will return the name of the worktree itself.
- fn file_name<'a>(&'a self, cx: &'a App) -> &'a OsStr;
+ fn file_name<'a>(&'a self, cx: &'a App) -> &'a str;
/// Returns the id of the worktree to which this file belongs.
///
@@ -4626,13 +4628,12 @@ impl BufferSnapshot {
self.file.as_ref()
}
- /// Resolves the file path (relative to the worktree root) associated with the underlying file.
pub fn resolve_file_path(&self, cx: &App, include_root: bool) -> Option<PathBuf> {
if let Some(file) = self.file() {
if file.path().file_name().is_none() || include_root {
Some(file.full_path(cx))
} else {
- Some(file.path().to_path_buf())
+ Some(file.path().as_std_path().to_owned())
}
} else {
None
@@ -5117,19 +5118,19 @@ impl IndentSize {
#[cfg(any(test, feature = "test-support"))]
pub struct TestFile {
- pub path: Arc<Path>,
+ pub path: Arc<RelPath>,
pub root_name: String,
pub local_root: Option<PathBuf>,
}
#[cfg(any(test, feature = "test-support"))]
impl File for TestFile {
- fn path(&self) -> &Arc<Path> {
+ fn path(&self) -> &Arc<RelPath> {
&self.path
}
fn full_path(&self, _: &gpui::App) -> PathBuf {
- PathBuf::from(&self.root_name).join(self.path.as_ref())
+ PathBuf::from(self.root_name.clone()).join(self.path.as_std_path())
}
fn as_local(&self) -> Option<&dyn LocalFile> {
@@ -5144,7 +5145,7 @@ impl File for TestFile {
unimplemented!()
}
- fn file_name<'a>(&'a self, _: &'a gpui::App) -> &'a std::ffi::OsStr {
+ fn file_name<'a>(&'a self, _: &'a gpui::App) -> &'a str {
self.path().file_name().unwrap_or(self.root_name.as_ref())
}
@@ -5159,6 +5160,10 @@ impl File for TestFile {
fn is_private(&self) -> bool {
false
}
+
+ fn path_style(&self, _cx: &App) -> PathStyle {
+ PathStyle::local()
+ }
}
#[cfg(any(test, feature = "test-support"))]
@@ -5166,7 +5171,7 @@ impl LocalFile for TestFile {
fn abs_path(&self, _cx: &App) -> PathBuf {
PathBuf::from(self.local_root.as_ref().unwrap())
.join(&self.root_name)
- .join(self.path.as_ref())
+ .join(self.path.as_std_path())
}
fn load(&self, _cx: &App) -> Task<Result<String>> {
@@ -24,6 +24,7 @@ use text::{BufferId, LineEnding};
use text::{Point, ToPoint};
use theme::ActiveTheme;
use unindent::Unindent as _;
+use util::rel_path::rel_path;
use util::test::marked_text_offsets;
use util::{RandomCharIter, assert_set_eq, post_inc, test::marked_text_ranges};
@@ -380,7 +381,7 @@ async fn test_language_for_file_with_custom_file_types(cx: &mut TestAppContext)
fn file(path: &str) -> Arc<dyn File> {
Arc::new(TestFile {
- path: Path::new(path).into(),
+ path: Arc::from(rel_path(path)),
root_name: "zed".into(),
local_root: None,
})
@@ -70,6 +70,7 @@ pub use toolchain::{
ToolchainMetadata, ToolchainScope,
};
use tree_sitter::{self, Query, QueryCursor, WasmStore, wasmtime};
+use util::rel_path::RelPath;
use util::serde::default_true;
pub use buffer::Operation;
@@ -307,7 +308,7 @@ pub trait LspAdapterDelegate: Send + Sync {
) -> Result<Option<(PathBuf, String)>>;
async fn which(&self, command: &OsStr) -> Option<PathBuf>;
async fn shell_env(&self) -> HashMap<String, String>;
- async fn read_text_file(&self, path: PathBuf) -> Result<String>;
+ async fn read_text_file(&self, path: &RelPath) -> Result<String>;
async fn try_exec(&self, binary: LanguageServerBinary) -> Result<()>;
}
@@ -753,7 +753,7 @@ impl LanguageRegistry {
content: Option<&Rope>,
user_file_types: Option<&FxHashMap<Arc<str>, GlobSet>>,
) -> Option<AvailableLanguage> {
- let filename = path.file_name().and_then(|name| name.to_str());
+ let filename = path.file_name().and_then(|filename| filename.to_str());
// `Path.extension()` returns None for files with a leading '.'
// and no other extension which is not the desired behavior here,
// as we want `.zshrc` to result in extension being `Some("zshrc")`
@@ -390,7 +390,7 @@ impl EditPredictionSettings {
file.as_local()
.is_some_and(|local| glob.matcher.is_match(local.abs_path(cx)))
} else {
- glob.matcher.is_match(file.path())
+ glob.matcher.is_match(file.path().as_std_path())
}
})
}
@@ -798,6 +798,7 @@ pub struct JsxTagAutoCloseSettings {
mod tests {
use super::*;
use gpui::TestAppContext;
+ use util::rel_path::rel_path;
#[gpui::test]
fn test_edit_predictions_enabled_for_file(cx: &mut TestAppContext) {
@@ -839,11 +840,11 @@ mod tests {
const WORKTREE_NAME: &str = "project";
let make_test_file = |segments: &[&str]| -> Arc<dyn File> {
- let mut path_buf = PathBuf::new();
- path_buf.extend(segments);
+ let path = segments.join("/");
+ let path = rel_path(&path);
Arc::new(TestFile {
- path: path_buf.as_path().into(),
+ path: path.into(),
root_name: WORKTREE_NAME.to_string(),
local_root: Some(PathBuf::from(if cfg!(windows) {
"C:\\absolute\\"
@@ -896,7 +897,7 @@ mod tests {
assert!(!settings.enabled_for_file(&test_file, &cx));
let test_file_root: Arc<dyn File> = Arc::new(TestFile {
- path: PathBuf::from("file.rs").as_path().into(),
+ path: rel_path("file.rs").into(),
root_name: WORKTREE_NAME.to_string(),
local_root: Some(PathBuf::from("/absolute/")),
});
@@ -928,8 +929,12 @@ mod tests {
// Test tilde expansion
let home = shellexpand::tilde("~").into_owned();
- let home_file = make_test_file(&[&home, "test.rs"]);
- let settings = build_settings(&["~/test.rs"]);
+ let home_file = Arc::new(TestFile {
+ path: rel_path("test.rs").into(),
+ root_name: "the-dir".to_string(),
+ local_root: Some(PathBuf::from(home)),
+ }) as Arc<dyn File>;
+ let settings = build_settings(&["~/the-dir/test.rs"]);
assert!(!settings.enabled_for_file(&home_file, &cx));
}
@@ -1,7 +1,8 @@
-use std::{borrow::Borrow, path::Path, sync::Arc};
+use std::{borrow::Borrow, sync::Arc};
use gpui::SharedString;
use settings::WorktreeId;
+use util::rel_path::RelPath;
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ManifestName(SharedString);
@@ -42,17 +43,17 @@ impl AsRef<SharedString> for ManifestName {
/// For example, given a path like `foo/bar/baz`, a depth of 2 would explore `foo/bar/baz` and `foo/bar`, but not `foo`.
pub struct ManifestQuery {
/// Path to the file, relative to worktree root.
- pub path: Arc<Path>,
+ pub path: Arc<RelPath>,
pub depth: usize,
pub delegate: Arc<dyn ManifestDelegate>,
}
pub trait ManifestProvider {
fn name(&self) -> ManifestName;
- fn search(&self, query: ManifestQuery) -> Option<Arc<Path>>;
+ fn search(&self, query: ManifestQuery) -> Option<Arc<RelPath>>;
}
pub trait ManifestDelegate: Send + Sync {
fn worktree_id(&self) -> WorktreeId;
- fn exists(&self, path: &Path, is_dir: Option<bool>) -> bool;
+ fn exists(&self, path: &RelPath, is_dir: Option<bool>) -> bool;
}
@@ -4,10 +4,7 @@
//! which is a set of tools used to interact with the projects written in said language.
//! For example, a Python project can have an associated virtual environment; a Rust project can have a toolchain override.
-use std::{
- path::{Path, PathBuf},
- sync::Arc,
-};
+use std::{path::PathBuf, sync::Arc};
use async_trait::async_trait;
use collections::HashMap;
@@ -15,6 +12,7 @@ use fs::Fs;
use gpui::{AsyncApp, SharedString};
use settings::WorktreeId;
use task::ShellKind;
+use util::rel_path::RelPath;
use crate::{LanguageName, ManifestName};
@@ -23,6 +21,7 @@ use crate::{LanguageName, ManifestName};
pub struct Toolchain {
/// User-facing label
pub name: SharedString,
+ /// Absolute path
pub path: SharedString,
pub language_name: LanguageName,
/// Full toolchain data (including language-specific details)
@@ -37,7 +36,7 @@ pub struct Toolchain {
/// - Only in the subproject they're currently in.
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum ToolchainScope {
- Subproject(WorktreeId, Arc<Path>),
+ Subproject(WorktreeId, Arc<RelPath>),
Project,
/// Available in all projects on this box. It wouldn't make sense to show suggestions across machines.
Global,
@@ -97,7 +96,7 @@ pub trait ToolchainLister: Send + Sync + 'static {
async fn list(
&self,
worktree_root: PathBuf,
- subroot_relative_path: Arc<Path>,
+ subroot_relative_path: Arc<RelPath>,
project_env: Option<HashMap<String, String>>,
) -> ToolchainList;
@@ -134,7 +133,7 @@ pub trait LanguageToolchainStore: Send + Sync + 'static {
async fn active_toolchain(
self: Arc<Self>,
worktree_id: WorktreeId,
- relative_path: Arc<Path>,
+ relative_path: Arc<RelPath>,
language_name: LanguageName,
cx: &mut AsyncApp,
) -> Option<Toolchain>;
@@ -144,7 +143,7 @@ pub trait LocalLanguageToolchainStore: Send + Sync + 'static {
fn active_toolchain(
self: Arc<Self>,
worktree_id: WorktreeId,
- relative_path: &Arc<Path>,
+ relative_path: &Arc<RelPath>,
language_name: LanguageName,
cx: &mut AsyncApp,
) -> Option<Toolchain>;
@@ -155,7 +154,7 @@ impl<T: LocalLanguageToolchainStore> LanguageToolchainStore for T {
async fn active_toolchain(
self: Arc<Self>,
worktree_id: WorktreeId,
- relative_path: Arc<Path>,
+ relative_path: Arc<RelPath>,
language_name: LanguageName,
cx: &mut AsyncApp,
) -> Option<Toolchain> {
@@ -19,7 +19,7 @@ use lsp::{
};
use serde::Serialize;
use serde_json::Value;
-use util::{ResultExt, fs::make_file_executable, maybe};
+use util::{ResultExt, fs::make_file_executable, maybe, rel_path::RelPath};
use crate::{LanguageServerRegistryProxy, LspAccess};
@@ -36,7 +36,7 @@ impl WorktreeDelegate for WorktreeDelegateAdapter {
self.0.worktree_root_path().to_string_lossy().to_string()
}
- async fn read_text_file(&self, path: PathBuf) -> Result<String> {
+ async fn read_text_file(&self, path: &RelPath) -> Result<String> {
self.0.read_text_file(path).await
}
@@ -21,6 +21,7 @@ use ui::{
DocumentationSide, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, Window, prelude::*,
};
+use util::{ResultExt, rel_path::RelPath};
use workspace::{StatusItemView, Workspace};
use crate::lsp_log_view;
@@ -148,6 +149,7 @@ impl LanguageServerState {
return;
};
let project = workspace.read(cx).project().clone();
+ let path_style = project.read(cx).path_style(cx);
let buffer_store = project.read(cx).buffer_store().clone();
let buffers = state
.read(cx)
@@ -159,6 +161,9 @@ impl LanguageServerState {
servers.worktree.as_ref()?.upgrade()?.read(cx);
let relative_path =
abs_path.strip_prefix(&worktree.abs_path()).ok()?;
+ let relative_path =
+ RelPath::from_std_path(relative_path, path_style)
+ .log_err()?;
let entry = worktree.entry_for_path(&relative_path)?;
let project_path =
project.read(cx).path_for_entry(entry.id, cx)?;
@@ -767,7 +772,7 @@ impl LspButton {
});
servers_with_health_checks.insert(&health.name);
let worktree_name =
- worktree.map(|worktree| SharedString::new(worktree.read(cx).root_name()));
+ worktree.map(|worktree| SharedString::new(worktree.read(cx).root_name_str()));
let binary_status = state.language_servers.binary_statuses.get(&health.name);
let server_data = ServerData::WithHealthCheck {
@@ -826,7 +831,7 @@ impl LspButton {
{
Some((worktree, server_id)) => {
let worktree_name =
- SharedString::new(worktree.read(cx).root_name());
+ SharedString::new(worktree.read(cx).root_name_str());
servers_per_worktree
.entry(worktree_name.clone())
.or_default()
@@ -376,7 +376,7 @@ impl LspLogView {
let worktree_root_name = state
.worktree_id
.and_then(|id| self.project.read(cx).worktree_for_id(id, cx))
- .map(|worktree| worktree.read(cx).root_name().to_string())
+ .map(|worktree| worktree.read(cx).root_name_str().to_string())
.unwrap_or_else(|| "Unknown worktree".to_string());
LogMenuItem {
@@ -91,7 +91,7 @@ async fn test_lsp_log_view(cx: &mut TestAppContext) {
.next()
.unwrap()
.read(cx)
- .root_name()
+ .root_name_str()
.to_string(),
rpc_trace_enabled: false,
selected_entry: LogKind::Logs,
@@ -30,7 +30,10 @@ use std::{
};
use task::{AdapterSchemas, TaskTemplate, TaskTemplates, VariableName};
use theme::ThemeRegistry;
-use util::{ResultExt, archive::extract_zip, fs::remove_matching, maybe, merge_json_value_into};
+use util::{
+ ResultExt, archive::extract_zip, fs::remove_matching, maybe, merge_json_value_into,
+ rel_path::RelPath,
+};
use crate::PackageJsonData;
@@ -52,8 +55,8 @@ impl ContextProvider for JsonTaskProvider {
let Some(file) = project::File::from_dyn(file.as_ref()).cloned() else {
return Task::ready(None);
};
- let is_package_json = file.path.ends_with("package.json");
- let is_composer_json = file.path.ends_with("composer.json");
+ let is_package_json = file.path.ends_with(RelPath::new("package.json").unwrap());
+ let is_composer_json = file.path.ends_with(RelPath::new("composer.json").unwrap());
if !is_package_json && !is_composer_json {
return Task::ready(None);
}
@@ -24,6 +24,7 @@ use smol::lock::OnceCell;
use std::cmp::Ordering;
use std::env::consts;
use util::fs::{make_file_executable, remove_matching};
+use util::rel_path::RelPath;
use parking_lot::Mutex;
use std::str::FromStr;
@@ -52,9 +53,9 @@ impl ManifestProvider for PyprojectTomlManifestProvider {
depth,
delegate,
}: ManifestQuery,
- ) -> Option<Arc<Path>> {
+ ) -> Option<Arc<RelPath>> {
for path in path.ancestors().take(depth) {
- let p = path.join("pyproject.toml");
+ let p = path.join(RelPath::new("pyproject.toml").unwrap());
if delegate.exists(&p, Some(false)) {
return Some(path.into());
}
@@ -679,7 +680,7 @@ impl ContextProvider for PythonContextProvider {
.as_ref()
.and_then(|f| f.path().parent())
.map(Arc::from)
- .unwrap_or_else(|| Arc::from("".as_ref()));
+ .unwrap_or_else(|| RelPath::empty().into());
toolchains
.active_toolchain(worktree_id, file_path, "Python".into(), cx)
@@ -1012,7 +1013,7 @@ impl ToolchainLister for PythonToolchainProvider {
async fn list(
&self,
worktree_root: PathBuf,
- subroot_relative_path: Arc<Path>,
+ subroot_relative_path: Arc<RelPath>,
project_env: Option<HashMap<String, String>>,
) -> ToolchainList {
let env = project_env.unwrap_or_default();
@@ -1024,7 +1025,6 @@ impl ToolchainLister for PythonToolchainProvider {
);
let mut config = Configuration::default();
- debug_assert!(subroot_relative_path.is_relative());
// `.ancestors()` will yield at least one path, so in case of empty `subroot_relative_path`, we'll just use
// worktree root as the workspace directory.
config.workspace_directories = Some(
@@ -23,6 +23,7 @@ use std::{
use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
use util::fs::{make_file_executable, remove_matching};
use util::merge_json_value_into;
+use util::rel_path::RelPath;
use util::{ResultExt, maybe};
use crate::github_download::{GithubBinaryMetadata, download_server_binary};
@@ -88,10 +89,10 @@ impl ManifestProvider for CargoManifestProvider {
depth,
delegate,
}: ManifestQuery,
- ) -> Option<Arc<Path>> {
+ ) -> Option<Arc<RelPath>> {
let mut outermost_cargo_toml = None;
for path in path.ancestors().take(depth) {
- let p = path.join("Cargo.toml");
+ let p = path.join(RelPath::new("Cargo.toml").unwrap());
if delegate.exists(&p, Some(false)) {
outermost_cargo_toml = Some(Arc::from(path));
}
@@ -22,8 +22,8 @@ use std::{
sync::{Arc, LazyLock},
};
use task::{TaskTemplate, TaskTemplates, VariableName};
-use util::merge_json_value_into;
use util::{ResultExt, fs::remove_matching, maybe};
+use util::{merge_json_value_into, rel_path::RelPath};
use crate::{PackageJson, PackageJsonData, github_download::download_server_binary};
@@ -264,7 +264,7 @@ impl TypeScriptContextProvider {
&self,
fs: Arc<dyn Fs>,
worktree_root: &Path,
- file_relative_path: &Path,
+ file_relative_path: &RelPath,
cx: &App,
) -> Task<anyhow::Result<PackageJsonData>> {
let new_json_data = file_relative_path
@@ -533,7 +533,7 @@ impl TypeScriptLspAdapter {
}
async fn tsdk_path(&self, adapter: &Arc<dyn LspAdapterDelegate>) -> Option<&'static str> {
let is_yarn = adapter
- .read_text_file(PathBuf::from(".yarn/sdks/typescript/lib/typescript.js"))
+ .read_text_file(RelPath::new(".yarn/sdks/typescript/lib/typescript.js").unwrap())
.await
.is_ok();
@@ -1014,7 +1014,7 @@ mod tests {
use serde_json::json;
use task::TaskTemplates;
use unindent::Unindent;
- use util::path;
+ use util::{path, rel_path::rel_path};
use crate::typescript::{
PackageJsonData, TypeScriptContextProvider, replace_test_name_parameters,
@@ -1164,7 +1164,7 @@ mod tests {
provider.combined_package_json_data(
fs.clone(),
path!("/root").as_ref(),
- "sub/file1.js".as_ref(),
+ rel_path("sub/file1.js"),
cx,
)
})
@@ -12,7 +12,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
-use util::{ResultExt, maybe, merge_json_value_into};
+use util::{ResultExt, maybe, merge_json_value_into, rel_path::RelPath};
fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
vec![server_path.into(), "--stdio".into()]
@@ -36,7 +36,7 @@ impl VtslsLspAdapter {
async fn tsdk_path(&self, adapter: &Arc<dyn LspAdapterDelegate>) -> Option<&'static str> {
let is_yarn = adapter
- .read_text_file(PathBuf::from(".yarn/sdks/typescript/lib/typescript.js"))
+ .read_text_file(RelPath::new(".yarn/sdks/typescript/lib/typescript.js").unwrap())
.await
.is_ok();
@@ -16,7 +16,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
-use util::{ResultExt, maybe, merge_json_value_into};
+use util::{ResultExt, maybe, merge_json_value_into, rel_path::RelPath};
const SERVER_PATH: &str = "node_modules/yaml-language-server/bin/yaml-language-server";
@@ -141,7 +141,7 @@ impl LspAdapter for YamlLspAdapter {
) -> Result<Value> {
let location = SettingsLocation {
worktree_id: delegate.worktree_id(),
- path: delegate.worktree_root_path(),
+ path: RelPath::empty(),
};
let tab_size = cx.update(|cx| {
@@ -334,7 +334,10 @@ impl Markdown {
}
for path in paths {
- if let Ok(language) = registry.language_for_file_path(&path).await {
+ if let Ok(language) = registry
+ .language_for_file_path(Path::new(path.as_ref()))
+ .await
+ {
languages_by_path.insert(path, language);
}
}
@@ -434,7 +437,7 @@ pub struct ParsedMarkdown {
pub source: SharedString,
pub events: Arc<[(Range<usize>, MarkdownEvent)]>,
pub languages_by_name: TreeMap<SharedString, Arc<Language>>,
- pub languages_by_path: TreeMap<Arc<Path>, Arc<Language>>,
+ pub languages_by_path: TreeMap<Arc<str>, Arc<Language>>,
}
impl ParsedMarkdown {
@@ -4,7 +4,7 @@ pub use pulldown_cmark::TagEnd as MarkdownTagEnd;
use pulldown_cmark::{
Alignment, CowStr, HeadingLevel, LinkType, MetadataBlockKind, Options, Parser,
};
-use std::{ops::Range, path::Path, sync::Arc};
+use std::{ops::Range, sync::Arc};
use collections::HashSet;
@@ -25,7 +25,7 @@ pub fn parse_markdown(
) -> (
Vec<(Range<usize>, MarkdownEvent)>,
HashSet<SharedString>,
- HashSet<Arc<Path>>,
+ HashSet<Arc<str>>,
) {
let mut events = Vec::new();
let mut language_names = HashSet::default();
@@ -1,8 +1,8 @@
-use std::{ops::Range, path::Path, sync::Arc};
+use std::{ops::Range, sync::Arc};
#[derive(Debug, Clone, PartialEq)]
pub struct PathWithRange {
- pub path: Arc<Path>,
+ pub path: Arc<str>,
pub range: Option<Range<LineCol>>,
}
@@ -78,12 +78,12 @@ impl PathWithRange {
};
Self {
- path: Path::new(path).into(),
+ path: path.into(),
range,
}
}
None => Self {
- path: Path::new(str).into(),
+ path: str.into(),
range: None,
},
}
@@ -123,7 +123,7 @@ mod tests {
#[test]
fn test_pathrange_parsing() {
let path_range = PathWithRange::new("file.rs#L10-L20");
- assert_eq!(path_range.path.as_ref(), Path::new("file.rs"));
+ assert_eq!(path_range.path.as_ref(), "file.rs");
assert!(path_range.range.is_some());
if let Some(range) = path_range.range {
assert_eq!(range.start.line, 10);
@@ -133,7 +133,7 @@ mod tests {
}
let single_line = PathWithRange::new("file.rs#L15");
- assert_eq!(single_line.path.as_ref(), Path::new("file.rs"));
+ assert_eq!(single_line.path.as_ref(), "file.rs");
assert!(single_line.range.is_some());
if let Some(range) = single_line.range {
assert_eq!(range.start.line, 15);
@@ -141,11 +141,11 @@ mod tests {
}
let no_range = PathWithRange::new("file.rs");
- assert_eq!(no_range.path.as_ref(), Path::new("file.rs"));
+ assert_eq!(no_range.path.as_ref(), "file.rs");
assert!(no_range.range.is_none());
let lowercase = PathWithRange::new("file.rs#l5-l10");
- assert_eq!(lowercase.path.as_ref(), Path::new("file.rs"));
+ assert_eq!(lowercase.path.as_ref(), "file.rs");
assert!(lowercase.range.is_some());
if let Some(range) = lowercase.range {
assert_eq!(range.start.line, 5);
@@ -153,7 +153,7 @@ mod tests {
}
let complex = PathWithRange::new("src/path/to/file.rs#L100");
- assert_eq!(complex.path.as_ref(), Path::new("src/path/to/file.rs"));
+ assert_eq!(complex.path.as_ref(), "src/path/to/file.rs");
assert!(complex.range.is_some());
}
@@ -161,7 +161,7 @@ mod tests {
fn test_pathrange_from_str() {
let with_range = PathWithRange::new("file.rs#L10-L20");
assert!(with_range.range.is_some());
- assert_eq!(with_range.path.as_ref(), Path::new("file.rs"));
+ assert_eq!(with_range.path.as_ref(), "file.rs");
let without_range = PathWithRange::new("file.rs");
assert!(without_range.range.is_none());
@@ -173,18 +173,18 @@ mod tests {
#[test]
fn test_pathrange_leading_text_trimming() {
let with_language = PathWithRange::new("```rust file.rs#L10");
- assert_eq!(with_language.path.as_ref(), Path::new("file.rs"));
+ assert_eq!(with_language.path.as_ref(), "file.rs");
assert!(with_language.range.is_some());
if let Some(range) = with_language.range {
assert_eq!(range.start.line, 10);
}
let with_spaces = PathWithRange::new("``` file.rs#L10-L20");
- assert_eq!(with_spaces.path.as_ref(), Path::new("file.rs"));
+ assert_eq!(with_spaces.path.as_ref(), "file.rs");
assert!(with_spaces.range.is_some());
let with_words = PathWithRange::new("```rust code example file.rs#L15:10");
- assert_eq!(with_words.path.as_ref(), Path::new("file.rs"));
+ assert_eq!(with_words.path.as_ref(), "file.rs");
assert!(with_words.range.is_some());
if let Some(range) = with_words.range {
assert_eq!(range.start.line, 15);
@@ -192,18 +192,18 @@ mod tests {
}
let with_whitespace = PathWithRange::new(" file.rs#L5");
- assert_eq!(with_whitespace.path.as_ref(), Path::new("file.rs"));
+ assert_eq!(with_whitespace.path.as_ref(), "file.rs");
assert!(with_whitespace.range.is_some());
let no_leading = PathWithRange::new("file.rs#L10");
- assert_eq!(no_leading.path.as_ref(), Path::new("file.rs"));
+ assert_eq!(no_leading.path.as_ref(), "file.rs");
assert!(no_leading.range.is_some());
}
#[test]
fn test_pathrange_with_line_and_column() {
let line_and_col = PathWithRange::new("file.rs#L10:5");
- assert_eq!(line_and_col.path.as_ref(), Path::new("file.rs"));
+ assert_eq!(line_and_col.path.as_ref(), "file.rs");
assert!(line_and_col.range.is_some());
if let Some(range) = line_and_col.range {
assert_eq!(range.start.line, 10);
@@ -213,7 +213,7 @@ mod tests {
}
let full_range = PathWithRange::new("file.rs#L10:5-L20:15");
- assert_eq!(full_range.path.as_ref(), Path::new("file.rs"));
+ assert_eq!(full_range.path.as_ref(), "file.rs");
assert!(full_range.range.is_some());
if let Some(range) = full_range.range {
assert_eq!(range.start.line, 10);
@@ -223,7 +223,7 @@ mod tests {
}
let mixed_range1 = PathWithRange::new("file.rs#L10:5-L20");
- assert_eq!(mixed_range1.path.as_ref(), Path::new("file.rs"));
+ assert_eq!(mixed_range1.path.as_ref(), "file.rs");
assert!(mixed_range1.range.is_some());
if let Some(range) = mixed_range1.range {
assert_eq!(range.start.line, 10);
@@ -233,7 +233,7 @@ mod tests {
}
let mixed_range2 = PathWithRange::new("file.rs#L10-L20:15");
- assert_eq!(mixed_range2.path.as_ref(), Path::new("file.rs"));
+ assert_eq!(mixed_range2.path.as_ref(), "file.rs");
assert!(mixed_range2.range.is_some());
if let Some(range) = mixed_range2.range {
assert_eq!(range.start.line, 10);
@@ -37,7 +37,6 @@ use std::{
iter::{self, FromIterator},
mem,
ops::{Range, RangeBounds, Sub},
- path::{Path, PathBuf},
rc::Rc,
str,
sync::Arc,
@@ -169,23 +168,23 @@ impl MultiBufferDiffHunk {
#[derive(PartialEq, Eq, Ord, PartialOrd, Clone, Hash, Debug)]
pub struct PathKey {
namespace: u32,
- path: Arc<Path>,
+ path: Arc<str>,
}
impl PathKey {
- pub fn namespaced(namespace: u32, path: Arc<Path>) -> Self {
+ pub fn namespaced(namespace: u32, path: Arc<str>) -> Self {
Self { namespace, path }
}
pub fn for_buffer(buffer: &Entity<Buffer>, cx: &App) -> Self {
if let Some(file) = buffer.read(cx).file() {
- Self::namespaced(1, Arc::from(file.full_path(cx)))
+ Self::namespaced(1, file.full_path(cx).to_string_lossy().to_string().into())
} else {
- Self::namespaced(0, Arc::from(PathBuf::from(buffer.entity_id().to_string())))
+ Self::namespaced(0, buffer.entity_id().to_string().into())
}
}
- pub fn path(&self) -> &Arc<Path> {
+ pub fn path(&self) -> &Arc<str> {
&self.path
}
}
@@ -2603,7 +2602,7 @@ impl MultiBuffer {
let buffer = buffer.read(cx);
if let Some(file) = buffer.file() {
- return file.file_name(cx).to_string_lossy();
+ return file.file_name(cx).into();
}
if let Some(title) = self.buffer_content_title(buffer) {
@@ -1524,7 +1524,7 @@ fn test_set_excerpts_for_buffer_ordering(cx: &mut TestAppContext) {
cx,
)
});
- let path1: PathKey = PathKey::namespaced(0, Path::new("/").into());
+ let path1: PathKey = PathKey::namespaced(0, "/".into());
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
multibuffer.update(cx, |multibuffer, cx| {
@@ -1619,7 +1619,7 @@ fn test_set_excerpts_for_buffer(cx: &mut TestAppContext) {
cx,
)
});
- let path1: PathKey = PathKey::namespaced(0, Path::new("/").into());
+ let path1: PathKey = PathKey::namespaced(0, "/".into());
let buf2 = cx.new(|cx| {
Buffer::local(
indoc! {
@@ -1638,7 +1638,7 @@ fn test_set_excerpts_for_buffer(cx: &mut TestAppContext) {
cx,
)
});
- let path2 = PathKey::namespaced(1, Path::new("/").into());
+ let path2 = PathKey::namespaced(1, "/".into());
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
multibuffer.update(cx, |multibuffer, cx| {
@@ -1815,7 +1815,7 @@ fn test_set_excerpts_for_buffer_rename(cx: &mut TestAppContext) {
cx,
)
});
- let path: PathKey = PathKey::namespaced(0, Path::new("/").into());
+ let path: PathKey = PathKey::namespaced(0, "/".into());
let buf2 = cx.new(|cx| {
Buffer::local(
indoc! {
@@ -389,7 +389,7 @@ mod tests {
use language::{Language, LanguageConfig, LanguageMatcher};
use project::{FakeFs, Project};
use serde_json::json;
- use util::path;
+ use util::{path, rel_path::rel_path};
use workspace::{AppState, Workspace};
#[gpui::test]
@@ -430,7 +430,7 @@ mod tests {
.unwrap();
let editor = workspace
.update_in(cx, |workspace, window, cx| {
- workspace.open_path((worktree_id, "a.rs"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("a.rs")), None, true, window, cx)
})
.await
.unwrap()
@@ -29,7 +29,7 @@ use std::{
collections::BTreeMap,
hash::Hash,
ops::Range,
- path::{MAIN_SEPARATOR_STR, Path, PathBuf},
+ path::{Path, PathBuf},
sync::{
Arc, OnceLock,
atomic::{self, AtomicBool},
@@ -51,7 +51,7 @@ use ui::{
IndentGuideLayout, Label, LabelCommon, ListItem, ScrollAxes, Scrollbars, StyledExt,
StyledTypography, Toggleable, Tooltip, WithScrollbar, h_flex, v_flex,
};
-use util::{RangeExt, ResultExt, TryFutureExt, debug_panic};
+use util::{RangeExt, ResultExt, TryFutureExt, debug_panic, rel_path::RelPath};
use workspace::{
OpenInTerminal, WeakItemHandle, Workspace,
dock::{DockPosition, Panel, PanelEvent},
@@ -107,7 +107,7 @@ pub struct OutlinePanel {
pending_serialization: Task<Option<()>>,
fs_entries_depth: HashMap<(WorktreeId, ProjectEntryId), usize>,
fs_entries: Vec<FsEntry>,
- fs_children_count: HashMap<WorktreeId, HashMap<Arc<Path>, FsChildren>>,
+ fs_children_count: HashMap<WorktreeId, HashMap<Arc<RelPath>, FsChildren>>,
collapsed_entries: HashSet<CollapsedEntry>,
unfolded_dirs: HashMap<WorktreeId, BTreeSet<ProjectEntryId>>,
selected_entry: SelectedEntry,
@@ -1905,6 +1905,7 @@ impl OutlinePanel {
_: &mut Window,
cx: &mut Context<Self>,
) {
+ let path_style = self.project.read(cx).path_style(cx);
if let Some(clipboard_text) = self
.selected_entry()
.and_then(|entry| match entry {
@@ -1914,7 +1915,7 @@ impl OutlinePanel {
}
PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
})
- .map(|p| p.to_string_lossy().to_string())
+ .map(|p| p.display(path_style).to_string())
{
cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
}
@@ -2272,7 +2273,7 @@ impl OutlinePanel {
let color =
entry_git_aware_label_color(entry.git_summary, entry.is_ignored, is_active);
let icon = if settings.file_icons {
- FileIcons::get_icon(&entry.path, cx)
+ FileIcons::get_icon(entry.path.as_std_path(), cx)
.map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
} else {
None
@@ -2303,7 +2304,7 @@ impl OutlinePanel {
is_active,
);
let icon = if settings.folder_icons {
- FileIcons::get_folder_icon(is_expanded, &directory.entry.path, cx)
+ FileIcons::get_folder_icon(is_expanded, directory.entry.path.as_std_path(), cx)
} else {
FileIcons::get_chevron_icon(is_expanded, cx)
}
@@ -2329,13 +2330,13 @@ impl OutlinePanel {
Some(file) => {
let path = file.path();
let icon = if settings.file_icons {
- FileIcons::get_icon(path.as_ref(), cx)
+ FileIcons::get_icon(path.as_std_path(), cx)
} else {
None
}
.map(Icon::from_path)
.map(|icon| icon.color(color).into_any_element());
- (icon, file_name(path.as_ref()))
+ (icon, file_name(path.as_std_path()))
}
None => (None, "Untitled".to_string()),
},
@@ -2615,19 +2616,17 @@ impl OutlinePanel {
if root_entry.id == entry.id {
file_name(worktree.abs_path().as_ref())
} else {
- let path = worktree.absolutize(entry.path.as_ref()).ok();
- let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref());
- file_name(path)
+ let path = worktree.absolutize(entry.path.as_ref());
+ file_name(&path)
}
}
None => {
- let path = worktree.absolutize(entry.path.as_ref()).ok();
- let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref());
- file_name(path)
+ let path = worktree.absolutize(entry.path.as_ref());
+ file_name(&path)
}
}
}
- None => file_name(entry.path.as_ref()),
+ None => file_name(entry.path.as_std_path()),
}
}
@@ -2842,7 +2841,7 @@ impl OutlinePanel {
}
let mut new_children_count =
- HashMap::<WorktreeId, HashMap<Arc<Path>, FsChildren>>::default();
+ HashMap::<WorktreeId, HashMap<Arc<RelPath>, FsChildren>>::default();
let worktree_entries = new_worktree_entries
.into_iter()
@@ -3518,17 +3517,17 @@ impl OutlinePanel {
.buffer_snapshot_for_id(*buffer_id, cx)
.and_then(|buffer_snapshot| {
let file = File::from_dyn(buffer_snapshot.file())?;
- file.worktree.read(cx).absolutize(&file.path).ok()
+ Some(file.worktree.read(cx).absolutize(&file.path))
}),
PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
worktree_id, entry, ..
- })) => self
- .project
- .read(cx)
- .worktree_for_id(*worktree_id, cx)?
- .read(cx)
- .absolutize(&entry.path)
- .ok(),
+ })) => Some(
+ self.project
+ .read(cx)
+ .worktree_for_id(*worktree_id, cx)?
+ .read(cx)
+ .absolutize(&entry.path),
+ ),
PanelEntry::FoldedDirs(FoldedDirsEntry {
worktree_id,
entries: dirs,
@@ -3537,13 +3536,13 @@ impl OutlinePanel {
self.project
.read(cx)
.worktree_for_id(*worktree_id, cx)
- .and_then(|worktree| worktree.read(cx).absolutize(&entry.path).ok())
+ .map(|worktree| worktree.read(cx).absolutize(&entry.path))
}),
PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
}
}
- fn relative_path(&self, entry: &FsEntry, cx: &App) -> Option<Arc<Path>> {
+ fn relative_path(&self, entry: &FsEntry, cx: &App) -> Option<Arc<RelPath>> {
match entry {
FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => {
let buffer_snapshot = self.buffer_snapshot_for_id(*buffer_id, cx)?;
@@ -3627,7 +3626,7 @@ impl OutlinePanel {
#[derive(Debug)]
struct ParentStats {
- path: Arc<Path>,
+ path: Arc<RelPath>,
folded: bool,
expanded: bool,
depth: usize,
@@ -4023,8 +4022,9 @@ impl OutlinePanel {
let id = state.entries.len();
match &entry {
PanelEntry::Fs(fs_entry) => {
- if let Some(file_name) =
- self.relative_path(fs_entry, cx).as_deref().map(file_name)
+ if let Some(file_name) = self
+ .relative_path(fs_entry, cx)
+ .and_then(|path| Some(path.file_name()?.to_string()))
{
state
.match_candidates
@@ -4477,21 +4477,19 @@ impl OutlinePanel {
let item_text_chars = match entry {
PanelEntry::Fs(FsEntry::ExternalFile(external)) => self
.buffer_snapshot_for_id(external.buffer_id, cx)
- .and_then(|snapshot| {
- Some(snapshot.file()?.path().file_name()?.to_string_lossy().len())
- })
+ .and_then(|snapshot| Some(snapshot.file()?.path().file_name()?.len()))
.unwrap_or_default(),
PanelEntry::Fs(FsEntry::Directory(directory)) => directory
.entry
.path
.file_name()
- .map(|name| name.to_string_lossy().len())
+ .map(|name| name.len())
.unwrap_or_default(),
PanelEntry::Fs(FsEntry::File(file)) => file
.entry
.path
.file_name()
- .map(|name| name.to_string_lossy().len())
+ .map(|name| name.len())
.unwrap_or_default(),
PanelEntry::FoldedDirs(folded_dirs) => {
folded_dirs
@@ -4500,11 +4498,11 @@ impl OutlinePanel {
.map(|dir| {
dir.path
.file_name()
- .map(|name| name.to_string_lossy().len())
+ .map(|name| name.len())
.unwrap_or_default()
})
.sum::<usize>()
- + folded_dirs.entries.len().saturating_sub(1) * MAIN_SEPARATOR_STR.len()
+ + folded_dirs.entries.len().saturating_sub(1) * "/".len()
}
PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => self
.excerpt_label(excerpt.buffer_id, &excerpt.range, cx)
@@ -4799,7 +4797,7 @@ fn workspace_active_editor(
}
fn back_to_common_visited_parent(
- visited_dirs: &mut Vec<(ProjectEntryId, Arc<Path>)>,
+ visited_dirs: &mut Vec<(ProjectEntryId, Arc<RelPath>)>,
worktree_id: &WorktreeId,
new_entry: &Entry,
) -> Option<(WorktreeId, ProjectEntryId)> {
@@ -5281,16 +5279,15 @@ mod tests {
});
});
- let all_matches = format!(
- r#"{root}/
+ let all_matches = r#"rust-analyzer/
crates/
ide/src/
inlay_hints/
fn_lifetime_fn.rs
- search: match config.param_names_for_lifetime_elision_hints {{
- search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {{
- search: Some(it) if config.param_names_for_lifetime_elision_hints => {{
- search: InlayHintsConfig {{ param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }},
+ search: match config.param_names_for_lifetime_elision_hints {
+ search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
+ search: Some(it) if config.param_names_for_lifetime_elision_hints => {
+ search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
inlay_hints.rs
search: pub param_names_for_lifetime_elision_hints: bool,
search: param_names_for_lifetime_elision_hints: self
@@ -5302,7 +5299,7 @@ mod tests {
search: param_names_for_lifetime_elision_hints: true,
config.rs
search: param_names_for_lifetime_elision_hints: self"#
- );
+ .to_string();
let select_first_in_all_matches = |line_to_select: &str| {
assert!(all_matches.contains(line_to_select));
@@ -5360,7 +5357,7 @@ mod tests {
cx,
),
format!(
- r#"{root}/
+ r#"rust-analyzer/
crates/
ide/src/
inlay_hints/
@@ -5430,7 +5427,7 @@ mod tests {
cx,
),
format!(
- r#"{root}/
+ r#"rust-analyzer/
crates/
ide/src/{SELECTED_MARKER}
rust-analyzer/src/
@@ -5513,16 +5510,15 @@ mod tests {
);
});
});
- let all_matches = format!(
- r#"{root}/
+ let all_matches = r#"rust-analyzer/
crates/
ide/src/
inlay_hints/
fn_lifetime_fn.rs
- search: match config.param_names_for_lifetime_elision_hints {{
- search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {{
- search: Some(it) if config.param_names_for_lifetime_elision_hints => {{
- search: InlayHintsConfig {{ param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }},
+ search: match config.param_names_for_lifetime_elision_hints {
+ search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
+ search: Some(it) if config.param_names_for_lifetime_elision_hints => {
+ search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
inlay_hints.rs
search: pub param_names_for_lifetime_elision_hints: bool,
search: param_names_for_lifetime_elision_hints: self
@@ -5534,7 +5530,7 @@ mod tests {
search: param_names_for_lifetime_elision_hints: true,
config.rs
search: param_names_for_lifetime_elision_hints: self"#
- );
+ .to_string();
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
@@ -5653,16 +5649,15 @@ mod tests {
);
});
});
- let all_matches = format!(
- r#"{root}/
+ let all_matches = r#"rust-analyzer/
crates/
ide/src/
inlay_hints/
fn_lifetime_fn.rs
- search: match config.param_names_for_lifetime_elision_hints {{
- search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {{
- search: Some(it) if config.param_names_for_lifetime_elision_hints => {{
- search: InlayHintsConfig {{ param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }},
+ search: match config.param_names_for_lifetime_elision_hints {
+ search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
+ search: Some(it) if config.param_names_for_lifetime_elision_hints => {
+ search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
inlay_hints.rs
search: pub param_names_for_lifetime_elision_hints: bool,
search: param_names_for_lifetime_elision_hints: self
@@ -5674,7 +5669,7 @@ mod tests {
search: param_names_for_lifetime_elision_hints: true,
config.rs
search: param_names_for_lifetime_elision_hints: self"#
- );
+ .to_string();
let select_first_in_all_matches = |line_to_select: &str| {
assert!(all_matches.contains(line_to_select));
all_matches.replacen(
@@ -5904,15 +5899,13 @@ mod tests {
cx,
),
format!(
- r#"{}/
+ r#"one/
a.txt
search: aaa aaa <==== selected
search: aaa aaa
-{}/
+two/
b.txt
search: a aaa"#,
- path!("/root/one"),
- path!("/root/two"),
),
);
});
@@ -5934,13 +5927,11 @@ mod tests {
cx,
),
format!(
- r#"{}/
+ r#"one/
a.txt <==== selected
-{}/
+two/
b.txt
search: a aaa"#,
- path!("/root/one"),
- path!("/root/two"),
),
);
});
@@ -5962,11 +5953,9 @@ mod tests {
cx,
),
format!(
- r#"{}/
+ r#"one/
a.txt
-{}/ <==== selected"#,
- path!("/root/one"),
- path!("/root/two"),
+two/ <==== selected"#,
),
);
});
@@ -5987,13 +5976,11 @@ mod tests {
cx,
),
format!(
- r#"{}/
+ r#"one/
a.txt
-{}/ <==== selected
+two/ <==== selected
b.txt
search: a aaa"#,
- path!("/root/one"),
- path!("/root/two"),
)
);
});
@@ -6455,7 +6442,7 @@ outline: struct OutlineEntryExcerpt
cx,
),
format!(
- r#"{root}/
+ r#"frontend-project/
public/lottie/
syntax-tree.json
search: {{ "something": "static" }} <==== selected
@@ -6494,7 +6481,7 @@ outline: struct OutlineEntryExcerpt
cx,
),
format!(
- r#"{root}/
+ r#"frontend-project/
public/lottie/
syntax-tree.json
search: {{ "something": "static" }}
@@ -6524,7 +6511,7 @@ outline: struct OutlineEntryExcerpt
cx,
),
format!(
- r#"{root}/
+ r#"frontend-project/
public/lottie/
syntax-tree.json
search: {{ "something": "static" }}
@@ -6558,7 +6545,7 @@ outline: struct OutlineEntryExcerpt
cx,
),
format!(
- r#"{root}/
+ r#"frontend-project/
public/lottie/
syntax-tree.json
search: {{ "something": "static" }}
@@ -6591,7 +6578,7 @@ outline: struct OutlineEntryExcerpt
cx,
),
format!(
- r#"{root}/
+ r#"frontend-project/
public/lottie/
syntax-tree.json
search: {{ "something": "static" }}
@@ -6649,6 +6636,7 @@ outline: struct OutlineEntryExcerpt
selected_entry: Option<&PanelEntry>,
cx: &mut App,
) -> String {
+ let project = project.read(cx);
let mut display_string = String::new();
for entry in cached_entries {
if !display_string.is_empty() {
@@ -6663,44 +6651,39 @@ outline: struct OutlineEntryExcerpt
panic!("Did not cover external files with tests")
}
FsEntry::Directory(directory) => {
- match project
- .read(cx)
+ let path = if let Some(worktree) = project
.worktree_for_id(directory.worktree_id, cx)
- .and_then(|worktree| {
- if worktree.read(cx).root_entry() == Some(&directory.entry.entry) {
- Some(worktree.read(cx).abs_path())
- } else {
- None
- }
+ .filter(|worktree| {
+ worktree.read(cx).root_entry() == Some(&directory.entry.entry)
}) {
- Some(root_path) => format!(
- "{}/{}",
- root_path.display(),
- directory.entry.path.display(),
- ),
- None => format!(
- "{}/",
- directory
- .entry
- .path
- .file_name()
- .unwrap_or_default()
- .to_string_lossy()
- ),
- }
+ worktree
+ .read(cx)
+ .root_name()
+ .join(&directory.entry.path)
+ .as_str()
+ .to_string()
+ } else {
+ directory
+ .entry
+ .path
+ .file_name()
+ .unwrap_or_default()
+ .to_string()
+ };
+ format!("{path}/")
}
FsEntry::File(file) => file
.entry
.path
.file_name()
- .map(|name| name.to_string_lossy().to_string())
+ .map(|name| name.to_string())
.unwrap_or_default(),
},
PanelEntry::FoldedDirs(folded_dirs) => folded_dirs
.entries
.iter()
.filter_map(|dir| dir.path.file_name())
- .map(|name| name.to_string_lossy().to_string() + "/")
+ .map(|name| name.to_string() + "/")
.collect(),
PanelEntry::Outline(outline_entry) => match outline_entry {
OutlineEntry::Excerpt(_) => continue,
@@ -5,6 +5,7 @@ use std::path::{Path, PathBuf};
use std::sync::OnceLock;
pub use util::paths::home_dir;
+use util::rel_path::RelPath;
/// A default editorconfig file name to use when resolving project settings.
pub const EDITORCONFIG_NAME: &str = ".editorconfig";
@@ -29,13 +30,13 @@ static CURRENT_DATA_DIR: OnceLock<PathBuf> = OnceLock::new();
static CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
/// Returns the relative path to the zed_server directory on the ssh host.
-pub fn remote_server_dir_relative() -> &'static Path {
- Path::new(".zed_server")
+pub fn remote_server_dir_relative() -> &'static RelPath {
+ RelPath::new(".zed_server").unwrap()
}
/// Returns the relative path to the zed_wsl_server directory on the wsl host.
-pub fn remote_wsl_server_dir_relative() -> &'static Path {
- Path::new(".zed_wsl_server")
+pub fn remote_wsl_server_dir_relative() -> &'static RelPath {
+ RelPath::new(".zed_wsl_server").unwrap()
}
/// Sets a custom directory for all user data, overriding the default data directory.
@@ -398,28 +399,28 @@ pub fn remote_servers_dir() -> &'static PathBuf {
}
/// Returns the relative path to a `.zed` folder within a project.
-pub fn local_settings_folder_relative_path() -> &'static Path {
- Path::new(".zed")
+pub fn local_settings_folder_name() -> &'static str {
+ ".zed"
}
/// Returns the relative path to a `.vscode` folder within a project.
-pub fn local_vscode_folder_relative_path() -> &'static Path {
- Path::new(".vscode")
+pub fn local_vscode_folder_name() -> &'static str {
+ ".vscode"
}
/// Returns the relative path to a `settings.json` file within a project.
-pub fn local_settings_file_relative_path() -> &'static Path {
- Path::new(".zed/settings.json")
+pub fn local_settings_file_relative_path() -> &'static RelPath {
+ RelPath::new(".zed/settings.json").unwrap()
}
/// Returns the relative path to a `tasks.json` file within a project.
-pub fn local_tasks_file_relative_path() -> &'static Path {
- Path::new(".zed/tasks.json")
+pub fn local_tasks_file_relative_path() -> &'static RelPath {
+ RelPath::new(".zed/tasks.json").unwrap()
}
/// Returns the relative path to a `.vscode/tasks.json` file within a project.
-pub fn local_vscode_tasks_file_relative_path() -> &'static Path {
- Path::new(".vscode/tasks.json")
+pub fn local_vscode_tasks_file_relative_path() -> &'static RelPath {
+ RelPath::new(".vscode/tasks.json").unwrap()
}
pub fn debug_task_file_name() -> &'static str {
@@ -432,13 +433,13 @@ pub fn task_file_name() -> &'static str {
/// Returns the relative path to a `debug.json` file within a project.
/// .zed/debug.json
-pub fn local_debug_file_relative_path() -> &'static Path {
- Path::new(".zed/debug.json")
+pub fn local_debug_file_relative_path() -> &'static RelPath {
+ RelPath::new(".zed/debug.json").unwrap()
}
/// Returns the relative path to a `.vscode/launch.json` file within a project.
-pub fn local_vscode_launch_file_relative_path() -> &'static Path {
- Path::new(".vscode/launch.json")
+pub fn local_vscode_launch_file_relative_path() -> &'static RelPath {
+ RelPath::new(".vscode/launch.json").unwrap()
}
pub fn user_ssh_config_file() -> PathBuf {
@@ -12,7 +12,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
-use util::paths::PathMatcher;
+use util::paths::{PathMatcher, PathStyle};
#[derive(Debug, Clone)]
pub enum Prettier {
@@ -119,7 +119,7 @@ impl Prettier {
None
}
}).any(|workspace_definition| {
- workspace_definition == subproject_path.to_string_lossy() || PathMatcher::new(&[workspace_definition]).ok().is_some_and(|path_matcher| path_matcher.is_match(subproject_path))
+ workspace_definition == subproject_path.to_string_lossy() || PathMatcher::new(&[workspace_definition], PathStyle::local()).ok().is_some_and(|path_matcher| path_matcher.is_match(subproject_path))
}) {
anyhow::ensure!(has_prettier_in_node_modules(fs, &path_to_check).await?, "Path {path_to_check:?} is the workspace root for project in {closest_package_json_path:?}, but it has no prettier installed");
log::info!("Found prettier path {path_to_check:?} in the workspace root for project in {closest_package_json_path:?}");
@@ -215,11 +215,14 @@ impl Prettier {
})
.any(|workspace_definition| {
workspace_definition == subproject_path.to_string_lossy()
- || PathMatcher::new(&[workspace_definition])
- .ok()
- .is_some_and(|path_matcher| {
- path_matcher.is_match(subproject_path)
- })
+ || PathMatcher::new(
+ &[workspace_definition],
+ PathStyle::local(),
+ )
+ .ok()
+ .is_some_and(
+ |path_matcher| path_matcher.is_match(subproject_path),
+ )
})
{
let workspace_ignore = path_to_check.join(".prettierignore");
@@ -58,7 +58,6 @@ lsp.workspace = true
markdown.workspace = true
node_runtime.workspace = true
parking_lot.workspace = true
-pathdiff.workspace = true
paths.workspace = true
postage.workspace = true
prettier.workspace = true
@@ -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))
})
}
@@ -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)
})?
@@ -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)
})?
@@ -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
}
@@ -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,
@@ -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,
@@ -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)
})
@@ -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| {
@@ -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,
});
}
}
@@ -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())
})
@@ -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()))
@@ -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();
@@ -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,56 +5403,13 @@ 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(),
- }
- }
-}
-
-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;
- }
+ path: path.into(),
}
}
- 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
@@ -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()),
)
});
}
@@ -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()))));
}
}
}
@@ -13,7 +13,7 @@ use fs::FakeFs;
use futures::{StreamExt, future};
use git::{
GitHostingProviderRegistry,
- repository::RepoPath,
+ repository::{RepoPath, repo_path},
status::{StatusCode, TrackedStatus},
};
use git2::RepositoryInitOptions;
@@ -44,6 +44,7 @@ use unindent::Unindent as _;
use util::{
TryFutureExt as _, assert_set_eq, maybe, path,
paths::PathMatcher,
+ rel_path::rel_path,
test::{TempTree, marked_text_offsets},
uri,
};
@@ -122,8 +123,10 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) {
let tree = project.worktrees(cx).next().unwrap().read(cx);
assert_eq!(tree.file_count(), 5);
assert_eq!(
- tree.inode_for_path("fennel/grape"),
- tree.inode_for_path("finnochio/grape")
+ tree.entry_for_path(rel_path("fennel/grape")).unwrap().inode,
+ tree.entry_for_path(rel_path("finnochio/grape"))
+ .unwrap()
+ .inode
);
});
}
@@ -186,12 +189,12 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) {
cx.update(|cx| {
let tree = worktree.read(cx);
let settings_for = |path: &str| {
- let file_entry = tree.entry_for_path(path).unwrap().clone();
+ let file_entry = tree.entry_for_path(rel_path(path)).unwrap().clone();
let file = File::for_entry(file_entry, worktree.clone());
let file_language = project
.read(cx)
.languages()
- .language_for_file_path(file.path.as_ref());
+ .language_for_file_path(file.path.as_std_path());
let file_language = cx
.background_executor()
.block(file_language)
@@ -343,7 +346,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
let topmost_local_task_source_kind = TaskSourceKind::Worktree {
id: worktree_id,
- directory_in_worktree: PathBuf::from(".zed"),
+ directory_in_worktree: rel_path(".zed").into(),
id_base: "local worktree tasks from directory \".zed\"".into(),
};
@@ -352,12 +355,12 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
let tree = worktree.read(cx);
let file_a = File::for_entry(
- tree.entry_for_path("a/a.rs").unwrap().clone(),
+ tree.entry_for_path(rel_path("a/a.rs")).unwrap().clone(),
worktree.clone(),
) as _;
let settings_a = language_settings(None, Some(&file_a), cx);
let file_b = File::for_entry(
- tree.entry_for_path("b/b.rs").unwrap().clone(),
+ tree.entry_for_path(rel_path("b/b.rs")).unwrap().clone(),
worktree.clone(),
) as _;
let settings_b = language_settings(None, Some(&file_b), cx);
@@ -385,12 +388,8 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
(
TaskSourceKind::Worktree {
id: worktree_id,
- directory_in_worktree: PathBuf::from(path!("b/.zed")),
- id_base: if cfg!(windows) {
- "local worktree tasks from directory \"b\\\\.zed\"".into()
- } else {
- "local worktree tasks from directory \"b/.zed\"".into()
- },
+ directory_in_worktree: rel_path("b/.zed").into(),
+ id_base: "local worktree tasks from directory \"b/.zed\"".into()
},
"cargo check".to_string(),
vec!["check".to_string()],
@@ -470,12 +469,8 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
(
TaskSourceKind::Worktree {
id: worktree_id,
- directory_in_worktree: PathBuf::from(path!("b/.zed")),
- id_base: if cfg!(windows) {
- "local worktree tasks from directory \"b\\\\.zed\"".into()
- } else {
- "local worktree tasks from directory \"b/.zed\"".into()
- },
+ directory_in_worktree: rel_path("b/.zed").into(),
+ id_base: "local worktree tasks from directory \"b/.zed\"".into()
},
"cargo check".to_string(),
vec!["check".to_string()],
@@ -585,12 +580,8 @@ async fn test_fallback_to_single_worktree_tasks(cx: &mut gpui::TestAppContext) {
vec![(
TaskSourceKind::Worktree {
id: worktree_id,
- directory_in_worktree: PathBuf::from(path!(".zed")),
- id_base: if cfg!(windows) {
- "local worktree tasks from directory \".zed\"".into()
- } else {
- "local worktree tasks from directory \".zed\"".into()
- },
+ directory_in_worktree: rel_path(".zed").into(),
+ id_base: "local worktree tasks from directory \".zed\"".into(),
},
"echo /dir".to_string(),
)]
@@ -615,9 +606,9 @@ async fn test_running_multiple_instances_of_a_single_server_in_one_worktree(
depth,
delegate,
}: ManifestQuery,
- ) -> Option<Arc<Path>> {
+ ) -> Option<Arc<RelPath>> {
for path in path.ancestors().take(depth) {
- let p = path.join("pyproject.toml");
+ let p = path.join(RelPath::new("pyproject.toml").unwrap());
if delegate.exists(&p, Some(false)) {
return Some(path.into());
}
@@ -738,7 +729,7 @@ async fn test_running_multiple_instances_of_a_single_server_in_one_worktree(
this.available_toolchains(
ProjectPath {
worktree_id,
- path: Arc::from("project-b/source_file.py".as_ref()),
+ path: rel_path("project-b/source_file.py").into(),
},
LanguageName::new("Python"),
cx,
@@ -746,7 +737,7 @@ async fn test_running_multiple_instances_of_a_single_server_in_one_worktree(
})
.await
.expect("A toolchain to be discovered");
- assert_eq!(root_path.as_ref(), Path::new("project-b"));
+ assert_eq!(root_path.as_ref(), RelPath::new("project-b").unwrap());
assert_eq!(available_toolchains_for_b.toolchains().len(), 1);
let currently_active_toolchain = project
.update(cx, |this, cx| {
@@ -754,7 +745,7 @@ async fn test_running_multiple_instances_of_a_single_server_in_one_worktree(
this.active_toolchain(
ProjectPath {
worktree_id,
- path: Arc::from("project-b/source_file.py".as_ref()),
+ path: rel_path("project-b/source_file.py").into(),
},
LanguageName::new("Python"),
cx,
@@ -1294,16 +1285,16 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
.read(cx)
.snapshot()
.entries(true, 0)
- .map(|entry| (entry.path.as_ref(), entry.is_ignored))
+ .map(|entry| (entry.path.as_str(), entry.is_ignored))
.collect::<Vec<_>>(),
&[
- (Path::new(""), false),
- (Path::new(".gitignore"), false),
- (Path::new("Cargo.lock"), false),
- (Path::new("src"), false),
- (Path::new("src/a.rs"), false),
- (Path::new("src/b.rs"), false),
- (Path::new("target"), true),
+ ("", false),
+ (".gitignore", false),
+ ("Cargo.lock", false),
+ ("src", false),
+ ("src/a.rs", false),
+ ("src/b.rs", false),
+ ("target", true),
]
);
});
@@ -1412,21 +1403,21 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
.read(cx)
.snapshot()
.entries(true, 0)
- .map(|entry| (entry.path.as_ref(), entry.is_ignored))
+ .map(|entry| (entry.path.as_str(), entry.is_ignored))
.collect::<Vec<_>>(),
&[
- (Path::new(""), false),
- (Path::new(".gitignore"), false),
- (Path::new("Cargo.lock"), false),
- (Path::new("src"), false),
- (Path::new("src/a.rs"), false),
- (Path::new("src/b.rs"), false),
- (Path::new("target"), true),
- (Path::new("target/x"), true),
- (Path::new("target/y"), true),
- (Path::new("target/y/out"), true),
- (Path::new("target/y/out/y.rs"), true),
- (Path::new("target/z"), true),
+ ("", false),
+ (".gitignore", false),
+ ("Cargo.lock", false),
+ ("src", false),
+ ("src/a.rs", false),
+ ("src/b.rs", false),
+ ("target", true),
+ ("target/x", true),
+ ("target/y", true),
+ ("target/y/out", true),
+ ("target/y/out/y.rs", true),
+ ("target/z", true),
]
);
});
@@ -1694,7 +1685,7 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
let main_ignored_buffer = project
.update(cx, |project, cx| {
- project.open_buffer((main_worktree_id, "b.rs"), cx)
+ project.open_buffer((main_worktree_id, rel_path("b.rs")), cx)
})
.await
.unwrap();
@@ -1715,7 +1706,7 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
});
let other_buffer = project
.update(cx, |project, cx| {
- project.open_buffer((other_worktree_id, ""), cx)
+ project.open_buffer((other_worktree_id, rel_path("")), cx)
})
.await
.unwrap();
@@ -1742,7 +1733,7 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
vec![(
ProjectPath {
worktree_id: main_worktree_id,
- path: Arc::from(Path::new("b.rs")),
+ path: rel_path("b.rs").into(),
},
server_id,
DiagnosticSummary {
@@ -1832,7 +1823,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
events.next().await.unwrap(),
Event::DiagnosticsUpdated {
language_server_id: LanguageServerId(0),
- paths: vec![(worktree_id, Path::new("a.rs")).into()],
+ paths: vec![(worktree_id, rel_path("a.rs")).into()],
}
);
@@ -1880,7 +1871,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
events.next().await.unwrap(),
Event::DiagnosticsUpdated {
language_server_id: LanguageServerId(0),
- paths: vec![(worktree_id, Path::new("a.rs")).into()],
+ paths: vec![(worktree_id, rel_path("a.rs")).into()],
}
);
@@ -3808,6 +3799,114 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
});
}
+#[gpui::test]
+async fn test_rename_file_to_new_directory(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+ let fs = FakeFs::new(cx.background_executor.clone());
+ let expected_contents = "content";
+ fs.as_fake()
+ .insert_tree(
+ "/root",
+ json!({
+ "test.txt": expected_contents
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
+
+ let (worktree, entry_id) = project.read_with(cx, |project, cx| {
+ let worktree = project.worktrees(cx).next().unwrap();
+ let entry_id = worktree
+ .read(cx)
+ .entry_for_path(rel_path("test.txt"))
+ .unwrap()
+ .id;
+ (worktree, entry_id)
+ });
+ let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
+ let _result = project
+ .update(cx, |project, cx| {
+ project.rename_entry(
+ entry_id,
+ (worktree_id, rel_path("dir1/dir2/dir3/test.txt")).into(),
+ cx,
+ )
+ })
+ .await
+ .unwrap();
+ worktree.read_with(cx, |worktree, _| {
+ assert!(
+ worktree.entry_for_path(rel_path("test.txt")).is_none(),
+ "Old file should have been removed"
+ );
+ assert!(
+ worktree
+ .entry_for_path(rel_path("dir1/dir2/dir3/test.txt"))
+ .is_some(),
+ "Whole directory hierarchy and the new file should have been created"
+ );
+ });
+ assert_eq!(
+ worktree
+ .update(cx, |worktree, cx| {
+ worktree.load_file(rel_path("dir1/dir2/dir3/test.txt"), cx)
+ })
+ .await
+ .unwrap()
+ .text,
+ expected_contents,
+ "Moved file's contents should be preserved"
+ );
+
+ let entry_id = worktree.read_with(cx, |worktree, _| {
+ worktree
+ .entry_for_path(rel_path("dir1/dir2/dir3/test.txt"))
+ .unwrap()
+ .id
+ });
+
+ let _result = project
+ .update(cx, |project, cx| {
+ project.rename_entry(
+ entry_id,
+ (worktree_id, rel_path("dir1/dir2/test.txt")).into(),
+ cx,
+ )
+ })
+ .await
+ .unwrap();
+ worktree.read_with(cx, |worktree, _| {
+ assert!(
+ worktree.entry_for_path(rel_path("test.txt")).is_none(),
+ "First file should not reappear"
+ );
+ assert!(
+ worktree
+ .entry_for_path(rel_path("dir1/dir2/dir3/test.txt"))
+ .is_none(),
+ "Old file should have been removed"
+ );
+ assert!(
+ worktree
+ .entry_for_path(rel_path("dir1/dir2/test.txt"))
+ .is_some(),
+ "No error should have occurred after moving into existing directory"
+ );
+ });
+ assert_eq!(
+ worktree
+ .update(cx, |worktree, cx| {
+ worktree.load_file(rel_path("dir1/dir2/test.txt"), cx)
+ })
+ .await
+ .unwrap()
+ .text,
+ expected_contents,
+ "Moved file's contents should be preserved"
+ );
+}
+
#[gpui::test(iterations = 10)]
async fn test_save_file(cx: &mut gpui::TestAppContext) {
init_test(cx);
@@ -3895,7 +3994,7 @@ async fn test_save_file_spawns_language_server(cx: &mut gpui::TestAppContext) {
buffer.clone(),
ProjectPath {
worktree_id,
- path: Arc::from("file.rs".as_ref()),
+ path: rel_path("file.rs").into(),
},
cx,
)
@@ -4104,7 +4203,7 @@ async fn test_save_as(cx: &mut gpui::TestAppContext) {
let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
let path = ProjectPath {
worktree_id,
- path: Arc::from(Path::new("file1.rs")),
+ path: rel_path("file1.rs").into(),
};
project.save_buffer_as(buffer.clone(), path, cx)
})
@@ -4163,7 +4262,7 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
project.update(cx, |project, cx| {
let tree = project.worktrees(cx).next().unwrap();
tree.read(cx)
- .entry_for_path(path)
+ .entry_for_path(rel_path(path))
.unwrap_or_else(|| panic!("no entry for path {}", path))
.id
})
@@ -4191,8 +4290,16 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
});
});
- let remote =
- cx.update(|cx| Worktree::remote(0, 1, metadata, project.read(cx).client().into(), cx));
+ let remote = cx.update(|cx| {
+ Worktree::remote(
+ 0,
+ 1,
+ metadata,
+ project.read(cx).client().into(),
+ project.read(cx).path_style(cx),
+ cx,
+ )
+ });
cx.executor().run_until_parked();
@@ -4213,18 +4320,15 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
cx.update(|app| {
assert_eq!(
- tree.read(app)
- .paths()
- .map(|p| p.to_str().unwrap())
- .collect::<Vec<_>>(),
+ tree.read(app).paths().collect::<Vec<_>>(),
vec![
- "a",
- path!("a/file1"),
- path!("a/file2.new"),
- "b",
- "d",
- path!("d/file3"),
- path!("d/file4"),
+ rel_path("a"),
+ rel_path("a/file1"),
+ rel_path("a/file2.new"),
+ rel_path("b"),
+ rel_path("d"),
+ rel_path("d/file3"),
+ rel_path("d/file4"),
]
);
});
@@ -4236,19 +4340,19 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
cx.update(|cx| {
assert_eq!(
buffer2.read(cx).file().unwrap().path().as_ref(),
- Path::new("a/file2.new")
+ rel_path("a/file2.new")
);
assert_eq!(
buffer3.read(cx).file().unwrap().path().as_ref(),
- Path::new("d/file3")
+ rel_path("d/file3")
);
assert_eq!(
buffer4.read(cx).file().unwrap().path().as_ref(),
- Path::new("d/file4")
+ rel_path("d/file4")
);
assert_eq!(
buffer5.read(cx).file().unwrap().path().as_ref(),
- Path::new("b/c/file5")
+ rel_path("b/c/file5")
);
assert_matches!(
@@ -4281,18 +4385,15 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
cx.executor().run_until_parked();
remote.update(cx, |remote, _| {
assert_eq!(
- remote
- .paths()
- .map(|p| p.to_str().unwrap())
- .collect::<Vec<_>>(),
+ remote.paths().collect::<Vec<_>>(),
vec![
- "a",
- path!("a/file1"),
- path!("a/file2.new"),
- "b",
- "d",
- path!("d/file3"),
- path!("d/file4"),
+ rel_path("a"),
+ rel_path("a/file1"),
+ rel_path("a/file2.new"),
+ rel_path("b"),
+ rel_path("d"),
+ rel_path("d/file3"),
+ rel_path("d/file4"),
]
);
});
@@ -4321,7 +4422,7 @@ async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) {
project.update(cx, |project, cx| {
let tree = project.worktrees(cx).next().unwrap();
tree.read(cx)
- .entry_for_path(path)
+ .entry_for_path(rel_path(path))
.unwrap_or_else(|| panic!("no entry for path {}", path))
.id
})
@@ -4330,14 +4431,16 @@ async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) {
let dir_id = id_for_path("a", cx);
let file_id = id_for_path("a/file1", cx);
let buffer = project
- .update(cx, |p, cx| p.open_buffer((tree_id, "a/file1"), cx))
+ .update(cx, |p, cx| {
+ p.open_buffer((tree_id, rel_path("a/file1")), cx)
+ })
.await
.unwrap();
buffer.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
project
.update(cx, |project, cx| {
- project.rename_entry(dir_id, Path::new("b"), cx)
+ project.rename_entry(dir_id, (tree_id, rel_path("b")).into(), cx)
})
.unwrap()
.await
@@ -5051,8 +5154,15 @@ async fn test_lsp_rename_notifications(cx: &mut gpui::TestAppContext) {
let fake_server = fake_servers.next().await.unwrap();
let response = project.update(cx, |project, cx| {
let worktree = project.worktrees(cx).next().unwrap();
- let entry = worktree.read(cx).entry_for_path("one.rs").unwrap();
- project.rename_entry(entry.id, "three.rs".as_ref(), cx)
+ let entry = worktree
+ .read(cx)
+ .entry_for_path(rel_path("one.rs"))
+ .unwrap();
+ project.rename_entry(
+ entry.id,
+ (worktree.read(cx).id(), rel_path("three.rs")).into(),
+ cx,
+ )
});
let expected_edit = lsp::WorkspaceEdit {
changes: None,
@@ -5356,7 +5466,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
false,
true,
false,
- PathMatcher::new(&["*.odd".to_owned()]).unwrap(),
+ PathMatcher::new(&["*.odd".to_owned()], PathStyle::local()).unwrap(),
Default::default(),
false,
None
@@ -5378,7 +5488,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
false,
true,
false,
- PathMatcher::new(&["*.rs".to_owned()]).unwrap(),
+ PathMatcher::new(&["*.rs".to_owned()], PathStyle::local()).unwrap(),
Default::default(),
false,
None
@@ -5403,7 +5513,8 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
false,
true,
false,
- PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
+ PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()], PathStyle::local())
+ .unwrap(),
Default::default(),
false,
None,
@@ -5428,8 +5539,11 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
false,
true,
false,
- PathMatcher::new(&["*.rs".to_owned(), "*.ts".to_owned(), "*.odd".to_owned()])
- .unwrap(),
+ PathMatcher::new(
+ &["*.rs".to_owned(), "*.ts".to_owned(), "*.odd".to_owned()],
+ PathStyle::local()
+ )
+ .unwrap(),
Default::default(),
false,
None,
@@ -5477,7 +5591,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
true,
false,
Default::default(),
- PathMatcher::new(&["*.odd".to_owned()]).unwrap(),
+ PathMatcher::new(&["*.odd".to_owned()], PathStyle::local()).unwrap(),
false,
None,
)
@@ -5504,7 +5618,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
true,
false,
Default::default(),
- PathMatcher::new(&["*.rs".to_owned()]).unwrap(),
+ PathMatcher::new(&["*.rs".to_owned()], PathStyle::local()).unwrap(),
false,
None,
)
@@ -5529,7 +5643,8 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
true,
false,
Default::default(),
- PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
+ PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()], PathStyle::local())
+ .unwrap(),
false,
None,
)
@@ -5554,8 +5669,11 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
true,
false,
Default::default(),
- PathMatcher::new(&["*.rs".to_owned(), "*.ts".to_owned(), "*.odd".to_owned()])
- .unwrap(),
+ PathMatcher::new(
+ &["*.rs".to_owned(), "*.ts".to_owned(), "*.odd".to_owned()],
+ PathStyle::local(),
+ )
+ .unwrap(),
false,
None,
)
@@ -5588,6 +5706,7 @@ async fn test_search_with_buffer_exclusions(cx: &mut gpui::TestAppContext) {
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
+ let path_style = PathStyle::local();
let _buffer = project.update(cx, |project, cx| {
project.create_local_buffer("file", None, false, cx)
});
@@ -5601,7 +5720,7 @@ async fn test_search_with_buffer_exclusions(cx: &mut gpui::TestAppContext) {
true,
false,
Default::default(),
- PathMatcher::new(&["*.odd".to_owned()]).unwrap(),
+ PathMatcher::new(&["*.odd".to_owned()], path_style).unwrap(),
false,
None,
)
@@ -5628,7 +5747,7 @@ async fn test_search_with_buffer_exclusions(cx: &mut gpui::TestAppContext) {
true,
false,
Default::default(),
- PathMatcher::new(&["*.rs".to_owned()]).unwrap(),
+ PathMatcher::new(&["*.rs".to_owned()], path_style).unwrap(),
false,
None,
)
@@ -5653,7 +5772,7 @@ async fn test_search_with_buffer_exclusions(cx: &mut gpui::TestAppContext) {
true,
false,
Default::default(),
- PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
+ PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()], path_style).unwrap(),
false,
None,
)
@@ -5678,8 +5797,11 @@ async fn test_search_with_buffer_exclusions(cx: &mut gpui::TestAppContext) {
true,
false,
Default::default(),
- PathMatcher::new(&["*.rs".to_owned(), "*.ts".to_owned(), "*.odd".to_owned()])
- .unwrap(),
+ PathMatcher::new(
+ &["*.rs".to_owned(), "*.ts".to_owned(), "*.odd".to_owned()],
+ PathStyle::local(),
+ )
+ .unwrap(),
false,
None,
)
@@ -5711,7 +5833,6 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
)
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
-
assert!(
search(
&project,
@@ -5720,8 +5841,8 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
false,
true,
false,
- PathMatcher::new(&["*.odd".to_owned()]).unwrap(),
- PathMatcher::new(&["*.odd".to_owned()]).unwrap(),
+ PathMatcher::new(&["*.odd".to_owned()], PathStyle::local()).unwrap(),
+ PathMatcher::new(&["*.odd".to_owned()], PathStyle::local()).unwrap(),
false,
None,
)
@@ -5742,8 +5863,8 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
false,
true,
false,
- PathMatcher::new(&["*.ts".to_owned()]).unwrap(),
- PathMatcher::new(&["*.ts".to_owned()]).unwrap(),
+ PathMatcher::new(&["*.ts".to_owned()], PathStyle::local()).unwrap(),
+ PathMatcher::new(&["*.ts".to_owned()], PathStyle::local()).unwrap(),
false,
None,
)
@@ -5764,8 +5885,10 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
false,
true,
false,
- PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
- PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
+ PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()], PathStyle::local())
+ .unwrap(),
+ PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()], PathStyle::local())
+ .unwrap(),
false,
None,
)
@@ -5786,8 +5909,10 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
false,
true,
false,
- PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
- PathMatcher::new(&["*.rs".to_owned(), "*.odd".to_owned()]).unwrap(),
+ PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()], PathStyle::local())
+ .unwrap(),
+ PathMatcher::new(&["*.rs".to_owned(), "*.odd".to_owned()], PathStyle::local())
+ .unwrap(),
false,
None,
)
@@ -5826,6 +5951,7 @@ async fn test_search_multiple_worktrees_with_inclusions(cx: &mut gpui::TestAppCo
)
.await;
+ let path_style = PathStyle::local();
let project = Project::test(
fs.clone(),
[path!("/worktree-a").as_ref(), path!("/worktree-b").as_ref()],
@@ -5841,7 +5967,7 @@ async fn test_search_multiple_worktrees_with_inclusions(cx: &mut gpui::TestAppCo
false,
true,
false,
- PathMatcher::new(&["worktree-a/*.rs".to_owned()]).unwrap(),
+ PathMatcher::new(&["worktree-a/*.rs".to_owned()], path_style).unwrap(),
Default::default(),
true,
None,
@@ -5862,7 +5988,7 @@ async fn test_search_multiple_worktrees_with_inclusions(cx: &mut gpui::TestAppCo
false,
true,
false,
- PathMatcher::new(&["worktree-b/*.rs".to_owned()]).unwrap(),
+ PathMatcher::new(&["worktree-b/*.rs".to_owned()], path_style).unwrap(),
Default::default(),
true,
None,
@@ -5884,7 +6010,7 @@ async fn test_search_multiple_worktrees_with_inclusions(cx: &mut gpui::TestAppCo
false,
true,
false,
- PathMatcher::new(&["*.ts".to_owned()]).unwrap(),
+ PathMatcher::new(&["*.ts".to_owned()], path_style).unwrap(),
Default::default(),
false,
None,
@@ -5955,6 +6081,7 @@ async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) {
);
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
+ let path_style = PathStyle::local();
assert_eq!(
search(
&project,
@@ -5996,8 +6123,9 @@ async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) {
"Unrestricted search with ignored directories should find every file with the query"
);
- let files_to_include = PathMatcher::new(&["node_modules/prettier/**".to_owned()]).unwrap();
- let files_to_exclude = PathMatcher::new(&["*.ts".to_owned()]).unwrap();
+ let files_to_include =
+ PathMatcher::new(&["node_modules/prettier/**".to_owned()], path_style).unwrap();
+ let files_to_exclude = PathMatcher::new(&["*.ts".to_owned()], path_style).unwrap();
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
assert_eq!(
search(
@@ -6040,7 +6168,6 @@ async fn test_search_with_unicode(cx: &mut gpui::TestAppContext) {
)
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
-
let unicode_case_sensitive_query = SearchQuery::text(
"привет",
false,
@@ -6130,31 +6257,13 @@ async fn test_create_entry(cx: &mut gpui::TestAppContext) {
project
.update(cx, |project, cx| {
let id = project.worktrees(cx).next().unwrap().read(cx).id();
- project.create_entry((id, "b.."), true, cx)
+ project.create_entry((id, rel_path("b..")), true, cx)
})
.await
.unwrap()
.into_included()
.unwrap();
- // Can't create paths outside the project
- let result = project
- .update(cx, |project, cx| {
- let id = project.worktrees(cx).next().unwrap().read(cx).id();
- project.create_entry((id, "../../boop"), true, cx)
- })
- .await;
- assert!(result.is_err());
-
- // Can't create paths with '..'
- let result = project
- .update(cx, |project, cx| {
- let id = project.worktrees(cx).next().unwrap().read(cx).id();
- project.create_entry((id, "four/../beep"), true, cx)
- })
- .await;
- assert!(result.is_err());
-
assert_eq!(
fs.paths(true),
vec![
@@ -6168,15 +6277,6 @@ async fn test_create_entry(cx: &mut gpui::TestAppContext) {
PathBuf::from(path!("/one/two/three/four")),
]
);
-
- // And we cannot open buffers with '..'
- let result = project
- .update(cx, |project, cx| {
- let id = project.worktrees(cx).next().unwrap().read(cx).id();
- project.open_buffer((id, "../c.rs"), cx)
- })
- .await;
- assert!(result.is_err())
}
#[gpui::test]
@@ -6875,10 +6975,7 @@ async fn test_unstaged_diff_for_buffer(cx: &mut gpui::TestAppContext) {
)
.await;
- fs.set_index_for_repo(
- Path::new("/dir/.git"),
- &[("src/main.rs".into(), staged_contents)],
- );
+ fs.set_index_for_repo(Path::new("/dir/.git"), &[("src/main.rs", staged_contents)]);
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
@@ -6921,10 +7018,7 @@ async fn test_unstaged_diff_for_buffer(cx: &mut gpui::TestAppContext) {
"#
.unindent();
- fs.set_index_for_repo(
- Path::new("/dir/.git"),
- &[("src/main.rs".into(), staged_contents)],
- );
+ fs.set_index_for_repo(Path::new("/dir/.git"), &[("src/main.rs", staged_contents)]);
cx.run_until_parked();
unstaged_diff.update(cx, |unstaged_diff, cx| {
@@ -6982,16 +7076,16 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) {
fs.set_head_for_repo(
Path::new("/dir/.git"),
&[
- ("src/modification.rs".into(), committed_contents),
- ("src/deletion.rs".into(), "// the-deleted-contents\n".into()),
+ ("src/modification.rs", committed_contents),
+ ("src/deletion.rs", "// the-deleted-contents\n".into()),
],
"deadbeef",
);
fs.set_index_for_repo(
Path::new("/dir/.git"),
&[
- ("src/modification.rs".into(), staged_contents),
- ("src/deletion.rs".into(), "// the-deleted-contents\n".into()),
+ ("src/modification.rs", staged_contents),
+ ("src/deletion.rs", "// the-deleted-contents\n".into()),
],
);
@@ -7049,8 +7143,8 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) {
fs.set_head_for_repo(
Path::new("/dir/.git"),
&[
- ("src/modification.rs".into(), committed_contents.clone()),
- ("src/deletion.rs".into(), "// the-deleted-contents\n".into()),
+ ("src/modification.rs", committed_contents.clone()),
+ ("src/deletion.rs", "// the-deleted-contents\n".into()),
],
"deadbeef",
);
@@ -7104,7 +7198,7 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) {
// Stage the deletion of this file
fs.set_index_for_repo(
Path::new("/dir/.git"),
- &[("src/modification.rs".into(), committed_contents.clone())],
+ &[("src/modification.rs", committed_contents.clone())],
);
cx.run_until_parked();
diff_2.update(cx, |diff, cx| {
@@ -7157,8 +7251,8 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
.await;
fs.set_head_and_index_for_repo(
- "/dir/.git".as_ref(),
- &[("file.txt".into(), committed_contents.clone())],
+ path!("/dir/.git").as_ref(),
+ &[("file.txt", committed_contents.clone())],
);
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
@@ -7498,12 +7592,12 @@ async fn test_staging_hunks_with_delayed_fs_event(cx: &mut gpui::TestAppContext)
fs.set_head_for_repo(
"/dir/.git".as_ref(),
- &[("file.txt".into(), committed_contents.clone())],
+ &[("file.txt", committed_contents.clone())],
"deadbeef",
);
fs.set_index_for_repo(
"/dir/.git".as_ref(),
- &[("file.txt".into(), committed_contents.clone())],
+ &[("file.txt", committed_contents.clone())],
);
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
@@ -7695,12 +7789,12 @@ async fn test_staging_random_hunks(
.await;
fs.set_head_for_repo(
path!("/dir/.git").as_ref(),
- &[("file.txt".into(), committed_text.clone())],
+ &[("file.txt", committed_text.clone())],
"deadbeef",
);
fs.set_index_for_repo(
path!("/dir/.git").as_ref(),
- &[("file.txt".into(), index_text.clone())],
+ &[("file.txt", index_text.clone())],
);
let repo = fs.open_repo(path!("/dir/.git").as_ref()).unwrap();
@@ -7760,7 +7854,9 @@ async fn test_staging_random_hunks(
log::info!(
"index text:\n{}",
- repo.load_index_text("file.txt".into()).await.unwrap()
+ repo.load_index_text(rel_path("file.txt").into())
+ .await
+ .unwrap()
);
uncommitted_diff.update(cx, |diff, cx| {
@@ -7807,12 +7903,12 @@ async fn test_single_file_diffs(cx: &mut gpui::TestAppContext) {
fs.set_head_for_repo(
Path::new("/dir/.git"),
- &[("src/main.rs".into(), committed_contents.clone())],
+ &[("src/main.rs", committed_contents.clone())],
"deadbeef",
);
fs.set_index_for_repo(
Path::new("/dir/.git"),
- &[("src/main.rs".into(), committed_contents.clone())],
+ &[("src/main.rs", committed_contents.clone())],
);
let project = Project::test(fs.clone(), ["/dir/src/main.rs".as_ref()], cx).await;
@@ -7903,7 +7999,7 @@ async fn test_repository_and_path_for_project_path(
(
path,
result.map(|(repo, repo_path)| {
- (Path::new(repo).into(), RepoPath::from(repo_path))
+ (Path::new(repo).into(), RepoPath::new(repo_path).unwrap())
}),
)
})
@@ -7911,7 +8007,7 @@ async fn test_repository_and_path_for_project_path(
let actual = pairs
.iter()
.map(|(path, _)| {
- let project_path = (tree_id, Path::new(path)).into();
+ let project_path = (tree_id, rel_path(path)).into();
let result = maybe!({
let (repo, repo_path) =
git_store.repository_and_path_for_project_path(&project_path, cx)?;
@@ -7932,7 +8028,7 @@ async fn test_repository_and_path_for_project_path(
let git_store = project.git_store().read(cx);
assert_eq!(
git_store.repository_and_path_for_project_path(
- &(tree_id, Path::new("dir1/src/b.txt")).into(),
+ &(tree_id, rel_path("dir1/src/b.txt")).into(),
cx
),
None
@@ -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}"),
}
@@ -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()),
@@ -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))
})
}
@@ -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)
@@ -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}");
}
@@ -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>,
@@ -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 {
@@ -20,7 +20,6 @@ db.workspace = true
editor.workspace = true
file_icons.workspace = true
git_ui.workspace = true
-indexmap.workspace = true
git.workspace = true
gpui.workspace = true
menu.workspace = true
@@ -17,16 +17,14 @@ use file_icons::FileIcons;
use git::status::GitSummary;
use git_ui::file_diff_view::FileDiffView;
use gpui::{
- Action, AnyElement, App, ArcCow, AsyncWindowContext, Bounds, ClipboardItem, Context,
- CursorStyle, DismissEvent, Div, DragMoveEvent, Entity, EventEmitter, ExternalPaths,
- FocusHandle, Focusable, Hsla, InteractiveElement, KeyContext, ListHorizontalSizingBehavior,
- ListSizingBehavior, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent,
- ParentElement, Pixels, Point, PromptLevel, Render, ScrollStrategy, Stateful, Styled,
- Subscription, Task, UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred,
- div, hsla, linear_color_stop, linear_gradient, point, px, size, transparent_white,
- uniform_list,
+ Action, AnyElement, App, AsyncWindowContext, Bounds, ClipboardItem, Context, CursorStyle,
+ DismissEvent, Div, DragMoveEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable,
+ Hsla, InteractiveElement, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior,
+ Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
+ PromptLevel, Render, ScrollStrategy, Stateful, Styled, Subscription, Task,
+ UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred, div, hsla,
+ linear_color_stop, linear_gradient, point, px, size, transparent_white, uniform_list,
};
-use indexmap::IndexMap;
use language::DiagnosticSeverity;
use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
use project::{
@@ -34,7 +32,6 @@ use project::{
ProjectPath, Worktree, WorktreeId,
git_store::{GitStoreEvent, git_traversal::ChildEntriesGitIter},
project_settings::GoToDiagnosticSeverityFilter,
- relativize_path,
};
use project_panel_settings::ProjectPanelSettings;
use schemars::JsonSchema;
@@ -49,7 +46,6 @@ use std::{
cell::OnceCell,
cmp,
collections::HashSet,
- ffi::OsStr,
ops::Range,
path::{Path, PathBuf},
sync::Arc,
@@ -62,7 +58,7 @@ use ui::{
ScrollAxes, ScrollableHandle, Scrollbars, StickyCandidate, Tooltip, WithScrollbar, prelude::*,
v_flex,
};
-use util::{ResultExt, TakeUntilExt, TryFutureExt, maybe, paths::compare_paths};
+use util::{ResultExt, TakeUntilExt, TryFutureExt, maybe, paths::compare_paths, rel_path::RelPath};
use workspace::{
DraggedSelection, OpenInTerminal, OpenOptions, OpenVisible, PreviewTabsSettings, SelectedEntry,
SplitDirection, Workspace,
@@ -78,7 +74,7 @@ const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
struct VisibleEntriesForWorktree {
worktree_id: WorktreeId,
entries: Vec<GitEntry>,
- index: OnceCell<HashSet<Arc<Path>>>,
+ index: OnceCell<HashSet<Arc<RelPath>>>,
}
pub struct ProjectPanel {
@@ -110,7 +106,7 @@ pub struct ProjectPanel {
workspace: WeakEntity<Workspace>,
width: Option<Pixels>,
pending_serialization: Task<Option<()>>,
- diagnostics: HashMap<(WorktreeId, PathBuf), DiagnosticSeverity>,
+ diagnostics: HashMap<(WorktreeId, Arc<RelPath>), DiagnosticSeverity>,
max_width_item_index: Option<usize>,
diagnostic_summary_update: Task<()>,
// We keep track of the mouse down state on entries so we don't flash the UI
@@ -156,7 +152,7 @@ struct EditState {
leaf_entry_id: Option<ProjectEntryId>,
is_dir: bool,
depth: usize,
- processing_filename: Option<String>,
+ processing_filename: Option<Arc<RelPath>>,
previously_focused: Option<SelectedEntry>,
validation_state: ValidationState,
}
@@ -177,7 +173,7 @@ enum ClipboardEntry {
struct EntryDetails {
filename: String,
icon: Option<SharedString>,
- path: Arc<Path>,
+ path: Arc<RelPath>,
depth: usize,
kind: EntryKind,
is_ignored: bool,
@@ -459,6 +455,7 @@ impl ProjectPanel {
) -> Entity<Self> {
let project = workspace.project().clone();
let git_store = project.read(cx).git_store().clone();
+ let path_style = project.read(cx).path_style(cx);
let project_panel = cx.new(|cx| {
let focus_handle = cx.focus_handle();
cx.on_focus(&focus_handle, window, Self::focus_in).detach();
@@ -705,7 +702,7 @@ impl ProjectPanel {
},
ErrorCode::UnsharedItem => Some(format!(
"{} is not shared by the host. This could be because it has been marked as `private`",
- file_path.display()
+ file_path.display(path_style)
)),
// See note in worktree.rs where this error originates. Returning Some in this case prevents
// the error popup from saying "Try Again", which is a red herring in this case
@@ -795,7 +792,7 @@ impl ProjectPanel {
}
fn update_diagnostics(&mut self, cx: &mut Context<Self>) {
- let mut diagnostics: HashMap<(WorktreeId, PathBuf), DiagnosticSeverity> =
+ let mut diagnostics: HashMap<(WorktreeId, Arc<RelPath>), DiagnosticSeverity> =
Default::default();
let show_diagnostics_setting = ProjectPanelSettings::get_global(cx).show_diagnostics;
@@ -815,20 +812,12 @@ impl ProjectPanel {
}
})
.for_each(|(project_path, diagnostic_severity)| {
- let mut path_buffer = PathBuf::new();
- Self::update_strongest_diagnostic_severity(
- &mut diagnostics,
- &project_path,
- path_buffer.clone(),
- diagnostic_severity,
- );
-
- for component in project_path.path.components() {
- path_buffer.push(component);
+ let ancestors = project_path.path.ancestors().collect::<Vec<_>>();
+ for path in ancestors.into_iter().rev() {
Self::update_strongest_diagnostic_severity(
&mut diagnostics,
&project_path,
- path_buffer.clone(),
+ path.into(),
diagnostic_severity,
);
}
@@ -838,9 +827,9 @@ impl ProjectPanel {
}
fn update_strongest_diagnostic_severity(
- diagnostics: &mut HashMap<(WorktreeId, PathBuf), DiagnosticSeverity>,
+ diagnostics: &mut HashMap<(WorktreeId, Arc<RelPath>), DiagnosticSeverity>,
project_path: &ProjectPath,
- path_buffer: PathBuf,
+ path_buffer: Arc<RelPath>,
diagnostic_severity: DiagnosticSeverity,
) {
diagnostics
@@ -1419,6 +1408,31 @@ impl ProjectPanel {
};
let filename = self.filename_editor.read(cx).text(cx);
if !filename.is_empty() {
+ if filename.is_empty() {
+ edit_state.validation_state =
+ ValidationState::Error("File or directory name cannot be empty.".to_string());
+ cx.notify();
+ return;
+ }
+
+ let trimmed_filename = filename.trim();
+ if trimmed_filename != filename {
+ edit_state.validation_state = ValidationState::Warning(
+ "File or directory name contains leading or trailing whitespace.".to_string(),
+ );
+ cx.notify();
+ return;
+ }
+ let trimmed_filename = trimmed_filename.trim_start_matches('/');
+
+ let Ok(filename) = RelPath::new(trimmed_filename) else {
+ edit_state.validation_state = ValidationState::Warning(
+ "File or directory name contains leading or trailing whitespace.".to_string(),
+ );
+ cx.notify();
+ return;
+ };
+
if let Some(worktree) = self
.project
.read(cx)
@@ -1427,21 +1441,17 @@ impl ProjectPanel {
{
let mut already_exists = false;
if edit_state.is_new_entry() {
- let new_path = entry.path.join(filename.trim_start_matches('/'));
- if worktree
- .read(cx)
- .entry_for_path(new_path.as_path())
- .is_some()
- {
+ let new_path = entry.path.join(filename);
+ if worktree.read(cx).entry_for_path(&new_path).is_some() {
already_exists = true;
}
} else {
let new_path = if let Some(parent) = entry.path.clone().parent() {
parent.join(&filename)
} else {
- filename.clone().into()
+ filename.into()
};
- if let Some(existing) = worktree.read(cx).entry_for_path(new_path.as_path())
+ if let Some(existing) = worktree.read(cx).entry_for_path(&new_path)
&& existing.id != entry.id
{
already_exists = true;
@@ -1450,26 +1460,12 @@ impl ProjectPanel {
if already_exists {
edit_state.validation_state = ValidationState::Error(format!(
"File or directory '{}' already exists at location. Please choose a different name.",
- filename
+ filename.as_str()
));
cx.notify();
return;
}
}
- let trimmed_filename = filename.trim();
- if trimmed_filename.is_empty() {
- edit_state.validation_state =
- ValidationState::Error("File or directory name cannot be empty.".to_string());
- cx.notify();
- return;
- }
- if trimmed_filename != filename {
- edit_state.validation_state = ValidationState::Warning(
- "File or directory name contains leading or trailing whitespace.".to_string(),
- );
- cx.notify();
- return;
- }
}
edit_state.validation_state = ValidationState::None;
cx.notify();
@@ -1487,11 +1483,20 @@ impl ProjectPanel {
if filename.trim().is_empty() {
return None;
}
- #[cfg(not(target_os = "windows"))]
- let filename_indicates_dir = filename.ends_with("/");
- // On Windows, path separator could be either `/` or `\`.
- #[cfg(target_os = "windows")]
- let filename_indicates_dir = filename.ends_with("/") || filename.ends_with("\\");
+
+ let path_style = self.project.read(cx).path_style(cx);
+ let filename_indicates_dir = if path_style.is_windows() {
+ filename.ends_with('/') || filename.ends_with('\\')
+ } else {
+ filename.ends_with('/')
+ };
+ let filename = if path_style.is_windows() {
+ filename.trim_start_matches(&['/', '\\'])
+ } else {
+ filename.trim_start_matches('/')
+ };
+ let filename = RelPath::from_std_path(filename.as_ref(), path_style).ok()?;
+
edit_state.is_dir =
edit_state.is_dir || (edit_state.is_new_entry() && filename_indicates_dir);
let is_dir = edit_state.is_dir;
@@ -1505,26 +1510,22 @@ impl ProjectPanel {
worktree_id,
entry_id: NEW_ENTRY_ID,
});
- let new_path = entry.path.join(filename.trim_start_matches('/'));
- if worktree
- .read(cx)
- .entry_for_path(new_path.as_path())
- .is_some()
- {
+ let new_path = entry.path.join(&filename);
+ if worktree.read(cx).entry_for_path(&new_path).is_some() {
return None;
}
edited_entry_id = NEW_ENTRY_ID;
edit_task = self.project.update(cx, |project, cx| {
- project.create_entry((worktree_id, &new_path), is_dir, cx)
+ project.create_entry((worktree_id, new_path), is_dir, cx)
});
} else {
let new_path = if let Some(parent) = entry.path.clone().parent() {
parent.join(&filename)
} else {
- filename.clone().into()
+ filename.clone()
};
- if let Some(existing) = worktree.read(cx).entry_for_path(new_path.as_path()) {
+ if let Some(existing) = worktree.read(cx).entry_for_path(&new_path) {
if existing.id == entry.id {
window.focus(&self.focus_handle);
}
@@ -1532,7 +1533,7 @@ impl ProjectPanel {
}
edited_entry_id = entry.id;
edit_task = self.project.update(cx, |project, cx| {
- project.rename_entry(entry.id, new_path.as_path(), cx)
+ project.rename_entry(entry.id, (worktree_id, new_path).into(), cx)
});
};
@@ -1793,14 +1794,9 @@ impl ProjectPanel {
depth: 0,
validation_state: ValidationState::None,
});
- let file_name = entry
- .path
- .file_name()
- .map(|s| s.to_string_lossy())
- .unwrap_or_default()
- .to_string();
+ let file_name = entry.path.file_name().unwrap_or_default().to_string();
let selection = selection.unwrap_or_else(|| {
- let file_stem = entry.path.file_stem().map(|s| s.to_string_lossy());
+ let file_stem = entry.path.file_stem().map(|s| s.to_string());
let selection_end =
file_stem.map_or(file_name.len(), |file_stem| file_stem.len());
0..selection_end
@@ -1854,11 +1850,7 @@ impl ProjectPanel {
project.dirty_buffers(cx).any(|path| path == project_path) as usize;
Some((
selection.entry_id,
- project_path
- .path
- .file_name()?
- .to_string_lossy()
- .into_owned(),
+ project_path.path.file_name()?.to_string(),
))
})
.collect::<Vec<_>>();
@@ -1977,9 +1969,10 @@ impl ProjectPanel {
worktree.entry_for_id(a.entry_id),
worktree.entry_for_id(b.entry_id),
) {
- (Some(a), Some(b)) => {
- compare_paths((&a.path, a.is_file()), (&b.path, b.is_file()))
- }
+ (Some(a), Some(b)) => compare_paths(
+ (a.path.as_std_path(), a.is_file()),
+ (b.path.as_std_path(), b.is_file()),
+ ),
_ => cmp::Ordering::Equal,
}
})
@@ -2161,7 +2154,7 @@ impl ProjectPanel {
&& entry.is_file()
&& self
.diagnostics
- .get(&(worktree_id, entry.path.to_path_buf()))
+ .get(&(worktree_id, entry.path.clone()))
.is_some_and(|severity| action.severity.matches(*severity))
},
cx,
@@ -2197,7 +2190,7 @@ impl ProjectPanel {
&& entry.is_file()
&& self
.diagnostics
- .get(&(worktree_id, entry.path.to_path_buf()))
+ .get(&(worktree_id, entry.path.clone()))
.is_some_and(|severity| action.severity.matches(*severity))
},
cx,
@@ -2432,8 +2425,8 @@ impl ProjectPanel {
source: &SelectedEntry,
(worktree, target_entry): (Entity<Worktree>, &Entry),
cx: &App,
- ) -> Option<(PathBuf, Option<Range<usize>>)> {
- let mut new_path = target_entry.path.to_path_buf();
+ ) -> Option<(Arc<RelPath>, Option<Range<usize>>)> {
+ let mut new_path = target_entry.path.to_rel_path_buf();
// If we're pasting into a file, or a directory into itself, go up one level.
if target_entry.is_file() || (target_entry.is_dir() && target_entry.id == source.entry_id) {
new_path.pop();
@@ -2444,11 +2437,11 @@ impl ProjectPanel {
.path_for_entry(source.entry_id, cx)?
.path
.file_name()?
- .to_os_string();
- new_path.push(&clipboard_entry_file_name);
- let extension = new_path.extension().map(|e| e.to_os_string());
- let file_name_without_extension = Path::new(&clipboard_entry_file_name).file_stem()?;
- let file_name_len = file_name_without_extension.to_string_lossy().len();
+ .to_string();
+ new_path.push(RelPath::new(&clipboard_entry_file_name).unwrap());
+ let extension = new_path.extension().map(|s| s.to_string());
+ let file_name_without_extension = new_path.file_stem()?.to_string();
+ let file_name_len = file_name_without_extension.len();
let mut disambiguation_range = None;
let mut ix = 0;
{
@@ -2456,30 +2449,30 @@ impl ProjectPanel {
while worktree.entry_for_path(&new_path).is_some() {
new_path.pop();
- let mut new_file_name = file_name_without_extension.to_os_string();
+ let mut new_file_name = file_name_without_extension.to_string();
let disambiguation = " copy";
let mut disambiguation_len = disambiguation.len();
- new_file_name.push(disambiguation);
+ new_file_name.push_str(disambiguation);
if ix > 0 {
let extra_disambiguation = format!(" {}", ix);
disambiguation_len += extra_disambiguation.len();
-
- new_file_name.push(extra_disambiguation);
+ new_file_name.push_str(&extra_disambiguation);
}
if let Some(extension) = extension.as_ref() {
- new_file_name.push(".");
- new_file_name.push(extension);
+ new_file_name.push_str(".");
+ new_file_name.push_str(extension);
}
- new_path.push(new_file_name);
+ new_path.push(RelPath::new(&new_file_name).unwrap());
+
disambiguation_range = Some(file_name_len..(file_name_len + disambiguation_len));
ix += 1;
}
}
- Some((new_path, disambiguation_range))
+ Some((new_path.as_rel_path().into(), disambiguation_range))
}
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
@@ -2491,61 +2484,39 @@ impl ProjectPanel {
.clipboard
.as_ref()
.filter(|clipboard| !clipboard.items().is_empty())?;
+
enum PasteTask {
Rename(Task<Result<CreatedEntry>>),
Copy(Task<Result<Option<Entry>>>),
}
- let mut paste_entry_tasks: IndexMap<(ProjectEntryId, bool), PasteTask> =
- IndexMap::default();
+
+ let mut paste_tasks = Vec::new();
let mut disambiguation_range = None;
let clip_is_cut = clipboard_entries.is_cut();
for clipboard_entry in clipboard_entries.items() {
let (new_path, new_disambiguation_range) =
self.create_paste_path(clipboard_entry, self.selected_sub_entry(cx)?, cx)?;
let clip_entry_id = clipboard_entry.entry_id;
- let is_same_worktree = clipboard_entry.worktree_id == worktree_id;
- let relative_worktree_source_path = if !is_same_worktree {
- let target_base_path = worktree.read(cx).abs_path();
- let clipboard_project_path =
- self.project.read(cx).path_for_entry(clip_entry_id, cx)?;
- let clipboard_abs_path = self
- .project
- .read(cx)
- .absolute_path(&clipboard_project_path, cx)?;
- Some(relativize_path(
- &target_base_path,
- clipboard_abs_path.as_path(),
- ))
- } else {
- None
- };
- let task = if clip_is_cut && is_same_worktree {
+ let task = if clipboard_entries.is_cut() {
let task = self.project.update(cx, |project, cx| {
- project.rename_entry(clip_entry_id, new_path, cx)
+ project.rename_entry(clip_entry_id, (worktree_id, new_path).into(), cx)
});
PasteTask::Rename(task)
} else {
- let entry_id = if is_same_worktree {
- clip_entry_id
- } else {
- entry.id
- };
let task = self.project.update(cx, |project, cx| {
- project.copy_entry(entry_id, relative_worktree_source_path, new_path, cx)
+ project.copy_entry(clip_entry_id, (worktree_id, new_path).into(), cx)
});
PasteTask::Copy(task)
};
- let needs_delete = !is_same_worktree && clip_is_cut;
- paste_entry_tasks.insert((clip_entry_id, needs_delete), task);
+ paste_tasks.push(task);
disambiguation_range = new_disambiguation_range.or(disambiguation_range);
}
- let item_count = paste_entry_tasks.len();
+ let item_count = paste_tasks.len();
cx.spawn_in(window, async move |project_panel, cx| {
let mut last_succeed = None;
- let mut need_delete_ids = Vec::new();
- for ((entry_id, need_delete), task) in paste_entry_tasks.into_iter() {
+ for task in paste_tasks {
match task {
PasteTask::Rename(task) => {
if let Some(CreatedEntry::Included(entry)) = task.await.log_err() {
@@ -2555,24 +2526,10 @@ impl ProjectPanel {
PasteTask::Copy(task) => {
if let Some(Some(entry)) = task.await.log_err() {
last_succeed = Some(entry);
- if need_delete {
- need_delete_ids.push(entry_id);
- }
}
}
}
}
- // remove entry for cut in difference worktree
- for entry_id in need_delete_ids {
- project_panel
- .update(cx, |project_panel, cx| {
- project_panel
- .project
- .update(cx, |project, cx| project.delete_entry(entry_id, true, cx))
- .context("no such entry")
- })??
- .await?;
- }
// update selection
if let Some(entry) = last_succeed {
project_panel
@@ -2639,8 +2596,7 @@ impl ProjectPanel {
project
.worktree_for_id(entry.worktree_id, cx)?
.read(cx)
- .abs_path()
- .join(entry_path)
+ .absolutize(&entry_path)
.to_string_lossy()
.to_string(),
)
@@ -2658,6 +2614,7 @@ impl ProjectPanel {
_: &mut Window,
cx: &mut Context<Self>,
) {
+ let path_style = self.project.read(cx).path_style(cx);
let file_paths = {
let project = self.project.read(cx);
self.effective_entries()
@@ -2667,8 +2624,8 @@ impl ProjectPanel {
project
.path_for_entry(entry.entry_id, cx)?
.path
- .to_string_lossy()
- .to_string(),
+ .display(path_style)
+ .into_owned(),
)
})
.collect::<Vec<_>>()
@@ -2685,7 +2642,7 @@ impl ProjectPanel {
cx: &mut Context<Self>,
) {
if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
- cx.reveal_path(&worktree.read(cx).abs_path().join(&entry.path));
+ cx.reveal_path(&worktree.read(cx).absolutize(&entry.path));
}
}
@@ -2713,7 +2670,7 @@ impl ProjectPanel {
if !entry.is_file() {
return None;
}
- worktree.read(cx).absolutize(&entry.path).ok()
+ Some(worktree.read(cx).absolutize(&entry.path))
})
.rev();
@@ -2741,7 +2698,7 @@ impl ProjectPanel {
fn open_system(&mut self, _: &OpenWithSystem, _: &mut Window, cx: &mut Context<Self>) {
if let Some((worktree, entry)) = self.selected_entry(cx) {
- let abs_path = worktree.abs_path().join(&entry.path);
+ let abs_path = worktree.absolutize(&entry.path);
cx.open_with_system(&abs_path);
}
}
@@ -2754,14 +2711,14 @@ impl ProjectPanel {
) {
if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
let abs_path = match &entry.canonical_path {
- Some(canonical_path) => Some(canonical_path.to_path_buf()),
- None => worktree.read(cx).absolutize(&entry.path).ok(),
+ Some(canonical_path) => canonical_path.to_path_buf(),
+ None => worktree.read(cx).absolutize(&entry.path),
};
let working_directory = if entry.is_dir() {
- abs_path
+ Some(abs_path)
} else {
- abs_path.and_then(|path| Some(path.parent()?.to_path_buf()))
+ abs_path.parent().map(|path| path.to_path_buf())
};
if let Some(working_directory) = working_directory {
window.dispatch_action(
@@ -2791,7 +2748,7 @@ impl ProjectPanel {
.update(cx, |workspace, cx| {
search::ProjectSearchView::new_search_in_directory(
workspace,
- Path::new(""),
+ RelPath::empty(),
window,
cx,
);
@@ -2804,9 +2761,7 @@ impl ProjectPanel {
let include_root = self.project.read(cx).visible_worktrees(cx).count() > 1;
let dir_path = if include_root {
- let mut full_path = PathBuf::from(worktree.read(cx).root_name());
- full_path.push(&dir_path);
- Arc::from(full_path)
+ worktree.read(cx).root_name().join(&dir_path)
} else {
dir_path
};
@@ -2865,35 +2820,40 @@ impl ProjectPanel {
fn move_worktree_entry(
&mut self,
entry_to_move: ProjectEntryId,
- destination: ProjectEntryId,
+ destination_entry: ProjectEntryId,
destination_is_file: bool,
cx: &mut Context<Self>,
) {
- if entry_to_move == destination {
+ if entry_to_move == destination_entry {
return;
}
let destination_worktree = self.project.update(cx, |project, cx| {
- let entry_path = project.path_for_entry(entry_to_move, cx)?;
- let destination_entry_path = project.path_for_entry(destination, cx)?.path;
+ let source_path = project.path_for_entry(entry_to_move, cx)?;
+ let destination_path = project.path_for_entry(destination_entry, cx)?;
+ let destination_worktree_id = destination_path.worktree_id;
- let mut destination_path = destination_entry_path.as_ref();
+ let mut destination_path = destination_path.path.as_ref();
if destination_is_file {
destination_path = destination_path.parent()?;
}
- let mut new_path = destination_path.to_path_buf();
- new_path.push(entry_path.path.file_name()?);
- if new_path != entry_path.path.as_ref() {
- let task = project.rename_entry(entry_to_move, new_path, cx);
+ let mut new_path = destination_path.to_rel_path_buf();
+ new_path.push(RelPath::new(source_path.path.file_name()?).unwrap());
+ if new_path.as_rel_path() != source_path.path.as_ref() {
+ let task = project.rename_entry(
+ entry_to_move,
+ (destination_worktree_id, new_path).into(),
+ cx,
+ );
cx.foreground_executor().spawn(task).detach_and_log_err(cx);
}
- project.worktree_id_for_entry(destination, cx)
+ project.worktree_id_for_entry(destination_entry, cx)
});
if let Some(destination_worktree) = destination_worktree {
- self.expand_entry(destination_worktree, destination, cx);
+ self.expand_entry(destination_worktree, destination_entry, cx);
}
}
@@ -3047,7 +3007,7 @@ impl ProjectPanel {
entry: Entry {
id: NEW_ENTRY_ID,
kind: new_entry_kind,
- path: parent_entry.path.join("\0").into(),
+ path: parent_entry.path.join(RelPath::new("\0").unwrap()),
inode: 0,
mtime: parent_entry.mtime,
size: parent_entry.size,
@@ -3193,55 +3153,57 @@ impl ProjectPanel {
));
}
let worktree_abs_path = worktree.read(cx).abs_path();
- let (depth, path) = if Some(entry.entry) == worktree.read(cx).root_entry() {
- let Some(path_name) = worktree_abs_path.file_name() else {
- continue;
- };
- let path = ArcCow::Borrowed(Path::new(path_name));
- let depth = 0;
- (depth, path)
- } else if entry.is_file() {
- let Some(path_name) = entry
- .path
- .file_name()
- .with_context(|| format!("Non-root entry has no file name: {entry:?}"))
- .log_err()
- else {
- continue;
- };
- let path = ArcCow::Borrowed(Path::new(path_name));
- let depth = entry.path.ancestors().count() - 1;
- (depth, path)
- } else {
- let path = self
- .ancestors
- .get(&entry.id)
- .and_then(|ancestors| {
- let outermost_ancestor = ancestors.ancestors.last()?;
- let root_folded_entry = worktree
- .read(cx)
- .entry_for_id(*outermost_ancestor)?
- .path
- .as_ref();
- entry
- .path
- .strip_prefix(root_folded_entry)
- .ok()
- .and_then(|suffix| {
- let full_path = Path::new(root_folded_entry.file_name()?);
- Some(ArcCow::Owned(Arc::<Path>::from(full_path.join(suffix))))
+ let (depth, chars) =
+ if Some(entry.entry) == worktree.read(cx).root_entry() {
+ let Some(path_name) = worktree_abs_path.file_name() else {
+ continue;
+ };
+ let depth = 0;
+ (depth, path_name.to_string_lossy().chars().count())
+ } else if entry.is_file() {
+ let Some(path_name) = entry
+ .path
+ .file_name()
+ .with_context(|| format!("Non-root entry has no file name: {entry:?}"))
+ .log_err()
+ else {
+ continue;
+ };
+ let depth = entry.path.ancestors().count() - 1;
+ (depth, path_name.chars().count())
+ } else {
+ let path =
+ self.ancestors
+ .get(&entry.id)
+ .and_then(|ancestors| {
+ let outermost_ancestor = ancestors.ancestors.last()?;
+ let root_folded_entry = worktree
+ .read(cx)
+ .entry_for_id(*outermost_ancestor)?
+ .path
+ .as_ref();
+ entry.path.strip_prefix(root_folded_entry).ok().and_then(
+ |suffix| {
+ Some(
+ RelPath::new(root_folded_entry.file_name()?)
+ .unwrap()
+ .join(suffix),
+ )
+ },
+ )
})
- })
- .or_else(|| entry.path.file_name().map(Path::new).map(ArcCow::Borrowed))
- .unwrap_or_else(|| ArcCow::Owned(entry.path.clone()));
- let depth = path.components().count();
- (depth, path)
- };
- let width_estimate = item_width_estimate(
- depth,
- path.to_string_lossy().chars().count(),
- entry.canonical_path.is_some(),
- );
+ .or_else(|| {
+ entry
+ .path
+ .file_name()
+ .map(|file_name| RelPath::new(file_name).unwrap().into())
+ })
+ .unwrap_or_else(|| entry.path.clone());
+ let depth = path.components().count();
+ (depth, path.as_str().chars().count())
+ };
+ let width_estimate =
+ item_width_estimate(depth, chars, entry.canonical_path.is_some());
match max_width_item.as_mut() {
Some((id, worktree_id, width)) => {
@@ -3361,9 +3323,9 @@ impl ProjectPanel {
let entry = worktree.read(cx).entry_for_id(entry_id)?;
let path = entry.path.clone();
let target_directory = if entry.is_dir() {
- path.to_path_buf()
+ path
} else {
- path.parent()?.to_path_buf()
+ path.parent()?.into()
};
Some((target_directory, worktree, fs))
}) else {
@@ -3372,11 +3334,12 @@ impl ProjectPanel {
let mut paths_to_replace = Vec::new();
for path in &paths {
- if let Some(name) = path.file_name() {
- let mut target_path = target_directory.clone();
- target_path.push(name);
- if target_path.exists() {
- paths_to_replace.push((name.to_string_lossy().to_string(), path.clone()));
+ if let Some(name) = path.file_name()
+ && let Some(name) = name.to_str()
+ {
+ let target_path = target_directory.join(RelPath::new(name).unwrap());
+ if worktree.read(cx).entry_for_path(&target_path).is_some() {
+ paths_to_replace.push((name.to_string(), path.clone()));
}
}
}
@@ -3406,7 +3369,7 @@ impl ProjectPanel {
}
let task = worktree.update( cx, |worktree, cx| {
- worktree.copy_external_entries(target_directory.into(), paths, fs, cx)
+ worktree.copy_external_entries(target_directory, paths, fs, cx)
})?;
let opened_entries = task.await.with_context(|| "failed to copy external paths")?;
@@ -3472,7 +3435,7 @@ impl ProjectPanel {
)?;
let task = self.project.update(cx, |project, cx| {
- project.copy_entry(selection.entry_id, None, new_path, cx)
+ project.copy_entry(selection.entry_id, (worktree_id, new_path).into(), cx)
});
copy_tasks.push(task);
disambiguation_range = new_disambiguation_range.or(disambiguation_range);
@@ -3559,7 +3522,7 @@ impl ProjectPanel {
mut callback: impl FnMut(
&Entry,
usize,
- &HashSet<Arc<Path>>,
+ &HashSet<Arc<RelPath>>,
&mut Window,
&mut Context<ProjectPanel>,
),
@@ -3618,7 +3581,7 @@ impl ProjectPanel {
.worktree_for_id(visible.worktree_id, cx)
{
let snapshot = worktree.read(cx).snapshot();
- let root_name = OsStr::new(snapshot.root_name());
+ let root_name = snapshot.root_name();
let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
let entries = visible
@@ -3672,7 +3635,7 @@ impl ProjectPanel {
.take(prefix_components)
.collect::<PathBuf>();
if let Some(last_component) =
- Path::new(processing_filename).components().next_back()
+ processing_filename.components().next_back()
{
new_path.push(last_component);
previous_components.next();
@@ -3687,7 +3650,7 @@ impl ProjectPanel {
}
} else {
details.filename.clear();
- details.filename.push_str(processing_filename);
+ details.filename.push_str(processing_filename.as_str());
}
} else {
if edit_state.is_new_entry() {
@@ -3953,7 +3916,7 @@ impl ProjectPanel {
fn calculate_depth_and_difference(
entry: &Entry,
- visible_worktree_entries: &HashSet<Arc<Path>>,
+ visible_worktree_entries: &HashSet<Arc<RelPath>>,
) -> (usize, usize) {
let (depth, difference) = entry
.path
@@ -4119,6 +4082,7 @@ impl ProjectPanel {
.canonical_path
.as_ref()
.map(|f| f.to_string_lossy().to_string());
+ let path_style = self.project.read(cx).path_style(cx);
let path = details.path.clone();
let path_for_external_paths = path.clone();
let path_for_dragged_selection = path.clone();
@@ -4578,8 +4542,7 @@ impl ProjectPanel {
.collect::<Vec<_>>();
let active_index = folded_ancestors.active_index();
let components_len = components.len();
- const DELIMITER: SharedString =
- SharedString::new_static(std::path::MAIN_SEPARATOR_STR);
+ let delimiter = SharedString::new(path_style.separator());
for (index, component) in components.iter().enumerate() {
if index != 0 {
let delimiter_target_index = index - 1;
@@ -4623,7 +4586,7 @@ impl ProjectPanel {
)))
})
.child(
- Label::new(DELIMITER.clone())
+ Label::new(delimiter.clone())
.single_line()
.color(filename_text_color)
)
@@ -4769,8 +4732,8 @@ impl ProjectPanel {
&self,
entry: &Entry,
worktree_id: WorktreeId,
- root_name: &OsStr,
- entries_paths: &HashSet<Arc<Path>>,
+ root_name: &RelPath,
+ entries_paths: &HashSet<Arc<RelPath>>,
git_status: GitSummary,
sticky: Option<StickyDetails>,
_window: &mut Window,
@@ -4791,37 +4754,37 @@ impl ProjectPanel {
let icon = match entry.kind {
EntryKind::File => {
if show_file_icons {
- FileIcons::get_icon(&entry.path, cx)
+ FileIcons::get_icon(entry.path.as_std_path(), cx)
} else {
None
}
}
_ => {
if show_folder_icons {
- FileIcons::get_folder_icon(is_expanded, &entry.path, cx)
+ FileIcons::get_folder_icon(is_expanded, entry.path.as_std_path(), cx)
} else {
FileIcons::get_chevron_icon(is_expanded, cx)
}
}
};
+ let path_style = self.project.read(cx).path_style(cx);
let (depth, difference) =
ProjectPanel::calculate_depth_and_difference(entry, entries_paths);
- let filename = match difference {
- diff if diff > 1 => entry
+ let filename = if difference > 1 {
+ entry
.path
- .iter()
- .skip(entry.path.components().count() - diff)
- .collect::<PathBuf>()
- .to_str()
- .unwrap_or_default()
- .to_string(),
- _ => entry
+ .last_n_components(difference)
+ .map_or(String::new(), |suffix| {
+ suffix.display(path_style).to_string()
+ })
+ } else {
+ entry
.path
.file_name()
- .map(|name| name.to_string_lossy().into_owned())
- .unwrap_or_else(|| root_name.to_string_lossy().to_string()),
+ .map(|name| name.to_string())
+ .unwrap_or_else(|| root_name.as_str().to_string())
};
let selection = SelectedEntry {
@@ -4833,7 +4796,7 @@ impl ProjectPanel {
let diagnostic_severity = self
.diagnostics
- .get(&(worktree_id, entry.path.to_path_buf()))
+ .get(&(worktree_id, entry.path.clone()))
.cloned();
let filename_text_color =
@@ -5048,7 +5011,7 @@ impl ProjectPanel {
let panel_settings = ProjectPanelSettings::get_global(cx);
let git_status_enabled = panel_settings.git_status;
- let root_name = OsStr::new(worktree.root_name());
+ let root_name = worktree.root_name();
let git_summaries_by_id = if git_status_enabled {
visible
@@ -6,7 +6,7 @@ use project::FakeFs;
use serde_json::json;
use settings::SettingsStore;
use std::path::{Path, PathBuf};
-use util::path;
+use util::{path, paths::PathStyle, rel_path::rel_path};
use workspace::{
AppState, ItemHandle, Pane,
item::{Item, ProjectItem},
@@ -978,7 +978,7 @@ async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
" > a",
" > b",
" > C",
- " [PROCESSING: '/bdir1/dir2/the-new-filename'] <== selected",
+ " [PROCESSING: 'bdir1/dir2/the-new-filename'] <== selected",
" .dockerignore",
"v root2",
" > d",
@@ -1068,7 +1068,7 @@ async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) {
&[
"v root1",
" > .git",
- " [PROCESSING: 'new_dir/'] <== selected",
+ " [PROCESSING: 'new_dir'] <== selected",
" .dockerignore",
]
);
@@ -1992,7 +1992,7 @@ async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
})
.unwrap();
- select_path(&panel, "src/", cx);
+ select_path(&panel, "src", cx);
panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
cx.executor().run_until_parked();
assert_eq!(
@@ -2045,7 +2045,7 @@ async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
"File list should be unchanged after failed folder create confirmation"
);
- select_path(&panel, "src/test/", cx);
+ select_path(&panel, "src/test", cx);
panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
cx.executor().run_until_parked();
assert_eq!(
@@ -2193,20 +2193,20 @@ async fn test_select_git_entry(cx: &mut gpui::TestAppContext) {
.await;
// Mark files as git modified
- fs.set_git_content_for_repo(
+ fs.set_head_and_index_for_repo(
path!("/root/tree1/.git").as_ref(),
&[
- ("dir1/modified1.txt".into(), "modified".into(), None),
- ("dir1/modified2.txt".into(), "modified".into(), None),
- ("modified4.txt".into(), "modified".into(), None),
- ("dir2/modified3.txt".into(), "modified".into(), None),
+ ("dir1/modified1.txt", "modified".into()),
+ ("dir1/modified2.txt", "modified".into()),
+ ("modified4.txt", "modified".into()),
+ ("dir2/modified3.txt", "modified".into()),
],
);
- fs.set_git_content_for_repo(
+ fs.set_head_and_index_for_repo(
path!("/root/tree2/.git").as_ref(),
&[
- ("dir3/modified5.txt".into(), "modified".into(), None),
- ("modified6.txt".into(), "modified".into(), None),
+ ("dir3/modified5.txt", "modified".into()),
+ ("modified6.txt", "modified".into()),
],
);
@@ -3178,7 +3178,7 @@ async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) {
let target_entry = this
.project
.read(cx)
- .entry_for_path(&(worktree_id, "").into(), cx)
+ .entry_for_path(&(worktree_id, rel_path("")).into(), cx)
.unwrap();
this.drag_onto(&drag, target_entry.id, false, window, cx);
});
@@ -3322,7 +3322,7 @@ async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) {
.next()
.unwrap()
.read(cx)
- .entry_for_path("target_destination")
+ .entry_for_path(rel_path("target_destination"))
.unwrap();
panel.drag_onto(&drag, target_entry.id, false, window, cx);
});
@@ -3355,7 +3355,7 @@ async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) {
.next()
.unwrap()
.read(cx)
- .entry_for_path("a/b/c")
+ .entry_for_path(rel_path("a/b/c"))
.unwrap();
panel.drag_onto(&drag, target_entry.id, false, window, cx);
});
@@ -3378,7 +3378,7 @@ async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) {
.next()
.unwrap()
.read(cx)
- .entry_for_path("target_destination")
+ .entry_for_path(rel_path("target_destination"))
.unwrap();
panel.drag_onto(&drag, target_entry.id, false, window, cx);
});
@@ -3407,7 +3407,7 @@ async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) {
.next()
.unwrap()
.read(cx)
- .entry_for_path("a")
+ .entry_for_path(rel_path("a"))
.unwrap();
panel.drag_onto(&drag, target_entry.id, false, window, cx);
});
@@ -3430,7 +3430,7 @@ async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) {
.next()
.unwrap()
.read(cx)
- .entry_for_path("target_destination")
+ .entry_for_path(rel_path("target_destination"))
.unwrap();
panel.drag_onto(&drag, target_entry.id, false, window, cx);
});
@@ -4098,7 +4098,7 @@ async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
.clone();
assert_eq!(
active_entry_path.path.as_ref(),
- Path::new(excluded_file_path),
+ rel_path(excluded_file_path),
"Should open the excluded file"
);
@@ -4228,7 +4228,7 @@ async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppC
})
.unwrap();
- select_path(&panel, "src/", cx);
+ select_path(&panel, "src", cx);
panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
cx.executor().run_until_parked();
assert_eq!(
@@ -5126,12 +5126,8 @@ async fn test_selection_fallback_to_next_highest_worktree(cx: &mut gpui::TestApp
);
}
-fn toggle_expand_dir(
- panel: &Entity<ProjectPanel>,
- path: impl AsRef<Path>,
- cx: &mut VisualTestContext,
-) {
- let path = path.as_ref();
+fn toggle_expand_dir(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
+ let path = rel_path(path);
panel.update_in(cx, |panel, window, cx| {
for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
let worktree = worktree.read(cx);
@@ -5764,7 +5760,7 @@ async fn test_highlight_entry_for_external_drag(cx: &mut gpui::TestAppContext) {
let worktree = worktree.read(cx);
// Test 1: Target is a directory, should highlight the directory itself
- let dir_entry = worktree.entry_for_path("dir1").unwrap();
+ let dir_entry = worktree.entry_for_path(rel_path("dir1")).unwrap();
let result = panel.highlight_entry_for_external_drag(dir_entry, worktree);
assert_eq!(
result,
@@ -5773,8 +5769,10 @@ async fn test_highlight_entry_for_external_drag(cx: &mut gpui::TestAppContext) {
);
// Test 2: Target is nested file, should highlight immediate parent
- let nested_file = worktree.entry_for_path("dir1/dir2/file2.txt").unwrap();
- let nested_parent = worktree.entry_for_path("dir1/dir2").unwrap();
+ let nested_file = worktree
+ .entry_for_path(rel_path("dir1/dir2/file2.txt"))
+ .unwrap();
+ let nested_parent = worktree.entry_for_path(rel_path("dir1/dir2")).unwrap();
let result = panel.highlight_entry_for_external_drag(nested_file, worktree);
assert_eq!(
result,
@@ -5783,7 +5781,7 @@ async fn test_highlight_entry_for_external_drag(cx: &mut gpui::TestAppContext) {
);
// Test 3: Target is root level file, should highlight root
- let root_file = worktree.entry_for_path("file3.txt").unwrap();
+ let root_file = worktree.entry_for_path(rel_path("file3.txt")).unwrap();
let result = panel.highlight_entry_for_external_drag(root_file, worktree);
assert_eq!(
result,
@@ -5835,16 +5833,20 @@ async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext)
let worktree_id = worktree.read(cx).id();
let worktree = worktree.read(cx);
- let parent_dir = worktree.entry_for_path("parent_dir").unwrap();
+ let parent_dir = worktree.entry_for_path(rel_path("parent_dir")).unwrap();
let child_file = worktree
- .entry_for_path("parent_dir/child_file.txt")
+ .entry_for_path(rel_path("parent_dir/child_file.txt"))
.unwrap();
let sibling_file = worktree
- .entry_for_path("parent_dir/sibling_file.txt")
+ .entry_for_path(rel_path("parent_dir/sibling_file.txt"))
+ .unwrap();
+ let child_dir = worktree
+ .entry_for_path(rel_path("parent_dir/child_dir"))
+ .unwrap();
+ let other_dir = worktree.entry_for_path(rel_path("other_dir")).unwrap();
+ let other_file = worktree
+ .entry_for_path(rel_path("other_dir/other_file.txt"))
.unwrap();
- let child_dir = worktree.entry_for_path("parent_dir/child_dir").unwrap();
- let other_dir = worktree.entry_for_path("other_dir").unwrap();
- let other_file = worktree.entry_for_path("other_dir/other_file.txt").unwrap();
// Test 1: Single item drag, don't highlight parent directory
let dragged_selection = DraggedSelection {
@@ -5969,11 +5971,17 @@ async fn test_highlight_entry_for_selection_drag_cross_worktree(cx: &mut gpui::T
let worktrees: Vec<_> = project.visible_worktrees(cx).collect();
let worktree_a = &worktrees[0];
- let main_rs_from_a = worktree_a.read(cx).entry_for_path("src/main.rs").unwrap();
+ let main_rs_from_a = worktree_a
+ .read(cx)
+ .entry_for_path(rel_path("src/main.rs"))
+ .unwrap();
let worktree_b = &worktrees[1];
- let src_dir_from_b = worktree_b.read(cx).entry_for_path("src").unwrap();
- let main_rs_from_b = worktree_b.read(cx).entry_for_path("src/main.rs").unwrap();
+ let src_dir_from_b = worktree_b.read(cx).entry_for_path(rel_path("src")).unwrap();
+ let main_rs_from_b = worktree_b
+ .read(cx)
+ .entry_for_path(rel_path("src/main.rs"))
+ .unwrap();
// Test dragging file from worktree A onto parent of file with same relative path in worktree B
let dragged_selection = DraggedSelection {
@@ -6058,14 +6066,14 @@ async fn test_should_highlight_background_for_selection_drag(cx: &mut gpui::Test
let root1_entry = worktree1.root_entry().unwrap();
let root2_entry = worktree2.root_entry().unwrap();
- let _parent_dir = worktree1.entry_for_path("parent_dir").unwrap();
+ let _parent_dir = worktree1.entry_for_path(rel_path("parent_dir")).unwrap();
let child_file = worktree1
- .entry_for_path("parent_dir/child_file.txt")
+ .entry_for_path(rel_path("parent_dir/child_file.txt"))
.unwrap();
let nested_file = worktree1
- .entry_for_path("parent_dir/nested_dir/nested_file.txt")
+ .entry_for_path(rel_path("parent_dir/nested_dir/nested_file.txt"))
.unwrap();
- let root_file = worktree1.entry_for_path("root_file.txt").unwrap();
+ let root_file = worktree1.entry_for_path(rel_path("root_file.txt")).unwrap();
// Test 1: Multiple entries - should always highlight background
let multiple_dragged_selection = DraggedSelection {
@@ -6368,8 +6376,8 @@ async fn test_compare_selected_files(cx: &mut gpui::TestAppContext) {
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
- let file1_path = path!("root/file1.txt");
- let file2_path = path!("root/file2.txt");
+ let file1_path = "root/file1.txt";
+ let file2_path = "root/file2.txt";
select_path_with_mark(&panel, file1_path, cx);
select_path_with_mark(&panel, file2_path, cx);
@@ -6395,7 +6403,11 @@ async fn test_compare_selected_files(cx: &mut gpui::TestAppContext) {
assert_eq!(diff_view.tab_content_text(0, cx), "file1.txt ↔ file2.txt");
assert_eq!(
diff_view.tab_tooltip_text(cx).unwrap(),
- format!("{} ↔ {}", file1_path, file2_path)
+ format!(
+ "{} ↔ {}",
+ rel_path(file1_path).display(PathStyle::local()),
+ rel_path(file2_path).display(PathStyle::local())
+ )
);
})
.unwrap();
@@ -6526,8 +6538,8 @@ async fn test_compare_files_context_menu(cx: &mut gpui::TestAppContext) {
}
}
-fn select_path(panel: &Entity<ProjectPanel>, path: impl AsRef<Path>, cx: &mut VisualTestContext) {
- let path = path.as_ref();
+fn select_path(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
+ let path = rel_path(path);
panel.update(cx, |panel, cx| {
for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
let worktree = worktree.read(cx);
@@ -6544,12 +6556,8 @@ fn select_path(panel: &Entity<ProjectPanel>, path: impl AsRef<Path>, cx: &mut Vi
});
}
-fn select_path_with_mark(
- panel: &Entity<ProjectPanel>,
- path: impl AsRef<Path>,
- cx: &mut VisualTestContext,
-) {
- let path = path.as_ref();
+fn select_path_with_mark(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
+ let path = rel_path(path);
panel.update(cx, |panel, cx| {
for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
let worktree = worktree.read(cx);
@@ -6572,10 +6580,10 @@ fn select_path_with_mark(
fn find_project_entry(
panel: &Entity<ProjectPanel>,
- path: impl AsRef<Path>,
+ path: &str,
cx: &mut VisualTestContext,
) -> Option<ProjectEntryId> {
- let path = path.as_ref();
+ let path = rel_path(path);
panel.update(cx, |panel, cx| {
for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
let worktree = worktree.read(cx);
@@ -6713,7 +6721,7 @@ fn ensure_single_file_is_opened(
open_project_paths,
vec![ProjectPath {
worktree_id,
- path: Arc::from(Path::new(expected_path))
+ path: Arc::from(rel_path(expected_path))
}],
"Should have opened file, selected in project panel"
);
@@ -6,9 +6,9 @@ use gpui::{
};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
-use project::{Project, Symbol};
+use project::{Project, Symbol, lsp_store::SymbolLocation};
use settings::Settings;
-use std::{borrow::Cow, cmp::Reverse, sync::Arc};
+use std::{cmp::Reverse, sync::Arc};
use theme::{ActiveTheme, ThemeSettings};
use util::ResultExt;
use workspace::{
@@ -195,9 +195,13 @@ impl PickerDelegate for ProjectSymbolsDelegate {
StringMatchCandidate::new(id, symbol.label.filter_text())
})
.partition(|candidate| {
- project
- .entry_for_path(&symbols[candidate.id].path, cx)
- .is_some_and(|e| !e.is_ignored)
+ if let SymbolLocation::InProject(path) = &symbols[candidate.id].path {
+ project
+ .entry_for_path(path, cx)
+ .is_some_and(|e| !e.is_ignored)
+ } else {
+ false
+ }
});
delegate.visible_match_candidates = visible_match_candidates;
@@ -217,22 +221,27 @@ impl PickerDelegate for ProjectSymbolsDelegate {
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
+ let path_style = self.project.read(cx).path_style(cx);
let string_match = &self.matches.get(ix)?;
let symbol = &self.symbols.get(string_match.candidate_id)?;
let syntax_runs = styled_runs_for_code_label(&symbol.label, cx.theme().syntax());
- let mut path = symbol.path.path.to_string_lossy();
- if self.show_worktree_root_name {
- let project = self.project.read(cx);
- if let Some(worktree) = project.worktree_for_id(symbol.path.worktree_id, cx) {
- path = Cow::Owned(format!(
- "{}{}{}",
- worktree.read(cx).root_name(),
- std::path::MAIN_SEPARATOR,
- path.as_ref()
- ));
+ let path = match &symbol.path {
+ SymbolLocation::InProject(project_path) => {
+ let project = self.project.read(cx);
+ let mut path = project_path.path.clone();
+ if self.show_worktree_root_name
+ && let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx)
+ {
+ path = worktree.read(cx).root_name().join(&path);
+ }
+ path.display(path_style).into_owned().into()
}
- }
+ SymbolLocation::OutsideProject {
+ abs_path,
+ signature: _,
+ } => abs_path.to_string_lossy(),
+ };
let label = symbol.label.text.clone();
let path = path.to_string();
@@ -14,7 +14,7 @@ use std::{
time::Duration,
};
use text::LineEnding;
-use util::{ResultExt, get_system_shell};
+use util::{ResultExt, get_system_shell, rel_path::RelPath};
use crate::UserPromptId;
@@ -80,7 +80,7 @@ pub struct WorktreeContext {
#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
pub struct RulesFileContext {
- pub path_in_worktree: Arc<Path>,
+ pub path_in_worktree: Arc<RelPath>,
pub text: String,
// This used for opening rules files. TODO: Since it isn't related to prompt templating, this
// should be moved elsewhere.
@@ -447,6 +447,7 @@ impl PromptBuilder {
mod test {
use super::*;
use serde_json;
+ use util::rel_path::rel_path;
use uuid::Uuid;
#[test]
@@ -455,7 +456,7 @@ mod test {
root_name: "path".into(),
abs_path: Path::new("/path/to/root").into(),
rules_file: Some(RulesFileContext {
- path_in_worktree: Path::new(".rules").into(),
+ path_in_worktree: rel_path(".rules").into(),
text: "".into(),
project_entry_id: 0,
}),
@@ -173,6 +173,7 @@ message ShareProject {
repeated WorktreeMetadata worktrees = 2;
reserved 3;
bool is_ssh_project = 4;
+ optional bool windows_paths = 5;
}
message ShareProjectResponse {
@@ -202,6 +203,7 @@ message JoinProjectResponse {
repeated LanguageServer language_servers = 4;
repeated string language_server_capabilities = 8;
ChannelRole role = 6;
+ bool windows_paths = 9;
reserved 7;
}
@@ -98,13 +98,15 @@ message RenameProjectEntry {
uint64 project_id = 1;
uint64 entry_id = 2;
string new_path = 3;
+ uint64 new_worktree_id = 4;
}
message CopyProjectEntry {
uint64 project_id = 1;
uint64 entry_id = 2;
string new_path = 3;
- optional string relative_worktree_source_path = 4;
+ uint64 new_worktree_id = 5;
+ reserved 4;
}
message DeleteProjectEntry {
@@ -873,24 +873,4 @@ mod tests {
};
assert_eq!(PeerId::from_u64(peer_id.as_u64()), peer_id);
}
-
- #[test]
- #[cfg(target_os = "windows")]
- fn test_proto() {
- use std::path::PathBuf;
-
- fn generate_proto_path(path: PathBuf) -> PathBuf {
- let proto = path.to_proto();
- PathBuf::from_proto(proto)
- }
-
- let path = PathBuf::from("C:\\foo\\bar");
- assert_eq!(path, generate_proto_path(path.clone()));
-
- let path = PathBuf::from("C:/foo/bar/");
- assert_eq!(path, generate_proto_path(path.clone()));
-
- let path = PathBuf::from("C:/foo\\bar\\");
- assert_eq!(path, generate_proto_path(path.clone()));
- }
}
@@ -5,8 +5,6 @@ use std::{
any::{Any, TypeId},
cmp,
fmt::{self, Debug},
- path::{Path, PathBuf},
- sync::Arc,
};
use std::{marker::PhantomData, time::Instant};
@@ -171,57 +169,6 @@ impl fmt::Display for PeerId {
}
}
-pub trait FromProto {
- fn from_proto(proto: String) -> Self;
-}
-
-pub trait ToProto {
- fn to_proto(self) -> String;
-}
-
-#[inline]
-fn from_proto_path(proto: String) -> PathBuf {
- #[cfg(target_os = "windows")]
- let proto = proto.replace('/', "\\");
-
- PathBuf::from(proto)
-}
-
-#[inline]
-fn to_proto_path(path: &Path) -> String {
- #[cfg(target_os = "windows")]
- let proto = path.to_string_lossy().replace('\\', "/");
-
- #[cfg(not(target_os = "windows"))]
- let proto = path.to_string_lossy().to_string();
-
- proto
-}
-
-impl FromProto for PathBuf {
- fn from_proto(proto: String) -> Self {
- from_proto_path(proto)
- }
-}
-
-impl FromProto for Arc<Path> {
- fn from_proto(proto: String) -> Self {
- from_proto_path(proto).into()
- }
-}
-
-impl ToProto for PathBuf {
- fn to_proto(self) -> String {
- to_proto_path(&self)
- }
-}
-
-impl ToProto for &Path {
- fn to_proto(self) -> String {
- to_proto_path(self)
- }
-}
-
pub struct Receipt<T> {
pub sender_id: PeerId,
pub message_id: u32,
@@ -261,103 +208,3 @@ impl<T: RequestMessage> TypedEnvelope<T> {
}
}
}
-
-#[cfg(test)]
-mod tests {
- use typed_path::{UnixPath, UnixPathBuf, WindowsPath, WindowsPathBuf};
-
- fn windows_path_from_proto(proto: String) -> WindowsPathBuf {
- let proto = proto.replace('/', "\\");
- WindowsPathBuf::from(proto)
- }
-
- fn unix_path_from_proto(proto: String) -> UnixPathBuf {
- UnixPathBuf::from(proto)
- }
-
- fn windows_path_to_proto(path: &WindowsPath) -> String {
- path.to_string_lossy().replace('\\', "/")
- }
-
- fn unix_path_to_proto(path: &UnixPath) -> String {
- path.to_string_lossy().to_string()
- }
-
- #[test]
- fn test_path_proto_interop() {
- const WINDOWS_PATHS: &[&str] = &[
- "C:\\Users\\User\\Documents\\file.txt",
- "C:/Program Files/App/app.exe",
- "projects\\zed\\crates\\proto\\src\\typed_envelope.rs",
- "projects/my project/src/main.rs",
- ];
- const UNIX_PATHS: &[&str] = &[
- "/home/user/documents/file.txt",
- "/usr/local/bin/my app/app",
- "projects/zed/crates/proto/src/typed_envelope.rs",
- "projects/my project/src/main.rs",
- ];
-
- // Windows path to proto and back
- for &windows_path_str in WINDOWS_PATHS {
- let windows_path = WindowsPathBuf::from(windows_path_str);
- let proto = windows_path_to_proto(&windows_path);
- let recovered_path = windows_path_from_proto(proto);
- assert_eq!(windows_path, recovered_path);
- assert_eq!(
- recovered_path.to_string_lossy(),
- windows_path_str.replace('/', "\\")
- );
- }
- // Unix path to proto and back
- for &unix_path_str in UNIX_PATHS {
- let unix_path = UnixPathBuf::from(unix_path_str);
- let proto = unix_path_to_proto(&unix_path);
- let recovered_path = unix_path_from_proto(proto);
- assert_eq!(unix_path, recovered_path);
- assert_eq!(recovered_path.to_string_lossy(), unix_path_str);
- }
- // Windows host, Unix client, host sends Windows path to client
- for &windows_path_str in WINDOWS_PATHS {
- let windows_host_path = WindowsPathBuf::from(windows_path_str);
- let proto = windows_path_to_proto(&windows_host_path);
- let unix_client_received_path = unix_path_from_proto(proto);
- let proto = unix_path_to_proto(&unix_client_received_path);
- let windows_host_recovered_path = windows_path_from_proto(proto);
- assert_eq!(windows_host_path, windows_host_recovered_path);
- assert_eq!(
- windows_host_recovered_path.to_string_lossy(),
- windows_path_str.replace('/', "\\")
- );
- }
- // Unix host, Windows client, host sends Unix path to client
- for &unix_path_str in UNIX_PATHS {
- let unix_host_path = UnixPathBuf::from(unix_path_str);
- let proto = unix_path_to_proto(&unix_host_path);
- let windows_client_received_path = windows_path_from_proto(proto);
- let proto = windows_path_to_proto(&windows_client_received_path);
- let unix_host_recovered_path = unix_path_from_proto(proto);
- assert_eq!(unix_host_path, unix_host_recovered_path);
- assert_eq!(unix_host_recovered_path.to_string_lossy(), unix_path_str);
- }
- }
-
- // todo(zjk)
- #[test]
- fn test_unsolved_case() {
- // Unix host, Windows client
- // The Windows client receives a Unix path with backslashes in it, then
- // sends it back to the host.
- // This currently fails.
- let unix_path = UnixPathBuf::from("/home/user/projects/my\\project/src/main.rs");
- let proto = unix_path_to_proto(&unix_path);
- let windows_client_received_path = windows_path_from_proto(proto);
- let proto = windows_path_to_proto(&windows_client_received_path);
- let unix_host_recovered_path = unix_path_from_proto(proto);
- assert_ne!(unix_path, unix_host_recovered_path);
- assert_eq!(
- unix_host_recovered_path.to_string_lossy(),
- "/home/user/projects/my/project/src/main.rs"
- );
- }
-}
@@ -1509,7 +1509,7 @@ mod fake {
}
fn path_style(&self) -> PathStyle {
- PathStyle::current()
+ PathStyle::local()
}
fn shell(&self) -> String {
@@ -194,7 +194,6 @@ async fn build_remote_server_from_source(
)
.await?;
} else if build_remote_server.contains("cross") {
- #[cfg(target_os = "windows")]
use util::paths::SanitizedPath;
delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx);
@@ -216,11 +215,7 @@ async fn build_remote_server_from_source(
);
log::info!("building remote server binary from source for {}", &triple);
- // On Windows, the binding needs to be set to the canonical path
- #[cfg(target_os = "windows")]
- let src = SanitizedPath::new(&smol::fs::canonicalize("./target").await?).to_glob_string();
- #[cfg(not(target_os = "windows"))]
- let src = "./target";
+ let src = SanitizedPath::new(&smol::fs::canonicalize("target").await?).to_string();
run_cmd(
Command::new("cross")
@@ -13,6 +13,7 @@ use futures::{
use gpui::{App, AppContext as _, AsyncApp, SemanticVersion, Task};
use itertools::Itertools;
use parking_lot::Mutex;
+use paths::remote_server_dir_relative;
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
use rpc::proto::Envelope;
pub use settings::SshPortForwardOption;
@@ -27,12 +28,15 @@ use std::{
time::Instant,
};
use tempfile::TempDir;
-use util::paths::{PathStyle, RemotePathBuf};
+use util::{
+ paths::{PathStyle, RemotePathBuf},
+ rel_path::RelPath,
+};
pub(crate) struct SshRemoteConnection {
socket: SshSocket,
master_process: Mutex<Option<Child>>,
- remote_binary_path: Option<RemotePathBuf>,
+ remote_binary_path: Option<Arc<RelPath>>,
ssh_platform: RemotePlatform,
ssh_path_style: PathStyle,
ssh_shell: String,
@@ -204,7 +208,7 @@ impl RemoteConnection for SshRemoteConnection {
let mut start_proxy_command = shell_script!(
"exec {binary_path} proxy --identifier {identifier}",
- binary_path = &remote_binary_path.to_string(),
+ binary_path = &remote_binary_path.display(self.path_style()),
identifier = &unique_identifier,
);
@@ -400,7 +404,7 @@ impl SshRemoteConnection {
version: SemanticVersion,
commit: Option<AppCommitSha>,
cx: &mut AsyncApp,
- ) -> Result<RemotePathBuf> {
+ ) -> Result<Arc<RelPath>> {
let version_str = match release_channel {
ReleaseChannel::Nightly => {
let commit = commit.map(|s| s.full()).unwrap_or_default();
@@ -414,23 +418,21 @@ impl SshRemoteConnection {
release_channel.dev_name(),
version_str
);
- let dst_path = RemotePathBuf::new(
- paths::remote_server_dir_relative().join(binary_name),
- self.ssh_path_style,
- );
+ let dst_path =
+ paths::remote_server_dir_relative().join(RelPath::new(&binary_name).unwrap());
#[cfg(debug_assertions)]
if let Some(remote_server_path) =
super::build_remote_server_from_source(&self.ssh_platform, delegate.as_ref(), cx)
.await?
{
- let tmp_path = RemotePathBuf::new(
- paths::remote_server_dir_relative().join(format!(
+ let tmp_path = paths::remote_server_dir_relative().join(
+ RelPath::new(&format!(
"download-{}-{}",
std::process::id(),
remote_server_path.file_name().unwrap().to_string_lossy()
- )),
- self.ssh_path_style,
+ ))
+ .unwrap(),
);
self.upload_local_server_binary(&remote_server_path, &tmp_path, delegate, cx)
.await?;
@@ -441,7 +443,7 @@ impl SshRemoteConnection {
if self
.socket
- .run_command(&dst_path.to_string(), &["version"])
+ .run_command(&dst_path.display(self.path_style()), &["version"])
.await
.is_ok()
{
@@ -459,9 +461,13 @@ impl SshRemoteConnection {
_ => Ok(Some(AppVersion::global(cx))),
})??;
- let tmp_path_gz = RemotePathBuf::new(
- PathBuf::from(format!("{}-download-{}.gz", dst_path, std::process::id())),
- self.ssh_path_style,
+ let tmp_path_gz = remote_server_dir_relative().join(
+ RelPath::new(&format!(
+ "{}-download-{}.gz",
+ binary_name,
+ std::process::id()
+ ))
+ .unwrap(),
);
if !self.socket.connection_options.upload_binary_over_ssh
&& let Some((url, body)) = delegate
@@ -500,7 +506,7 @@ impl SshRemoteConnection {
&self,
url: &str,
body: &str,
- tmp_path_gz: &RemotePathBuf,
+ tmp_path_gz: &RelPath,
delegate: &Arc<dyn RemoteClientDelegate>,
cx: &mut AsyncApp,
) -> Result<()> {
@@ -510,7 +516,10 @@ impl SshRemoteConnection {
"sh",
&[
"-c",
- &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()),
+ &shell_script!(
+ "mkdir -p {parent}",
+ parent = parent.display(self.path_style()).as_ref()
+ ),
],
)
.await?;
@@ -533,7 +542,7 @@ impl SshRemoteConnection {
body,
url,
"-o",
- &tmp_path_gz.to_string(),
+ &tmp_path_gz.display(self.path_style()),
],
)
.await
@@ -555,7 +564,7 @@ impl SshRemoteConnection {
body,
url,
"-O",
- &tmp_path_gz.to_string(),
+ &tmp_path_gz.display(self.path_style()),
],
)
.await
@@ -578,7 +587,7 @@ impl SshRemoteConnection {
async fn upload_local_server_binary(
&self,
src_path: &Path,
- tmp_path_gz: &RemotePathBuf,
+ tmp_path_gz: &RelPath,
delegate: &Arc<dyn RemoteClientDelegate>,
cx: &mut AsyncApp,
) -> Result<()> {
@@ -588,7 +597,10 @@ impl SshRemoteConnection {
"sh",
&[
"-c",
- &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()),
+ &shell_script!(
+ "mkdir -p {parent}",
+ parent = parent.display(self.path_style()).as_ref()
+ ),
],
)
.await?;
@@ -613,33 +625,33 @@ impl SshRemoteConnection {
async fn extract_server_binary(
&self,
- dst_path: &RemotePathBuf,
- tmp_path: &RemotePathBuf,
+ dst_path: &RelPath,
+ tmp_path: &RelPath,
delegate: &Arc<dyn RemoteClientDelegate>,
cx: &mut AsyncApp,
) -> Result<()> {
delegate.set_status(Some("Extracting remote development server"), cx);
let server_mode = 0o755;
- let orig_tmp_path = tmp_path.to_string();
+ let orig_tmp_path = tmp_path.display(self.path_style());
let script = if let Some(tmp_path) = orig_tmp_path.strip_suffix(".gz") {
shell_script!(
"gunzip -f {orig_tmp_path} && chmod {server_mode} {tmp_path} && mv {tmp_path} {dst_path}",
server_mode = &format!("{:o}", server_mode),
- dst_path = &dst_path.to_string(),
+ dst_path = &dst_path.display(self.path_style()),
)
} else {
shell_script!(
"chmod {server_mode} {orig_tmp_path} && mv {orig_tmp_path} {dst_path}",
server_mode = &format!("{:o}", server_mode),
- dst_path = &dst_path.to_string()
+ dst_path = &dst_path.display(self.path_style())
)
};
self.socket.run_command("sh", &["-c", &script]).await?;
Ok(())
}
- async fn upload_file(&self, src_path: &Path, dest_path: &RemotePathBuf) -> Result<()> {
+ async fn upload_file(&self, src_path: &Path, dest_path: &RelPath) -> Result<()> {
log::debug!("uploading file {:?} to {:?}", src_path, dest_path);
let mut command = util::command::new_smol_command("scp");
let output = self
@@ -656,7 +668,7 @@ impl SshRemoteConnection {
.arg(format!(
"{}:{}",
self.socket.connection_options.scp_url(),
- dest_path
+ dest_path.display(self.path_style())
))
.output()
.await?;
@@ -665,7 +677,7 @@ impl SshRemoteConnection {
output.status.success(),
"failed to upload file {} -> {}: {}",
src_path.display(),
- dest_path.to_string(),
+ dest_path.display(self.path_style()),
String::from_utf8_lossy(&output.stderr)
);
Ok(())
@@ -1040,7 +1052,7 @@ fn build_command(
let mut exec = String::from("exec env -C ");
if let Some(working_dir) = working_dir {
- let working_dir = RemotePathBuf::new(working_dir.into(), ssh_path_style).to_string();
+ let working_dir = RemotePathBuf::new(working_dir, ssh_path_style).to_string();
// shlex will wrap the command in single quotes (''), disabling ~ expansion,
// replace with with something that works
@@ -17,7 +17,10 @@ use std::{
sync::Arc,
time::Instant,
};
-use util::paths::{PathStyle, RemotePathBuf};
+use util::{
+ paths::{PathStyle, RemotePathBuf},
+ rel_path::RelPath,
+};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct WslConnectionOptions {
@@ -35,7 +38,7 @@ impl From<settings::WslConnection> for WslConnectionOptions {
}
pub(crate) struct WslRemoteConnection {
- remote_binary_path: Option<RemotePathBuf>,
+ remote_binary_path: Option<Arc<RelPath>>,
platform: RemotePlatform,
shell: String,
default_system_shell: String,
@@ -122,7 +125,7 @@ impl WslRemoteConnection {
version: SemanticVersion,
commit: Option<AppCommitSha>,
cx: &mut AsyncApp,
- ) -> Result<RemotePathBuf> {
+ ) -> Result<Arc<RelPath>> {
let version_str = match release_channel {
ReleaseChannel::Nightly => {
let commit = commit.map(|s| s.full()).unwrap_or_default();
@@ -138,13 +141,11 @@ impl WslRemoteConnection {
version_str
);
- let dst_path = RemotePathBuf::new(
- paths::remote_wsl_server_dir_relative().join(binary_name),
- PathStyle::Posix,
- );
+ let dst_path =
+ paths::remote_wsl_server_dir_relative().join(RelPath::new(&binary_name).unwrap());
if let Some(parent) = dst_path.parent() {
- self.run_wsl_command("mkdir", &["-p", &parent.to_string()])
+ self.run_wsl_command("mkdir", &["-p", &parent.display(PathStyle::Posix)])
.await
.map_err(|e| anyhow!("Failed to create directory: {}", e))?;
}
@@ -153,13 +154,13 @@ impl WslRemoteConnection {
if let Some(remote_server_path) =
super::build_remote_server_from_source(&self.platform, delegate.as_ref(), cx).await?
{
- let tmp_path = RemotePathBuf::new(
- paths::remote_wsl_server_dir_relative().join(format!(
+ let tmp_path = paths::remote_wsl_server_dir_relative().join(
+ &RelPath::new(&format!(
"download-{}-{}",
std::process::id(),
remote_server_path.file_name().unwrap().to_string_lossy()
- )),
- PathStyle::Posix,
+ ))
+ .unwrap(),
);
self.upload_file(&remote_server_path, &tmp_path, delegate, cx)
.await?;
@@ -169,7 +170,7 @@ impl WslRemoteConnection {
}
if self
- .run_wsl_command(&dst_path.to_string(), &["version"])
+ .run_wsl_command(&dst_path.display(PathStyle::Posix), &["version"])
.await
.is_ok()
{
@@ -187,10 +188,12 @@ impl WslRemoteConnection {
.download_server_binary_locally(self.platform, release_channel, wanted_version, cx)
.await?;
- let tmp_path = RemotePathBuf::new(
- PathBuf::from(format!("{}.{}.gz", dst_path, std::process::id())),
- PathStyle::Posix,
+ let tmp_path = format!(
+ "{}.{}.gz",
+ dst_path.display(PathStyle::Posix),
+ std::process::id()
);
+ let tmp_path = RelPath::new(&tmp_path).unwrap();
self.upload_file(&src_path, &tmp_path, delegate, cx).await?;
self.extract_and_install(&tmp_path, &dst_path, delegate, cx)
@@ -202,14 +205,14 @@ impl WslRemoteConnection {
async fn upload_file(
&self,
src_path: &Path,
- dst_path: &RemotePathBuf,
+ dst_path: &RelPath,
delegate: &Arc<dyn RemoteClientDelegate>,
cx: &mut AsyncApp,
) -> Result<()> {
delegate.set_status(Some("Uploading remote server to WSL"), cx);
if let Some(parent) = dst_path.parent() {
- self.run_wsl_command("mkdir", &["-p", &parent.to_string()])
+ self.run_wsl_command("mkdir", &["-p", &parent.display(PathStyle::Posix)])
.await
.map_err(|e| anyhow!("Failed to create directory when uploading file: {}", e))?;
}
@@ -224,17 +227,20 @@ impl WslRemoteConnection {
);
let src_path_in_wsl = self.windows_path_to_wsl_path(src_path).await?;
- self.run_wsl_command("cp", &["-f", &src_path_in_wsl, &dst_path.to_string()])
- .await
- .map_err(|e| {
- anyhow!(
- "Failed to copy file {}({}) to WSL {:?}: {}",
- src_path.display(),
- src_path_in_wsl,
- dst_path,
- e
- )
- })?;
+ self.run_wsl_command(
+ "cp",
+ &["-f", &src_path_in_wsl, &dst_path.display(PathStyle::Posix)],
+ )
+ .await
+ .map_err(|e| {
+ anyhow!(
+ "Failed to copy file {}({}) to WSL {:?}: {}",
+ src_path.display(),
+ src_path_in_wsl,
+ dst_path,
+ e
+ )
+ })?;
log::info!("uploaded remote server in {:?}", t0.elapsed());
Ok(())
@@ -242,15 +248,15 @@ impl WslRemoteConnection {
async fn extract_and_install(
&self,
- tmp_path: &RemotePathBuf,
- dst_path: &RemotePathBuf,
+ tmp_path: &RelPath,
+ dst_path: &RelPath,
delegate: &Arc<dyn RemoteClientDelegate>,
cx: &mut AsyncApp,
) -> Result<()> {
delegate.set_status(Some("Extracting remote server"), cx);
- let tmp_path_str = tmp_path.to_string();
- let dst_path_str = dst_path.to_string();
+ let tmp_path_str = tmp_path.display(PathStyle::Posix);
+ let dst_path_str = dst_path.display(PathStyle::Posix);
// Build extraction script with proper error handling
let script = if tmp_path_str.ends_with(".gz") {
@@ -293,7 +299,8 @@ impl RemoteConnection for WslRemoteConnection {
let mut proxy_command = format!(
"exec {} proxy --identifier {}",
- remote_binary_path, unique_identifier
+ remote_binary_path.display(PathStyle::Posix),
+ unique_identifier
);
if reconnect {
@@ -377,7 +384,7 @@ impl RemoteConnection for WslRemoteConnection {
}
let working_dir = working_dir
- .map(|working_dir| RemotePathBuf::new(working_dir.into(), PathStyle::Posix).to_string())
+ .map(|working_dir| RemotePathBuf::new(working_dir, PathStyle::Posix).to_string())
.unwrap_or("~".to_string());
let mut script = String::new();
@@ -1,4 +1,3 @@
-use ::proto::{FromProto, ToProto};
use anyhow::{Context as _, Result, anyhow};
use lsp::LanguageServerId;
@@ -34,7 +33,7 @@ use std::{
sync::{Arc, atomic::AtomicUsize},
};
use sysinfo::System;
-use util::ResultExt;
+use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
use worktree::Worktree;
pub struct HeadlessProject {
@@ -405,7 +404,7 @@ impl HeadlessProject {
) -> Result<proto::AddWorktreeResponse> {
use client::ErrorCodeExt;
let fs = this.read_with(&cx, |this, _| this.fs.clone())?;
- let path = PathBuf::from_proto(shellexpand::tilde(&message.payload.path).to_string());
+ let path = PathBuf::from(shellexpand::tilde(&message.payload.path).to_string());
let canonicalized = match fs.canonicalize(&path).await {
Ok(path) => path,
@@ -443,7 +442,7 @@ impl HeadlessProject {
let worktree = worktree.read(cx);
proto::AddWorktreeResponse {
worktree_id: worktree.id().to_proto(),
- canonicalized_path: canonicalized.to_proto(),
+ canonicalized_path: canonicalized.to_string_lossy().to_string(),
}
})?;
@@ -492,16 +491,11 @@ impl HeadlessProject {
mut cx: AsyncApp,
) -> Result<proto::OpenBufferResponse> {
let worktree_id = WorktreeId::from_proto(message.payload.worktree_id);
+ let path = RelPath::from_proto(&message.payload.path)?;
let (buffer_store, buffer) = this.update(&mut cx, |this, cx| {
let buffer_store = this.buffer_store.clone();
let buffer = this.buffer_store.update(cx, |buffer_store, cx| {
- buffer_store.open_buffer(
- ProjectPath {
- worktree_id,
- path: Arc::<Path>::from_proto(message.payload.path),
- },
- cx,
- )
+ buffer_store.open_buffer(ProjectPath { worktree_id, path }, cx)
});
anyhow::Ok((buffer_store, buffer))
})??;
@@ -590,7 +584,7 @@ impl HeadlessProject {
buffer_store.open_buffer(
ProjectPath {
worktree_id: worktree.read(cx).id(),
- path: path.into(),
+ path: path,
},
cx,
)
@@ -630,7 +624,10 @@ impl HeadlessProject {
mut cx: AsyncApp,
) -> Result<proto::FindSearchCandidatesResponse> {
let message = envelope.payload;
- let query = SearchQuery::from_proto(message.query.context("missing query field")?)?;
+ let query = SearchQuery::from_proto(
+ message.query.context("missing query field")?,
+ PathStyle::local(),
+ )?;
let results = this.update(&mut cx, |this, cx| {
this.buffer_store.update(cx, |buffer_store, cx| {
buffer_store.find_search_candidates(&query, message.limit as _, this.fs.clone(), cx)
@@ -662,7 +659,7 @@ impl HeadlessProject {
cx: AsyncApp,
) -> Result<proto::ListRemoteDirectoryResponse> {
let fs = cx.read_entity(&this, |this, _| this.fs.clone())?;
- let expanded = PathBuf::from_proto(shellexpand::tilde(&envelope.payload.path).to_string());
+ let expanded = PathBuf::from(shellexpand::tilde(&envelope.payload.path).to_string());
let check_info = envelope
.payload
.config
@@ -694,7 +691,7 @@ impl HeadlessProject {
cx: AsyncApp,
) -> Result<proto::GetPathMetadataResponse> {
let fs = cx.read_entity(&this, |this, _| this.fs.clone())?;
- let expanded = PathBuf::from_proto(shellexpand::tilde(&envelope.payload.path).to_string());
+ let expanded = PathBuf::from(shellexpand::tilde(&envelope.payload.path).to_string());
let metadata = fs.metadata(&expanded).await?;
let is_dir = metadata.map(|metadata| metadata.is_dir).unwrap_or(false);
@@ -702,7 +699,7 @@ impl HeadlessProject {
Ok(proto::GetPathMetadataResponse {
exists: metadata.is_some(),
is_dir,
- path: expanded.to_proto(),
+ path: expanded.to_string_lossy().to_string(),
})
}
@@ -20,7 +20,7 @@ use language::{
use lsp::{CompletionContext, CompletionResponse, CompletionTriggerKind, LanguageServerName};
use node_runtime::NodeRuntime;
use project::{
- Project, ProjectPath,
+ Project,
agent_server_store::AgentServerCommand,
search::{SearchQuery, SearchResult},
};
@@ -34,7 +34,7 @@ use std::{
};
#[cfg(not(windows))]
use unindent::Unindent as _;
-use util::path;
+use util::{path, rel_path::rel_path};
#[gpui::test]
async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
@@ -57,7 +57,7 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test
.await;
fs.set_index_for_repo(
Path::new(path!("/code/project1/.git")),
- &[("src/lib.rs".into(), "fn one() -> usize { 0 }".into())],
+ &[("src/lib.rs", "fn one() -> usize { 0 }".into())],
);
let (project, _headless) = init_test(&fs, cx, server_cx).await;
@@ -73,11 +73,11 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
worktree.update(cx, |worktree, _cx| {
assert_eq!(
- worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
+ worktree.paths().collect::<Vec<_>>(),
vec![
- Path::new("README.md"),
- Path::new("src"),
- Path::new("src/lib.rs"),
+ rel_path("README.md"),
+ rel_path("src"),
+ rel_path("src/lib.rs"),
]
);
});
@@ -86,7 +86,7 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test
// contents are loaded from the remote filesystem.
let buffer = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
+ project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
})
.await
.unwrap();
@@ -130,12 +130,12 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test
cx.executor().run_until_parked();
worktree.update(cx, |worktree, _cx| {
assert_eq!(
- worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
+ worktree.paths().collect::<Vec<_>>(),
vec![
- Path::new("README.md"),
- Path::new("src"),
- Path::new("src/lib.rs"),
- Path::new("src/main.rs"),
+ rel_path("README.md"),
+ rel_path("src"),
+ rel_path("src/lib.rs"),
+ rel_path("src/main.rs"),
]
);
});
@@ -150,12 +150,12 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test
.unwrap();
cx.executor().run_until_parked();
buffer.update(cx, |buffer, _| {
- assert_eq!(&**buffer.file().unwrap().path(), Path::new("src/lib2.rs"));
+ assert_eq!(&**buffer.file().unwrap().path(), rel_path("src/lib2.rs"));
});
fs.set_index_for_repo(
Path::new(path!("/code/project1/.git")),
- &[("src/lib2.rs".into(), "fn one() -> usize { 100 }".into())],
+ &[("src/lib2.rs", "fn one() -> usize { 100 }".into())],
);
cx.executor().run_until_parked();
diff.update(cx, |diff, _| {
@@ -336,7 +336,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo
let buffer = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
+ project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
})
.await
.unwrap();
@@ -356,7 +356,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo
AllLanguageSettings::get(
Some(SettingsLocation {
worktree_id,
- path: Path::new("src/lib.rs")
+ path: rel_path("src/lib.rs")
}),
cx
)
@@ -463,7 +463,7 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext
let (buffer, _handle) = project
.update(cx, |project, cx| {
- project.open_buffer_with_lsp((worktree_id, Path::new("src/lib.rs")), cx)
+ project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
})
.await
.unwrap();
@@ -643,7 +643,7 @@ async fn test_remote_cancel_language_server_work(
let (buffer, _handle) = project
.update(cx, |project, cx| {
- project.open_buffer_with_lsp((worktree_id, Path::new("src/lib.rs")), cx)
+ project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
})
.await
.unwrap();
@@ -757,7 +757,7 @@ async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppCont
let buffer = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
+ project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
})
.await
.unwrap();
@@ -851,7 +851,7 @@ async fn test_remote_resolve_path_in_buffer(
let buffer2 = project
.update(cx, |project, cx| {
- project.open_buffer((worktree2_id, Path::new("src/lib.rs")), cx)
+ project.open_buffer((worktree2_id, rel_path("src/lib.rs")), cx)
})
.await
.unwrap();
@@ -863,10 +863,7 @@ async fn test_remote_resolve_path_in_buffer(
.await
.unwrap();
assert!(path.is_file());
- assert_eq!(
- path.abs_path().unwrap().to_string_lossy(),
- path!("/code/project2/README.md")
- );
+ assert_eq!(path.abs_path().unwrap(), path!("/code/project2/README.md"));
let path = project
.update(cx, |project, cx| {
@@ -877,7 +874,7 @@ async fn test_remote_resolve_path_in_buffer(
assert!(path.is_file());
assert_eq!(
path.project_path().unwrap().clone(),
- ProjectPath::from((worktree2_id, "README.md"))
+ (worktree2_id, rel_path("README.md")).into()
);
let path = project
@@ -888,7 +885,7 @@ async fn test_remote_resolve_path_in_buffer(
.unwrap();
assert_eq!(
path.project_path().unwrap().clone(),
- ProjectPath::from((worktree2_id, "src"))
+ (worktree2_id, rel_path("src")).into()
);
assert!(path.is_dir());
}
@@ -920,10 +917,7 @@ async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut T
.unwrap();
assert!(path.is_file());
- assert_eq!(
- path.abs_path().unwrap().to_string_lossy(),
- path!("/code/project1/README.md")
- );
+ assert_eq!(path.abs_path().unwrap(), path!("/code/project1/README.md"));
let path = project
.update(cx, |project, cx| {
@@ -933,10 +927,7 @@ async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut T
.unwrap();
assert!(path.is_dir());
- assert_eq!(
- path.abs_path().unwrap().to_string_lossy(),
- path!("/code/project1/src")
- );
+ assert_eq!(path.abs_path().unwrap(), path!("/code/project1/src"));
let path = project
.update(cx, |project, cx| {
@@ -973,14 +964,18 @@ async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut
let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
// Open a buffer on the client but cancel after a random amount of time.
- let buffer = project.update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx));
+ let buffer = project.update(cx, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
+ });
cx.executor().simulate_random_delay().await;
drop(buffer);
// Try opening the same buffer again as the client, and ensure we can
// still do it despite the cancellation above.
let buffer = project
- .update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx))
+ .update(cx, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
+ })
.await
.unwrap();
@@ -1042,10 +1037,7 @@ async fn test_adding_then_removing_then_adding_worktrees(
assert!(worktree.is_visible());
let entries = worktree.entries(true, 0).collect::<Vec<_>>();
assert_eq!(entries.len(), 2);
- assert_eq!(
- entries[1].path.to_string_lossy().to_string(),
- "README.md".to_string()
- )
+ assert_eq!(entries[1].path.as_str(), "README.md")
})
}
@@ -1111,7 +1103,7 @@ async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext)
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
let buffer = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
+ project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
})
.await
.unwrap();
@@ -1203,10 +1195,11 @@ async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestA
cx.run_until_parked();
- let entry = worktree
- .update(cx, |worktree, cx| {
- let entry = worktree.entry_for_path("README.md").unwrap();
- worktree.rename_entry(entry.id, Path::new("README.rst"), cx)
+ let entry = project
+ .update(cx, |project, cx| {
+ let worktree = worktree.read(cx);
+ let entry = worktree.entry_for_path(rel_path("README.md")).unwrap();
+ project.rename_entry(entry.id, (worktree.id(), rel_path("README.rst")).into(), cx)
})
.await
.unwrap()
@@ -1216,7 +1209,10 @@ async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestA
cx.run_until_parked();
worktree.update(cx, |worktree, _| {
- assert_eq!(worktree.entry_for_path("README.rst").unwrap().id, entry.id)
+ assert_eq!(
+ worktree.entry_for_path(rel_path("README.rst")).unwrap().id,
+ entry.id
+ )
});
}
@@ -1277,7 +1273,7 @@ async fn test_copy_file_into_remote_project(
worktree
.update(cx, |worktree, cx| {
worktree.copy_external_entries(
- Path::new("src").into(),
+ rel_path("src").into(),
vec![
Path::new(path!("/local-code/dir1/file1")).into(),
Path::new(path!("/local-code/dir1/dir2")).into(),
@@ -1363,11 +1359,11 @@ async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppC
.await;
fs.set_index_for_repo(
Path::new("/code/project1/.git"),
- &[("src/lib.rs".into(), text_1.clone())],
+ &[("src/lib.rs", text_1.clone())],
);
fs.set_head_for_repo(
Path::new("/code/project1/.git"),
- &[("src/lib.rs".into(), text_1.clone())],
+ &[("src/lib.rs", text_1.clone())],
"deadbeef",
);
@@ -1383,7 +1379,7 @@ async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppC
let buffer = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
+ project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
})
.await
.unwrap();
@@ -1409,7 +1405,7 @@ async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppC
// stage the current buffer's contents
fs.set_index_for_repo(
Path::new("/code/project1/.git"),
- &[("src/lib.rs".into(), text_2.clone())],
+ &[("src/lib.rs", text_2.clone())],
);
cx.executor().run_until_parked();
@@ -1428,7 +1424,7 @@ async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppC
// commit the current buffer's contents
fs.set_head_for_repo(
Path::new("/code/project1/.git"),
- &[("src/lib.rs".into(), text_2.clone())],
+ &[("src/lib.rs", text_2.clone())],
"deadbeef",
);
@@ -1492,7 +1488,7 @@ async fn test_remote_git_diffs_when_recv_update_repository_delay(
let worktree_id = cx.update(|cx| worktree.read(cx).id());
let buffer = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
+ project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
})
.await
.unwrap();
@@ -1519,11 +1515,11 @@ async fn test_remote_git_diffs_when_recv_update_repository_delay(
fs.set_index_for_repo(
Path::new("/code/project1/.git"),
- &[("src/lib.rs".into(), text_1.clone())],
+ &[("src/lib.rs", text_1.clone())],
);
fs.set_head_for_repo(
Path::new("/code/project1/.git"),
- &[("src/lib.rs".into(), text_1.clone())],
+ &[("src/lib.rs", text_1.clone())],
"sha",
);
@@ -1551,7 +1547,7 @@ async fn test_remote_git_diffs_when_recv_update_repository_delay(
// stage the current buffer's contents
fs.set_index_for_repo(
Path::new("/code/project1/.git"),
- &[("src/lib.rs".into(), text_2.clone())],
+ &[("src/lib.rs", text_2.clone())],
);
cx.executor().run_until_parked();
@@ -1570,7 +1566,7 @@ async fn test_remote_git_diffs_when_recv_update_repository_delay(
// commit the current buffer's contents
fs.set_head_for_repo(
Path::new("/code/project1/.git"),
- &[("src/lib.rs".into(), text_2.clone())],
+ &[("src/lib.rs", text_2.clone())],
"sha",
);
@@ -1,5 +1,5 @@
mod native_kernel;
-use std::{fmt::Debug, future::Future, path::PathBuf, sync::Arc};
+use std::{fmt::Debug, future::Future, path::PathBuf};
use futures::{
channel::mpsc::{self, Receiver},
@@ -18,6 +18,7 @@ use anyhow::Result;
use jupyter_protocol::JupyterKernelspec;
use runtimelib::{ExecutionState, JupyterMessage, KernelInfoReply};
use ui::{Icon, IconName, SharedString};
+use util::rel_path::RelPath;
pub type JupyterMessageChannel = stream::SelectAll<Receiver<JupyterMessage>>;
@@ -84,7 +85,7 @@ pub fn python_env_kernel_specifications(
let toolchains = project.read(cx).available_toolchains(
ProjectPath {
worktree_id,
- path: Arc::from("".as_ref()),
+ path: RelPath::empty().into(),
},
python_language,
cx,
@@ -34,7 +34,7 @@ use ui::{
BASE_REM_SIZE_IN_PX, IconButton, IconButtonShape, IconName, Tooltip, h_flex, prelude::*,
utils::SearchInputWidth,
};
-use util::ResultExt;
+use util::{ResultExt, paths::PathMatcher};
use workspace::{
ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
item::ItemHandle,
@@ -1214,6 +1214,8 @@ impl BufferSearchBar {
{
search
} else {
+ // Value doesn't matter, we only construct empty matchers with it
+
if self.search_options.contains(SearchOptions::REGEX) {
match SearchQuery::regex(
query,
@@ -1222,8 +1224,8 @@ impl BufferSearchBar {
false,
self.search_options
.contains(SearchOptions::ONE_MATCH_PER_LINE),
- Default::default(),
- Default::default(),
+ PathMatcher::default(),
+ PathMatcher::default(),
false,
None,
) {
@@ -1241,8 +1243,8 @@ impl BufferSearchBar {
self.search_options.contains(SearchOptions::WHOLE_WORD),
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
false,
- Default::default(),
- Default::default(),
+ PathMatcher::default(),
+ PathMatcher::default(),
false,
None,
) {
@@ -32,12 +32,11 @@ use std::{
any::{Any, TypeId},
mem,
ops::{Not, Range},
- path::Path,
pin::pin,
sync::Arc,
};
use ui::{IconButtonShape, KeyBinding, Toggleable, Tooltip, prelude::*, utils::SearchInputWidth};
-use util::{ResultExt as _, paths::PathMatcher};
+use util::{ResultExt as _, paths::PathMatcher, rel_path::RelPath};
use workspace::{
DeploySearch, ItemNavHistory, NewSearch, ToolbarItemEvent, ToolbarItemLocation,
ToolbarItemView, Workspace, WorkspaceId,
@@ -908,13 +907,11 @@ impl ProjectSearchView {
pub fn new_search_in_directory(
workspace: &mut Workspace,
- dir_path: &Path,
+ dir_path: &RelPath,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
- let Some(filter_str) = dir_path.to_str() else {
- return;
- };
+ let filter_str = dir_path.display(workspace.path_style(cx));
let weak_workspace = cx.entity().downgrade();
@@ -1145,6 +1142,7 @@ impl ProjectSearchView {
fn build_search_query(&mut self, cx: &mut Context<Self>) -> Option<SearchQuery> {
// Do not bail early in this function, as we want to fill out `self.panels_with_errors`.
+
let text = self.search_query_text(cx);
let open_buffers = if self.included_opened_only {
Some(self.open_buffers(cx))
@@ -1154,7 +1152,7 @@ impl ProjectSearchView {
let included_files = self
.filters_enabled
.then(|| {
- match Self::parse_path_matches(&self.included_files_editor.read(cx).text(cx)) {
+ match self.parse_path_matches(self.included_files_editor.read(cx).text(cx), cx) {
Ok(included_files) => {
let should_unmark_error =
self.panels_with_errors.remove(&InputPanel::Include);
@@ -1174,11 +1172,11 @@ impl ProjectSearchView {
}
}
})
- .unwrap_or_default();
+ .unwrap_or(PathMatcher::default());
let excluded_files = self
.filters_enabled
.then(|| {
- match Self::parse_path_matches(&self.excluded_files_editor.read(cx).text(cx)) {
+ match self.parse_path_matches(self.excluded_files_editor.read(cx).text(cx), cx) {
Ok(excluded_files) => {
let should_unmark_error =
self.panels_with_errors.remove(&InputPanel::Exclude);
@@ -1199,7 +1197,7 @@ impl ProjectSearchView {
}
}
})
- .unwrap_or_default();
+ .unwrap_or(PathMatcher::default());
// If the project contains multiple visible worktrees, we match the
// include/exclude patterns against full paths to allow them to be
@@ -1300,14 +1298,15 @@ impl ProjectSearchView {
buffers
}
- fn parse_path_matches(text: &str) -> anyhow::Result<PathMatcher> {
+ fn parse_path_matches(&self, text: String, cx: &App) -> anyhow::Result<PathMatcher> {
+ let path_style = self.entity.read(cx).project.read(cx).path_style(cx);
let queries = text
.split(',')
.map(str::trim)
.filter(|maybe_glob_str| !maybe_glob_str.is_empty())
.map(str::to_owned)
.collect::<Vec<_>>();
- Ok(PathMatcher::new(&queries)?)
+ Ok(PathMatcher::new(&queries, path_style)?)
}
fn select_match(&mut self, direction: Direction, window: &mut Window, cx: &mut Context<Self>) {
@@ -2354,7 +2353,7 @@ pub mod tests {
use project::FakeFs;
use serde_json::json;
use settings::SettingsStore;
- use util::path;
+ use util::{path, paths::PathStyle, rel_path::rel_path};
use workspace::DeploySearch;
#[gpui::test]
@@ -3204,7 +3203,7 @@ pub mod tests {
.read(cx)
.project()
.read(cx)
- .entry_for_path(&(worktree_id, "a").into(), cx)
+ .entry_for_path(&(worktree_id, rel_path("a")).into(), cx)
.expect("no entry for /a/ directory")
.clone()
});
@@ -3242,7 +3241,7 @@ pub mod tests {
search_view.included_files_editor.update(cx, |editor, cx| {
assert_eq!(
editor.display_text(cx),
- a_dir_entry.path.to_str().unwrap(),
+ a_dir_entry.path.display(PathStyle::local()),
"New search in directory should have included dir entry path"
);
});
@@ -3638,7 +3637,7 @@ pub mod tests {
window
.update(cx, |workspace, window, cx| {
workspace.open_path(
- (worktree_id, "one.rs"),
+ (worktree_id, rel_path("one.rs")),
Some(first_pane.downgrade()),
true,
window,
@@ -3855,7 +3854,7 @@ pub mod tests {
window
.update(cx, |workspace, window, cx| {
workspace.open_path(
- (worktree_id, "one.rs"),
+ (worktree_id, rel_path("one.rs")),
Some(first_pane.downgrade()),
true,
window,
@@ -4089,7 +4088,7 @@ pub mod tests {
let editor = workspace
.update_in(&mut cx, |workspace, window, cx| {
- workspace.open_path((worktree_id, "one.rs"), None, true, window, cx)
+ workspace.open_path((worktree_id, rel_path("one.rs")), None, true, window, cx)
})
.await
.unwrap()
@@ -17,13 +17,14 @@ use std::{
any::{Any, TypeId, type_name},
fmt::Debug,
ops::Range,
- path::{Path, PathBuf},
+ path::PathBuf,
rc::Rc,
str::{self, FromStr},
sync::Arc,
};
use util::{
ResultExt as _,
+ rel_path::RelPath,
schemars::{DefaultDenyUnknownFields, replace_subschema},
};
@@ -134,7 +135,7 @@ pub trait Settings: 'static + Send + Sync + Sized {
#[derive(Clone, Copy, Debug)]
pub struct SettingsLocation<'a> {
pub worktree_id: WorktreeId,
- pub path: &'a Path,
+ pub path: &'a RelPath,
}
pub struct SettingsStore {
@@ -148,8 +149,8 @@ pub struct SettingsStore {
merged_settings: Rc<SettingsContent>,
- local_settings: BTreeMap<(WorktreeId, Arc<Path>), SettingsContent>,
- raw_editorconfig_settings: BTreeMap<(WorktreeId, Arc<Path>), (String, Option<Editorconfig>)>,
+ local_settings: BTreeMap<(WorktreeId, Arc<RelPath>), SettingsContent>,
+ raw_editorconfig_settings: BTreeMap<(WorktreeId, Arc<RelPath>), (String, Option<Editorconfig>)>,
_setting_file_updates: Task<()>,
setting_file_updates_tx:
@@ -199,7 +200,7 @@ impl Global for SettingsStore {}
#[derive(Debug)]
struct SettingValue<T> {
global_value: Option<T>,
- local_values: Vec<(WorktreeId, Arc<Path>, T)>,
+ local_values: Vec<(WorktreeId, Arc<RelPath>, T)>,
}
trait AnySettingValue: 'static + Send + Sync {
@@ -208,9 +209,9 @@ trait AnySettingValue: 'static + Send + Sync {
fn from_settings(&self, s: &SettingsContent, cx: &mut App) -> Box<dyn Any>;
fn value_for_path(&self, path: Option<SettingsLocation>) -> &dyn Any;
- fn all_local_values(&self) -> Vec<(WorktreeId, Arc<Path>, &dyn Any)>;
+ fn all_local_values(&self) -> Vec<(WorktreeId, Arc<RelPath>, &dyn Any)>;
fn set_global_value(&mut self, value: Box<dyn Any>);
- fn set_local_value(&mut self, root_id: WorktreeId, path: Arc<Path>, value: Box<dyn Any>);
+ fn set_local_value(&mut self, root_id: WorktreeId, path: Arc<RelPath>, value: Box<dyn Any>);
fn import_from_vscode(
&self,
vscode_settings: &VsCodeSettings,
@@ -299,7 +300,7 @@ impl SettingsStore {
}
/// Get all values from project specific settings
- pub fn get_all_locals<T: Settings>(&self) -> Vec<(WorktreeId, Arc<Path>, &T)> {
+ pub fn get_all_locals<T: Settings>(&self) -> Vec<(WorktreeId, Arc<RelPath>, &T)> {
self.setting_values
.get(&TypeId::of::<T>())
.unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
@@ -644,7 +645,7 @@ impl SettingsStore {
pub fn set_local_settings(
&mut self,
root_id: WorktreeId,
- directory_path: Arc<Path>,
+ directory_path: Arc<RelPath>,
kind: LocalSettingsKind,
settings_content: Option<&str>,
cx: &mut App,
@@ -659,14 +660,20 @@ impl SettingsStore {
(LocalSettingsKind::Tasks, _) => {
return Err(InvalidSettingsError::Tasks {
message: "Attempted to submit tasks into the settings store".to_string(),
- path: directory_path.join(task_file_name()),
+ path: directory_path
+ .join(RelPath::new(task_file_name()).unwrap())
+ .as_std_path()
+ .to_path_buf(),
});
}
(LocalSettingsKind::Debug, _) => {
return Err(InvalidSettingsError::Debug {
message: "Attempted to submit debugger config into the settings store"
.to_string(),
- path: directory_path.join(task_file_name()),
+ path: directory_path
+ .join(RelPath::new(task_file_name()).unwrap())
+ .as_std_path()
+ .to_path_buf(),
});
}
(LocalSettingsKind::Settings, None) => {
@@ -719,7 +726,7 @@ impl SettingsStore {
v.insert((editorconfig_contents.to_owned(), None));
return Err(InvalidSettingsError::Editorconfig {
message: e.to_string(),
- path: directory_path.join(EDITORCONFIG_NAME),
+ path: directory_path.join(RelPath::new(EDITORCONFIG_NAME).unwrap()),
});
}
},
@@ -736,7 +743,8 @@ impl SettingsStore {
o.insert((editorconfig_contents.to_owned(), None));
return Err(InvalidSettingsError::Editorconfig {
message: e.to_string(),
- path: directory_path.join(EDITORCONFIG_NAME),
+ path: directory_path
+ .join(RelPath::new(EDITORCONFIG_NAME).unwrap()),
});
}
}
@@ -772,20 +780,20 @@ impl SettingsStore {
pub fn clear_local_settings(&mut self, root_id: WorktreeId, cx: &mut App) -> Result<()> {
self.local_settings
.retain(|(worktree_id, _), _| worktree_id != &root_id);
- self.recompute_values(Some((root_id, "".as_ref())), cx)?;
+ self.recompute_values(Some((root_id, RelPath::empty())), cx)?;
Ok(())
}
pub fn local_settings(
&self,
root_id: WorktreeId,
- ) -> impl '_ + Iterator<Item = (Arc<Path>, &ProjectSettingsContent)> {
+ ) -> impl '_ + Iterator<Item = (Arc<RelPath>, &ProjectSettingsContent)> {
self.local_settings
.range(
- (root_id, Path::new("").into())
+ (root_id, RelPath::empty().into())
..(
WorktreeId::from_usize(root_id.to_usize() + 1),
- Path::new("").into(),
+ RelPath::empty().into(),
),
)
.map(|((_, path), content)| (path.clone(), &content.project))
@@ -794,13 +802,13 @@ impl SettingsStore {
pub fn local_editorconfig_settings(
&self,
root_id: WorktreeId,
- ) -> impl '_ + Iterator<Item = (Arc<Path>, String, Option<Editorconfig>)> {
+ ) -> impl '_ + Iterator<Item = (Arc<RelPath>, String, Option<Editorconfig>)> {
self.raw_editorconfig_settings
.range(
- (root_id, Path::new("").into())
+ (root_id, RelPath::empty().into())
..(
WorktreeId::from_usize(root_id.to_usize() + 1),
- Path::new("").into(),
+ RelPath::empty().into(),
),
)
.map(|((_, path), (content, parsed_content))| {
@@ -862,12 +870,12 @@ impl SettingsStore {
fn recompute_values(
&mut self,
- changed_local_path: Option<(WorktreeId, &Path)>,
+ changed_local_path: Option<(WorktreeId, &RelPath)>,
cx: &mut App,
) -> std::result::Result<(), InvalidSettingsError> {
// Reload the global and local values for every setting.
let mut project_settings_stack = Vec::<SettingsContent>::new();
- let mut paths_stack = Vec::<Option<(WorktreeId, &Path)>>::new();
+ let mut paths_stack = Vec::<Option<(WorktreeId, &RelPath)>>::new();
if changed_local_path.is_none() {
let mut merged = self.default_settings.as_ref().clone();
@@ -931,7 +939,7 @@ impl SettingsStore {
pub fn editorconfig_properties(
&self,
for_worktree: WorktreeId,
- for_path: &Path,
+ for_path: &RelPath,
) -> Option<EditorconfigProperties> {
let mut properties = EditorconfigProperties::new();
@@ -947,7 +955,9 @@ impl SettingsStore {
properties = EditorconfigProperties::new();
}
for section in parsed_editorconfig.sections {
- section.apply_to(&mut properties, for_path).log_err()?;
+ section
+ .apply_to(&mut properties, for_path.as_std_path())
+ .log_err()?;
}
}
@@ -958,11 +968,11 @@ impl SettingsStore {
#[derive(Debug, Clone, PartialEq)]
pub enum InvalidSettingsError {
- LocalSettings { path: PathBuf, message: String },
+ LocalSettings { path: Arc<RelPath>, message: String },
UserSettings { message: String },
ServerSettings { message: String },
DefaultSettings { message: String },
- Editorconfig { path: PathBuf, message: String },
+ Editorconfig { path: Arc<RelPath>, message: String },
Tasks { path: PathBuf, message: String },
Debug { path: PathBuf, message: String },
}
@@ -1011,7 +1021,7 @@ impl<T: Settings> AnySettingValue for SettingValue<T> {
type_name::<T>()
}
- fn all_local_values(&self) -> Vec<(WorktreeId, Arc<Path>, &dyn Any)> {
+ fn all_local_values(&self) -> Vec<(WorktreeId, Arc<RelPath>, &dyn Any)> {
self.local_values
.iter()
.map(|(id, path, value)| (*id, path.clone(), value as _))
@@ -1036,7 +1046,7 @@ impl<T: Settings> AnySettingValue for SettingValue<T> {
self.global_value = Some(*value.downcast().unwrap());
}
- fn set_local_value(&mut self, root_id: WorktreeId, path: Arc<Path>, value: Box<dyn Any>) {
+ fn set_local_value(&mut self, root_id: WorktreeId, path: Arc<RelPath>, value: Box<dyn Any>) {
let value = *value.downcast().unwrap();
match self
.local_values
@@ -1067,6 +1077,7 @@ mod tests {
use super::*;
use unindent::Unindent;
+ use util::rel_path::rel_path;
#[derive(Debug, PartialEq)]
struct AutoUpdateSetting {
@@ -1178,7 +1189,7 @@ mod tests {
store
.set_local_settings(
WorktreeId::from_usize(1),
- Path::new("/root1").into(),
+ rel_path("root1").into(),
LocalSettingsKind::Settings,
Some(r#"{ "tab_size": 5 }"#),
cx,
@@ -1187,7 +1198,7 @@ mod tests {
store
.set_local_settings(
WorktreeId::from_usize(1),
- Path::new("/root1/subdir").into(),
+ rel_path("root1/subdir").into(),
LocalSettingsKind::Settings,
Some(r#"{ "preferred_line_length": 50 }"#),
cx,
@@ -1197,7 +1208,7 @@ mod tests {
store
.set_local_settings(
WorktreeId::from_usize(1),
- Path::new("/root2").into(),
+ rel_path("root2").into(),
LocalSettingsKind::Settings,
Some(r#"{ "tab_size": 9, "auto_update": true}"#),
cx,
@@ -1207,7 +1218,7 @@ mod tests {
assert_eq!(
store.get::<DefaultLanguageSettings>(Some(SettingsLocation {
worktree_id: WorktreeId::from_usize(1),
- path: Path::new("/root1/something"),
+ path: rel_path("root1/something"),
})),
&DefaultLanguageSettings {
preferred_line_length: 80,
@@ -1217,7 +1228,7 @@ mod tests {
assert_eq!(
store.get::<DefaultLanguageSettings>(Some(SettingsLocation {
worktree_id: WorktreeId::from_usize(1),
- path: Path::new("/root1/subdir/something")
+ path: rel_path("root1/subdir/something"),
})),
&DefaultLanguageSettings {
preferred_line_length: 50,
@@ -1227,7 +1238,7 @@ mod tests {
assert_eq!(
store.get::<DefaultLanguageSettings>(Some(SettingsLocation {
worktree_id: WorktreeId::from_usize(1),
- path: Path::new("/root2/something")
+ path: rel_path("root2/something"),
})),
&DefaultLanguageSettings {
preferred_line_length: 80,
@@ -1237,7 +1248,7 @@ mod tests {
assert_eq!(
store.get::<AutoUpdateSetting>(Some(SettingsLocation {
worktree_id: WorktreeId::from_usize(1),
- path: Path::new("/root2/something")
+ path: rel_path("root2/something")
})),
&AutoUpdateSetting { auto_update: false }
);
@@ -240,7 +240,6 @@ impl SvgPreviewView {
return file
.path()
.extension()
- .and_then(|ext| ext.to_str())
.map(|ext| ext.eq_ignore_ascii_case("svg"))
.unwrap_or(false);
}
@@ -4,8 +4,7 @@ use gpui::{TestAppContext, VisualTestContext};
use menu::SelectPrevious;
use project::{Project, ProjectPath};
use serde_json::json;
-use std::path::Path;
-use util::path;
+use util::{path, rel_path::rel_path};
use workspace::{AppState, Workspace};
#[ctor::ctor]
@@ -331,7 +330,7 @@ async fn open_buffer(
});
let project_path = ProjectPath {
worktree_id,
- path: Arc::from(Path::new(file_path)),
+ path: rel_path(file_path).into(),
};
workspace
.update_in(cx, move |workspace, window, cx| {
@@ -183,7 +183,7 @@ impl TasksModal {
id: _,
directory_in_worktree: dir,
id_base: _,
- } => dir.ends_with(".zed"),
+ } => dir.file_name().is_some_and(|name| name == ".zed"),
_ => false,
});
// todo(debugger): We're always adding lsp tasks here even if prefer_lsp is false
@@ -194,7 +194,7 @@ impl TasksModal {
TaskSourceKind::Worktree {
directory_in_worktree: dir,
..
- } => !(hide_vscode && dir.ends_with(".vscode")),
+ } => !(hide_vscode && dir.file_name().is_some_and(|name| name == ".vscode")),
TaskSourceKind::Language { .. } => add_current_language_tasks,
_ => true,
}
@@ -399,7 +399,7 @@ mod tests {
use serde_json::json;
use task::{TaskContext, TaskVariables, VariableName};
use ui::VisualContext;
- use util::path;
+ use util::{path, rel_path::rel_path};
use workspace::{AppState, Workspace};
use crate::task_contexts;
@@ -479,8 +479,9 @@ mod tests {
let buffer1 = workspace
.update(cx, |this, cx| {
- this.project()
- .update(cx, |this, cx| this.open_buffer((worktree_id, "a.ts"), cx))
+ this.project().update(cx, |this, cx| {
+ this.open_buffer((worktree_id, rel_path("a.ts")), cx)
+ })
})
.await
.unwrap();
@@ -493,7 +494,7 @@ mod tests {
let buffer2 = workspace
.update(cx, |this, cx| {
this.project().update(cx, |this, cx| {
- this.open_buffer((worktree_id, "rust/b.rs"), cx)
+ this.open_buffer((worktree_id, rel_path("rust/b.rs")), cx)
})
})
.await
@@ -4,9 +4,13 @@ use editor::Editor;
use gpui::{App, AppContext, Context, Task, WeakEntity, Window};
use itertools::Itertools;
use project::{Entry, Metadata};
-use std::path::{Path, PathBuf};
+use std::path::PathBuf;
use terminal::PathLikeTarget;
-use util::{ResultExt, debug_panic, paths::PathWithPosition};
+use util::{
+ ResultExt, debug_panic,
+ paths::{PathStyle, PathWithPosition},
+ rel_path::RelPath,
+};
use workspace::{OpenOptions, OpenVisible, Workspace};
/// The way we found the open target. This is important to have for test assertions.
@@ -179,8 +183,9 @@ fn possible_open_target(
let mut paths_to_check = Vec::with_capacity(potential_paths.len());
let relative_cwd = cwd
.and_then(|cwd| cwd.strip_prefix(&worktree_root).ok())
+ .and_then(|cwd| RelPath::from_std_path(cwd, PathStyle::local()).ok())
.and_then(|cwd_stripped| {
- (cwd_stripped != Path::new("")).then(|| {
+ (cwd_stripped.as_ref() != RelPath::empty()).then(|| {
is_cwd_in_worktree = true;
cwd_stripped
})
@@ -217,19 +222,21 @@ fn possible_open_target(
}
};
- if path_to_check.path.is_relative()
+ if let Ok(relative_path_to_check) =
+ RelPath::from_std_path(&path_to_check.path, PathStyle::local())
&& !worktree.read(cx).is_single_file()
&& let Some(entry) = relative_cwd
+ .clone()
.and_then(|relative_cwd| {
worktree
.read(cx)
- .entry_for_path(&relative_cwd.join(&path_to_check.path))
+ .entry_for_path(&relative_cwd.join(&relative_path_to_check))
})
- .or_else(|| worktree.read(cx).entry_for_path(&path_to_check.path))
+ .or_else(|| worktree.read(cx).entry_for_path(&relative_path_to_check))
{
open_target = Some(OpenTarget::Worktree(
PathWithPosition {
- path: worktree_root.join(&entry.path),
+ path: worktree.read(cx).absolutize(&entry.path),
row: path_to_check.row,
column: path_to_check.column,
},
@@ -357,16 +364,18 @@ fn possible_open_target(
for (worktree, worktree_paths_to_check) in worktree_paths_to_check {
let found_entry = worktree
.update(cx, |worktree, _| -> Option<OpenTarget> {
- let worktree_root = worktree.abs_path();
- let traversal = worktree.traverse_from_path(true, true, false, "".as_ref());
+ let traversal =
+ worktree.traverse_from_path(true, true, false, RelPath::empty());
for entry in traversal {
- if let Some(path_in_worktree) = worktree_paths_to_check
- .iter()
- .find(|path_to_check| entry.path.ends_with(&path_to_check.path))
+ if let Some(path_in_worktree) =
+ worktree_paths_to_check.iter().find(|path_to_check| {
+ RelPath::from_std_path(&path_to_check.path, PathStyle::local())
+ .is_ok_and(|path| entry.path.ends_with(&path))
+ })
{
return Some(OpenTarget::Worktree(
PathWithPosition {
- path: worktree_root.join(&entry.path),
+ path: worktree.absolutize(&entry.path),
row: path_in_worktree.row,
column: path_in_worktree.column,
},
@@ -536,7 +545,7 @@ mod tests {
fs.insert_tree(path, tree).await;
}
- let project = Project::test(
+ let project: gpui::Entity<Project> = Project::test(
fs.clone(),
worktree_roots.into_iter().map(Path::new),
app_cx,
@@ -1005,30 +1014,32 @@ mod tests {
test_local!(
"foo/./bar.txt",
"/tmp/issue28339/foo/bar.txt",
- "/tmp/issue28339"
+ "/tmp/issue28339",
+ WorktreeExact
);
test_local!(
"foo/../foo/bar.txt",
"/tmp/issue28339/foo/bar.txt",
"/tmp/issue28339",
- FileSystemBackground
+ WorktreeExact
);
test_local!(
"foo/..///foo/bar.txt",
"/tmp/issue28339/foo/bar.txt",
"/tmp/issue28339",
- FileSystemBackground
+ WorktreeExact
);
test_local!(
"issue28339/../issue28339/foo/../foo/bar.txt",
"/tmp/issue28339/foo/bar.txt",
"/tmp/issue28339",
- FileSystemBackground
+ WorktreeExact
);
test_local!(
"./bar.txt",
"/tmp/issue28339/foo/bar.txt",
- "/tmp/issue28339/foo"
+ "/tmp/issue28339/foo",
+ WorktreeExact
);
test_local!(
"../foo/bar.txt",
@@ -1574,6 +1574,7 @@ mod tests {
use gpui::TestAppContext;
use project::{Entry, Project, ProjectPath, Worktree};
use std::path::Path;
+ use util::rel_path::RelPath;
use workspace::AppState;
// Working directory calculation tests
@@ -1735,7 +1736,7 @@ mod tests {
let entry = cx
.update(|cx| {
wt.update(cx, |wt, cx| {
- wt.create_entry(Path::new(""), is_dir, None, cx)
+ wt.create_entry(RelPath::empty().into(), is_dir, None, cx)
})
})
.await
@@ -33,14 +33,14 @@ use onboarding_banner::OnboardingBanner;
use project::{Project, WorktreeSettings};
use remote::RemoteConnectionOptions;
use settings::{Settings, SettingsLocation};
-use std::{path::Path, sync::Arc};
+use std::sync::Arc;
use theme::ActiveTheme;
use title_bar_settings::TitleBarSettings;
use ui::{
Avatar, Button, ButtonLike, ButtonStyle, Chip, ContextMenu, Icon, IconName, IconSize,
IconWithIndicator, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, h_flex, prelude::*,
};
-use util::ResultExt;
+use util::{ResultExt, rel_path::RelPath};
use workspace::{Workspace, notifications::NotifyResultExt};
use zed_actions::{OpenRecent, OpenRemote};
@@ -439,13 +439,13 @@ impl TitleBar {
let worktree = worktree.read(cx);
let settings_location = SettingsLocation {
worktree_id: worktree.id(),
- path: Path::new(""),
+ path: RelPath::empty(),
};
let settings = WorktreeSettings::get(Some(settings_location), cx);
match &settings.project_name {
Some(name) => name.as_str(),
- None => worktree.root_name(),
+ None => worktree.root_name_str(),
}
})
.next();
@@ -1,4 +1,4 @@
-use std::{path::Path, sync::Arc};
+use std::sync::Arc;
use editor::Editor;
use gpui::{
@@ -8,7 +8,7 @@ use gpui::{
use language::{Buffer, BufferEvent, LanguageName, Toolchain, ToolchainScope};
use project::{Project, ProjectPath, Toolchains, WorktreeId, toolchain_store::ToolchainStoreEvent};
use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, SharedString, Tooltip};
-use util::maybe;
+use util::{maybe, rel_path::RelPath};
use workspace::{StatusItemView, Workspace, item::ItemHandle};
use crate::ToolchainSelector;
@@ -83,10 +83,7 @@ impl ActiveToolchain {
let (worktree_id, path) = active_file
.update(cx, |this, cx| {
this.file().and_then(|file| {
- Some((
- file.worktree_id(cx),
- Arc::<Path>::from(file.path().parent()?),
- ))
+ Some((file.worktree_id(cx), file.path().parent()?.into()))
})
})
.ok()
@@ -142,7 +139,7 @@ impl ActiveToolchain {
fn active_toolchain(
workspace: WeakEntity<Workspace>,
worktree_id: WorktreeId,
- relative_path: Arc<Path>,
+ relative_path: Arc<RelPath>,
language_name: LanguageName,
cx: &mut AsyncWindowContext,
) -> Task<Option<Toolchain>> {
@@ -205,7 +202,7 @@ impl ActiveToolchain {
.set_toolchain(
workspace_id,
worktree_id,
- relative_path.to_string_lossy().into_owned(),
+ relative_path.clone(),
toolchain.clone(),
)
.await
@@ -24,7 +24,7 @@ use ui::{
Divider, HighlightedLabel, KeyBinding, List, ListItem, ListItemSpacing, Navigable,
NavigableEntry, prelude::*,
};
-use util::{ResultExt, maybe, paths::PathStyle};
+use util::{ResultExt, maybe, paths::PathStyle, rel_path::RelPath};
use workspace::{ModalView, Workspace};
actions!(
@@ -48,7 +48,7 @@ pub struct ToolchainSelector {
project: Entity<Project>,
language_name: LanguageName,
worktree_id: WorktreeId,
- relative_path: Arc<Path>,
+ relative_path: Arc<RelPath>,
}
#[derive(Clone)]
@@ -132,7 +132,7 @@ impl AddToolchainState {
tx,
DirectoryLister::Project(project),
false,
- PathStyle::current(),
+ PathStyle::local(),
)
.show_hidden()
.with_footer(Arc::new(move |_, cx| {
@@ -241,9 +241,7 @@ impl AddToolchainState {
// Suggest a default scope based on the applicability.
let scope = if let Some(project_path) = resolved_toolchain_path {
- if root_path.path.as_ref() != Path::new("")
- && project_path.starts_with(&root_path)
- {
+ if !root_path.path.as_ref().is_empty() && project_path.starts_with(&root_path) {
ToolchainScope::Subproject(root_path.worktree_id, root_path.path)
} else {
ToolchainScope::Project
@@ -584,7 +582,7 @@ impl ToolchainSelector {
let language_name = buffer.read(cx).language()?.name();
let worktree_id = buffer.read(cx).file()?.worktree_id(cx);
- let relative_path: Arc<Path> = Arc::from(buffer.read(cx).file()?.path().parent()?);
+ let relative_path: Arc<RelPath> = buffer.read(cx).file()?.path().parent()?.into();
let worktree_root_path = project
.read(cx)
.worktree_for_id(worktree_id, cx)?
@@ -593,9 +591,13 @@ impl ToolchainSelector {
let workspace_id = workspace.database_id()?;
let weak = workspace.weak_handle();
cx.spawn_in(window, async move |workspace, cx| {
- let as_str = relative_path.to_string_lossy().into_owned();
let active_toolchain = workspace::WORKSPACE_DB
- .toolchain(workspace_id, worktree_id, as_str, language_name.clone())
+ .toolchain(
+ workspace_id,
+ worktree_id,
+ relative_path.clone(),
+ language_name.clone(),
+ )
.await
.ok()
.flatten();
@@ -628,7 +630,7 @@ impl ToolchainSelector {
active_toolchain: Option<Toolchain>,
worktree_id: WorktreeId,
worktree_root: Arc<Path>,
- relative_path: Arc<Path>,
+ relative_path: Arc<RelPath>,
language_name: LanguageName,
window: &mut Window,
cx: &mut Context<Self>,
@@ -741,7 +743,7 @@ pub struct ToolchainSelectorDelegate {
workspace: WeakEntity<Workspace>,
worktree_id: WorktreeId,
worktree_abs_path_root: Arc<Path>,
- relative_path: Arc<Path>,
+ relative_path: Arc<RelPath>,
placeholder_text: Arc<str>,
add_toolchain_text: Arc<str>,
project: Entity<Project>,
@@ -757,12 +759,13 @@ impl ToolchainSelectorDelegate {
worktree_id: WorktreeId,
worktree_abs_path_root: Arc<Path>,
project: Entity<Project>,
- relative_path: Arc<Path>,
+ relative_path: Arc<RelPath>,
language_name: LanguageName,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Self {
let _project = project.clone();
+ let path_style = project.read(cx).path_style(cx);
let _fetch_candidates_task = cx.spawn_in(window, {
async move |this, cx| {
@@ -802,11 +805,10 @@ impl ToolchainSelectorDelegate {
.ok()?
.await?;
let pretty_path = {
- let path = relative_path.to_string_lossy();
- if path.is_empty() {
+ if relative_path.is_empty() {
Cow::Borrowed("worktree root")
} else {
- Cow::Owned(format!("`{}`", path))
+ Cow::Owned(format!("`{}`", relative_path.display(path_style)))
}
};
let placeholder_text =
@@ -898,7 +900,7 @@ impl PickerDelegate for ToolchainSelectorDelegate {
let workspace = self.workspace.clone();
let worktree_id = self.worktree_id;
let path = self.relative_path.clone();
- let relative_path = self.relative_path.to_string_lossy().into_owned();
+ let relative_path = self.relative_path.clone();
cx.spawn_in(window, async move |_, cx| {
workspace::WORKSPACE_DB
.set_toolchain(workspace_id, worktree_id, relative_path, toolchain.clone())
@@ -21,6 +21,14 @@ impl HighlightedLabel {
highlight_indices,
}
}
+
+ pub fn text(&self) -> &str {
+ self.label.as_str()
+ }
+
+ pub fn highlight_indices(&self) -> &[usize] {
+ &self.highlight_indices
+ }
}
impl LabelCommon for HighlightedLabel {
@@ -21,6 +21,7 @@ async-fs.workspace = true
async_zip.workspace = true
collections.workspace = true
dirs.workspace = true
+dunce = "1.0"
futures-lite.workspace = true
futures.workspace = true
git2 = { workspace = true, optional = true }
@@ -50,7 +51,6 @@ nix = { workspace = true, features = ["user"] }
[target.'cfg(windows)'.dependencies]
tendril = "0.4.3"
-dunce = "1.0"
[dev-dependencies]
git2.workspace = true
@@ -31,17 +31,9 @@ pub fn home_dir() -> &'static PathBuf {
})
}
-#[cfg(any(test, feature = "test-support"))]
-pub fn set_home_dir(path: PathBuf) {
- HOME_DIR
- .set(path)
- .expect("set_home_dir called after home_dir was already accessed");
-}
-
pub trait PathExt {
fn compact(&self) -> PathBuf;
fn extension_or_hidden_file_name(&self) -> Option<&str>;
- fn to_sanitized_string(&self) -> String;
fn try_from_bytes<'a>(bytes: &'a [u8]) -> anyhow::Result<Self>
where
Self: From<&'a Path>,
@@ -106,20 +98,6 @@ impl<T: AsRef<Path>> PathExt for T {
.or_else(|| path.file_stem()?.to_str())
}
- /// Returns a sanitized string representation of the path.
- /// Note, on Windows, this assumes that the path is a valid UTF-8 string and
- /// is not a UNC path.
- fn to_sanitized_string(&self) -> String {
- #[cfg(target_os = "windows")]
- {
- self.as_ref().to_string_lossy().replace("/", "\\")
- }
- #[cfg(not(target_os = "windows"))]
- {
- self.as_ref().to_string_lossy().to_string()
- }
- }
-
/// Converts a local path to one that can be used inside of WSL.
/// Returns `None` if the path cannot be converted into a WSL one (network share).
fn local_to_wsl(&self) -> Option<PathBuf> {
@@ -220,17 +198,6 @@ impl SanitizedPath {
pub fn to_path_buf(&self) -> PathBuf {
self.0.to_path_buf()
}
-
- pub fn to_glob_string(&self) -> String {
- #[cfg(target_os = "windows")]
- {
- self.0.to_string_lossy().replace("/", "\\")
- }
- #[cfg(not(target_os = "windows"))]
- {
- self.0.to_string_lossy().to_string()
- }
- }
}
impl std::fmt::Debug for SanitizedPath {
@@ -265,7 +232,7 @@ impl AsRef<Path> for SanitizedPath {
}
}
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PathStyle {
Posix,
Windows,
@@ -273,83 +240,69 @@ pub enum PathStyle {
impl PathStyle {
#[cfg(target_os = "windows")]
- pub const fn current() -> Self {
+ pub const fn local() -> Self {
PathStyle::Windows
}
#[cfg(not(target_os = "windows"))]
- pub const fn current() -> Self {
+ pub const fn local() -> Self {
PathStyle::Posix
}
#[inline]
- pub fn separator(&self) -> &str {
+ pub fn separator(&self) -> &'static str {
match self {
PathStyle::Posix => "/",
PathStyle::Windows => "\\",
}
}
+
+ pub fn is_windows(&self) -> bool {
+ *self == PathStyle::Windows
+ }
+
+ pub fn join(self, left: impl AsRef<Path>, right: impl AsRef<Path>) -> Option<String> {
+ let right = right.as_ref().to_str()?;
+ if is_absolute(right, self) {
+ return None;
+ }
+ let left = left.as_ref().to_str()?;
+ if left.is_empty() {
+ Some(right.into())
+ } else {
+ Some(format!(
+ "{left}{}{right}",
+ if left.ends_with(self.separator()) {
+ ""
+ } else {
+ self.separator()
+ }
+ ))
+ }
+ }
}
#[derive(Debug, Clone)]
pub struct RemotePathBuf {
- inner: PathBuf,
style: PathStyle,
- string: String, // Cached string representation
+ string: String,
}
impl RemotePathBuf {
- pub fn new(path: PathBuf, style: PathStyle) -> Self {
- #[cfg(target_os = "windows")]
- let string = match style {
- PathStyle::Posix => path.to_string_lossy().replace('\\', "/"),
- PathStyle::Windows => path.to_string_lossy().into(),
- };
- #[cfg(not(target_os = "windows"))]
- let string = match style {
- PathStyle::Posix => path.to_string_lossy().to_string(),
- PathStyle::Windows => path.to_string_lossy().replace('/', "\\"),
- };
- Self {
- inner: path,
- style,
- string,
- }
+ pub fn new(string: String, style: PathStyle) -> Self {
+ Self { style, string }
}
pub fn from_str(path: &str, style: PathStyle) -> Self {
- let path_buf = PathBuf::from(path);
- Self::new(path_buf, style)
- }
-
- #[cfg(target_os = "windows")]
- pub fn to_proto(&self) -> String {
- match self.path_style() {
- PathStyle::Posix => self.to_string(),
- PathStyle::Windows => self.inner.to_string_lossy().replace('\\', "/"),
- }
- }
-
- #[cfg(not(target_os = "windows"))]
- pub fn to_proto(&self) -> String {
- match self.path_style() {
- PathStyle::Posix => self.inner.to_string_lossy().to_string(),
- PathStyle::Windows => self.to_string(),
- }
- }
-
- pub fn as_path(&self) -> &Path {
- &self.inner
+ Self::new(path.to_string(), style)
}
pub fn path_style(&self) -> PathStyle {
self.style
}
- pub fn parent(&self) -> Option<RemotePathBuf> {
- self.inner
- .parent()
- .map(|p| RemotePathBuf::new(p.to_path_buf(), self.style))
+ pub fn to_proto(self) -> String {
+ self.string
}
}
@@ -359,6 +312,19 @@ impl Display for RemotePathBuf {
}
}
+pub fn is_absolute(path_like: &str, path_style: PathStyle) -> bool {
+ path_like.starts_with('/')
+ || path_style == PathStyle::Windows
+ && (path_like.starts_with('\\')
+ || path_like
+ .chars()
+ .next()
+ .is_some_and(|c| c.is_ascii_alphabetic())
+ && path_like[1..]
+ .strip_prefix(':')
+ .is_some_and(|path| path.starts_with('/') || path.starts_with('\\')))
+}
+
/// A delimiter to use in `path_query:row_number:column_number` strings parsing.
pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
@@ -589,10 +555,11 @@ impl PathWithPosition {
}
}
-#[derive(Clone, Debug, Default)]
+#[derive(Clone, Debug)]
pub struct PathMatcher {
sources: Vec<String>,
glob: GlobSet,
+ path_style: PathStyle,
}
// impl std::fmt::Display for PathMatcher {
@@ -610,7 +577,10 @@ impl PartialEq for PathMatcher {
impl Eq for PathMatcher {}
impl PathMatcher {
- pub fn new(globs: impl IntoIterator<Item = impl AsRef<str>>) -> Result<Self, globset::Error> {
+ pub fn new(
+ globs: impl IntoIterator<Item = impl AsRef<str>>,
+ path_style: PathStyle,
+ ) -> Result<Self, globset::Error> {
let globs = globs
.into_iter()
.map(|as_str| Glob::new(as_str.as_ref()))
@@ -621,7 +591,11 @@ impl PathMatcher {
glob_builder.add(single_glob);
}
let glob = glob_builder.build()?;
- Ok(PathMatcher { glob, sources })
+ Ok(PathMatcher {
+ glob,
+ sources,
+ path_style,
+ })
}
pub fn sources(&self) -> &[String] {
@@ -639,7 +613,7 @@ impl PathMatcher {
fn check_with_end_separator(&self, path: &Path) -> bool {
let path_str = path.to_string_lossy();
- let separator = std::path::MAIN_SEPARATOR_STR;
+ let separator = self.path_style.separator();
if path_str.ends_with(separator) {
false
} else {
@@ -648,6 +622,16 @@ impl PathMatcher {
}
}
+impl Default for PathMatcher {
+ fn default() -> Self {
+ Self {
+ path_style: PathStyle::local(),
+ glob: GlobSet::empty(),
+ sources: vec![],
+ }
+ }
+}
+
/// Custom character comparison that prioritizes lowercase for same letters
fn compare_chars(a: char, b: char) -> Ordering {
// First compare case-insensitive
@@ -1275,7 +1259,8 @@ mod tests {
#[test]
fn edge_of_glob() {
let path = Path::new("/work/node_modules");
- let path_matcher = PathMatcher::new(&["**/node_modules/**".to_owned()]).unwrap();
+ let path_matcher =
+ PathMatcher::new(&["**/node_modules/**".to_owned()], PathStyle::Posix).unwrap();
assert!(
path_matcher.is_match(path),
"Path matcher should match {path:?}"
@@ -1285,7 +1270,8 @@ mod tests {
#[test]
fn project_search() {
let path = Path::new("/Users/someonetoignore/work/zed/zed.dev/node_modules");
- let path_matcher = PathMatcher::new(&["**/node_modules/**".to_owned()]).unwrap();
+ let path_matcher =
+ PathMatcher::new(&["**/node_modules/**".to_owned()], PathStyle::Posix).unwrap();
assert!(
path_matcher.is_match(path),
"Path matcher should match {path:?}"
@@ -0,0 +1,515 @@
+use crate::paths::{PathStyle, is_absolute};
+use anyhow::{Context as _, Result, anyhow, bail};
+use serde::{Deserialize, Serialize};
+use std::{
+ borrow::Cow,
+ ffi::OsStr,
+ fmt,
+ ops::Deref,
+ path::{Path, PathBuf},
+ sync::Arc,
+};
+
+#[repr(transparent)]
+#[derive(PartialEq, Eq, Hash, Serialize)]
+pub struct RelPath(str);
+
+#[derive(Clone, Serialize, Deserialize)]
+pub struct RelPathBuf(String);
+
+impl RelPath {
+ pub fn empty() -> &'static Self {
+ unsafe { Self::new_unchecked("") }
+ }
+
+ #[track_caller]
+ pub fn new<S: AsRef<str> + ?Sized>(s: &S) -> anyhow::Result<&Self> {
+ let this = unsafe { Self::new_unchecked(s) };
+ if this.0.starts_with("/")
+ || this.0.ends_with("/")
+ || this
+ .components()
+ .any(|component| component == ".." || component == "." || component.is_empty())
+ {
+ bail!("invalid relative path: {:?}", &this.0);
+ }
+ Ok(this)
+ }
+
+ #[track_caller]
+ pub fn from_std_path(path: &Path, path_style: PathStyle) -> Result<Arc<Self>> {
+ let path = path.to_str().context("non utf-8 path")?;
+ let mut string = Cow::Borrowed(path);
+
+ if is_absolute(&string, path_style) {
+ return Err(anyhow!("absolute path not allowed: {path:?}"));
+ }
+
+ if path_style == PathStyle::Windows {
+ string = Cow::Owned(string.as_ref().replace('\\', "/"))
+ }
+
+ let mut this = RelPathBuf::new();
+ for component in unsafe { Self::new_unchecked(string.as_ref()) }.components() {
+ match component {
+ "" => {}
+ "." => {}
+ ".." => {
+ if !this.pop() {
+ return Err(anyhow!("path is not relative: {string:?}"));
+ }
+ }
+ other => this.push(RelPath::new(other)?),
+ }
+ }
+
+ Ok(this.into())
+ }
+
+ pub unsafe fn new_unchecked<S: AsRef<str> + ?Sized>(s: &S) -> &Self {
+ unsafe { &*(s.as_ref() as *const str as *const Self) }
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.0.is_empty()
+ }
+
+ pub fn components(&self) -> RelPathComponents<'_> {
+ RelPathComponents(&self.0)
+ }
+
+ pub fn ancestors(&self) -> RelPathAncestors<'_> {
+ RelPathAncestors(Some(&self.0))
+ }
+
+ pub fn file_name(&self) -> Option<&str> {
+ self.components().next_back()
+ }
+
+ pub fn file_stem(&self) -> Option<&str> {
+ Some(self.as_std_path().file_stem()?.to_str().unwrap())
+ }
+
+ pub fn extension(&self) -> Option<&str> {
+ Some(self.as_std_path().extension()?.to_str().unwrap())
+ }
+
+ pub fn parent(&self) -> Option<&Self> {
+ let mut components = self.components();
+ components.next_back()?;
+ Some(components.rest())
+ }
+
+ pub fn starts_with(&self, other: &Self) -> bool {
+ self.strip_prefix(other).is_ok()
+ }
+
+ pub fn ends_with(&self, other: &Self) -> bool {
+ if let Some(suffix) = self.0.strip_suffix(&other.0) {
+ if suffix.ends_with('/') {
+ return true;
+ } else if suffix.is_empty() {
+ return true;
+ }
+ }
+ false
+ }
+
+ pub fn strip_prefix(&self, other: &Self) -> Result<&Self> {
+ if other.is_empty() {
+ return Ok(self);
+ }
+ if let Some(suffix) = self.0.strip_prefix(&other.0) {
+ if let Some(suffix) = suffix.strip_prefix('/') {
+ return Ok(unsafe { Self::new_unchecked(suffix) });
+ } else if suffix.is_empty() {
+ return Ok(Self::empty());
+ }
+ }
+ Err(anyhow!("failed to strip prefix: {other:?} from {self:?}"))
+ }
+
+ pub fn len(&self) -> usize {
+ self.0.matches('/').count() + 1
+ }
+
+ pub fn last_n_components(&self, count: usize) -> Option<&Self> {
+ let len = self.len();
+ if len >= count {
+ let mut components = self.components();
+ for _ in 0..(len - count) {
+ components.next()?;
+ }
+ Some(components.rest())
+ } else {
+ None
+ }
+ }
+
+ pub fn push(&self, component: &str) -> Result<Arc<Self>> {
+ if component.is_empty() {
+ bail!("pushed component is empty");
+ } else if component.contains('/') {
+ bail!("pushed component contains a separator: {component:?}");
+ }
+ let path = format!(
+ "{}{}{}",
+ &self.0,
+ if self.is_empty() { "" } else { "/" },
+ component
+ );
+ Ok(Arc::from(unsafe { Self::new_unchecked(&path) }))
+ }
+
+ pub fn join(&self, other: &Self) -> Arc<Self> {
+ let result = if self.0.is_empty() {
+ Cow::Borrowed(&other.0)
+ } else if other.0.is_empty() {
+ Cow::Borrowed(&self.0)
+ } else {
+ Cow::Owned(format!("{}/{}", &self.0, &other.0))
+ };
+ Arc::from(unsafe { Self::new_unchecked(result.as_ref()) })
+ }
+
+ pub fn to_proto(&self) -> String {
+ self.0.to_owned()
+ }
+
+ pub fn to_rel_path_buf(&self) -> RelPathBuf {
+ RelPathBuf(self.0.to_string())
+ }
+
+ pub fn from_proto(path: &str) -> Result<Arc<Self>> {
+ Ok(Arc::from(Self::new(path)?))
+ }
+
+ pub fn as_str(&self) -> &str {
+ &self.0
+ }
+
+ pub fn display(&self, style: PathStyle) -> Cow<'_, str> {
+ match style {
+ PathStyle::Posix => Cow::Borrowed(&self.0),
+ PathStyle::Windows => Cow::Owned(self.0.replace('/', "\\")),
+ }
+ }
+
+ pub fn as_bytes(&self) -> &[u8] {
+ &self.0.as_bytes()
+ }
+
+ pub fn as_os_str(&self) -> &OsStr {
+ self.0.as_ref()
+ }
+
+ pub fn as_std_path(&self) -> &Path {
+ Path::new(&self.0)
+ }
+}
+
+impl PartialOrd for RelPath {
+ fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl Ord for RelPath {
+ fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+ self.components().cmp(other.components())
+ }
+}
+
+impl fmt::Debug for RelPath {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ fmt::Debug::fmt(&self.0, f)
+ }
+}
+
+impl fmt::Debug for RelPathBuf {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ fmt::Debug::fmt(&self.0, f)
+ }
+}
+
+impl RelPathBuf {
+ pub fn new() -> Self {
+ Self(String::new())
+ }
+
+ pub fn pop(&mut self) -> bool {
+ if let Some(ix) = self.0.rfind('/') {
+ self.0.truncate(ix);
+ true
+ } else if !self.is_empty() {
+ self.0.clear();
+ true
+ } else {
+ false
+ }
+ }
+
+ pub fn push(&mut self, path: &RelPath) {
+ if !self.is_empty() {
+ self.0.push('/');
+ }
+ self.0.push_str(&path.0);
+ }
+
+ pub fn as_rel_path(&self) -> &RelPath {
+ unsafe { RelPath::new_unchecked(self.0.as_str()) }
+ }
+
+ pub fn set_extension(&mut self, extension: &str) -> bool {
+ if let Some(filename) = self.file_name() {
+ let mut filename = PathBuf::from(filename);
+ filename.set_extension(extension);
+ self.pop();
+ self.0.push_str(filename.to_str().unwrap());
+ true
+ } else {
+ false
+ }
+ }
+}
+
+impl Into<Arc<RelPath>> for RelPathBuf {
+ fn into(self) -> Arc<RelPath> {
+ Arc::from(self.as_rel_path())
+ }
+}
+
+impl AsRef<RelPath> for RelPathBuf {
+ fn as_ref(&self) -> &RelPath {
+ self.as_rel_path()
+ }
+}
+
+impl Deref for RelPathBuf {
+ type Target = RelPath;
+
+ fn deref(&self) -> &Self::Target {
+ self.as_ref()
+ }
+}
+
+impl AsRef<Path> for RelPath {
+ fn as_ref(&self) -> &Path {
+ Path::new(&self.0)
+ }
+}
+
+impl From<&RelPath> for Arc<RelPath> {
+ fn from(rel_path: &RelPath) -> Self {
+ let bytes: Arc<str> = Arc::from(&rel_path.0);
+ unsafe { Arc::from_raw(Arc::into_raw(bytes) as *const RelPath) }
+ }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+#[track_caller]
+pub fn rel_path(path: &str) -> &RelPath {
+ RelPath::new(path).unwrap()
+}
+
+impl PartialEq<str> for RelPath {
+ fn eq(&self, other: &str) -> bool {
+ self.0 == *other
+ }
+}
+
+pub struct RelPathComponents<'a>(&'a str);
+
+pub struct RelPathAncestors<'a>(Option<&'a str>);
+
+const SEPARATOR: char = '/';
+
+impl<'a> RelPathComponents<'a> {
+ pub fn rest(&self) -> &'a RelPath {
+ unsafe { RelPath::new_unchecked(self.0) }
+ }
+}
+
+impl<'a> Iterator for RelPathComponents<'a> {
+ type Item = &'a str;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if let Some(sep_ix) = self.0.find(SEPARATOR) {
+ let (head, tail) = self.0.split_at(sep_ix);
+ self.0 = &tail[1..];
+ Some(head)
+ } else if self.0.is_empty() {
+ None
+ } else {
+ let result = self.0;
+ self.0 = "";
+ Some(result)
+ }
+ }
+}
+
+impl<'a> Iterator for RelPathAncestors<'a> {
+ type Item = &'a RelPath;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let result = self.0?;
+ if let Some(sep_ix) = result.rfind(SEPARATOR) {
+ self.0 = Some(&result[..sep_ix]);
+ } else if !result.is_empty() {
+ self.0 = Some("");
+ } else {
+ self.0 = None;
+ }
+ Some(unsafe { RelPath::new_unchecked(result) })
+ }
+}
+
+impl<'a> DoubleEndedIterator for RelPathComponents<'a> {
+ fn next_back(&mut self) -> Option<Self::Item> {
+ if let Some(sep_ix) = self.0.rfind(SEPARATOR) {
+ let (head, tail) = self.0.split_at(sep_ix);
+ self.0 = head;
+ Some(&tail[1..])
+ } else if self.0.is_empty() {
+ None
+ } else {
+ let result = self.0;
+ self.0 = "";
+ Some(result)
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use itertools::Itertools;
+ use std::path::PathBuf;
+
+ #[test]
+ fn test_path_construction() {
+ assert!(RelPath::new("/").is_err());
+ assert!(RelPath::new("/foo").is_err());
+ assert!(RelPath::new("foo/").is_err());
+ assert!(RelPath::new("foo//bar").is_err());
+ assert!(RelPath::new("foo/../bar").is_err());
+ assert!(RelPath::new("./foo/bar").is_err());
+ assert!(RelPath::new("..").is_err());
+
+ assert!(RelPath::from_std_path(Path::new("/"), PathStyle::local()).is_err());
+ assert!(RelPath::from_std_path(Path::new("//"), PathStyle::local()).is_err());
+ assert!(RelPath::from_std_path(Path::new("/foo/"), PathStyle::local()).is_err());
+ assert_eq!(
+ RelPath::from_std_path(&PathBuf::from_iter(["foo", ""]), PathStyle::local()).unwrap(),
+ Arc::from(rel_path("foo"))
+ );
+ }
+
+ #[test]
+ fn test_rel_path_from_std_path() {
+ assert_eq!(
+ RelPath::from_std_path(Path::new("foo/bar/../baz/./quux/"), PathStyle::local())
+ .unwrap()
+ .as_ref(),
+ rel_path("foo/baz/quux")
+ );
+ }
+
+ #[test]
+ fn test_rel_path_components() {
+ let path = rel_path("foo/bar/baz");
+ assert_eq!(
+ path.components().collect::<Vec<_>>(),
+ vec!["foo", "bar", "baz"]
+ );
+ assert_eq!(
+ path.components().rev().collect::<Vec<_>>(),
+ vec!["baz", "bar", "foo"]
+ );
+
+ let path = rel_path("");
+ let mut components = path.components();
+ assert_eq!(components.next(), None);
+ }
+
+ #[test]
+ fn test_rel_path_ancestors() {
+ let path = rel_path("foo/bar/baz");
+ let mut ancestors = path.ancestors();
+ assert_eq!(ancestors.next(), Some(rel_path("foo/bar/baz")));
+ assert_eq!(ancestors.next(), Some(rel_path("foo/bar")));
+ assert_eq!(ancestors.next(), Some(rel_path("foo")));
+ assert_eq!(ancestors.next(), Some(rel_path("")));
+ assert_eq!(ancestors.next(), None);
+
+ let path = rel_path("foo");
+ let mut ancestors = path.ancestors();
+ assert_eq!(ancestors.next(), Some(rel_path("foo")));
+ assert_eq!(ancestors.next(), Some(RelPath::empty()));
+ assert_eq!(ancestors.next(), None);
+
+ let path = RelPath::empty();
+ let mut ancestors = path.ancestors();
+ assert_eq!(ancestors.next(), Some(RelPath::empty()));
+ assert_eq!(ancestors.next(), None);
+ }
+
+ #[test]
+ fn test_rel_path_parent() {
+ assert_eq!(
+ rel_path("foo/bar/baz").parent(),
+ Some(RelPath::new("foo/bar").unwrap())
+ );
+ assert_eq!(rel_path("foo").parent(), Some(RelPath::empty()));
+ assert_eq!(rel_path("").parent(), None);
+ }
+
+ #[test]
+ fn test_rel_path_partial_ord_is_compatible_with_std() {
+ let test_cases = ["a/b/c", "relative/path/with/dot.", "relative/path/with.dot"];
+ for [lhs, rhs] in test_cases.iter().array_combinations::<2>() {
+ assert_eq!(
+ Path::new(lhs).cmp(Path::new(rhs)),
+ RelPath::new(lhs).unwrap().cmp(RelPath::new(rhs).unwrap())
+ );
+ }
+ }
+
+ #[test]
+ fn test_strip_prefix() {
+ let parent = rel_path("");
+ let child = rel_path(".foo");
+
+ assert!(child.starts_with(parent));
+ assert_eq!(child.strip_prefix(parent).unwrap(), child);
+ }
+
+ #[test]
+ fn test_rel_path_constructors_absolute_path() {
+ assert!(RelPath::from_std_path(Path::new("/a/b"), PathStyle::Windows).is_err());
+ assert!(RelPath::from_std_path(Path::new("\\a\\b"), PathStyle::Windows).is_err());
+ assert!(RelPath::from_std_path(Path::new("/a/b"), PathStyle::Posix).is_err());
+ assert!(RelPath::from_std_path(Path::new("C:/a/b"), PathStyle::Windows).is_err());
+ assert!(RelPath::from_std_path(Path::new("C:\\a\\b"), PathStyle::Windows).is_err());
+ assert!(RelPath::from_std_path(Path::new("C:/a/b"), PathStyle::Posix).is_ok());
+ }
+
+ #[test]
+ fn test_push() {
+ assert_eq!(rel_path("a/b").push("c").unwrap().as_str(), "a/b/c");
+ assert_eq!(rel_path("").push("c").unwrap().as_str(), "c");
+ assert!(rel_path("a/b").push("").is_err());
+ assert!(rel_path("a/b").push("c/d").is_err());
+ }
+
+ #[test]
+ fn test_pop() {
+ let mut path = rel_path("a/b").to_rel_path_buf();
+ path.pop();
+ assert_eq!(path.as_rel_path().as_str(), "a");
+ path.pop();
+ assert_eq!(path.as_rel_path().as_str(), "");
+ path.pop();
+ assert_eq!(path.as_rel_path().as_str(), "");
+ }
+}
@@ -5,6 +5,7 @@ pub mod fs;
pub mod markdown;
pub mod paths;
pub mod redact;
+pub mod rel_path;
pub mod schemars;
pub mod serde;
pub mod shell_env;
@@ -1,4 +1,4 @@
-use anyhow::Result;
+use anyhow::{Result, anyhow};
use collections::{HashMap, HashSet};
use command_palette_hooks::CommandInterceptResult;
use editor::{
@@ -6,7 +6,7 @@ use editor::{
actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive},
display_map::ToDisplayPoint,
};
-use gpui::{Action, App, AppContext as _, Context, Global, Keystroke, Window, actions};
+use gpui::{Action, App, AppContext as _, Context, Global, Keystroke, Task, Window, actions};
use itertools::Itertools;
use language::Point;
use multi_buffer::MultiBufferRow;
@@ -22,12 +22,12 @@ use std::{
path::Path,
process::Stdio,
str::Chars,
- sync::{Arc, OnceLock},
+ sync::OnceLock,
time::Instant,
};
use task::{HideStrategy, RevealStrategy, SpawnInTerminal, TaskId};
use ui::ActiveTheme;
-use util::ResultExt;
+use util::{ResultExt, rel_path::RelPath};
use workspace::{Item, SaveIntent, notifications::NotifyResultExt};
use workspace::{SplitDirection, notifications::DetachAndPromptErr};
use zed_actions::{OpenDocs, RevealTarget};
@@ -305,31 +305,54 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else {
return;
};
- let project_path = ProjectPath {
- worktree_id: worktree.read(cx).id(),
- path: Arc::from(Path::new(&action.filename)),
+ let path_style = worktree.read(cx).path_style();
+ let Ok(project_path) = RelPath::from_std_path(Path::new(&action.filename), path_style)
+ .map(|path| ProjectPath {
+ worktree_id: worktree.read(cx).id(),
+ path,
+ })
+ else {
+ // TODO implement save_as with absolute path
+ Task::ready(Err::<(), _>(anyhow!(
+ "Cannot save buffer with absolute path"
+ )))
+ .detach_and_prompt_err(
+ "Failed to save",
+ window,
+ cx,
+ |_, _, _| None,
+ );
+ return;
};
- if project.read(cx).entry_for_path(&project_path, cx).is_some() && action.save_intent != Some(SaveIntent::Overwrite) {
+ if project.read(cx).entry_for_path(&project_path, cx).is_some()
+ && action.save_intent != Some(SaveIntent::Overwrite)
+ {
let answer = window.prompt(
gpui::PromptLevel::Critical,
- &format!("{} already exists. Do you want to replace it?", project_path.path.to_string_lossy()),
+ &format!(
+ "{} already exists. Do you want to replace it?",
+ project_path.path.display(path_style)
+ ),
Some(
- "A file or folder with the same name already exists. Replacing it will overwrite its current contents.",
+ "A file or folder with the same name already exists. \
+ Replacing it will overwrite its current contents.",
),
&["Replace", "Cancel"],
- cx);
+ cx,
+ );
cx.spawn_in(window, async move |editor, cx| {
if answer.await.ok() != Some(0) {
return;
}
- let _ = editor.update_in(cx, |editor, window, cx|{
+ let _ = editor.update_in(cx, |editor, window, cx| {
editor
.save_as(project, project_path, window, cx)
.detach_and_prompt_err("Failed to :w", window, cx, |_, _, _| None);
});
- }).detach();
+ })
+ .detach();
} else {
editor
.save_as(project, project_path, window, cx)
@@ -348,9 +371,15 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else {
return;
};
+ let path_style = worktree.read(cx).path_style();
+ let Some(path) =
+ RelPath::from_std_path(Path::new(&action.filename), path_style).log_err()
+ else {
+ return;
+ };
let project_path = ProjectPath {
worktree_id: worktree.read(cx).id(),
- path: Arc::from(Path::new(&action.filename)),
+ path,
};
let direction = if action.vertical {
@@ -442,9 +471,15 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else {
return;
};
+ let path_style = worktree.read(cx).path_style();
+ let Some(path) =
+ RelPath::from_std_path(Path::new(&action.filename), path_style).log_err()
+ else {
+ return;
+ };
let project_path = ProjectPath {
worktree_id: worktree.read(cx).id(),
- path: Arc::from(Path::new(&action.filename)),
+ path,
};
let _ = workspace.update(cx, |workspace, cx| {
@@ -1710,9 +1745,8 @@ impl Vim {
if let Some((_, buffer, _)) = editor.active_excerpt(cx)
&& let Some(file) = buffer.read(cx).file()
&& let Some(local) = file.as_local()
- && let Some(str) = local.path().to_str()
{
- ret.push_str(str)
+ ret.push_str(&local.path().display(local.path_style(cx)));
}
});
}
@@ -863,7 +863,7 @@ impl Vim {
file.full_path(cx).to_string_lossy().to_string()
}
} else {
- file.path().to_string_lossy().to_string()
+ file.path().display(file.path_style(cx)).into_owned()
}
} else {
"[No Name]".into()
@@ -34,6 +34,7 @@ use ui::{
StyledTypography, Window, h_flex, rems,
};
use util::ResultExt;
+use util::rel_path::RelPath;
use workspace::searchable::Direction;
use workspace::{Workspace, WorkspaceDb, WorkspaceId};
@@ -343,9 +344,11 @@ impl MarksState {
.worktrees(cx)
.filter_map(|worktree| {
let relative = path.strip_prefix(worktree.read(cx).abs_path()).ok()?;
+ let path = RelPath::from_std_path(relative, worktree.read(cx).path_style())
+ .log_err()?;
Some(ProjectPath {
worktree_id: worktree.read(cx).id(),
- path: relative.into(),
+ path,
})
})
.next();
@@ -872,7 +875,7 @@ impl VimGlobals {
buffer
.read(cx)
.file()
- .map(|file| file.path().to_string_lossy().to_string().into())
+ .map(|file| file.path().display(file.path_style(cx)).into_owned().into())
} else {
None
}
@@ -1298,7 +1298,8 @@ pub mod test {
InteractiveElement, IntoElement, Render, SharedString, Task, WeakEntity, Window,
};
use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
- use std::{any::Any, cell::Cell, path::Path};
+ use std::{any::Any, cell::Cell};
+ use util::rel_path::rel_path;
pub struct TestProjectItem {
pub entry_id: Option<ProjectEntryId>,
@@ -1355,7 +1356,7 @@ pub mod test {
let entry_id = Some(ProjectEntryId::from_proto(id));
let project_path = Some(ProjectPath {
worktree_id: WorktreeId::from_usize(0),
- path: Path::new(path).into(),
+ path: rel_path(path).into(),
});
cx.new(|_| Self {
entry_id,
@@ -1376,7 +1377,7 @@ pub mod test {
let entry_id = Some(ProjectEntryId::from_proto(id));
let project_path = Some(ProjectPath {
worktree_id: WorktreeId::from_usize(0),
- path: Path::new(path).into(),
+ path: rel_path(path).into(),
});
cx.new(|_| Self {
entry_id,
@@ -50,7 +50,7 @@ use ui::{
PopoverMenu, PopoverMenuHandle, Tab, TabBar, TabPosition, Tooltip, prelude::*,
right_click_menu,
};
-use util::{ResultExt, debug_panic, maybe, truncate_and_remove_front};
+use util::{ResultExt, debug_panic, maybe, paths::PathStyle, truncate_and_remove_front};
/// A selected entry in e.g. project panel.
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -1652,11 +1652,9 @@ impl Pane {
if !project_item.is_dirty() {
return;
}
- let filename = project_item.project_path(cx).and_then(|path| {
- path.path
- .file_name()
- .and_then(|name| name.to_str().map(ToOwned::to_owned))
- });
+ let filename = project_item
+ .project_path(cx)
+ .and_then(|path| path.path.file_name().map(ToOwned::to_owned));
file_names.insert(filename.unwrap_or("untitled".to_string()));
});
}
@@ -1965,9 +1963,10 @@ impl Pane {
const DELETED_MESSAGE: &str = "This file has been deleted on disk since you started editing it. Do you want to recreate it?";
+ let path_style = project.read_with(cx, |project, cx| project.path_style(cx))?;
if save_intent == SaveIntent::Skip {
return Ok(true);
- }
+ };
let Some(item_ix) = pane
.read_with(cx, |pane, _| pane.index_for_item(item))
.ok()
@@ -2090,7 +2089,7 @@ impl Pane {
let answer_task = pane.update_in(cx, |pane, window, cx| {
if pane.save_modals_spawned.insert(item_id) {
pane.activate_item(item_ix, true, true, window, cx);
- let prompt = dirty_message_for(item.project_path(cx));
+ let prompt = dirty_message_for(item.project_path(cx), path_style);
Some(window.prompt(
PromptLevel::Warning,
&prompt,
@@ -2184,7 +2183,7 @@ impl Pane {
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?;
let new_path = ProjectPath {
worktree_id,
- path: path.into(),
+ path: path,
};
pane.update_in(cx, |pane, window, cx| {
@@ -2321,10 +2320,10 @@ impl Pane {
.worktree_for_entry(entry, cx)?
.read(cx);
let entry = worktree.entry_for_id(entry)?;
- match &entry.canonical_path {
- Some(canonical_path) => Some(canonical_path.to_path_buf()),
- None => worktree.absolutize(&entry.path).ok(),
- }
+ Some(match &entry.canonical_path {
+ Some(canonical_path) => canonical_path.to_path_buf(),
+ None => worktree.absolutize(&entry.path),
+ })
}
pub fn icon_color(selected: bool) -> Color {
@@ -2875,9 +2874,14 @@ impl Pane {
menu.entry(
"Copy Relative Path",
Some(Box::new(zed_actions::workspace::CopyRelativePath)),
- window.handler_for(&pane, move |_, _, cx| {
+ window.handler_for(&pane, move |this, _, cx| {
+ let Some(project) = this.project.upgrade() else {
+ return;
+ };
+ let path_style = project
+ .update(cx, |project, cx| project.path_style(cx));
cx.write_to_clipboard(ClipboardItem::new_string(
- relative_path.to_string_lossy().to_string(),
+ relative_path.display(path_style).to_string(),
));
}),
)
@@ -4007,16 +4011,15 @@ impl NavHistoryState {
}
}
-fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
+fn dirty_message_for(buffer_path: Option<ProjectPath>, path_style: PathStyle) -> String {
let path = buffer_path
.as_ref()
.and_then(|p| {
- p.path
- .to_str()
- .and_then(|s| if s.is_empty() { None } else { Some(s) })
+ let path = p.path.display(path_style);
+ if path.is_empty() { None } else { Some(path) }
})
- .unwrap_or("This buffer");
- let path = truncate_and_remove_front(path, 80);
+ .unwrap_or("This buffer".into());
+ let path = truncate_and_remove_front(&path, 80);
format!("{path} contains unsaved edits. Do you want to save it?")
}
@@ -28,7 +28,7 @@ use sqlez::{
};
use ui::{App, SharedString, px};
-use util::{ResultExt, maybe};
+use util::{ResultExt, maybe, rel_path::RelPath};
use uuid::Uuid;
use crate::{
@@ -915,10 +915,13 @@ impl WorkspaceDb {
relative_worktree_path == String::default()
);
+ let Some(relative_path) = RelPath::new(&relative_worktree_path).log_err() else {
+ continue;
+ };
if worktree_id != u64::MAX && relative_worktree_path != String::default() {
ToolchainScope::Subproject(
WorktreeId::from_usize(worktree_id as usize),
- Arc::from(relative_worktree_path.as_ref()),
+ relative_path.into(),
)
} else {
ToolchainScope::Project
@@ -998,7 +1001,7 @@ impl WorkspaceDb {
for toolchain in toolchains {
let query = sql!(INSERT OR REPLACE INTO user_toolchains(remote_connection_id, workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8));
let (workspace_id, worktree_id, relative_worktree_path) = match scope {
- ToolchainScope::Subproject(worktree_id, ref path) => (Some(workspace.id), Some(worktree_id), Some(path.to_string_lossy().into_owned())),
+ ToolchainScope::Subproject(worktree_id, ref path) => (Some(workspace.id), Some(worktree_id), Some(path.as_str().to_owned())),
ToolchainScope::Project => (Some(workspace.id), None, None),
ToolchainScope::Global => (None, None, None),
};
@@ -1637,25 +1640,41 @@ impl WorkspaceDb {
&self,
workspace_id: WorkspaceId,
worktree_id: WorktreeId,
- relative_worktree_path: String,
+ relative_worktree_path: Arc<RelPath>,
language_name: LanguageName,
) -> Result<Option<Toolchain>> {
self.write(move |this| {
let mut select = this
.select_bound(sql!(
- SELECT name, path, raw_json FROM toolchains WHERE workspace_id = ? AND language_name = ? AND worktree_id = ? AND relative_worktree_path = ?
+ SELECT
+ name, path, raw_json
+ FROM toolchains
+ WHERE
+ workspace_id = ? AND
+ language_name = ? AND
+ worktree_id = ? AND
+ relative_worktree_path = ?
))
.context("select toolchain")?;
- let toolchain: Vec<(String, String, String)> =
- select((workspace_id, language_name.as_ref().to_string(), worktree_id.to_usize(), relative_worktree_path))?;
+ let toolchain: Vec<(String, String, String)> = select((
+ workspace_id,
+ language_name.as_ref().to_string(),
+ worktree_id.to_usize(),
+ relative_worktree_path.as_str().to_string(),
+ ))?;
- Ok(toolchain.into_iter().next().and_then(|(name, path, raw_json)| Some(Toolchain {
- name: name.into(),
- path: path.into(),
- language_name,
- as_json: serde_json::Value::from_str(&raw_json).ok()?,
- })))
+ Ok(toolchain
+ .into_iter()
+ .next()
+ .and_then(|(name, path, raw_json)| {
+ Some(Toolchain {
+ name: name.into(),
+ path: path.into(),
+ language_name,
+ as_json: serde_json::Value::from_str(&raw_json).ok()?,
+ })
+ }))
})
.await
}
@@ -1663,31 +1682,46 @@ impl WorkspaceDb {
pub(crate) async fn toolchains(
&self,
workspace_id: WorkspaceId,
- ) -> Result<Vec<(Toolchain, WorktreeId, Arc<Path>)>> {
+ ) -> Result<Vec<(Toolchain, WorktreeId, Arc<RelPath>)>> {
self.write(move |this| {
let mut select = this
.select_bound(sql!(
- SELECT name, path, worktree_id, relative_worktree_path, language_name, raw_json FROM toolchains WHERE workspace_id = ?
+ SELECT
+ name, path, worktree_id, relative_worktree_path, language_name, raw_json
+ FROM toolchains
+ WHERE workspace_id = ?
))
.context("select toolchains")?;
let toolchain: Vec<(String, String, u64, String, String, String)> =
select(workspace_id)?;
- Ok(toolchain.into_iter().filter_map(|(name, path, worktree_id, relative_worktree_path, language_name, raw_json)| Some((Toolchain {
- name: name.into(),
- path: path.into(),
- language_name: LanguageName::new(&language_name),
- as_json: serde_json::Value::from_str(&raw_json).ok()?,
- }, WorktreeId::from_proto(worktree_id), Arc::from(relative_worktree_path.as_ref())))).collect())
+ Ok(toolchain
+ .into_iter()
+ .filter_map(
+ |(name, path, worktree_id, relative_worktree_path, language, json)| {
+ Some((
+ Toolchain {
+ name: name.into(),
+ path: path.into(),
+ language_name: LanguageName::new(&language),
+ as_json: serde_json::Value::from_str(&json).ok()?,
+ },
+ WorktreeId::from_proto(worktree_id),
+ RelPath::from_proto(&relative_worktree_path).log_err()?,
+ ))
+ },
+ )
+ .collect())
})
.await
}
+
pub async fn set_toolchain(
&self,
workspace_id: WorkspaceId,
worktree_id: WorktreeId,
- relative_worktree_path: String,
+ relative_worktree_path: Arc<RelPath>,
toolchain: Toolchain,
) -> Result<()> {
log::debug!(
@@ -1709,7 +1743,7 @@ impl WorkspaceDb {
insert((
workspace_id,
worktree_id.to_usize(),
- relative_worktree_path,
+ relative_worktree_path.as_str(),
toolchain.language_name.as_ref(),
toolchain.name.as_ref(),
toolchain.path.as_ref(),
@@ -104,7 +104,12 @@ use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
pub use ui;
use ui::{Window, prelude::*};
-use util::{ResultExt, TryFutureExt, paths::SanitizedPath, serde::default_true};
+use util::{
+ ResultExt, TryFutureExt,
+ paths::{PathStyle, SanitizedPath},
+ rel_path::RelPath,
+ serde::default_true,
+};
use uuid::Uuid;
pub use workspace_settings::{
AutosaveSetting, BottomDockLayout, RestoreOnStartupBehavior, TabBarSettings, WorkspaceSettings,
@@ -625,7 +630,7 @@ impl ProjectItemRegistry {
match project_item.await.with_context(|| {
format!(
"opening project path {:?}",
- entry_abs_path.as_deref().unwrap_or(&project_path.path)
+ entry_abs_path.as_deref().unwrap_or(&project_path.path.as_std_path())
)
}) {
Ok(project_item) => {
@@ -1754,6 +1759,10 @@ impl Workspace {
&self.project
}
+ pub fn path_style(&self, cx: &App) -> PathStyle {
+ self.project.read(cx).path_style(cx)
+ }
+
pub fn recently_activated_items(&self, cx: &App) -> HashMap<EntityId, usize> {
let mut history: HashMap<EntityId, usize> = HashMap::default();
@@ -2622,7 +2631,12 @@ impl Workspace {
.strip_prefix(worktree_abs_path.as_ref())
.ok()
.and_then(|relative_path| {
- worktree.entry_for_path(relative_path)
+ let relative_path = RelPath::from_std_path(
+ relative_path,
+ PathStyle::local(),
+ )
+ .log_err()?;
+ worktree.entry_for_path(&relative_path)
})
}
.map(|entry| entry.id);
@@ -2668,7 +2682,7 @@ impl Workspace {
self.open_path(project_path, None, true, window, cx)
}
ResolvedPath::AbsPath { path, .. } => self.open_abs_path(
- path,
+ PathBuf::from(path),
OpenOptions {
visible: Some(OpenVisible::None),
..Default::default()
@@ -2757,7 +2771,7 @@ impl Workspace {
worktree,
ProjectPath {
worktree_id,
- path: path.into(),
+ path: path,
},
))
})
@@ -4399,17 +4413,17 @@ impl Workspace {
let project = self.project().read(cx);
let mut title = String::new();
- for (i, worktree) in project.worktrees(cx).enumerate() {
+ for (i, worktree) in project.visible_worktrees(cx).enumerate() {
let name = {
let settings_location = SettingsLocation {
worktree_id: worktree.read(cx).id(),
- path: Path::new(""),
+ path: RelPath::empty(),
};
let settings = WorktreeSettings::get(Some(settings_location), cx);
match &settings.project_name {
Some(name) => name.as_str(),
- None => worktree.read(cx).root_name(),
+ None => worktree.read(cx).root_name_str(),
}
};
if i > 0 {
@@ -4423,18 +4437,14 @@ impl Workspace {
}
if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
- let filename = path
- .path
- .file_name()
- .map(|s| s.to_string_lossy())
- .or_else(|| {
- Some(Cow::Borrowed(
- project
- .worktree_for_id(path.worktree_id, cx)?
- .read(cx)
- .root_name(),
- ))
- });
+ let filename = path.path.file_name().or_else(|| {
+ Some(
+ project
+ .worktree_for_id(path.worktree_id, cx)?
+ .read(cx)
+ .root_name_str(),
+ )
+ });
if let Some(filename) = filename {
title.push_str(" — ");
@@ -8174,6 +8184,7 @@ mod tests {
use project::{Project, ProjectEntryId};
use serde_json::json;
use settings::SettingsStore;
+ use util::rel_path::rel_path;
#[gpui::test]
async fn test_tab_disambiguation(cx: &mut TestAppContext) {
@@ -8268,7 +8279,7 @@ mod tests {
assert_eq!(
project.active_entry(),
project
- .entry_for_path(&(worktree_id, "one.txt").into(), cx)
+ .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx)
.map(|e| e.id)
);
});
@@ -8283,7 +8294,7 @@ mod tests {
assert_eq!(
project.active_entry(),
project
- .entry_for_path(&(worktree_id, "two.txt").into(), cx)
+ .entry_for_path(&(worktree_id, rel_path("two.txt")).into(), cx)
.map(|e| e.id)
);
});
@@ -8299,7 +8310,7 @@ mod tests {
assert_eq!(
project.active_entry(),
project
- .entry_for_path(&(worktree_id, "one.txt").into(), cx)
+ .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx)
.map(|e| e.id)
);
});
@@ -10665,7 +10676,7 @@ mod tests {
let handle = workspace
.update_in(cx, |workspace, window, cx| {
- let project_path = (worktree_id, "one.png");
+ let project_path = (worktree_id, rel_path("one.png"));
workspace.open_path(project_path, None, true, window, cx)
})
.await
@@ -10679,7 +10690,7 @@ mod tests {
let handle = workspace
.update_in(cx, |workspace, window, cx| {
- let project_path = (worktree_id, "two.ipynb");
+ let project_path = (worktree_id, rel_path("two.ipynb"));
workspace.open_path(project_path, None, true, window, cx)
})
.await
@@ -10692,7 +10703,7 @@ mod tests {
let handle = workspace
.update_in(cx, |workspace, window, cx| {
- let project_path = (worktree_id, "three.txt");
+ let project_path = (worktree_id, rel_path("three.txt"));
workspace.open_path(project_path, None, true, window, cx)
})
.await;
@@ -10727,7 +10738,7 @@ mod tests {
let handle = workspace
.update_in(cx, |workspace, window, cx| {
- let project_path = (worktree_id, "one.png");
+ let project_path = (worktree_id, rel_path("one.png"));
workspace.open_path(project_path, None, true, window, cx)
})
.await
@@ -10741,7 +10752,7 @@ mod tests {
let handle = workspace
.update_in(cx, |workspace, window, cx| {
- let project_path = (worktree_id, "three.txt");
+ let project_path = (worktree_id, rel_path("three.txt"));
workspace.open_path(project_path, None, true, window, cx)
})
.await;
@@ -10755,7 +10766,7 @@ mod tests {
.flat_map(|item| {
item.project_paths(cx)
.into_iter()
- .map(|path| path.path.to_string_lossy().to_string())
+ .map(|path| path.path.as_str().to_string())
})
.collect()
}
@@ -19,8 +19,7 @@ use futures::{
};
use fuzzy::CharBag;
use git::{
- COMMIT_MESSAGE, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE, INDEX_LOCK, LFS_DIR,
- repository::RepoPath, status::GitSummary,
+ COMMIT_MESSAGE, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE, INDEX_LOCK, LFS_DIR, status::GitSummary,
};
use gpui::{
App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, Task,
@@ -29,7 +28,7 @@ use ignore::IgnoreStack;
use language::DiskState;
use parking_lot::Mutex;
-use paths::{local_settings_folder_relative_path, local_vscode_folder_relative_path};
+use paths::{local_settings_folder_name, local_vscode_folder_name};
use postage::{
barrier,
prelude::{Sink as _, Stream as _},
@@ -37,7 +36,7 @@ use postage::{
};
use rpc::{
AnyProtoClient,
- proto::{self, FromProto, ToProto, split_worktree_update},
+ proto::{self, split_worktree_update},
};
pub use settings::WorktreeId;
use settings::{Settings, SettingsLocation, SettingsStore};
@@ -54,7 +53,7 @@ use std::{
future::Future,
mem::{self},
ops::{Deref, DerefMut},
- path::{Component, Path, PathBuf},
+ path::{Path, PathBuf},
pin::Pin,
sync::{
Arc,
@@ -66,7 +65,8 @@ use sum_tree::{Bias, Dimensions, Edit, KeyedItem, SeekTarget, SumTree, Summary,
use text::{LineEnding, Rope};
use util::{
ResultExt, debug_panic,
- paths::{PathMatcher, SanitizedPath, home_dir},
+ paths::{PathMatcher, PathStyle, SanitizedPath, home_dir},
+ rel_path::RelPath,
};
pub use worktree_settings::WorktreeSettings;
@@ -132,12 +132,12 @@ pub struct LocalWorktree {
}
pub struct PathPrefixScanRequest {
- path: Arc<Path>,
+ path: Arc<RelPath>,
done: SmallVec<[barrier::Sender; 1]>,
}
struct ScanRequest {
- relative_paths: Vec<Arc<Path>>,
+ relative_paths: Vec<Arc<RelPath>>,
done: SmallVec<[barrier::Sender; 1]>,
}
@@ -159,11 +159,12 @@ pub struct RemoteWorktree {
pub struct Snapshot {
id: WorktreeId,
abs_path: Arc<SanitizedPath>,
- root_name: String,
+ path_style: PathStyle,
+ root_name: Arc<RelPath>,
root_char_bag: CharBag,
entries_by_path: SumTree<Entry>,
entries_by_id: SumTree<PathEntry>,
- always_included_entries: Vec<Arc<Path>>,
+ always_included_entries: Vec<Arc<RelPath>>,
/// A number that increases every time the worktree begins scanning
/// a set of paths from the filesystem. This scanning could be caused
@@ -186,7 +187,7 @@ pub struct Snapshot {
#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub enum WorkDirectory {
InProject {
- relative_path: Arc<Path>,
+ relative_path: Arc<RelPath>,
},
AboveProject {
absolute_path: Arc<Path>,
@@ -195,34 +196,10 @@ pub enum WorkDirectory {
}
impl WorkDirectory {
- #[cfg(test)]
- fn in_project(path: &str) -> Self {
- let path = Path::new(path);
- Self::InProject {
- relative_path: path.into(),
- }
- }
-
- //#[cfg(test)]
- //fn canonicalize(&self) -> Self {
- // match self {
- // WorkDirectory::InProject { relative_path } => WorkDirectory::InProject {
- // relative_path: relative_path.clone(),
- // },
- // WorkDirectory::AboveProject {
- // absolute_path,
- // location_in_repo,
- // } => WorkDirectory::AboveProject {
- // absolute_path: absolute_path.canonicalize().unwrap().into(),
- // location_in_repo: location_in_repo.clone(),
- // },
- // }
- //}
-
fn path_key(&self) -> PathKey {
match self {
WorkDirectory::InProject { relative_path } => PathKey(relative_path.clone()),
- WorkDirectory::AboveProject { .. } => PathKey(Path::new("").into()),
+ WorkDirectory::AboveProject { .. } => PathKey(RelPath::empty().into()),
}
}
@@ -232,106 +209,18 @@ impl WorkDirectory {
/// is a repository in a directory between these two paths
/// external .git folder in a parent folder of the project root.
#[track_caller]
- pub fn directory_contains(&self, path: impl AsRef<Path>) -> bool {
- let path = path.as_ref();
- debug_assert!(path.is_relative());
+ pub fn directory_contains(&self, path: &RelPath) -> bool {
match self {
WorkDirectory::InProject { relative_path } => path.starts_with(relative_path),
WorkDirectory::AboveProject { .. } => true,
}
}
-
- /// relativize returns the given project path relative to the root folder of the
- /// repository.
- /// If the root of the repository (and its .git folder) are located in a parent folder
- /// of the project root folder, then the returned RepoPath is relative to the root
- /// of the repository and not a valid path inside the project.
- pub fn relativize(&self, path: &Path) -> Result<RepoPath> {
- // path is assumed to be relative to worktree root.
- debug_assert!(path.is_relative());
- match self {
- WorkDirectory::InProject { relative_path } => Ok(path
- .strip_prefix(relative_path)
- .map_err(|_| anyhow!("could not relativize {path:?} against {relative_path:?}"))?
- .into()),
- WorkDirectory::AboveProject {
- location_in_repo, ..
- } => {
- // Avoid joining a `/` to location_in_repo in the case of a single-file worktree.
- if path == Path::new("") {
- Ok(RepoPath(location_in_repo.clone()))
- } else {
- Ok(location_in_repo.join(path).into())
- }
- }
- }
- }
-
- /// This is the opposite operation to `relativize` above
- pub fn try_unrelativize(&self, path: &RepoPath) -> Option<Arc<Path>> {
- match self {
- WorkDirectory::InProject { relative_path } => Some(relative_path.join(path).into()),
- WorkDirectory::AboveProject {
- location_in_repo, ..
- } => {
- // If we fail to strip the prefix, that means this status entry is
- // external to this worktree, and we definitely won't have an entry_id
- path.strip_prefix(location_in_repo).ok().map(Into::into)
- }
- }
- }
-
- pub fn unrelativize(&self, path: &RepoPath) -> Arc<Path> {
- match self {
- WorkDirectory::InProject { relative_path } => relative_path.join(path).into(),
- WorkDirectory::AboveProject {
- location_in_repo, ..
- } => {
- if &path.0 == location_in_repo {
- // Single-file worktree
- return location_in_repo
- .file_name()
- .map(Path::new)
- .unwrap_or(Path::new(""))
- .into();
- }
- let mut location_in_repo = &**location_in_repo;
- let mut parents = PathBuf::new();
- loop {
- if let Ok(segment) = path.strip_prefix(location_in_repo) {
- return parents.join(segment).into();
- }
- location_in_repo = location_in_repo.parent().unwrap_or(Path::new(""));
- parents.push(Component::ParentDir);
- }
- }
- }
- }
-
- pub fn display_name(&self) -> String {
- match self {
- WorkDirectory::InProject { relative_path } => relative_path.display().to_string(),
- WorkDirectory::AboveProject {
- absolute_path,
- location_in_repo,
- } => {
- let num_of_dots = location_in_repo.components().count();
-
- "../".repeat(num_of_dots)
- + &absolute_path
- .file_name()
- .map(|s| s.to_string_lossy())
- .unwrap_or_default()
- + "/"
- }
- }
- }
}
impl Default for WorkDirectory {
fn default() -> Self {
Self::InProject {
- relative_path: Arc::from(Path::new("")),
+ relative_path: Arc::from(RelPath::empty()),
}
}
}
@@ -340,7 +229,7 @@ impl Default for WorkDirectory {
pub struct LocalSnapshot {
snapshot: Snapshot,
global_gitignore: Option<Arc<Gitignore>>,
- /// All of the gitignore files in the worktree, indexed by their relative path.
+ /// All of the gitignore files in the worktree, indexed by their absolute path.
/// The boolean indicates whether the gitignore needs to be updated.
ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, bool)>,
/// All of the git repositories in the worktree, indexed by the project entry
@@ -354,14 +243,14 @@ pub struct LocalSnapshot {
struct BackgroundScannerState {
snapshot: LocalSnapshot,
scanned_dirs: HashSet<ProjectEntryId>,
- path_prefixes_to_scan: HashSet<Arc<Path>>,
- paths_to_scan: HashSet<Arc<Path>>,
+ path_prefixes_to_scan: HashSet<Arc<RelPath>>,
+ paths_to_scan: HashSet<Arc<RelPath>>,
/// The ids of all of the entries that were removed from the snapshot
/// as part of the current update. These entry ids may be re-used
/// if the same inode is discovered at a new path, or if the given
/// path is re-created after being deleted.
removed_entries: HashMap<u64, Entry>,
- changed_paths: Vec<Arc<Path>>,
+ changed_paths: Vec<Arc<RelPath>>,
prev_snapshot: Snapshot,
}
@@ -458,8 +347,6 @@ pub enum Event {
DeletedEntry(ProjectEntryId),
}
-const EMPTY_PATH: &str = "";
-
impl EventEmitter<Event> for Worktree {}
impl Worktree {
@@ -498,8 +385,10 @@ impl Worktree {
cx.entity_id().as_u64(),
abs_path
.file_name()
- .map_or(String::new(), |f| f.to_string_lossy().to_string()),
+ .and_then(|f| f.to_str())
+ .map_or(RelPath::empty().into(), |f| RelPath::new(f).unwrap().into()),
abs_path.clone(),
+ PathStyle::local(),
),
root_file_handle,
};
@@ -507,7 +396,7 @@ impl Worktree {
let worktree_id = snapshot.id();
let settings_location = Some(SettingsLocation {
worktree_id,
- path: Path::new(EMPTY_PATH),
+ path: RelPath::empty(),
});
let settings = WorktreeSettings::get(settings_location, cx).clone();
@@ -525,15 +414,19 @@ impl Worktree {
let share_private_files = false;
if let Some(metadata) = metadata {
let mut entry = Entry::new(
- Arc::from(Path::new("")),
+ RelPath::empty().into(),
&metadata,
&next_entry_id,
snapshot.root_char_bag,
None,
);
if !metadata.is_dir {
- entry.is_private = !share_private_files
- && settings.is_path_private(abs_path.file_name().unwrap().as_ref());
+ if let Some(file_name) = abs_path.file_name()
+ && let Some(file_name) = file_name.to_str()
+ && let Ok(path) = RelPath::new(file_name)
+ {
+ entry.is_private = !share_private_files && settings.is_path_private(path);
+ }
}
snapshot.insert_entry(entry, fs.as_ref());
}
@@ -564,13 +457,16 @@ impl Worktree {
replica_id: ReplicaId,
worktree: proto::WorktreeMetadata,
client: AnyProtoClient,
+ path_style: PathStyle,
cx: &mut App,
) -> Entity<Self> {
cx.new(|cx: &mut Context<Self>| {
let snapshot = Snapshot::new(
worktree.id,
- worktree.root_name,
- Arc::<Path>::from_proto(worktree.abs_path),
+ RelPath::from_proto(&worktree.root_name)
+ .unwrap_or_else(|_| RelPath::empty().into()),
+ Path::new(&worktree.abs_path).into(),
+ path_style,
);
let background_snapshot = Arc::new(Mutex::new((
@@ -584,7 +480,7 @@ impl Worktree {
let worktree_id = snapshot.id();
let settings_location = Some(SettingsLocation {
worktree_id,
- path: Path::new(EMPTY_PATH),
+ path: RelPath::empty(),
});
let settings = WorktreeSettings::get(settings_location, cx).clone();
@@ -701,7 +597,7 @@ impl Worktree {
pub fn settings_location(&self, _: &Context<Self>) -> SettingsLocation<'static> {
SettingsLocation {
worktree_id: self.id(),
- path: Path::new(EMPTY_PATH),
+ path: RelPath::empty(),
}
}
@@ -722,9 +618,9 @@ impl Worktree {
pub fn metadata_proto(&self) -> proto::WorktreeMetadata {
proto::WorktreeMetadata {
id: self.id().to_proto(),
- root_name: self.root_name().to_string(),
+ root_name: self.root_name().to_proto(),
visible: self.is_visible(),
- abs_path: self.abs_path().to_proto(),
+ abs_path: self.abs_path().to_string_lossy().to_string(),
}
}
@@ -791,7 +687,7 @@ impl Worktree {
}
}
- pub fn load_file(&self, path: &Path, cx: &Context<Worktree>) -> Task<Result<LoadedFile>> {
+ pub fn load_file(&self, path: &RelPath, cx: &Context<Worktree>) -> Task<Result<LoadedFile>> {
match self {
Worktree::Local(this) => this.load_file(path, cx),
Worktree::Remote(_) => {
@@ -802,7 +698,7 @@ impl Worktree {
pub fn load_binary_file(
&self,
- path: &Path,
+ path: &RelPath,
cx: &Context<Worktree>,
) -> Task<Result<LoadedBinaryFile>> {
match self {
@@ -815,7 +711,7 @@ impl Worktree {
pub fn write_file(
&self,
- path: &Path,
+ path: Arc<RelPath>,
text: Rope,
line_ending: LineEnding,
cx: &Context<Worktree>,
@@ -830,12 +726,11 @@ impl Worktree {
pub fn create_entry(
&mut self,
- path: impl Into<Arc<Path>>,
+ path: Arc<RelPath>,
is_directory: bool,
content: Option<Vec<u8>>,
cx: &Context<Worktree>,
) -> Task<Result<CreatedEntry>> {
- let path: Arc<Path> = path.into();
let worktree_id = self.id();
match self {
Worktree::Local(this) => this.create_entry(path, is_directory, content, cx),
@@ -862,11 +757,8 @@ impl Worktree {
.await
.map(CreatedEntry::Included),
None => {
- let abs_path = this.read_with(cx, |worktree, _| {
- worktree
- .absolutize(&path)
- .with_context(|| format!("absolutizing {path:?}"))
- })??;
+ let abs_path =
+ this.read_with(cx, |worktree, _| worktree.absolutize(&path))?;
Ok(CreatedEntry::Excluded { abs_path })
}
}
@@ -902,7 +794,7 @@ impl Worktree {
Some(task)
}
- fn get_children_ids_recursive(&self, path: &Path, ids: &mut Vec<ProjectEntryId>) {
+ fn get_children_ids_recursive(&self, path: &RelPath, ids: &mut Vec<ProjectEntryId>) {
let children_iter = self.child_entries(path);
for child in children_iter {
ids.push(child.id);
@@ -910,63 +802,21 @@ impl Worktree {
}
}
- pub fn rename_entry(
- &mut self,
- entry_id: ProjectEntryId,
- new_path: impl Into<Arc<Path>>,
- cx: &Context<Self>,
- ) -> Task<Result<CreatedEntry>> {
- let new_path = new_path.into();
- match self {
- Worktree::Local(this) => this.rename_entry(entry_id, new_path, cx),
- Worktree::Remote(this) => this.rename_entry(entry_id, new_path, cx),
- }
- }
-
- pub fn copy_entry(
- &mut self,
- entry_id: ProjectEntryId,
- relative_worktree_source_path: Option<PathBuf>,
- new_path: impl Into<Arc<Path>>,
- cx: &Context<Self>,
- ) -> Task<Result<Option<Entry>>> {
- let new_path: Arc<Path> = new_path.into();
- match self {
- Worktree::Local(this) => {
- this.copy_entry(entry_id, relative_worktree_source_path, new_path, cx)
- }
- Worktree::Remote(this) => {
- let relative_worktree_source_path = relative_worktree_source_path
- .map(|relative_worktree_source_path| relative_worktree_source_path.to_proto());
- let response = this.client.request(proto::CopyProjectEntry {
- project_id: this.project_id,
- entry_id: entry_id.to_proto(),
- relative_worktree_source_path,
- new_path: new_path.to_proto(),
- });
- cx.spawn(async move |this, cx| {
- let response = response.await?;
- match response.entry {
- Some(entry) => this
- .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_path: Arc<RelPath>,
+ // cx: &Context<Self>,
+ // ) -> Task<Result<CreatedEntry>> {
+ // match self {
+ // Worktree::Local(this) => this.rename_entry(entry_id, new_path, cx),
+ // Worktree::Remote(this) => this.rename_entry(entry_id, new_path, cx),
+ // }
+ // }
pub fn copy_external_entries(
&mut self,
- target_directory: Arc<Path>,
+ target_directory: Arc<RelPath>,
paths: Vec<Arc<Path>>,
fs: Arc<dyn Fs>,
cx: &Context<Worktree>,
@@ -1035,16 +885,18 @@ impl Worktree {
mut cx: AsyncApp,
) -> Result<proto::ProjectEntryResponse> {
let (scan_id, entry) = this.update(&mut cx, |this, cx| {
- (
+ anyhow::Ok((
this.scan_id(),
this.create_entry(
- Arc::<Path>::from_proto(request.path),
+ RelPath::from_proto(&request.path).with_context(|| {
+ format!("received invalid relative path {:?}", request.path)
+ })?,
request.is_directory,
request.content,
cx,
),
- )
- })?;
+ ))
+ })??;
Ok(proto::ProjectEntryResponse {
entry: match &entry.await? {
CreatedEntry::Included(entry) => Some(entry.into()),
@@ -1106,91 +958,38 @@ impl Worktree {
})
}
- pub async fn handle_rename_entry(
- this: Entity<Self>,
- request: proto::RenameProjectEntry,
- mut cx: AsyncApp,
- ) -> Result<proto::ProjectEntryResponse> {
- let (scan_id, task) = this.update(&mut cx, |this, cx| {
- (
- this.scan_id(),
- this.rename_entry(
- ProjectEntryId::from_proto(request.entry_id),
- Arc::<Path>::from_proto(request.new_path),
- 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_copy_entry(
- this: Entity<Self>,
- request: proto::CopyProjectEntry,
- mut cx: AsyncApp,
- ) -> Result<proto::ProjectEntryResponse> {
- let (scan_id, task) = this.update(&mut cx, |this, cx| {
- let relative_worktree_source_path = request
- .relative_worktree_source_path
- .map(PathBuf::from_proto);
- (
- this.scan_id(),
- this.copy_entry(
- ProjectEntryId::from_proto(request.entry_id),
- relative_worktree_source_path,
- PathBuf::from_proto(request.new_path),
- cx,
- ),
- )
- })?;
- Ok(proto::ProjectEntryResponse {
- entry: task.await?.as_ref().map(|e| e.into()),
- worktree_scan_id: scan_id as u64,
- })
- }
-
- pub fn dot_git_abs_path(&self, work_directory: &WorkDirectory) -> PathBuf {
- let mut path = match work_directory {
- WorkDirectory::InProject { relative_path } => self.abs_path().join(relative_path),
- WorkDirectory::AboveProject { absolute_path, .. } => absolute_path.as_ref().to_owned(),
- };
- path.push(".git");
- path
- }
-
pub fn is_single_file(&self) -> bool {
self.root_dir().is_none()
}
/// For visible worktrees, returns the path with the worktree name as the first component.
/// Otherwise, returns an absolute path.
- pub fn full_path(&self, worktree_relative_path: &Path) -> PathBuf {
- let mut full_path = PathBuf::new();
-
+ pub fn full_path(&self, worktree_relative_path: &RelPath) -> PathBuf {
if self.is_visible() {
- full_path.push(self.root_name());
+ self.root_name()
+ .join(worktree_relative_path)
+ .display(self.path_style)
+ .to_string()
+ .into()
} else {
- let path = self.abs_path();
-
- if self.is_local() && path.starts_with(home_dir().as_path()) {
- full_path.push("~");
- full_path.push(path.strip_prefix(home_dir().as_path()).unwrap());
+ let full_path = self.abs_path();
+ let mut full_path_string = if self.is_local()
+ && let Ok(stripped) = full_path.strip_prefix(home_dir())
+ {
+ self.path_style
+ .join("~", &*stripped.to_string_lossy())
+ .unwrap()
} else {
- full_path.push(path)
+ full_path.to_string_lossy().to_string()
+ };
+
+ if worktree_relative_path.components().next().is_some() {
+ full_path_string.push_str(self.path_style.separator());
+ full_path_string.push_str(&worktree_relative_path.display(self.path_style));
}
- }
- if worktree_relative_path.components().next().is_some() {
- full_path.push(&worktree_relative_path);
+ full_path_string.into()
}
-
- full_path
}
}
@@ -1199,10 +998,14 @@ impl LocalWorktree {
&self.fs
}
- pub fn is_path_private(&self, path: &Path) -> bool {
+ pub fn is_path_private(&self, path: &RelPath) -> bool {
!self.share_private_files && self.settings.is_path_private(path)
}
+ pub fn fs_is_case_sensitive(&self) -> bool {
+ self.fs_case_sensitive
+ }
+
fn restart_background_scanners(&mut self, cx: &Context<Worktree>) {
let (scan_requests_tx, scan_requests_rx) = channel::unbounded();
let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded();
@@ -1449,18 +1252,17 @@ impl LocalWorktree {
fn load_binary_file(
&self,
- path: &Path,
+ path: &RelPath,
cx: &Context<Worktree>,
) -> Task<Result<LoadedBinaryFile>> {
let path = Arc::from(path);
let abs_path = self.absolutize(&path);
let fs = self.fs.clone();
let entry = self.refresh_entry(path.clone(), None, cx);
- let is_private = self.is_path_private(path.as_ref());
+ let is_private = self.is_path_private(&path);
let worktree = cx.weak_entity();
cx.background_spawn(async move {
- let abs_path = abs_path?;
let content = fs.load_bytes(&abs_path).await?;
let worktree = worktree.upgrade().context("worktree was dropped")?;
@@ -1493,7 +1295,7 @@ impl LocalWorktree {
})
}
- fn load_file(&self, path: &Path, cx: &Context<Worktree>) -> Task<Result<LoadedFile>> {
+ fn load_file(&self, path: &RelPath, cx: &Context<Worktree>) -> Task<Result<LoadedFile>> {
let path = Arc::from(path);
let abs_path = self.absolutize(&path);
let fs = self.fs.clone();
@@ -1501,7 +1303,6 @@ impl LocalWorktree {
let is_private = self.is_path_private(path.as_ref());
cx.spawn(async move |this, _cx| {
- let abs_path = abs_path?;
// WARN: Temporary workaround for #27283.
// We are not efficient with our memory usage per file, and use in excess of 64GB for a 10GB file
// Therefore, as a temporary workaround to prevent system freezes, we just bail before opening a file
@@ -1549,31 +1350,27 @@ impl LocalWorktree {
}
/// Find the lowest path in the worktree's datastructures that is an ancestor
- fn lowest_ancestor(&self, path: &Path) -> PathBuf {
+ fn lowest_ancestor(&self, path: &RelPath) -> Arc<RelPath> {
let mut lowest_ancestor = None;
for path in path.ancestors() {
if self.entry_for_path(path).is_some() {
- lowest_ancestor = Some(path.to_path_buf());
+ lowest_ancestor = Some(path.into());
break;
}
}
- lowest_ancestor.unwrap_or_else(|| PathBuf::from(""))
+ lowest_ancestor.unwrap_or_else(|| RelPath::empty().into())
}
fn create_entry(
&self,
- path: impl Into<Arc<Path>>,
+ path: Arc<RelPath>,
is_dir: bool,
content: Option<Vec<u8>>,
cx: &Context<Worktree>,
) -> Task<Result<CreatedEntry>> {
- let path = path.into();
- let abs_path = match self.absolutize(&path) {
- Ok(path) => path,
- Err(e) => return Task::ready(Err(e.context(format!("absolutizing path {path:?}")))),
- };
- let path_excluded = self.settings.is_path_excluded(&abs_path);
+ let abs_path = self.absolutize(&path);
+ let path_excluded = self.settings.is_path_excluded(&path);
let fs = self.fs.clone();
let task_abs_path = abs_path.clone();
let write = cx.background_spawn(async move {
@@ -1599,13 +1396,13 @@ impl LocalWorktree {
let mut refreshes = Vec::new();
let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap();
for refresh_path in refresh_paths.ancestors() {
- if refresh_path == Path::new("") {
+ if refresh_path == RelPath::empty() {
continue;
}
let refresh_full_path = lowest_ancestor.join(refresh_path);
refreshes.push(this.as_local_mut().unwrap().refresh_entry(
- refresh_full_path.into(),
+ refresh_full_path,
None,
cx,
));
@@ -1628,17 +1425,14 @@ impl LocalWorktree {
fn write_file(
&self,
- path: impl Into<Arc<Path>>,
+ path: Arc<RelPath>,
text: Rope,
line_ending: LineEnding,
cx: &Context<Worktree>,
) -> Task<Result<Arc<File>>> {
- let path = path.into();
let fs = self.fs.clone();
let is_private = self.is_path_private(&path);
- let Ok(abs_path) = self.absolutize(&path) else {
- return Task::ready(Err(anyhow!("invalid path {path:?}")));
- };
+ let abs_path = self.absolutize(&path);
let write = cx.background_spawn({
let fs = fs.clone();
@@ -1695,13 +1489,13 @@ impl LocalWorktree {
let delete = cx.background_spawn(async move {
if entry.is_file() {
if trash {
- fs.trash_file(&abs_path?, Default::default()).await?;
+ fs.trash_file(&abs_path, Default::default()).await?;
} else {
- fs.remove_file(&abs_path?, Default::default()).await?;
+ fs.remove_file(&abs_path, Default::default()).await?;
}
} else if trash {
fs.trash_dir(
- &abs_path?,
+ &abs_path,
RemoveOptions {
recursive: true,
ignore_if_not_exists: false,
@@ -1710,7 +1504,7 @@ impl LocalWorktree {
.await?;
} else {
fs.remove_dir(
- &abs_path?,
+ &abs_path,
RemoveOptions {
recursive: true,
ignore_if_not_exists: false,
@@ -1734,160 +1528,13 @@ impl LocalWorktree {
}))
}
- /// Rename an entry.
- ///
- /// `new_path` is the new relative path to the worktree root.
- /// If the root entry is renamed then `new_path` is the new root name instead.
- fn rename_entry(
- &self,
- entry_id: ProjectEntryId,
- new_path: impl Into<Arc<Path>>,
- cx: &Context<Worktree>,
- ) -> Task<Result<CreatedEntry>> {
- let old_path = match self.entry_for_id(entry_id) {
- Some(entry) => entry.path.clone(),
- None => return Task::ready(Err(anyhow!("no entry to rename for id {entry_id:?}"))),
- };
- let new_path = new_path.into();
- let abs_old_path = self.absolutize(&old_path);
-
- let is_root_entry = self.root_entry().is_some_and(|e| e.id == entry_id);
- let abs_new_path = if is_root_entry {
- let Some(root_parent_path) = self.abs_path().parent() else {
- return Task::ready(Err(anyhow!("no parent for path {:?}", self.abs_path)));
- };
- root_parent_path.join(&new_path)
- } else {
- let Ok(absolutize_path) = self.absolutize(&new_path) else {
- return Task::ready(Err(anyhow!("absolutizing path {new_path:?}")));
- };
- absolutize_path
- };
-
- let fs = self.fs.clone();
- let abs_path = abs_new_path.clone();
- let case_sensitive = self.fs_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_task = cx.background_spawn(async move {
- let abs_old_path = abs_old_path?;
-
- // 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 |this, cx| {
- rename_task.await?;
- Ok(this
- .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_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_path.parent() {
- let _ = local.refresh_entries_for_paths(vec![parent.into()]);
- }
- // Then refresh the new path
- local.refresh_entry(new_path.clone(), Some(old_path), cx)
- }
- })?
- .await?
- .map(CreatedEntry::Included)
- .unwrap_or_else(|| CreatedEntry::Excluded { abs_path }))
- })
- }
-
- fn copy_entry(
- &self,
- entry_id: ProjectEntryId,
- relative_worktree_source_path: Option<PathBuf>,
- new_path: impl Into<Arc<Path>>,
- cx: &Context<Worktree>,
- ) -> Task<Result<Option<Entry>>> {
- let old_path = match self.entry_for_id(entry_id) {
- Some(entry) => entry.path.clone(),
- None => return Task::ready(Ok(None)),
- };
- let new_path = new_path.into();
- let abs_old_path =
- if let Some(relative_worktree_source_path) = relative_worktree_source_path {
- Ok(self.abs_path().join(relative_worktree_source_path))
- } else {
- self.absolutize(&old_path)
- };
- let abs_new_path = self.absolutize(&new_path);
- let fs = self.fs.clone();
- let copy = cx.background_spawn(async move {
- copy_recursive(
- fs.as_ref(),
- &abs_old_path?,
- &abs_new_path?,
- Default::default(),
- )
- .await
- });
-
- cx.spawn(async move |this, cx| {
- copy.await?;
- this.update(cx, |this, cx| {
- this.as_local_mut()
- .unwrap()
- .refresh_entry(new_path.clone(), None, cx)
- })?
- .await
- })
- }
-
pub fn copy_external_entries(
&self,
- target_directory: Arc<Path>,
+ target_directory: Arc<RelPath>,
paths: Vec<Arc<Path>>,
cx: &Context<Worktree>,
) -> Task<Result<Vec<ProjectEntryId>>> {
- let Ok(target_directory) = self.absolutize(&target_directory) else {
- return Task::ready(Err(anyhow!("invalid target path")));
- };
+ let target_directory = self.absolutize(&target_directory);
let worktree_path = self.abs_path().clone();
let fs = self.fs.clone();
let paths = paths
@@ -1908,7 +1555,13 @@ impl LocalWorktree {
let paths_to_refresh = paths
.iter()
- .filter_map(|(_, target)| Some(target.strip_prefix(&worktree_path).ok()?.into()))
+ .filter_map(|(_, target)| {
+ RelPath::from_std_path(
+ target.strip_prefix(&worktree_path).ok()?,
+ PathStyle::local(),
+ )
+ .ok()
+ })
.collect::<Vec<_>>();
cx.spawn(async move |this, cx| {
@@ -1986,7 +1639,7 @@ impl LocalWorktree {
}))
}
- fn refresh_entries_for_paths(&self, paths: Vec<Arc<Path>>) -> barrier::Receiver {
+ pub fn refresh_entries_for_paths(&self, paths: Vec<Arc<RelPath>>) -> barrier::Receiver {
let (tx, rx) = barrier::channel();
self.scan_requests_tx
.try_send(ScanRequest {
@@ -1998,11 +1651,14 @@ impl LocalWorktree {
}
#[cfg(feature = "test-support")]
- pub fn manually_refresh_entries_for_paths(&self, paths: Vec<Arc<Path>>) -> barrier::Receiver {
+ pub fn manually_refresh_entries_for_paths(
+ &self,
+ paths: Vec<Arc<RelPath>>,
+ ) -> barrier::Receiver {
self.refresh_entries_for_paths(paths)
}
- pub fn add_path_prefix_to_scan(&self, path_prefix: Arc<Path>) -> barrier::Receiver {
+ pub fn add_path_prefix_to_scan(&self, path_prefix: Arc<RelPath>) -> barrier::Receiver {
let (tx, rx) = barrier::channel();
self.path_prefixes_to_scan_tx
.try_send(PathPrefixScanRequest {
@@ -2013,10 +1669,10 @@ impl LocalWorktree {
rx
}
- fn refresh_entry(
+ pub fn refresh_entry(
&self,
- path: Arc<Path>,
- old_path: Option<Arc<Path>>,
+ path: Arc<RelPath>,
+ old_path: Option<Arc<RelPath>>,
cx: &Context<Worktree>,
) -> Task<Result<Option<Entry>>> {
if self.settings.is_path_excluded(&path) {
@@ -3,7 +3,11 @@ use std::path::Path;
use anyhow::Context as _;
use gpui::App;
use settings::{Settings, SettingsContent};
-use util::{ResultExt, paths::PathMatcher};
+use util::{
+ ResultExt,
+ paths::{PathMatcher, PathStyle},
+ rel_path::RelPath,
+};
#[derive(Clone, PartialEq, Eq)]
pub struct WorktreeSettings {
@@ -14,19 +18,19 @@ pub struct WorktreeSettings {
}
impl WorktreeSettings {
- pub fn is_path_private(&self, path: &Path) -> bool {
+ pub fn is_path_private(&self, path: &RelPath) -> bool {
path.ancestors()
- .any(|ancestor| self.private_files.is_match(ancestor))
+ .any(|ancestor| self.private_files.is_match(ancestor.as_std_path()))
}
- pub fn is_path_excluded(&self, path: &Path) -> bool {
+ pub fn is_path_excluded(&self, path: &RelPath) -> bool {
path.ancestors()
- .any(|ancestor| self.file_scan_exclusions.is_match(&ancestor))
+ .any(|ancestor| self.file_scan_exclusions.is_match(ancestor.as_std_path()))
}
- pub fn is_path_always_included(&self, path: &Path) -> bool {
+ pub fn is_path_always_included(&self, path: &RelPath) -> bool {
path.ancestors()
- .any(|ancestor| self.file_scan_inclusions.is_match(&ancestor))
+ .any(|ancestor| self.file_scan_inclusions.is_match(ancestor.as_std_path()))
}
}
@@ -90,5 +94,6 @@ impl Settings for WorktreeSettings {
fn path_matchers(mut values: Vec<String>, context: &'static str) -> anyhow::Result<PathMatcher> {
values.sort();
- PathMatcher::new(values).with_context(|| format!("Failed to parse globs from {}", context))
+ PathMatcher::new(values, PathStyle::local())
+ .with_context(|| format!("Failed to parse globs from {}", context))
}
@@ -1,5 +1,5 @@
use crate::{
- Entry, EntryKind, Event, PathChange, WorkDirectory, Worktree, WorktreeModelHandle,
+ Entry, EntryKind, Event, PathChange, Worktree, WorktreeModelHandle,
worktree_settings::WorktreeSettings,
};
use anyhow::Result;
@@ -20,7 +20,11 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
-use util::{ResultExt, path, test::TempTree};
+use util::{
+ ResultExt, path,
+ rel_path::{RelPath, rel_path},
+ test::TempTree,
+};
#[gpui::test]
async fn test_traversal(cx: &mut TestAppContext) {
@@ -56,10 +60,10 @@ async fn test_traversal(cx: &mut TestAppContext) {
.map(|entry| entry.path.as_ref())
.collect::<Vec<_>>(),
vec![
- Path::new(""),
- Path::new(".gitignore"),
- Path::new("a"),
- Path::new("a/c"),
+ rel_path(""),
+ rel_path(".gitignore"),
+ rel_path("a"),
+ rel_path("a/c"),
]
);
assert_eq!(
@@ -67,11 +71,11 @@ async fn test_traversal(cx: &mut TestAppContext) {
.map(|entry| entry.path.as_ref())
.collect::<Vec<_>>(),
vec![
- Path::new(""),
- Path::new(".gitignore"),
- Path::new("a"),
- Path::new("a/b"),
- Path::new("a/c"),
+ rel_path(""),
+ rel_path(".gitignore"),
+ rel_path("a"),
+ rel_path("a/b"),
+ rel_path("a/c"),
]
);
})
@@ -121,14 +125,14 @@ async fn test_circular_symlinks(cx: &mut TestAppContext) {
.map(|entry| entry.path.as_ref())
.collect::<Vec<_>>(),
vec![
- Path::new(""),
- Path::new("lib"),
- Path::new("lib/a"),
- Path::new("lib/a/a.txt"),
- Path::new("lib/a/lib"),
- Path::new("lib/b"),
- Path::new("lib/b/b.txt"),
- Path::new("lib/b/lib"),
+ rel_path(""),
+ rel_path("lib"),
+ rel_path("lib/a"),
+ rel_path("lib/a/a.txt"),
+ rel_path("lib/a/lib"),
+ rel_path("lib/b"),
+ rel_path("lib/b/b.txt"),
+ rel_path("lib/b/lib"),
]
);
});
@@ -147,14 +151,14 @@ async fn test_circular_symlinks(cx: &mut TestAppContext) {
.map(|entry| entry.path.as_ref())
.collect::<Vec<_>>(),
vec![
- Path::new(""),
- Path::new("lib"),
- Path::new("lib/a"),
- Path::new("lib/a/a.txt"),
- Path::new("lib/a/lib-2"),
- Path::new("lib/b"),
- Path::new("lib/b/b.txt"),
- Path::new("lib/b/lib"),
+ rel_path(""),
+ rel_path("lib"),
+ rel_path("lib/a"),
+ rel_path("lib/a/a.txt"),
+ rel_path("lib/a/lib-2"),
+ rel_path("lib/b"),
+ rel_path("lib/b/b.txt"),
+ rel_path("lib/b/lib"),
]
);
});
@@ -236,18 +240,18 @@ async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
.map(|entry| (entry.path.as_ref(), entry.is_external))
.collect::<Vec<_>>(),
vec![
- (Path::new(""), false),
- (Path::new("deps"), false),
- (Path::new("deps/dep-dir2"), true),
- (Path::new("deps/dep-dir3"), true),
- (Path::new("src"), false),
- (Path::new("src/a.rs"), false),
- (Path::new("src/b.rs"), false),
+ (rel_path(""), false),
+ (rel_path("deps"), false),
+ (rel_path("deps/dep-dir2"), true),
+ (rel_path("deps/dep-dir3"), true),
+ (rel_path("src"), false),
+ (rel_path("src/a.rs"), false),
+ (rel_path("src/b.rs"), false),
]
);
assert_eq!(
- tree.entry_for_path("deps/dep-dir2").unwrap().kind,
+ tree.entry_for_path(rel_path("deps/dep-dir2")).unwrap().kind,
EntryKind::UnloadedDir
);
});
@@ -256,7 +260,7 @@ async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
tree.read_with(cx, |tree, _| {
tree.as_local()
.unwrap()
- .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3").into()])
+ .refresh_entries_for_paths(vec![rel_path("deps/dep-dir3").into()])
})
.recv()
.await;
@@ -269,24 +273,24 @@ async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
.map(|entry| (entry.path.as_ref(), entry.is_external))
.collect::<Vec<_>>(),
vec![
- (Path::new(""), false),
- (Path::new("deps"), false),
- (Path::new("deps/dep-dir2"), true),
- (Path::new("deps/dep-dir3"), true),
- (Path::new("deps/dep-dir3/deps"), true),
- (Path::new("deps/dep-dir3/src"), true),
- (Path::new("src"), false),
- (Path::new("src/a.rs"), false),
- (Path::new("src/b.rs"), false),
+ (rel_path(""), false),
+ (rel_path("deps"), false),
+ (rel_path("deps/dep-dir2"), true),
+ (rel_path("deps/dep-dir3"), true),
+ (rel_path("deps/dep-dir3/deps"), true),
+ (rel_path("deps/dep-dir3/src"), true),
+ (rel_path("src"), false),
+ (rel_path("src/a.rs"), false),
+ (rel_path("src/b.rs"), false),
]
);
});
assert_eq!(
mem::take(&mut *tree_updates.lock()),
&[
- (Path::new("deps/dep-dir3").into(), PathChange::Loaded),
- (Path::new("deps/dep-dir3/deps").into(), PathChange::Loaded),
- (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded)
+ (rel_path("deps/dep-dir3").into(), PathChange::Loaded),
+ (rel_path("deps/dep-dir3/deps").into(), PathChange::Loaded),
+ (rel_path("deps/dep-dir3/src").into(), PathChange::Loaded)
]
);
@@ -294,7 +298,7 @@ async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
tree.read_with(cx, |tree, _| {
tree.as_local()
.unwrap()
- .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3/src").into()])
+ .refresh_entries_for_paths(vec![rel_path("deps/dep-dir3/src").into()])
})
.recv()
.await;
@@ -306,17 +310,17 @@ async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
.map(|entry| (entry.path.as_ref(), entry.is_external))
.collect::<Vec<_>>(),
vec![
- (Path::new(""), false),
- (Path::new("deps"), false),
- (Path::new("deps/dep-dir2"), true),
- (Path::new("deps/dep-dir3"), true),
- (Path::new("deps/dep-dir3/deps"), true),
- (Path::new("deps/dep-dir3/src"), true),
- (Path::new("deps/dep-dir3/src/e.rs"), true),
- (Path::new("deps/dep-dir3/src/f.rs"), true),
- (Path::new("src"), false),
- (Path::new("src/a.rs"), false),
- (Path::new("src/b.rs"), false),
+ (rel_path(""), false),
+ (rel_path("deps"), false),
+ (rel_path("deps/dep-dir2"), true),
+ (rel_path("deps/dep-dir3"), true),
+ (rel_path("deps/dep-dir3/deps"), true),
+ (rel_path("deps/dep-dir3/src"), true),
+ (rel_path("deps/dep-dir3/src/e.rs"), true),
+ (rel_path("deps/dep-dir3/src/f.rs"), true),
+ (rel_path("src"), false),
+ (rel_path("src/a.rs"), false),
+ (rel_path("src/b.rs"), false),
]
);
});
@@ -324,13 +328,13 @@ async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
assert_eq!(
mem::take(&mut *tree_updates.lock()),
&[
- (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded),
+ (rel_path("deps/dep-dir3/src").into(), PathChange::Loaded),
(
- Path::new("deps/dep-dir3/src/e.rs").into(),
+ rel_path("deps/dep-dir3/src/e.rs").into(),
PathChange::Loaded
),
(
- Path::new("deps/dep-dir3/src/f.rs").into(),
+ rel_path("deps/dep-dir3/src/f.rs").into(),
PathChange::Loaded
)
]
@@ -368,7 +372,7 @@ async fn test_renaming_case_only(cx: &mut TestAppContext) {
tree.entries(true, 0)
.map(|entry| entry.path.as_ref())
.collect::<Vec<_>>(),
- vec![Path::new(""), Path::new(OLD_NAME)]
+ vec![rel_path(""), rel_path(OLD_NAME)]
);
});
@@ -390,7 +394,7 @@ async fn test_renaming_case_only(cx: &mut TestAppContext) {
tree.entries(true, 0)
.map(|entry| entry.path.as_ref())
.collect::<Vec<_>>(),
- vec![Path::new(""), Path::new(NEW_NAME)]
+ vec![rel_path(""), rel_path(NEW_NAME)]
);
});
}
@@ -446,13 +450,13 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
.map(|entry| (entry.path.as_ref(), entry.is_ignored))
.collect::<Vec<_>>(),
vec![
- (Path::new(""), false),
- (Path::new(".gitignore"), false),
- (Path::new("one"), false),
- (Path::new("one/node_modules"), true),
- (Path::new("two"), false),
- (Path::new("two/x.js"), false),
- (Path::new("two/y.js"), false),
+ (rel_path(""), false),
+ (rel_path(".gitignore"), false),
+ (rel_path("one"), false),
+ (rel_path("one/node_modules"), true),
+ (rel_path("two"), false),
+ (rel_path("two/x.js"), false),
+ (rel_path("two/y.js"), false),
]
);
});
@@ -462,7 +466,7 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
let prev_read_dir_count = fs.read_dir_call_count();
let loaded = tree
.update(cx, |tree, cx| {
- tree.load_file("one/node_modules/b/b1.js".as_ref(), cx)
+ tree.load_file(rel_path("one/node_modules/b/b1.js"), cx)
})
.await
.unwrap();
@@ -473,24 +477,24 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
.map(|entry| (entry.path.as_ref(), entry.is_ignored))
.collect::<Vec<_>>(),
vec![
- (Path::new(""), false),
- (Path::new(".gitignore"), false),
- (Path::new("one"), false),
- (Path::new("one/node_modules"), true),
- (Path::new("one/node_modules/a"), true),
- (Path::new("one/node_modules/b"), true),
- (Path::new("one/node_modules/b/b1.js"), true),
- (Path::new("one/node_modules/b/b2.js"), true),
- (Path::new("one/node_modules/c"), true),
- (Path::new("two"), false),
- (Path::new("two/x.js"), false),
- (Path::new("two/y.js"), false),
+ (rel_path(""), false),
+ (rel_path(".gitignore"), false),
+ (rel_path("one"), false),
+ (rel_path("one/node_modules"), true),
+ (rel_path("one/node_modules/a"), true),
+ (rel_path("one/node_modules/b"), true),
+ (rel_path("one/node_modules/b/b1.js"), true),
+ (rel_path("one/node_modules/b/b2.js"), true),
+ (rel_path("one/node_modules/c"), true),
+ (rel_path("two"), false),
+ (rel_path("two/x.js"), false),
+ (rel_path("two/y.js"), false),
]
);
assert_eq!(
loaded.file.path.as_ref(),
- Path::new("one/node_modules/b/b1.js")
+ rel_path("one/node_modules/b/b1.js")
);
// Only the newly-expanded directories are scanned.
@@ -502,7 +506,7 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
let prev_read_dir_count = fs.read_dir_call_count();
let loaded = tree
.update(cx, |tree, cx| {
- tree.load_file("one/node_modules/a/a2.js".as_ref(), cx)
+ tree.load_file(rel_path("one/node_modules/a/a2.js"), cx)
})
.await
.unwrap();
@@ -513,26 +517,26 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
.map(|entry| (entry.path.as_ref(), entry.is_ignored))
.collect::<Vec<_>>(),
vec![
- (Path::new(""), false),
- (Path::new(".gitignore"), false),
- (Path::new("one"), false),
- (Path::new("one/node_modules"), true),
- (Path::new("one/node_modules/a"), true),
- (Path::new("one/node_modules/a/a1.js"), true),
- (Path::new("one/node_modules/a/a2.js"), true),
- (Path::new("one/node_modules/b"), true),
- (Path::new("one/node_modules/b/b1.js"), true),
- (Path::new("one/node_modules/b/b2.js"), true),
- (Path::new("one/node_modules/c"), true),
- (Path::new("two"), false),
- (Path::new("two/x.js"), false),
- (Path::new("two/y.js"), false),
+ (rel_path(""), false),
+ (rel_path(".gitignore"), false),
+ (rel_path("one"), false),
+ (rel_path("one/node_modules"), true),
+ (rel_path("one/node_modules/a"), true),
+ (rel_path("one/node_modules/a/a1.js"), true),
+ (rel_path("one/node_modules/a/a2.js"), true),
+ (rel_path("one/node_modules/b"), true),
+ (rel_path("one/node_modules/b/b1.js"), true),
+ (rel_path("one/node_modules/b/b2.js"), true),
+ (rel_path("one/node_modules/c"), true),
+ (rel_path("two"), false),
+ (rel_path("two/x.js"), false),
+ (rel_path("two/y.js"), false),
]
);
assert_eq!(
loaded.file.path.as_ref(),
- Path::new("one/node_modules/a/a2.js")
+ rel_path("one/node_modules/a/a2.js")
);
// Only the newly-expanded directory is scanned.
@@ -610,7 +614,7 @@ async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
tree.read_with(cx, |tree, _| {
tree.as_local()
.unwrap()
- .refresh_entries_for_paths(vec![Path::new("node_modules/d/d.js").into()])
+ .refresh_entries_for_paths(vec![rel_path("node_modules/d/d.js").into()])
})
.recv()
.await;
@@ -622,18 +626,18 @@ async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
.map(|e| (e.path.as_ref(), e.is_ignored))
.collect::<Vec<_>>(),
&[
- (Path::new(""), false),
- (Path::new(".gitignore"), false),
- (Path::new("a"), false),
- (Path::new("a/a.js"), false),
- (Path::new("b"), false),
- (Path::new("b/b.js"), false),
- (Path::new("node_modules"), true),
- (Path::new("node_modules/c"), true),
- (Path::new("node_modules/d"), true),
- (Path::new("node_modules/d/d.js"), true),
- (Path::new("node_modules/d/e"), true),
- (Path::new("node_modules/d/f"), true),
+ (rel_path(""), false),
+ (rel_path(".gitignore"), false),
+ (rel_path("a"), false),
+ (rel_path("a/a.js"), false),
+ (rel_path("b"), false),
+ (rel_path("b/b.js"), false),
+ (rel_path("node_modules"), true),
+ (rel_path("node_modules/c"), true),
+ (rel_path("node_modules/d"), true),
+ (rel_path("node_modules/d/d.js"), true),
+ (rel_path("node_modules/d/e"), true),
+ (rel_path("node_modules/d/f"), true),
]
);
});
@@ -654,23 +658,23 @@ async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
.map(|e| (e.path.as_ref(), e.is_ignored))
.collect::<Vec<_>>(),
&[
- (Path::new(""), false),
- (Path::new(".gitignore"), false),
- (Path::new("a"), false),
- (Path::new("a/a.js"), false),
- (Path::new("b"), false),
- (Path::new("b/b.js"), false),
+ (rel_path(""), false),
+ (rel_path(".gitignore"), false),
+ (rel_path("a"), false),
+ (rel_path("a/a.js"), false),
+ (rel_path("b"), false),
+ (rel_path("b/b.js"), false),
// This directory is no longer ignored
- (Path::new("node_modules"), false),
- (Path::new("node_modules/c"), false),
- (Path::new("node_modules/c/c.js"), false),
- (Path::new("node_modules/d"), false),
- (Path::new("node_modules/d/d.js"), false),
+ (rel_path("node_modules"), false),
+ (rel_path("node_modules/c"), false),
+ (rel_path("node_modules/c/c.js"), false),
+ (rel_path("node_modules/d"), false),
+ (rel_path("node_modules/d/d.js"), false),
// This subdirectory is now ignored
- (Path::new("node_modules/d/e"), true),
- (Path::new("node_modules/d/f"), false),
- (Path::new("node_modules/d/f/f1.js"), false),
- (Path::new("node_modules/d/f/f2.js"), false),
+ (rel_path("node_modules/d/e"), true),
+ (rel_path("node_modules/d/f"), false),
+ (rel_path("node_modules/d/f/f1.js"), false),
+ (rel_path("node_modules/d/f/f2.js"), false),
]
);
});
@@ -711,7 +715,7 @@ async fn test_write_file(cx: &mut TestAppContext) {
worktree
.update(cx, |tree, cx| {
tree.write_file(
- Path::new("tracked-dir/file.txt"),
+ rel_path("tracked-dir/file.txt").into(),
"hello".into(),
Default::default(),
cx,
@@ -722,7 +726,7 @@ async fn test_write_file(cx: &mut TestAppContext) {
worktree
.update(cx, |tree, cx| {
tree.write_file(
- Path::new("ignored-dir/file.txt"),
+ rel_path("ignored-dir/file.txt").into(),
"world".into(),
Default::default(),
cx,
@@ -732,8 +736,12 @@ async fn test_write_file(cx: &mut TestAppContext) {
.unwrap();
worktree.read_with(cx, |tree, _| {
- let tracked = tree.entry_for_path("tracked-dir/file.txt").unwrap();
- let ignored = tree.entry_for_path("ignored-dir/file.txt").unwrap();
+ let tracked = tree
+ .entry_for_path(rel_path("tracked-dir/file.txt"))
+ .unwrap();
+ let ignored = tree
+ .entry_for_path(rel_path("ignored-dir/file.txt"))
+ .unwrap();
assert!(!tracked.is_ignored);
assert!(ignored.is_ignored);
});
@@ -918,11 +926,11 @@ async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppC
tree.read_with(cx, |tree, _| {
assert!(
- tree.entry_for_path("node_modules")
+ tree.entry_for_path(rel_path("node_modules"))
.is_some_and(|f| f.is_always_included)
);
assert!(
- tree.entry_for_path("node_modules/prettier/package.json")
+ tree.entry_for_path(rel_path("node_modules/prettier/package.json"))
.is_some_and(|f| f.is_always_included)
);
});
@@ -941,11 +949,11 @@ async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppC
tree.read_with(cx, |tree, _| {
assert!(
- tree.entry_for_path("node_modules")
+ tree.entry_for_path(rel_path("node_modules"))
.is_some_and(|f| !f.is_always_included)
);
assert!(
- tree.entry_for_path("node_modules/prettier/package.json")
+ tree.entry_for_path(rel_path("node_modules/prettier/package.json"))
.is_some_and(|f| !f.is_always_included)
);
});
@@ -1272,7 +1280,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
.update(cx, |tree, cx| {
tree.as_local_mut()
.unwrap()
- .create_entry("a/e".as_ref(), true, None, cx)
+ .create_entry(rel_path("a/e").into(), true, None, cx)
})
.await
.unwrap()
@@ -1282,7 +1290,10 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
cx.executor().run_until_parked();
tree.read_with(cx, |tree, _| {
- assert_eq!(tree.entry_for_path("a/e").unwrap().kind, EntryKind::Dir);
+ assert_eq!(
+ tree.entry_for_path(rel_path("a/e")).unwrap().kind,
+ EntryKind::Dir
+ );
});
let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
@@ -1319,9 +1330,12 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
let entry = tree_fake
.update(cx, |tree, cx| {
- tree.as_local_mut()
- .unwrap()
- .create_entry("a/b/c/d.txt".as_ref(), false, None, cx)
+ tree.as_local_mut().unwrap().create_entry(
+ rel_path("a/b/c/d.txt").into(),
+ false,
+ None,
+ cx,
+ )
})
.await
.unwrap()
@@ -1331,9 +1345,13 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
cx.executor().run_until_parked();
tree_fake.read_with(cx, |tree, _| {
- assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
- assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
- assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
+ assert!(
+ tree.entry_for_path(rel_path("a/b/c/d.txt"))
+ .unwrap()
+ .is_file()
+ );
+ assert!(tree.entry_for_path(rel_path("a/b/c")).unwrap().is_dir());
+ assert!(tree.entry_for_path(rel_path("a/b")).unwrap().is_dir());
});
let fs_real = Arc::new(RealFs::new(None, cx.executor()));
@@ -1353,9 +1371,12 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
let entry = tree_real
.update(cx, |tree, cx| {
- tree.as_local_mut()
- .unwrap()
- .create_entry("a/b/c/d.txt".as_ref(), false, None, cx)
+ tree.as_local_mut().unwrap().create_entry(
+ rel_path("a/b/c/d.txt").into(),
+ false,
+ None,
+ cx,
+ )
})
.await
.unwrap()
@@ -1365,17 +1386,24 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
cx.executor().run_until_parked();
tree_real.read_with(cx, |tree, _| {
- assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
- assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
- assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
+ assert!(
+ tree.entry_for_path(rel_path("a/b/c/d.txt"))
+ .unwrap()
+ .is_file()
+ );
+ assert!(tree.entry_for_path(rel_path("a/b/c")).unwrap().is_dir());
+ assert!(tree.entry_for_path(rel_path("a/b")).unwrap().is_dir());
});
// Test smallest change
let entry = tree_real
.update(cx, |tree, cx| {
- tree.as_local_mut()
- .unwrap()
- .create_entry("a/b/c/e.txt".as_ref(), false, None, cx)
+ tree.as_local_mut().unwrap().create_entry(
+ rel_path("a/b/c/e.txt").into(),
+ false,
+ None,
+ cx,
+ )
})
.await
.unwrap()
@@ -1385,15 +1413,22 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
cx.executor().run_until_parked();
tree_real.read_with(cx, |tree, _| {
- assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
+ assert!(
+ tree.entry_for_path(rel_path("a/b/c/e.txt"))
+ .unwrap()
+ .is_file()
+ );
});
// Test largest change
let entry = tree_real
.update(cx, |tree, cx| {
- tree.as_local_mut()
- .unwrap()
- .create_entry("d/e/f/g.txt".as_ref(), false, None, cx)
+ tree.as_local_mut().unwrap().create_entry(
+ rel_path("d/e/f/g.txt").into(),
+ false,
+ None,
+ cx,
+ )
})
.await
.unwrap()
@@ -1403,10 +1438,14 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
cx.executor().run_until_parked();
tree_real.read_with(cx, |tree, _| {
- assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
- assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
- assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
- assert!(tree.entry_for_path("d/").unwrap().is_dir());
+ assert!(
+ tree.entry_for_path(rel_path("d/e/f/g.txt"))
+ .unwrap()
+ .is_file()
+ );
+ assert!(tree.entry_for_path(rel_path("d/e/f")).unwrap().is_dir());
+ assert!(tree.entry_for_path(rel_path("d/e")).unwrap().is_dir());
+ assert!(tree.entry_for_path(rel_path("d")).unwrap().is_dir());
});
}
@@ -1701,37 +1740,13 @@ fn randomly_mutate_worktree(
let entry = snapshot.entries(false, 0).choose(rng).unwrap();
match rng.random_range(0_u32..100) {
- 0..=33 if entry.path.as_ref() != Path::new("") => {
+ 0..=33 if entry.path.as_ref() != RelPath::empty() => {
log::info!("deleting entry {:?} ({})", entry.path, entry.id.0);
worktree.delete_entry(entry.id, false, cx).unwrap()
}
- ..=66 if entry.path.as_ref() != Path::new("") => {
- let other_entry = snapshot.entries(false, 0).choose(rng).unwrap();
- let new_parent_path = if other_entry.is_dir() {
- other_entry.path.clone()
- } else {
- other_entry.path.parent().unwrap().into()
- };
- let mut new_path = new_parent_path.join(random_filename(rng));
- if new_path.starts_with(&entry.path) {
- new_path = random_filename(rng).into();
- }
-
- log::info!(
- "renaming entry {:?} ({}) to {:?}",
- entry.path,
- entry.id.0,
- new_path
- );
- let task = worktree.rename_entry(entry.id, new_path, cx);
- cx.background_spawn(async move {
- task.await?.into_included().unwrap();
- Ok(())
- })
- }
_ => {
if entry.is_dir() {
- let child_path = entry.path.join(random_filename(rng));
+ let child_path = entry.path.join(rel_path(&random_filename(rng)));
let is_dir = rng.random_bool(0.3);
log::info!(
"creating {} at {:?}",
@@ -1744,7 +1759,7 @@ fn randomly_mutate_worktree(
Ok(())
})
} else {
- log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
+ log::info!("overwriting file {:?} ({})", &entry.path, entry.id.0);
let task =
worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx);
cx.background_spawn(async move {
@@ -1794,7 +1809,7 @@ async fn randomly_mutate_fs(
}
} else if rng.random_bool(0.05) {
let ignore_dir_path = dirs.choose(rng).unwrap();
- let ignore_path = ignore_dir_path.join(*GITIGNORE);
+ let ignore_path = ignore_dir_path.join(GITIGNORE);
let subdirs = dirs
.iter()
@@ -1923,101 +1938,6 @@ fn random_filename(rng: &mut impl Rng) -> String {
.collect()
}
-#[gpui::test]
-async fn test_rename_file_to_new_directory(cx: &mut TestAppContext) {
- init_test(cx);
- let fs = FakeFs::new(cx.background_executor.clone());
- let expected_contents = "content";
- fs.as_fake()
- .insert_tree(
- "/root",
- json!({
- "test.txt": expected_contents
- }),
- )
- .await;
- let worktree = Worktree::local(
- Path::new("/root"),
- true,
- fs.clone(),
- Arc::default(),
- &mut cx.to_async(),
- )
- .await
- .unwrap();
- cx.read(|cx| worktree.read(cx).as_local().unwrap().scan_complete())
- .await;
-
- let entry_id = worktree.read_with(cx, |worktree, _| {
- worktree.entry_for_path("test.txt").unwrap().id
- });
- let _result = worktree
- .update(cx, |worktree, cx| {
- worktree.rename_entry(entry_id, Path::new("dir1/dir2/dir3/test.txt"), cx)
- })
- .await
- .unwrap();
- worktree.read_with(cx, |worktree, _| {
- assert!(
- worktree.entry_for_path("test.txt").is_none(),
- "Old file should have been removed"
- );
- assert!(
- worktree.entry_for_path("dir1/dir2/dir3/test.txt").is_some(),
- "Whole directory hierarchy and the new file should have been created"
- );
- });
- assert_eq!(
- worktree
- .update(cx, |worktree, cx| {
- worktree.load_file("dir1/dir2/dir3/test.txt".as_ref(), cx)
- })
- .await
- .unwrap()
- .text,
- expected_contents,
- "Moved file's contents should be preserved"
- );
-
- let entry_id = worktree.read_with(cx, |worktree, _| {
- worktree
- .entry_for_path("dir1/dir2/dir3/test.txt")
- .unwrap()
- .id
- });
- let _result = worktree
- .update(cx, |worktree, cx| {
- worktree.rename_entry(entry_id, Path::new("dir1/dir2/test.txt"), cx)
- })
- .await
- .unwrap();
- worktree.read_with(cx, |worktree, _| {
- assert!(
- worktree.entry_for_path("test.txt").is_none(),
- "First file should not reappear"
- );
- assert!(
- worktree.entry_for_path("dir1/dir2/dir3/test.txt").is_none(),
- "Old file should have been removed"
- );
- assert!(
- worktree.entry_for_path("dir1/dir2/test.txt").is_some(),
- "No error should have occurred after moving into existing directory"
- );
- });
- assert_eq!(
- worktree
- .update(cx, |worktree, cx| {
- worktree.load_file("dir1/dir2/test.txt".as_ref(), cx)
- })
- .await
- .unwrap()
- .text,
- expected_contents,
- "Moved file's contents should be preserved"
- );
-}
-
#[gpui::test]
async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
init_test(cx);
@@ -2036,48 +1956,11 @@ async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
tree.read_with(cx, |tree, _| {
- let entry = tree.entry_for_path("").unwrap();
+ let entry = tree.entry_for_path(rel_path("")).unwrap();
assert!(entry.is_private);
});
}
-#[gpui::test]
-fn test_unrelativize() {
- let work_directory = WorkDirectory::in_project("");
- pretty_assertions::assert_eq!(
- work_directory.try_unrelativize(&"crates/gpui/gpui.rs".into()),
- Some(Path::new("crates/gpui/gpui.rs").into())
- );
-
- let work_directory = WorkDirectory::in_project("vendor/some-submodule");
- pretty_assertions::assert_eq!(
- work_directory.try_unrelativize(&"src/thing.c".into()),
- Some(Path::new("vendor/some-submodule/src/thing.c").into())
- );
-
- let work_directory = WorkDirectory::AboveProject {
- absolute_path: Path::new("/projects/zed").into(),
- location_in_repo: Path::new("crates/gpui").into(),
- };
-
- pretty_assertions::assert_eq!(
- work_directory.try_unrelativize(&"crates/util/util.rs".into()),
- None,
- );
-
- pretty_assertions::assert_eq!(
- work_directory.unrelativize(&"crates/util/util.rs".into()),
- Path::new("../util/util.rs").into()
- );
-
- pretty_assertions::assert_eq!(work_directory.try_unrelativize(&"README.md".into()), None,);
-
- pretty_assertions::assert_eq!(
- work_directory.unrelativize(&"README.md".into()),
- Path::new("../../README.md").into()
- );
-}
-
#[gpui::test]
async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx);
@@ -2259,7 +2142,7 @@ fn check_worktree_entries(
expected_included_paths: &[&str],
) {
for path in expected_excluded_paths {
- let entry = tree.entry_for_path(path);
+ let entry = tree.entry_for_path(rel_path(path));
assert!(
entry.is_none(),
"expected path '{path}' to be excluded, but got entry: {entry:?}",
@@ -2267,7 +2150,7 @@ fn check_worktree_entries(
}
for path in expected_ignored_paths {
let entry = tree
- .entry_for_path(path)
+ .entry_for_path(rel_path(path))
.unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
assert!(
entry.is_ignored,
@@ -2276,7 +2159,7 @@ fn check_worktree_entries(
}
for path in expected_tracked_paths {
let entry = tree
- .entry_for_path(path)
+ .entry_for_path(rel_path(path))
.unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
assert!(
!entry.is_ignored || entry.is_always_included,
@@ -2285,7 +2168,7 @@ fn check_worktree_entries(
}
for path in expected_included_paths {
let entry = tree
- .entry_for_path(path)
+ .entry_for_path(rel_path(path))
.unwrap_or_else(|| panic!("Missing entry for expected included path '{path}'"));
assert!(
entry.is_always_included,
@@ -71,6 +71,7 @@ use terminal_view::terminal_panel::{self, TerminalPanel};
use theme::{ActiveTheme, ThemeSettings};
use ui::{PopoverMenuHandle, prelude::*};
use util::markdown::MarkdownString;
+use util::rel_path::RelPath;
use util::{ResultExt, asset_str};
use uuid::Uuid;
use vim_mode_setting::VimModeSetting;
@@ -1653,7 +1654,7 @@ fn open_project_debug_tasks_file(
fn open_local_file(
workspace: &mut Workspace,
- settings_relative_path: &'static Path,
+ settings_relative_path: &'static RelPath,
initial_contents: Cow<'static, str>,
window: &mut Window,
cx: &mut Context<Workspace>,
@@ -1969,7 +1970,7 @@ mod tests {
time::Duration,
};
use theme::{ThemeRegistry, ThemeSettings};
- use util::path;
+ use util::{path, rel_path::rel_path};
use workspace::{
NewFile, OpenOptions, OpenVisible, SERIALIZATION_THROTTLE_TIME, SaveIntent, SplitDirection,
WorkspaceHandle,
@@ -2749,7 +2750,7 @@ mod tests {
fn assert_project_panel_selection(
workspace: &Workspace,
expected_worktree_path: &Path,
- expected_entry_path: &Path,
+ expected_entry_path: &RelPath,
cx: &App,
) {
let project_panel = [
@@ -2797,7 +2798,7 @@ mod tests {
assert_project_panel_selection(
workspace,
Path::new(path!("/dir1")),
- Path::new("a.txt"),
+ rel_path("a.txt"),
cx,
);
assert_eq!(
@@ -2835,7 +2836,7 @@ mod tests {
assert_project_panel_selection(
workspace,
Path::new(path!("/dir2/b.txt")),
- Path::new(""),
+ rel_path(""),
cx,
);
let worktree_roots = workspace
@@ -2884,7 +2885,7 @@ mod tests {
assert_project_panel_selection(
workspace,
Path::new(path!("/dir3")),
- Path::new("c.txt"),
+ rel_path("c.txt"),
cx,
);
let worktree_roots = workspace
@@ -2930,12 +2931,7 @@ mod tests {
.await;
cx.read(|cx| {
let workspace = workspace.read(cx);
- assert_project_panel_selection(
- workspace,
- Path::new(path!("/d.txt")),
- Path::new(""),
- cx,
- );
+ assert_project_panel_selection(workspace, Path::new(path!("/d.txt")), rel_path(""), cx);
let worktree_roots = workspace
.worktrees(cx)
.map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
@@ -3061,9 +3057,7 @@ mod tests {
.zip(paths_to_open.iter())
.map(|(i, path)| {
match i {
- Some(Ok(i)) => {
- Some(i.project_path(cx).map(|p| p.path.display().to_string()))
- }
+ Some(Ok(i)) => Some(i.project_path(cx).map(|p| p.path)),
Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
None => None,
}
@@ -3076,8 +3070,8 @@ mod tests {
opened_paths,
vec![
None,
- Some(path!(".git/HEAD").to_string()),
- Some(path!("excluded_dir/file").to_string()),
+ Some(rel_path(".git/HEAD").into()),
+ Some(rel_path("excluded_dir/file").into()),
],
"Excluded files should get opened, excluded dir should not get opened"
);
@@ -3096,14 +3090,12 @@ mod tests {
i.project_path(cx)
.expect("all excluded files that got open should have a path")
.path
- .display()
- .to_string()
})
.collect::<Vec<_>>();
opened_buffer_paths.sort();
assert_eq!(
opened_buffer_paths,
- vec![path!(".git/HEAD").to_string(), path!("excluded_dir/file").to_string()],
+ vec![rel_path(".git/HEAD").into(), rel_path("excluded_dir/file").into()],
"Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
);
});
@@ -3296,7 +3288,7 @@ mod tests {
cx,
);
workspace.open_path(
- (worktree.read(cx).id(), "the-new-name.rs"),
+ (worktree.read(cx).id(), rel_path("the-new-name.rs")),
None,
true,
window,
@@ -4828,7 +4820,8 @@ mod tests {
// 5. Critical: Verify .zed is actually excluded from worktree
let worktree = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap());
- let has_zed_entry = cx.update(|cx| worktree.read(cx).entry_for_path(".zed").is_some());
+ let has_zed_entry =
+ cx.update(|cx| worktree.read(cx).entry_for_path(rel_path(".zed")).is_some());
eprintln!(
"Is .zed directory visible in worktree after exclusion: {}",
@@ -2,7 +2,7 @@ use std::{
collections::BTreeSet,
fmt::{Display, Formatter},
ops::Range,
- path::{Path, PathBuf},
+ path::PathBuf,
sync::{Arc, LazyLock},
};
@@ -14,7 +14,7 @@ use itertools::Itertools;
use postage::watch;
use project::Worktree;
use strum::VariantArray;
-use util::{ResultExt as _, maybe};
+use util::{ResultExt as _, maybe, rel_path::RelPath};
use worktree::ChildEntriesOptions;
/// Matches the most common license locations, with US and UK English spelling.
@@ -283,14 +283,13 @@ impl LicenseDetectionWatcher {
return Self::Remote;
};
let fs = local_worktree.fs().clone();
- let worktree_abs_path = local_worktree.abs_path().clone();
let options = ChildEntriesOptions {
include_files: true,
include_dirs: false,
include_ignored: true,
};
- for top_file in local_worktree.child_entries_with_options(Path::new(""), options) {
+ for top_file in local_worktree.child_entries_with_options(RelPath::empty(), options) {
let path_bytes = top_file.path.as_os_str().as_encoded_bytes();
if top_file.is_created() && LICENSE_FILE_NAME_REGEX.is_match(path_bytes) {
let rel_path = top_file.path.clone();
@@ -312,12 +311,13 @@ impl LicenseDetectionWatcher {
worktree::Event::DeletedEntry(_) | worktree::Event::UpdatedGitRepositories(_) => {}
});
+ let worktree_snapshot = worktree.read(cx).snapshot();
let (mut is_open_source_tx, is_open_source_rx) = watch::channel_with::<bool>(false);
let _is_open_source_task = cx.background_spawn(async move {
let mut eligible_licenses = BTreeSet::new();
while let Some(rel_path) = files_to_check_rx.next().await {
- let abs_path = worktree_abs_path.join(&rel_path);
+ let abs_path = worktree_snapshot.absolutize(&rel_path);
let was_open_source = !eligible_licenses.is_empty();
if Self::is_path_eligible(&fs, abs_path).await.unwrap_or(false) {
eligible_licenses.insert(rel_path);
@@ -384,6 +384,8 @@ impl LicenseDetectionWatcher {
#[cfg(test)]
mod tests {
+ use std::path::Path;
+
use fs::FakeFs;
use gpui::TestAppContext;
use rand::Rng as _;
@@ -51,6 +51,7 @@ use std::{
use telemetry_events::EditPredictionRating;
use thiserror::Error;
use util::ResultExt;
+use util::rel_path::RelPath;
use uuid::Uuid;
use workspace::notifications::{ErrorMessagePrompt, NotificationId, show_app_notification};
use worktree::Worktree;
@@ -1180,11 +1181,11 @@ impl Event {
let old_path = old_snapshot
.file()
.map(|f| f.path().as_ref())
- .unwrap_or(Path::new("untitled"));
+ .unwrap_or(RelPath::new("untitled").unwrap());
let new_path = new_snapshot
.file()
.map(|f| f.path().as_ref())
- .unwrap_or(Path::new("untitled"));
+ .unwrap_or(RelPath::new("untitled").unwrap());
if old_path != new_path {
writeln!(prompt, "User renamed {:?} to {:?}\n", old_path, new_path).unwrap();
}
@@ -1631,7 +1632,7 @@ mod tests {
use parking_lot::Mutex;
use serde_json::json;
use settings::SettingsStore;
- use util::path;
+ use util::{path, rel_path::rel_path};
use super::*;
@@ -2026,7 +2027,7 @@ mod tests {
.worktree_for_root_name("closed_source_worktree", cx)
.unwrap();
worktree2.update(cx, |worktree2, cx| {
- worktree2.load_file(Path::new("main.rs"), cx)
+ worktree2.load_file(rel_path("main.rs"), cx)
})
})
.await
@@ -28,6 +28,7 @@ use std::str::FromStr as _;
use std::sync::Arc;
use std::time::{Duration, Instant};
use thiserror::Error;
+use util::rel_path::RelPathBuf;
use util::some_or_debug_panic;
use workspace::notifications::{ErrorMessagePrompt, NotificationId, show_app_notification};
@@ -342,20 +343,21 @@ impl Zeta {
new_snapshot,
..
} => {
- let path = new_snapshot.file().map(|f| f.path().to_path_buf());
+ let path = new_snapshot.file().map(|f| f.path().clone());
let old_path = old_snapshot.file().and_then(|f| {
- let old_path = f.path().as_ref();
- if Some(old_path) != path.as_deref() {
- Some(old_path.to_path_buf())
+ let old_path = f.path();
+ if Some(old_path) != path.as_ref() {
+ Some(old_path.clone())
} else {
None
}
});
predict_edits_v3::Event::BufferChange {
- old_path,
- path,
+ old_path: old_path
+ .map(|old_path| old_path.as_std_path().to_path_buf()),
+ path: path.map(|path| path.as_std_path().to_path_buf()),
diff: language::unified_diff(
&old_snapshot.text(),
&new_snapshot.text(),
@@ -731,7 +733,7 @@ fn make_cloud_request(
let project_entry_id = snippet.declaration.project_entry_id();
let Some(path) = worktrees.iter().find_map(|worktree| {
worktree.entry_for_id(project_entry_id).map(|entry| {
- let mut full_path = PathBuf::new();
+ let mut full_path = RelPathBuf::new();
full_path.push(worktree.root_name());
full_path.push(&entry.path);
full_path
@@ -753,7 +755,7 @@ fn make_cloud_request(
let (text, text_is_truncated) = snippet.declaration.item_text();
referenced_declarations.push(predict_edits_v3::ReferencedDeclaration {
- path,
+ path: path.as_std_path().to_path_buf(),
text: text.into(),
range: snippet.declaration.item_range(),
text_is_truncated,
@@ -1,11 +1,4 @@
-use std::{
- collections::hash_map::Entry,
- ffi::OsStr,
- path::{Path, PathBuf},
- str::FromStr,
- sync::Arc,
- time::Duration,
-};
+use std::{collections::hash_map::Entry, path::PathBuf, str::FromStr, sync::Arc, time::Duration};
use chrono::TimeDelta;
use client::{Client, UserStore};
@@ -20,7 +13,7 @@ use language::{Buffer, DiskState};
use project::{Project, WorktreeId};
use ui::prelude::*;
use ui_input::SingleLineInput;
-use util::ResultExt;
+use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
use workspace::{Item, SplitDirection, Workspace};
use zeta2::{Zeta, ZetaOptions};
@@ -271,9 +264,9 @@ impl Zeta2Inspector {
window: &mut Window,
cx: &mut Context<Self>,
) {
- let Some(worktree_id) = self
- .project
- .read(cx)
+ let project = self.project.read(cx);
+ let path_style = project.path_style(cx);
+ let Some(worktree_id) = project
.worktrees(cx)
.next()
.map(|worktree| worktree.read(cx).id())
@@ -311,7 +304,8 @@ impl Zeta2Inspector {
let multibuffer = cx.new(|cx| {
let mut multibuffer = MultiBuffer::new(language::Capability::ReadOnly);
let excerpt_file = Arc::new(ExcerptMetadataFile {
- title: PathBuf::from("Cursor Excerpt").into(),
+ title: RelPath::new("Cursor Excerpt").unwrap().into(),
+ path_style,
worktree_id,
});
@@ -344,13 +338,15 @@ impl Zeta2Inspector {
.path_for_entry(snippet.declaration.project_entry_id(), cx);
let snippet_file = Arc::new(ExcerptMetadataFile {
- title: PathBuf::from(format!(
+ title: RelPath::new(&format!(
"{} (Score density: {})",
- path.map(|p| p.path.to_string_lossy().to_string())
+ path.map(|p| p.path.display(path_style).to_string())
.unwrap_or_else(|| "".to_string()),
snippet.score_density(SnippetStyle::Declaration)
))
+ .unwrap()
.into(),
+ path_style,
worktree_id,
});
@@ -639,8 +635,9 @@ impl Render for Zeta2Inspector {
// Using same approach as commit view
struct ExcerptMetadataFile {
- title: Arc<Path>,
+ title: Arc<RelPath>,
worktree_id: WorktreeId,
+ path_style: PathStyle,
}
impl language::File for ExcerptMetadataFile {
@@ -652,18 +649,22 @@ impl language::File for ExcerptMetadataFile {
DiskState::New
}
- fn path(&self) -> &Arc<Path> {
+ fn path(&self) -> &Arc<RelPath> {
&self.title
}
fn full_path(&self, _: &App) -> PathBuf {
- self.title.as_ref().into()
+ self.title.as_std_path().to_path_buf()
}
- fn file_name<'a>(&'a self, _: &'a App) -> &'a OsStr {
+ fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
self.title.file_name().unwrap()
}
+ fn path_style(&self, _: &App) -> PathStyle {
+ self.path_style
+ }
+
fn worktree_id(&self, _: &App) -> WorktreeId {
self.worktree_id
}
@@ -19,6 +19,8 @@ use std::process::exit;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
+use util::paths::PathStyle;
+use util::rel_path::RelPath;
use zeta::{PerformPredictEditsParams, Zeta};
use crate::headless::ZetaCliAppState;
@@ -102,7 +104,7 @@ impl FromStr for FileOrStdin {
#[derive(Debug, Clone)]
struct CursorPosition {
- path: PathBuf,
+ path: Arc<RelPath>,
point: Point,
}
@@ -118,7 +120,7 @@ impl FromStr for CursorPosition {
));
}
- let path = PathBuf::from(parts[0]);
+ let path = RelPath::from_std_path(Path::new(&parts[0]), PathStyle::local())?;
let line: u32 = parts[1]
.parse()
.map_err(|_| anyhow!("Invalid line number: '{}'", parts[1]))?;
@@ -152,9 +154,6 @@ async fn get_context(
} = args;
let worktree_path = worktree_path.canonicalize()?;
- if cursor.path.is_absolute() {
- return Err(anyhow!("Absolute paths are not supported in --cursor"));
- }
let project = cx.update(|cx| {
Project::local(
@@ -183,12 +182,9 @@ async fn get_context(
(None, buffer)
};
- let worktree_name = worktree_path
- .file_name()
- .ok_or_else(|| anyhow!("--worktree path must end with a folder name"))?;
- let full_path_str = PathBuf::from(worktree_name)
- .join(&cursor.path)
- .to_string_lossy()
+ let full_path_str = worktree
+ .read_with(cx, |worktree, _| worktree.root_name().join(&cursor.path))?
+ .display(PathStyle::local())
.to_string();
let snapshot = cx.update(|cx| buffer.read(cx).snapshot())?;
@@ -275,12 +271,12 @@ async fn get_context(
pub async fn open_buffer(
project: &Entity<Project>,
worktree: &Entity<Worktree>,
- path: &Path,
+ path: &RelPath,
cx: &mut AsyncApp,
) -> Result<Entity<Buffer>> {
let project_path = worktree.read_with(cx, |worktree, _cx| ProjectPath {
worktree_id: worktree.id(),
- path: path.to_path_buf().into(),
+ path: path.into(),
})?;
project
@@ -291,17 +287,20 @@ pub async fn open_buffer(
pub async fn open_buffer_with_language_server(
project: &Entity<Project>,
worktree: &Entity<Worktree>,
- path: &Path,
+ path: &RelPath,
cx: &mut AsyncApp,
) -> Result<(Entity<Entity<Buffer>>, Entity<Buffer>)> {
let buffer = open_buffer(project, worktree, path, cx).await?;
- let lsp_open_handle = project.update(cx, |project, cx| {
- project.register_buffer_with_language_servers(&buffer, cx)
+ let (lsp_open_handle, path_style) = project.update(cx, |project, cx| {
+ (
+ project.register_buffer_with_language_servers(&buffer, cx),
+ project.path_style(cx),
+ )
})?;
- let log_prefix = path.to_string_lossy().to_string();
- wait_for_lang_server(&project, &buffer, log_prefix, cx).await?;
+ let log_prefix = path.display(path_style);
+ wait_for_lang_server(&project, &buffer, log_prefix.into_owned(), cx).await?;
Ok((lsp_open_handle, buffer))
}