windows: Fix inconsistent separators in buffer headers and breadcrumbs (#38898)

Cole Miller created

Make `resolve_full_path` use the appropriate separators, and return a
`String`.

As part of fixing the fallout from that type change, this also fixes a
bunch of places in the agent code that were using `std::path::Path`
operations on paths that could be non-local, by changing them to operate
instead on strings and use the project's `PathStyle`.

This clears the way a bit for making `full_path` also return a string
instead of a `PathBuf`, but I've left that for a follow-up.

Release Notes:

- N/A

Change summary

crates/agent/src/context.rs                              |  32 +
crates/agent/src/context_store.rs                        |   4 
crates/agent2/src/tools/read_file_tool.rs                |   9 
crates/agent_ui/src/acp/message_editor.rs                |  21 
crates/agent_ui/src/ui/context_pill.rs                   | 161 ++++++---
crates/assistant_slash_commands/src/file_command.rs      |  18 
crates/assistant_slash_commands/src/selection_command.rs |   8 
crates/assistant_slash_commands/src/symbols_command.rs   |  12 
crates/assistant_slash_commands/src/tab_command.rs       |  79 ++--
crates/assistant_tool/src/outline.rs                     |  12 
crates/assistant_tools/src/edit_agent.rs                 |  12 
crates/assistant_tools/src/read_file_tool.rs             |   3 
crates/editor/src/element.rs                             |  18 
crates/editor/src/items.rs                               |   3 
crates/language/src/buffer.rs                            |   6 
crates/util/src/paths.rs                                 |  11 
16 files changed, 239 insertions(+), 170 deletions(-)

Detailed changes

crates/agent/src/context.rs 🔗

@@ -159,7 +159,7 @@ pub struct FileContextHandle {
 #[derive(Debug, Clone)]
 pub struct FileContext {
     pub handle: FileContextHandle,
-    pub full_path: Arc<Path>,
+    pub full_path: String,
     pub text: SharedString,
     pub is_outline: bool,
 }
@@ -187,7 +187,7 @@ impl FileContextHandle {
             log::error!("file context missing path");
             return Task::ready(None);
         };
-        let full_path: Arc<Path> = file.full_path(cx).into();
+        let full_path = file.full_path(cx).to_string_lossy().to_string();
         let rope = buffer_ref.as_rope().clone();
         let buffer = self.buffer.clone();
 
@@ -236,7 +236,7 @@ pub struct DirectoryContextHandle {
 #[derive(Debug, Clone)]
 pub struct DirectoryContext {
     pub handle: DirectoryContextHandle,
-    pub full_path: Arc<Path>,
+    pub full_path: String,
     pub descendants: Vec<DirectoryContextDescendant>,
 }
 
@@ -274,13 +274,16 @@ impl DirectoryContextHandle {
         }
 
         let directory_path = entry.path.clone();
-        let directory_full_path = worktree_ref.full_path(&directory_path).into();
+        let directory_full_path = worktree_ref
+            .full_path(&directory_path)
+            .to_string_lossy()
+            .to_string();
 
         let file_paths = collect_files_in_path(worktree_ref, &directory_path);
         let descendants_future = future::join_all(file_paths.into_iter().map(|path| {
             let worktree_ref = worktree.read(cx);
             let worktree_id = worktree_ref.id();
-            let full_path = worktree_ref.full_path(&path);
+            let full_path = worktree_ref.full_path(&path).to_string_lossy().to_string();
 
             let rel_path = path
                 .strip_prefix(&directory_path)
@@ -361,7 +364,7 @@ pub struct SymbolContextHandle {
 #[derive(Debug, Clone)]
 pub struct SymbolContext {
     pub handle: SymbolContextHandle,
-    pub full_path: Arc<Path>,
+    pub full_path: String,
     pub line_range: Range<Point>,
     pub text: SharedString,
 }
@@ -400,7 +403,7 @@ impl SymbolContextHandle {
             log::error!("symbol context's file has no path");
             return Task::ready(None);
         };
-        let full_path = file.full_path(cx).into();
+        let full_path = file.full_path(cx).to_string_lossy().to_string();
         let line_range = self.enclosing_range.to_point(&buffer_ref.snapshot());
         let text = self.text(cx);
         let buffer = self.buffer.clone();
@@ -434,7 +437,7 @@ pub struct SelectionContextHandle {
 #[derive(Debug, Clone)]
 pub struct SelectionContext {
     pub handle: SelectionContextHandle,
-    pub full_path: Arc<Path>,
+    pub full_path: String,
     pub line_range: Range<Point>,
     pub text: SharedString,
 }
@@ -473,7 +476,7 @@ impl SelectionContextHandle {
         let text = self.text(cx);
         let buffer = self.buffer.clone();
         let context = AgentContext::Selection(SelectionContext {
-            full_path: full_path.into(),
+            full_path: full_path.to_string_lossy().to_string(),
             line_range: self.line_range(cx),
             text,
             handle: self,
@@ -703,7 +706,7 @@ impl Display for RulesContext {
 #[derive(Debug, Clone)]
 pub struct ImageContext {
     pub project_path: Option<ProjectPath>,
-    pub full_path: Option<Arc<Path>>,
+    pub full_path: Option<String>,
     pub original_image: Arc<gpui::Image>,
     // TODO: handle this elsewhere and remove `ignore-interior-mutability` opt-out in clippy.toml
     // needed due to a false positive of `clippy::mutable_key_type`.
@@ -983,14 +986,17 @@ fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<Arc<RelPath
     files
 }
 
-fn codeblock_tag(full_path: &Path, line_range: Option<Range<Point>>) -> String {
+fn codeblock_tag(full_path: &str, line_range: Option<Range<Point>>) -> String {
     let mut result = String::new();
 
-    if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) {
+    if let Some(extension) = Path::new(full_path)
+        .extension()
+        .and_then(|ext| ext.to_str())
+    {
         let _ = write!(result, "{} ", extension);
     }
 
-    let _ = write!(result, "{}", full_path.display());
+    let _ = write!(result, "{}", full_path);
 
     if let Some(range) = line_range {
         if range.start.row == range.end.row {

crates/agent/src/context_store.rs 🔗

@@ -312,7 +312,7 @@ impl ContextStore {
                 let item = image_item.read(cx);
                 this.insert_image(
                     Some(item.project_path(cx)),
-                    Some(item.file.full_path(cx).into()),
+                    Some(item.file.full_path(cx).to_string_lossy().to_string()),
                     item.image.clone(),
                     remove_if_exists,
                     cx,
@@ -328,7 +328,7 @@ impl ContextStore {
     fn insert_image(
         &mut self,
         project_path: Option<ProjectPath>,
-        full_path: Option<Arc<Path>>,
+        full_path: Option<String>,
         image: Arc<Image>,
         remove_if_exists: bool,
         cx: &mut Context<ContextStore>,

crates/agent2/src/tools/read_file_tool.rs 🔗

@@ -225,9 +225,12 @@ impl AgentTool for ReadFileTool {
                 Ok(result.into())
             } else {
                 // No line ranges specified, so check file size to see if it's too big.
-                let buffer_content =
-                    outline::get_buffer_content_or_outline(buffer.clone(), Some(&abs_path), cx)
-                        .await?;
+                let buffer_content = outline::get_buffer_content_or_outline(
+                    buffer.clone(),
+                    Some(&abs_path.to_string_lossy()),
+                    cx,
+                )
+                .await?;
 
                 action_log.update(cx, |log, cx| {
                     log.buffer_read(buffer.clone(), cx);

crates/agent_ui/src/acp/message_editor.rs 🔗

@@ -452,9 +452,12 @@ impl MessageEditor {
             .update(cx, |project, cx| project.open_buffer(project_path, cx));
         cx.spawn(async move |_, cx| {
             let buffer = buffer.await?;
-            let buffer_content =
-                outline::get_buffer_content_or_outline(buffer.clone(), Some(&abs_path), &cx)
-                    .await?;
+            let buffer_content = outline::get_buffer_content_or_outline(
+                buffer.clone(),
+                Some(&abs_path.to_string_lossy()),
+                &cx,
+            )
+            .await?;
 
             Ok(Mention::Text {
                 content: buffer_content.text,
@@ -1174,14 +1177,20 @@ fn full_mention_for_directory(
     abs_path: &Path,
     cx: &mut App,
 ) -> Task<Result<Mention>> {
-    fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<(Arc<RelPath>, PathBuf)> {
+    fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<(Arc<RelPath>, String)> {
         let mut files = Vec::new();
 
         for entry in worktree.child_entries(path) {
             if entry.is_dir() {
                 files.extend(collect_files_in_path(worktree, &entry.path));
             } else if entry.is_file() {
-                files.push((entry.path.clone(), worktree.full_path(&entry.path)));
+                files.push((
+                    entry.path.clone(),
+                    worktree
+                        .full_path(&entry.path)
+                        .to_string_lossy()
+                        .to_string(),
+                ));
             }
         }
 
@@ -1259,7 +1268,7 @@ fn full_mention_for_directory(
     })
 }
 
-fn render_directory_contents(entries: Vec<(Arc<RelPath>, PathBuf, String)>) -> String {
+fn render_directory_contents(entries: Vec<(Arc<RelPath>, String, 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);

crates/agent_ui/src/ui/context_pill.rs 🔗

@@ -17,6 +17,7 @@ use agent::context::{
     FileContextHandle, ImageContext, ImageStatus, RulesContextHandle, SelectionContextHandle,
     SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
 };
+use util::paths::PathStyle;
 
 #[derive(IntoElement)]
 pub enum ContextPill {
@@ -303,33 +304,54 @@ impl AddedContext {
         cx: &App,
     ) -> Option<AddedContext> {
         match handle {
-            AgentContextHandle::File(handle) => Self::pending_file(handle, cx),
+            AgentContextHandle::File(handle) => {
+                Self::pending_file(handle, project.path_style(cx), cx)
+            }
             AgentContextHandle::Directory(handle) => Self::pending_directory(handle, project, cx),
-            AgentContextHandle::Symbol(handle) => Self::pending_symbol(handle, cx),
-            AgentContextHandle::Selection(handle) => Self::pending_selection(handle, cx),
+            AgentContextHandle::Symbol(handle) => {
+                Self::pending_symbol(handle, project.path_style(cx), cx)
+            }
+            AgentContextHandle::Selection(handle) => {
+                Self::pending_selection(handle, project.path_style(cx), cx)
+            }
             AgentContextHandle::FetchedUrl(handle) => Some(Self::fetched_url(handle)),
             AgentContextHandle::Thread(handle) => Some(Self::pending_thread(handle, cx)),
             AgentContextHandle::TextThread(handle) => Some(Self::pending_text_thread(handle, cx)),
             AgentContextHandle::Rules(handle) => Self::pending_rules(handle, prompt_store, cx),
-            AgentContextHandle::Image(handle) => Some(Self::image(handle, model, cx)),
+            AgentContextHandle::Image(handle) => {
+                Some(Self::image(handle, model, project.path_style(cx), cx))
+            }
         }
     }
 
-    fn pending_file(handle: FileContextHandle, cx: &App) -> Option<AddedContext> {
-        let full_path = handle.buffer.read(cx).file()?.full_path(cx);
-        Some(Self::file(handle, &full_path, cx))
+    fn pending_file(
+        handle: FileContextHandle,
+        path_style: PathStyle,
+        cx: &App,
+    ) -> Option<AddedContext> {
+        let full_path = handle
+            .buffer
+            .read(cx)
+            .file()?
+            .full_path(cx)
+            .to_string_lossy()
+            .to_string();
+        Some(Self::file(handle, &full_path, path_style, cx))
     }
 
-    fn file(handle: FileContextHandle, full_path: &Path, cx: &App) -> AddedContext {
-        let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
-        let (name, parent) =
-            extract_file_name_and_directory_from_full_path(full_path, &full_path_string);
+    fn file(
+        handle: FileContextHandle,
+        full_path: &str,
+        path_style: PathStyle,
+        cx: &App,
+    ) -> AddedContext {
+        let (name, parent) = extract_file_name_and_directory_from_full_path(full_path, path_style);
         AddedContext {
             kind: ContextKind::File,
             name,
             parent,
-            tooltip: Some(full_path_string),
-            icon_path: FileIcons::get_icon(full_path, cx),
+            tooltip: Some(SharedString::new(full_path)),
+            icon_path: FileIcons::get_icon(Path::new(full_path), cx),
             status: ContextStatus::Ready,
             render_hover: None,
             handle: AgentContextHandle::File(handle),
@@ -343,19 +365,24 @@ impl AddedContext {
     ) -> Option<AddedContext> {
         let worktree = project.worktree_for_entry(handle.entry_id, cx)?.read(cx);
         let entry = worktree.entry_for_id(handle.entry_id)?;
-        let full_path = worktree.full_path(&entry.path);
-        Some(Self::directory(handle, &full_path))
+        let full_path = worktree
+            .full_path(&entry.path)
+            .to_string_lossy()
+            .to_string();
+        Some(Self::directory(handle, &full_path, project.path_style(cx)))
     }
 
-    fn directory(handle: DirectoryContextHandle, full_path: &Path) -> AddedContext {
-        let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
-        let (name, parent) =
-            extract_file_name_and_directory_from_full_path(full_path, &full_path_string);
+    fn directory(
+        handle: DirectoryContextHandle,
+        full_path: &str,
+        path_style: PathStyle,
+    ) -> AddedContext {
+        let (name, parent) = extract_file_name_and_directory_from_full_path(full_path, path_style);
         AddedContext {
             kind: ContextKind::Directory,
             name,
             parent,
-            tooltip: Some(full_path_string),
+            tooltip: Some(SharedString::new(full_path)),
             icon_path: None,
             status: ContextStatus::Ready,
             render_hover: None,
@@ -363,9 +390,17 @@ impl AddedContext {
         }
     }
 
-    fn pending_symbol(handle: SymbolContextHandle, cx: &App) -> Option<AddedContext> {
-        let excerpt =
-            ContextFileExcerpt::new(&handle.full_path(cx)?, handle.enclosing_line_range(cx), cx);
+    fn pending_symbol(
+        handle: SymbolContextHandle,
+        path_style: PathStyle,
+        cx: &App,
+    ) -> Option<AddedContext> {
+        let excerpt = ContextFileExcerpt::new(
+            &handle.full_path(cx)?.to_string_lossy(),
+            handle.enclosing_line_range(cx),
+            path_style,
+            cx,
+        );
         Some(AddedContext {
             kind: ContextKind::Symbol,
             name: handle.symbol.clone(),
@@ -383,8 +418,17 @@ impl AddedContext {
         })
     }
 
-    fn pending_selection(handle: SelectionContextHandle, cx: &App) -> Option<AddedContext> {
-        let excerpt = ContextFileExcerpt::new(&handle.full_path(cx)?, handle.line_range(cx), cx);
+    fn pending_selection(
+        handle: SelectionContextHandle,
+        path_style: PathStyle,
+        cx: &App,
+    ) -> Option<AddedContext> {
+        let excerpt = ContextFileExcerpt::new(
+            &handle.full_path(cx)?.to_string_lossy(),
+            handle.line_range(cx),
+            path_style,
+            cx,
+        );
         Some(AddedContext {
             kind: ContextKind::Selection,
             name: excerpt.file_name_and_range.clone(),
@@ -485,13 +529,13 @@ impl AddedContext {
     fn image(
         context: ImageContext,
         model: Option<&Arc<dyn language_model::LanguageModel>>,
+        path_style: PathStyle,
         cx: &App,
     ) -> AddedContext {
         let (name, parent, icon_path) = if let Some(full_path) = context.full_path.as_ref() {
-            let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
             let (name, parent) =
-                extract_file_name_and_directory_from_full_path(full_path, &full_path_string);
-            let icon_path = FileIcons::get_icon(full_path, cx);
+                extract_file_name_and_directory_from_full_path(full_path, path_style);
+            let icon_path = FileIcons::get_icon(Path::new(full_path), cx);
             (name, parent, icon_path)
         } else {
             ("Image".into(), None, None)
@@ -540,19 +584,20 @@ impl AddedContext {
 }
 
 fn extract_file_name_and_directory_from_full_path(
-    path: &Path,
-    name_fallback: &SharedString,
+    path: &str,
+    path_style: PathStyle,
 ) -> (SharedString, Option<SharedString>) {
-    let name = path
-        .file_name()
-        .map(|n| n.to_string_lossy().into_owned().into())
-        .unwrap_or_else(|| name_fallback.clone());
-    let parent = path
-        .parent()
-        .and_then(|p| p.file_name())
-        .map(|n| n.to_string_lossy().into_owned().into());
-
-    (name, parent)
+    let (parent, file_name) = path_style.split(path);
+    let parent = parent.and_then(|parent| {
+        let parent = parent.trim_end_matches(path_style.separator());
+        let (_, parent) = path_style.split(parent);
+        if parent.is_empty() {
+            None
+        } else {
+            Some(SharedString::new(parent))
+        }
+    });
+    (SharedString::new(file_name), parent)
 }
 
 #[derive(Debug, Clone)]
@@ -564,25 +609,25 @@ struct ContextFileExcerpt {
 }
 
 impl ContextFileExcerpt {
-    pub fn new(full_path: &Path, line_range: Range<Point>, cx: &App) -> Self {
-        let full_path_string = full_path.to_string_lossy().into_owned();
-        let file_name = full_path
-            .file_name()
-            .map(|n| n.to_string_lossy().into_owned())
-            .unwrap_or_else(|| full_path_string.clone());
-
+    pub fn new(full_path: &str, line_range: Range<Point>, path_style: PathStyle, cx: &App) -> Self {
+        let (parent, file_name) = path_style.split(full_path);
         let line_range_text = format!(" ({}-{})", line_range.start.row + 1, line_range.end.row + 1);
-        let mut full_path_and_range = full_path_string;
+        let mut full_path_and_range = full_path.to_owned();
         full_path_and_range.push_str(&line_range_text);
-        let mut file_name_and_range = file_name;
+        let mut file_name_and_range = file_name.to_owned();
         file_name_and_range.push_str(&line_range_text);
 
-        let parent_name = full_path
-            .parent()
-            .and_then(|p| p.file_name())
-            .map(|n| n.to_string_lossy().into_owned().into());
+        let parent_name = parent.and_then(|parent| {
+            let parent = parent.trim_end_matches(path_style.separator());
+            let (_, parent) = path_style.split(parent);
+            if parent.is_empty() {
+                None
+            } else {
+                Some(SharedString::new(parent))
+            }
+        });
 
-        let icon_path = FileIcons::get_icon(full_path, cx);
+        let icon_path = FileIcons::get_icon(Path::new(full_path), cx);
 
         ContextFileExcerpt {
             file_name_and_range: file_name_and_range.into(),
@@ -690,6 +735,7 @@ impl Component for AddedContext {
                     image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
                 },
                 None,
+                PathStyle::local(),
                 cx,
             ),
         );
@@ -710,6 +756,7 @@ impl Component for AddedContext {
                         .shared(),
                 },
                 None,
+                PathStyle::local(),
                 cx,
             ),
         );
@@ -725,6 +772,7 @@ impl Component for AddedContext {
                     image_task: Task::ready(None).shared(),
                 },
                 None,
+                PathStyle::local(),
                 cx,
             ),
         );
@@ -767,7 +815,8 @@ mod tests {
             full_path: None,
         };
 
-        let added_context = AddedContext::image(image_context, Some(&model), cx);
+        let added_context =
+            AddedContext::image(image_context, Some(&model), PathStyle::local(), cx);
 
         assert!(matches!(
             added_context.status,
@@ -790,7 +839,7 @@ mod tests {
             full_path: None,
         };
 
-        let added_context = AddedContext::image(image_context, None, cx);
+        let added_context = AddedContext::image(image_context, None, PathStyle::local(), cx);
 
         assert!(
             matches!(added_context.status, ContextStatus::Ready),

crates/assistant_slash_commands/src/file_command.rs 🔗

@@ -355,9 +355,7 @@ fn collect_files(
                         let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
                         append_buffer_to_output(
                             &snapshot,
-                            Some(Path::new(
-                                path_including_worktree_name.display(path_style).as_ref(),
-                            )),
+                            Some(path_including_worktree_name.display(path_style).as_ref()),
                             &mut output,
                         )
                         .log_err();
@@ -382,18 +380,18 @@ fn collect_files(
 }
 
 pub fn codeblock_fence_for_path(
-    path: Option<&Path>,
+    path: Option<&str>,
     row_range: Option<RangeInclusive<u32>>,
 ) -> String {
     let mut text = String::new();
     write!(text, "```").unwrap();
 
     if let Some(path) = path {
-        if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
+        if let Some(extension) = Path::new(path).extension().and_then(|ext| ext.to_str()) {
             write!(text, "{} ", extension).unwrap();
         }
 
-        write!(text, "{}", path.display()).unwrap();
+        write!(text, "{path}").unwrap();
     } else {
         write!(text, "untitled").unwrap();
     }
@@ -413,12 +411,12 @@ pub struct FileCommandMetadata {
 
 pub fn build_entry_output_section(
     range: Range<usize>,
-    path: Option<&Path>,
+    path: Option<&str>,
     is_directory: bool,
     line_range: Option<Range<u32>>,
 ) -> SlashCommandOutputSection<usize> {
     let mut label = if let Some(path) = path {
-        path.to_string_lossy().to_string()
+        path.to_string()
     } else {
         "untitled".to_string()
     };
@@ -441,7 +439,7 @@ pub fn build_entry_output_section(
         } else {
             path.and_then(|path| {
                 serde_json::to_value(FileCommandMetadata {
-                    path: path.to_string_lossy().to_string(),
+                    path: path.to_string(),
                 })
                 .ok()
             })
@@ -532,7 +530,7 @@ mod custom_path_matcher {
 
 pub fn append_buffer_to_output(
     buffer: &BufferSnapshot,
-    path: Option<&Path>,
+    path: Option<&str>,
     output: &mut SlashCommandOutput,
 ) -> Result<()> {
     let prev_len = output.text.len();

crates/assistant_slash_commands/src/selection_command.rs 🔗

@@ -137,7 +137,9 @@ pub fn selections_creases(
             None
         };
         let language_name = language_name.as_deref().unwrap_or("");
-        let filename = snapshot.file_at(range.start).map(|file| file.full_path(cx));
+        let filename = snapshot
+            .file_at(range.start)
+            .map(|file| file.full_path(cx).to_string_lossy().to_string());
         let text = if language_name == "markdown" {
             selected_text
                 .lines()
@@ -187,9 +189,9 @@ pub fn selections_creases(
             let start_line = range.start.row + 1;
             let end_line = range.end.row + 1;
             if start_line == end_line {
-                format!("{}, Line {}", path.display(), start_line)
+                format!("{path}, Line {start_line}")
             } else {
-                format!("{}, Lines {} to {}", path.display(), start_line, end_line)
+                format!("{path}, Lines {start_line} to {end_line}")
             }
         } else {
             "Quoted selection".to_string()

crates/assistant_slash_commands/src/symbols_command.rs 🔗

@@ -7,8 +7,8 @@ use editor::Editor;
 use gpui::{AppContext as _, Task, WeakEntity};
 use language::{BufferSnapshot, LspAdapterDelegate};
 use std::sync::Arc;
-use std::{path::Path, sync::atomic::AtomicBool};
-use ui::{App, IconName, Window};
+use std::sync::atomic::AtomicBool;
+use ui::{App, IconName, SharedString, Window};
 use workspace::Workspace;
 
 pub struct OutlineSlashCommand;
@@ -67,13 +67,13 @@ impl SlashCommand for OutlineSlashCommand {
             };
 
             let snapshot = buffer.read(cx).snapshot();
-            let path = snapshot.resolve_file_path(cx, true);
+            let path = snapshot.resolve_file_path(true, cx);
 
             cx.background_spawn(async move {
                 let outline = snapshot.outline(None);
 
-                let path = path.as_deref().unwrap_or(Path::new("untitled"));
-                let mut outline_text = format!("Symbols for {}:\n", path.display());
+                let path = path.as_deref().unwrap_or("untitled");
+                let mut outline_text = format!("Symbols for {path}:\n");
                 for item in &outline.path_candidates {
                     outline_text.push_str("- ");
                     outline_text.push_str(&item.string);
@@ -84,7 +84,7 @@ impl SlashCommand for OutlineSlashCommand {
                     sections: vec![SlashCommandOutputSection {
                         range: 0..outline_text.len(),
                         icon: IconName::ListTree,
-                        label: path.to_string_lossy().to_string().into(),
+                        label: SharedString::new(path),
                         metadata: None,
                     }],
                     text: outline_text,

crates/assistant_slash_commands/src/tab_command.rs 🔗

@@ -8,12 +8,9 @@ use editor::Editor;
 use futures::future::join_all;
 use gpui::{Task, WeakEntity};
 use language::{BufferSnapshot, CodeLabel, HighlightId, LspAdapterDelegate};
-use std::{
-    path::PathBuf,
-    sync::{Arc, atomic::AtomicBool},
-};
+use std::sync::{Arc, atomic::AtomicBool};
 use ui::{ActiveTheme, App, Window, prelude::*};
-use util::ResultExt;
+use util::{ResultExt, paths::PathStyle};
 use workspace::Workspace;
 
 use crate::file_command::append_buffer_to_output;
@@ -72,35 +69,42 @@ impl SlashCommand for TabSlashCommand {
             return Task::ready(Ok(Vec::new()));
         }
 
-        let active_item_path = workspace.as_ref().and_then(|workspace| {
-            workspace
-                .update(cx, |workspace, cx| {
-                    let snapshot = active_item_buffer(workspace, cx).ok()?;
-                    snapshot.resolve_file_path(cx, true)
-                })
-                .ok()
-                .flatten()
+        let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
+            return Task::ready(Err(anyhow::anyhow!("no workspace")));
+        };
+
+        let active_item_path = workspace.update(cx, |workspace, cx| {
+            let snapshot = active_item_buffer(workspace, cx).ok()?;
+            snapshot.resolve_file_path(true, cx)
         });
+        let path_style = workspace.read(cx).path_style(cx);
+
         let current_query = arguments.last().cloned().unwrap_or_default();
-        let tab_items_search =
-            tab_items_for_queries(workspace, &[current_query], cancel, false, window, cx);
+        let tab_items_search = tab_items_for_queries(
+            Some(workspace.downgrade()),
+            &[current_query],
+            cancel,
+            false,
+            window,
+            cx,
+        );
 
         let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
         window.spawn(cx, async move |_| {
             let tab_items = tab_items_search.await?;
             let run_command = tab_items.len() == 1;
             let tab_completion_items = tab_items.into_iter().filter_map(|(path, ..)| {
-                let path_string = path.as_deref()?.to_string_lossy().to_string();
-                if argument_set.contains(&path_string) {
+                let path = path?;
+                if argument_set.contains(&path) {
                     return None;
                 }
-                if active_item_path.is_some() && active_item_path == path {
+                if active_item_path.as_ref() == Some(&path) {
                     return None;
                 }
-                let label = create_tab_completion_label(path.as_ref()?, comment_id);
+                let label = create_tab_completion_label(&path, path_style, comment_id);
                 Some(ArgumentCompletion {
                     label,
-                    new_text: path_string,
+                    new_text: path,
                     replace_previous_arguments: false,
                     after_completion: run_command.into(),
                 })
@@ -109,8 +113,9 @@ impl SlashCommand for TabSlashCommand {
             let active_item_completion = active_item_path
                 .as_deref()
                 .map(|active_item_path| {
-                    let path_string = active_item_path.to_string_lossy().to_string();
-                    let label = create_tab_completion_label(active_item_path, comment_id);
+                    let path_string = active_item_path.to_string();
+                    let label =
+                        create_tab_completion_label(active_item_path, path_style, comment_id);
                     ArgumentCompletion {
                         label,
                         new_text: path_string,
@@ -169,7 +174,7 @@ fn tab_items_for_queries(
     strict_match: bool,
     window: &mut Window,
     cx: &mut App,
-) -> Task<anyhow::Result<Vec<(Option<PathBuf>, BufferSnapshot, usize)>>> {
+) -> Task<anyhow::Result<Vec<(Option<String>, BufferSnapshot, usize)>>> {
     let empty_query = queries.is_empty() || queries.iter().all(|query| query.trim().is_empty());
     let queries = queries.to_owned();
     window.spawn(cx, async move |cx| {
@@ -179,7 +184,7 @@ fn tab_items_for_queries(
                 .update(cx, |workspace, cx| {
                     if strict_match && empty_query {
                         let snapshot = active_item_buffer(workspace, cx)?;
-                        let full_path = snapshot.resolve_file_path(cx, true);
+                        let full_path = snapshot.resolve_file_path(true, cx);
                         return anyhow::Ok(vec![(full_path, snapshot, 0)]);
                     }
 
@@ -201,7 +206,7 @@ fn tab_items_for_queries(
                             && visited_buffers.insert(buffer.read(cx).remote_id())
                         {
                             let snapshot = buffer.read(cx).snapshot();
-                            let full_path = snapshot.resolve_file_path(cx, true);
+                            let full_path = snapshot.resolve_file_path(true, cx);
                             open_buffers.push((full_path, snapshot, *timestamp));
                         }
                     }
@@ -224,10 +229,7 @@ fn tab_items_for_queries(
                 let match_candidates = open_buffers
                     .iter()
                     .enumerate()
-                    .filter_map(|(id, (full_path, ..))| {
-                        let path_string = full_path.as_deref()?.to_string_lossy().to_string();
-                        Some((id, path_string))
-                    })
+                    .filter_map(|(id, (full_path, ..))| Some((id, full_path.clone()?)))
                     .fold(HashMap::default(), |mut candidates, (id, path_string)| {
                         candidates
                             .entry(path_string)
@@ -249,8 +251,7 @@ fn tab_items_for_queries(
                     .iter()
                     .enumerate()
                     .filter_map(|(id, (full_path, ..))| {
-                        let path_string = full_path.as_deref()?.to_string_lossy().to_string();
-                        Some(fuzzy::StringMatchCandidate::new(id, &path_string))
+                        Some(fuzzy::StringMatchCandidate::new(id, full_path.as_ref()?))
                     })
                     .collect::<Vec<_>>();
                 let mut processed_matches = HashSet::default();
@@ -302,21 +303,15 @@ fn active_item_buffer(
 }
 
 fn create_tab_completion_label(
-    path: &std::path::Path,
+    path: &str,
+    path_style: PathStyle,
     comment_id: Option<HighlightId>,
 ) -> CodeLabel {
-    let file_name = path
-        .file_name()
-        .map(|f| f.to_string_lossy())
-        .unwrap_or_default();
-    let parent_path = path
-        .parent()
-        .map(|p| p.to_string_lossy())
-        .unwrap_or_default();
+    let (parent_path, file_name) = path_style.split(path);
     let mut label = CodeLabel::default();
-    label.push_str(&file_name, None);
+    label.push_str(file_name, None);
     label.push_str(" ", None);
-    label.push_str(&parent_path, comment_id);
+    label.push_str(parent_path.unwrap_or_default(), comment_id);
     label.filter_range = 0..file_name.len();
     label
 }

crates/assistant_tool/src/outline.rs 🔗

@@ -5,7 +5,6 @@ use language::{Buffer, OutlineItem, ParseStatus};
 use project::Project;
 use regex::Regex;
 use std::fmt::Write;
-use std::path::Path;
 use text::Point;
 
 /// For files over this size, instead of reading them (or including them in context),
@@ -143,7 +142,7 @@ pub struct BufferContent {
 /// For smaller files, returns the full content.
 pub async fn get_buffer_content_or_outline(
     buffer: Entity<Buffer>,
-    path: Option<&Path>,
+    path: Option<&str>,
     cx: &AsyncApp,
 ) -> Result<BufferContent> {
     let file_size = buffer.read_with(cx, |buffer, _| buffer.text().len())?;
@@ -170,15 +169,10 @@ pub async fn get_buffer_content_or_outline(
 
         let text = if let Some(path) = path {
             format!(
-                "# File outline for {} (file too large to show full content)\n\n{}",
-                path.display(),
-                outline_text
+                "# File outline for {path} (file too large to show full content)\n\n{outline_text}",
             )
         } else {
-            format!(
-                "# File outline (file too large to show full content)\n\n{}",
-                outline_text
-            )
+            format!("# File outline (file too large to show full content)\n\n{outline_text}",)
         };
         Ok(BufferContent {
             text,

crates/assistant_tools/src/edit_agent.rs 🔗

@@ -26,13 +26,13 @@ use language_model::{
 use project::{AgentLocation, Project};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use std::{cmp, iter, mem, ops::Range, path::PathBuf, pin::Pin, sync::Arc, task::Poll};
+use std::{cmp, iter, mem, ops::Range, pin::Pin, sync::Arc, task::Poll};
 use streaming_diff::{CharOperation, StreamingDiff};
 use streaming_fuzzy_matcher::StreamingFuzzyMatcher;
 
 #[derive(Serialize)]
 struct CreateFilePromptTemplate {
-    path: Option<PathBuf>,
+    path: Option<String>,
     edit_description: String,
 }
 
@@ -42,7 +42,7 @@ impl Template for CreateFilePromptTemplate {
 
 #[derive(Serialize)]
 struct EditFileXmlPromptTemplate {
-    path: Option<PathBuf>,
+    path: Option<String>,
     edit_description: String,
 }
 
@@ -52,7 +52,7 @@ impl Template for EditFileXmlPromptTemplate {
 
 #[derive(Serialize)]
 struct EditFileDiffFencedPromptTemplate {
-    path: Option<PathBuf>,
+    path: Option<String>,
     edit_description: String,
 }
 
@@ -115,7 +115,7 @@ impl EditAgent {
         let conversation = conversation.clone();
         let output = cx.spawn(async move |cx| {
             let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
-            let path = cx.update(|cx| snapshot.resolve_file_path(cx, true))?;
+            let path = cx.update(|cx| snapshot.resolve_file_path(true, cx))?;
             let prompt = CreateFilePromptTemplate {
                 path,
                 edit_description,
@@ -229,7 +229,7 @@ impl EditAgent {
         let edit_format = self.edit_format;
         let output = cx.spawn(async move |cx| {
             let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
-            let path = cx.update(|cx| snapshot.resolve_file_path(cx, true))?;
+            let path = cx.update(|cx| snapshot.resolve_file_path(true, cx))?;
             let prompt = match edit_format {
                 EditFormat::XmlTags => EditFileXmlPromptTemplate {
                     path,

crates/assistant_tools/src/read_file_tool.rs 🔗

@@ -261,9 +261,8 @@ impl Tool for ReadFileTool {
                 Ok(result)
             } else {
                 // No line ranges specified, so check file size to see if it's too big.
-                let path_buf = std::path::PathBuf::from(&file_path);
                 let buffer_content =
-                    outline::get_buffer_content_or_outline(buffer.clone(), Some(&path_buf), cx)
+                    outline::get_buffer_content_or_outline(buffer.clone(), Some(&file_path), cx)
                         .await?;
 
                 action_log.update(cx, |log, cx| {

crates/editor/src/element.rs 🔗

@@ -3782,13 +3782,17 @@ impl EditorElement {
         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() + path_style?.separator())
-        });
+        let relative_path = for_excerpt.buffer.resolve_file_path(include_root, cx);
+        let (parent_path, filename) = if let Some(path) = &relative_path {
+            if let Some(path_style) = path_style {
+                let (dir, file_name) = path_style.split(path);
+                (dir.map(|dir| dir.to_owned()), Some(file_name.to_owned()))
+            } else {
+                (None, Some(path.clone()))
+            }
+        } else {
+            (None, None)
+        };
         let focus_handle = editor.focus_handle(cx);
         let colors = cx.theme().colors();
 

crates/editor/src/items.rs 🔗

@@ -963,13 +963,12 @@ impl Item for Editor {
             buffer
                 .snapshot()
                 .resolve_file_path(
-                    cx,
                     self.project
                         .as_ref()
                         .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
                         .unwrap_or_default(),
+                    cx,
                 )
-                .map(|path| path.to_string_lossy().to_string())
                 .unwrap_or_else(|| {
                     if multibuffer.is_singleton() {
                         multibuffer.title(cx).to_string()

crates/language/src/buffer.rs 🔗

@@ -4628,12 +4628,12 @@ impl BufferSnapshot {
         self.file.as_ref()
     }
 
-    pub fn resolve_file_path(&self, cx: &App, include_root: bool) -> Option<PathBuf> {
+    pub fn resolve_file_path(&self, include_root: bool, cx: &App) -> Option<String> {
         if let Some(file) = self.file() {
             if file.path().file_name().is_none() || include_root {
-                Some(file.full_path(cx))
+                Some(file.full_path(cx).to_string_lossy().to_string())
             } else {
-                Some(file.path().as_std_path().to_owned())
+                Some(file.path().display(file.path_style(cx)).to_string())
             }
         } else {
             None

crates/util/src/paths.rs 🔗

@@ -280,6 +280,17 @@ impl PathStyle {
             ))
         }
     }
+
+    pub fn split(self, path_like: &str) -> (Option<&str>, &str) {
+        let Some(pos) = path_like.rfind(self.separator()) else {
+            return (None, path_like);
+        };
+        let filename_start = pos + self.separator().len();
+        (
+            Some(&path_like[..filename_start]),
+            &path_like[filename_start..],
+        )
+    }
 }
 
 #[derive(Debug, Clone)]