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

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

### Background

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

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

### Solution

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

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

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Peter Tripp <petertripp@gmail.com>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
This commit is contained in:
Max Brunsfeld
2025-09-24 18:57:33 -04:00
committed by GitHub
co-authored by Cole Miller Piotr Osiewicz Peter Tripp Smit Barmase Lukas Wirth
parent 3c626f3758
commit 03f9cf4414
216 changed files with 5873 additions and 5257 deletions
+32 -17
View File
@@ -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(|_| ())
+12 -26
View File
@@ -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}")));
}
}
+37 -36
View File
@@ -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
+7 -4
View File
@@ -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(),
));
}),
)
+5 -4
View File
@@ -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()
+18 -23
View File
@@ -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,
};
+6 -20
View File
@@ -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());