agent_ui: Truncate file path in context completions (#42682)
Closes https://github.com/zed-industries/zed/issues/38753 Followed some prior art here: [a similar calculation ](https://github.com/zed-industries/zed/blob/e80b490ac00bfb5c206d3eb3f5c2dec10e742678/crates/file_finder/src/file_finder.rs#L1105) in `file_finder.rs` Release Notes: - improved visibility of long path names in context completions by truncating on the left when space is insufficient to render the full path Before: <img width="544" height="251" alt="Screenshot of overflowing file paths" src="https://github.com/user-attachments/assets/8440710d-3f59-4273-92bb-3db85022b57c" /> After: <img width="544" height="251" alt="Screenshot of truncated file paths" src="https://github.com/user-attachments/assets/e268ba87-7979-4bc3-8d08-9198b4baea02" />
This commit is contained in:
@@ -7,7 +7,9 @@ use std::sync::atomic::AtomicBool;
|
||||
use acp_thread::MentionUri;
|
||||
use agent::{HistoryEntry, HistoryStore};
|
||||
use anyhow::Result;
|
||||
use editor::{CompletionProvider, Editor, ExcerptId};
|
||||
use editor::{
|
||||
CompletionProvider, Editor, ExcerptId, code_context_menus::COMPLETION_MENU_MAX_WIDTH,
|
||||
};
|
||||
use fuzzy::{PathMatch, StringMatch, StringMatchCandidate};
|
||||
use gpui::{App, Entity, Task, WeakEntity};
|
||||
use language::{Buffer, CodeLabel, CodeLabelBuilder, HighlightId};
|
||||
@@ -25,6 +27,7 @@ use ui::prelude::*;
|
||||
use util::ResultExt as _;
|
||||
use util::paths::PathStyle;
|
||||
use util::rel_path::RelPath;
|
||||
use util::truncate_and_remove_front;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::AgentPanel;
|
||||
@@ -336,14 +339,20 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
|
||||
mention_set: WeakEntity<MentionSet>,
|
||||
workspace: Entity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
label_max_chars: usize,
|
||||
cx: &mut App,
|
||||
) -> Option<Completion> {
|
||||
let path_style = project.read(cx).path_style(cx);
|
||||
let (file_name, directory) =
|
||||
extract_file_name_and_directory(&project_path.path, path_prefix, path_style);
|
||||
|
||||
let label =
|
||||
build_code_label_for_path(&file_name, directory.as_ref().map(|s| s.as_ref()), None, cx);
|
||||
let label = build_code_label_for_path(
|
||||
&file_name,
|
||||
directory.as_ref().map(|s| s.as_ref()),
|
||||
None,
|
||||
label_max_chars,
|
||||
cx,
|
||||
);
|
||||
|
||||
let abs_path = project.read(cx).absolute_path(&project_path, cx)?;
|
||||
|
||||
@@ -392,6 +401,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
|
||||
editor: WeakEntity<Editor>,
|
||||
mention_set: WeakEntity<MentionSet>,
|
||||
workspace: Entity<Workspace>,
|
||||
label_max_chars: usize,
|
||||
cx: &mut App,
|
||||
) -> Option<Completion> {
|
||||
let project = workspace.read(cx).project().clone();
|
||||
@@ -414,6 +424,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
|
||||
&symbol.name,
|
||||
Some(&file_name),
|
||||
Some(symbol.range.start.0.row + 1),
|
||||
label_max_chars,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -852,7 +863,7 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
|
||||
buffer: &Entity<Buffer>,
|
||||
buffer_position: Anchor,
|
||||
_trigger: CompletionContext,
|
||||
_window: &mut Window,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Task<Result<Vec<CompletionResponse>>> {
|
||||
let state = buffer.update(cx, |buffer, cx| {
|
||||
@@ -948,6 +959,31 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
|
||||
let search_task =
|
||||
self.search_mentions(mode, query, Arc::<AtomicBool>::default(), cx);
|
||||
|
||||
// Calculate maximum characters available for the full label (file_name + space + directory)
|
||||
// based on maximum menu width after accounting for padding, spacing, and icon width
|
||||
let label_max_chars = {
|
||||
// Base06 left padding + Base06 gap + Base06 right padding + icon width
|
||||
let used_pixels = DynamicSpacing::Base06.px(cx) * 3.0
|
||||
+ IconSize::XSmall.rems() * window.rem_size();
|
||||
|
||||
let style = window.text_style();
|
||||
let font_id = window.text_system().resolve_font(&style.font());
|
||||
let font_size = TextSize::Small.rems(cx).to_pixels(window.rem_size());
|
||||
|
||||
// Fallback em_width of 10px matches file_finder.rs fallback for TextSize::Small
|
||||
let em_width = cx
|
||||
.text_system()
|
||||
.em_width(font_id, font_size)
|
||||
.unwrap_or(px(10.0));
|
||||
|
||||
// Calculate available pixels for text (file_name + directory)
|
||||
// Using max width since dynamic_width allows the menu to expand up to this
|
||||
let available_pixels = COMPLETION_MENU_MAX_WIDTH - used_pixels;
|
||||
|
||||
// Convert to character count (total available for file_name + directory)
|
||||
(f32::from(available_pixels) / f32::from(em_width)) as usize
|
||||
};
|
||||
|
||||
cx.spawn(async move |_, cx| {
|
||||
let matches = search_task.await;
|
||||
|
||||
@@ -984,6 +1020,7 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
|
||||
mention_set.clone(),
|
||||
workspace.clone(),
|
||||
project.clone(),
|
||||
label_max_chars,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
@@ -996,6 +1033,7 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
|
||||
editor.clone(),
|
||||
mention_set.clone(),
|
||||
workspace.clone(),
|
||||
label_max_chars,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
@@ -1595,6 +1633,7 @@ fn build_code_label_for_path(
|
||||
file: &str,
|
||||
directory: Option<&str>,
|
||||
line_number: Option<u32>,
|
||||
label_max_chars: usize,
|
||||
cx: &App,
|
||||
) -> CodeLabel {
|
||||
let variable_highlight_id = cx
|
||||
@@ -1608,7 +1647,13 @@ fn build_code_label_for_path(
|
||||
label.push_str(" ", None);
|
||||
|
||||
if let Some(directory) = directory {
|
||||
label.push_str(directory, variable_highlight_id);
|
||||
let file_name_chars = file.chars().count();
|
||||
// Account for: file_name + space (ellipsis is handled by truncate_and_remove_front)
|
||||
let directory_max_chars = label_max_chars
|
||||
.saturating_sub(file_name_chars)
|
||||
.saturating_sub(1);
|
||||
let truncated_directory = truncate_and_remove_front(directory, directory_max_chars.max(5));
|
||||
label.push_str(&truncated_directory, variable_highlight_id);
|
||||
}
|
||||
if let Some(line_number) = line_number {
|
||||
label.push_str(&format!(" L{}", line_number), variable_highlight_id);
|
||||
|
||||
@@ -49,6 +49,8 @@ pub const MENU_GAP: Pixels = px(4.);
|
||||
pub const MENU_ASIDE_X_PADDING: Pixels = px(16.);
|
||||
pub const MENU_ASIDE_MIN_WIDTH: Pixels = px(260.);
|
||||
pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.);
|
||||
pub const COMPLETION_MENU_MIN_WIDTH: Pixels = px(280.);
|
||||
pub const COMPLETION_MENU_MAX_WIDTH: Pixels = px(540.);
|
||||
|
||||
// Constants for the markdown cache. The purpose of this cache is to reduce flickering due to
|
||||
// documentation not yet being parsed.
|
||||
@@ -907,26 +909,29 @@ impl CompletionsMenu {
|
||||
})
|
||||
});
|
||||
|
||||
div().min_w(px(280.)).max_w(px(540.)).child(
|
||||
ListItem::new(mat.candidate_id)
|
||||
.inset(true)
|
||||
.toggle_state(item_ix == selected_item)
|
||||
.on_click(cx.listener(move |editor, _event, window, cx| {
|
||||
cx.stop_propagation();
|
||||
if let Some(task) = editor.confirm_completion(
|
||||
&ConfirmCompletion {
|
||||
item_ix: Some(item_ix),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
task.detach_and_log_err(cx)
|
||||
}
|
||||
}))
|
||||
.start_slot::<AnyElement>(start_slot)
|
||||
.child(h_flex().overflow_hidden().child(completion_label))
|
||||
.end_slot::<Label>(documentation_label),
|
||||
)
|
||||
div()
|
||||
.min_w(COMPLETION_MENU_MIN_WIDTH)
|
||||
.max_w(COMPLETION_MENU_MAX_WIDTH)
|
||||
.child(
|
||||
ListItem::new(mat.candidate_id)
|
||||
.inset(true)
|
||||
.toggle_state(item_ix == selected_item)
|
||||
.on_click(cx.listener(move |editor, _event, window, cx| {
|
||||
cx.stop_propagation();
|
||||
if let Some(task) = editor.confirm_completion(
|
||||
&ConfirmCompletion {
|
||||
item_ix: Some(item_ix),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
task.detach_and_log_err(cx)
|
||||
}
|
||||
}))
|
||||
.start_slot::<AnyElement>(start_slot)
|
||||
.child(h_flex().overflow_hidden().child(completion_label))
|
||||
.end_slot::<Label>(documentation_label),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user