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

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
This commit is contained in:
Cole Miller
2025-09-25 22:24:32 +00:00
committed by GitHub
parent 6470443271
commit d83d7d35cb
16 changed files with 239 additions and 170 deletions
+19 -13
View File
@@ -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 {
+2 -2
View File
@@ -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>,
+6 -3
View File
@@ -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);
+15 -6
View File
@@ -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);
+105 -56
View File
@@ -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),
@@ -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();
@@ -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()
@@ -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,
@@ -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
}
+3 -9
View File
@@ -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,
+6 -6
View File
@@ -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,
+1 -2
View File
@@ -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| {
+11 -7
View File
@@ -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();
+1 -2
View File
@@ -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()
+3 -3
View File
@@ -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
+11
View File
@@ -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)]