Represent relative paths using a dedicated, separator-agnostic type (#38744)
Closes https://github.com/zed-industries/zed/issues/38690 Closes #37353 ### Background On Windows, paths are normally separated by `\`, unlike mac and linux where they are separated by `/`. When editing code in a project that uses a different path style than your local system (e.g. remoting from Windows to Linux, using WSL, and collaboration between windows and unix users), the correct separator for a path may differ from the "native" separator. Previously, to work around this, Zed converted paths' separators in numerous places. This was applied to both absolute and relative paths, leading to incorrect conversions in some cases. ### Solution Many code paths in Zed use paths that are *relative* to either a worktree root or a git repository. This PR introduces a dedicated type for these paths called `RelPath`, which stores the path in the same way regardless of host platform, and offers `Path`-like manipulation APIs. RelPath supports *displaying* the path using either separator, so that we can display paths in a style that is determined at runtime based on the current project. The representation of absolute paths is left untouched, for now. Absolute paths are different from relative paths because (except in contexts where we know that the path refers to the local filesystem) they should generally be treated as opaque strings. Currently we use a mix of types for these paths (std::path::Path, String, SanitizedPath). Release Notes: - N/A --------- Co-authored-by: Cole Miller <cole@zed.dev> Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Co-authored-by: Peter Tripp <petertripp@gmail.com> Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com> Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
This commit is contained in:
co-authored by
Cole Miller
Piotr Osiewicz
Peter Tripp
Smit Barmase
Lukas Wirth
parent
3c626f3758
commit
03f9cf4414
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user