mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
agent: Render empty path-only code blocks as file reference pills
When an agent emits a fenced code block whose info string is a project path (e.g. ```path/to/foo.rs#L1-1) with no content between the fences, render it as a small clickable file-reference pill instead of an empty code block. Clicking the pill opens the file in the workspace; if the path doesn't resolve to a project file, the click is a no-op.
This commit is contained in:
parent
d2927ffa45
commit
369d209879
2 changed files with 109 additions and 2 deletions
|
|
@ -38,7 +38,8 @@ use gpui::{
|
|||
use language::Buffer;
|
||||
use language_model::{LanguageModelCompletionError, LanguageModelRegistry};
|
||||
use markdown::{
|
||||
CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownFont, MarkdownStyle,
|
||||
CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownFont,
|
||||
MarkdownStyle, PathWithRange,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use project::{AgentId, AgentServerStore, Project, ProjectEntryId};
|
||||
|
|
@ -3071,11 +3072,37 @@ fn render_agent_markdown(
|
|||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let path_reference_workspace = workspace.clone();
|
||||
MarkdownElement::new(markdown, style)
|
||||
.image_resolver(move |dest_url| resolve_agent_image(dest_url, &worktree_roots))
|
||||
.on_url_click(move |text, window, cx| {
|
||||
thread_view::open_link(text, &workspace, window, cx);
|
||||
})
|
||||
.on_path_reference_click(move |path_range, window, cx| {
|
||||
open_path_reference(path_range, &path_reference_workspace, window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn open_path_reference(
|
||||
path_range: &PathWithRange,
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let project = workspace.project();
|
||||
let Some(project_path) = project.update(cx, |project, cx| {
|
||||
project.find_project_path(path_range.path.as_ref(), cx)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
workspace
|
||||
.open_path(project_path, None, true, window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn plan_label_markdown_style(
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ use crate::parser::CodeBlockKind;
|
|||
type LinkStyleCallback = Rc<dyn Fn(&str, &App) -> Option<TextStyleRefinement>>;
|
||||
type SourceClickCallback = Box<dyn Fn(usize, usize, &mut Window, &mut App) -> bool>;
|
||||
type CheckboxToggleCallback = Rc<dyn Fn(Range<usize>, bool, &mut Window, &mut App)>;
|
||||
type PathReferenceClickCallback = Rc<dyn Fn(&PathWithRange, &mut Window, &mut App)>;
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct BlockQuoteKindColors {
|
||||
|
|
@ -1063,6 +1064,7 @@ pub struct MarkdownElement {
|
|||
on_url_click: Option<Box<dyn Fn(SharedString, &mut Window, &mut App)>>,
|
||||
on_source_click: Option<SourceClickCallback>,
|
||||
on_checkbox_toggle: Option<CheckboxToggleCallback>,
|
||||
on_path_reference_click: Option<PathReferenceClickCallback>,
|
||||
image_resolver: Option<Box<dyn Fn(&str) -> Option<ImageSource>>>,
|
||||
show_root_block_markers: bool,
|
||||
autoscroll: AutoscrollBehavior,
|
||||
|
|
@ -1080,6 +1082,7 @@ impl MarkdownElement {
|
|||
on_url_click: None,
|
||||
on_source_click: None,
|
||||
on_checkbox_toggle: None,
|
||||
on_path_reference_click: None,
|
||||
image_resolver: None,
|
||||
show_root_block_markers: false,
|
||||
autoscroll: AutoscrollBehavior::Propagate,
|
||||
|
|
@ -1136,6 +1139,17 @@ impl MarkdownElement {
|
|||
self
|
||||
}
|
||||
|
||||
/// Register a handler invoked when the user clicks a path-reference pill. A path-reference pill
|
||||
/// is rendered in place of an otherwise-empty fenced code block whose info string is a project
|
||||
/// path (e.g. ```` ```path/to/foo.rs#L1-1 ```` followed immediately by a closing fence).
|
||||
pub fn on_path_reference_click(
|
||||
mut self,
|
||||
handler: impl Fn(&PathWithRange, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_path_reference_click = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn image_resolver(
|
||||
mut self,
|
||||
resolver: impl Fn(&str) -> Option<ImageSource> + 'static,
|
||||
|
|
@ -1697,6 +1711,7 @@ impl Element for MarkdownElement {
|
|||
let mut current_img_block_range: Option<Range<usize>> = None;
|
||||
let mut handled_html_block = false;
|
||||
let mut rendered_mermaid_block = false;
|
||||
let mut rendered_path_reference_block = false;
|
||||
for (index, (range, event)) in parsed_markdown.events.iter().enumerate() {
|
||||
// Skip alt text for images that rendered
|
||||
if let Some(current_img_block_range) = ¤t_img_block_range
|
||||
|
|
@ -1720,6 +1735,13 @@ impl Element for MarkdownElement {
|
|||
continue;
|
||||
}
|
||||
|
||||
if rendered_path_reference_block {
|
||||
if matches!(event, MarkdownEvent::End(MarkdownTagEnd::CodeBlock)) {
|
||||
rendered_path_reference_block = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
match event {
|
||||
MarkdownEvent::RootStart => {
|
||||
if self.show_root_block_markers {
|
||||
|
|
@ -1803,7 +1825,7 @@ impl Element for MarkdownElement {
|
|||
markdown_end,
|
||||
);
|
||||
}
|
||||
MarkdownTag::CodeBlock { kind, .. } => {
|
||||
MarkdownTag::CodeBlock { kind, metadata } => {
|
||||
if render_mermaid_diagrams
|
||||
&& let Some(mermaid_diagram) =
|
||||
parsed_markdown.mermaid_diagrams.get(&range.start)
|
||||
|
|
@ -1833,6 +1855,26 @@ impl Element for MarkdownElement {
|
|||
continue;
|
||||
}
|
||||
|
||||
if let CodeBlockKind::FencedSrc(path_range) = kind
|
||||
&& let Some(on_click) = self.on_path_reference_click.clone()
|
||||
&& parsed_markdown
|
||||
.source
|
||||
.get(metadata.content_range.clone())
|
||||
.is_some_and(|content| content.trim().is_empty())
|
||||
{
|
||||
builder.push_sourced_element(
|
||||
range.clone(),
|
||||
render_path_reference_pill(
|
||||
path_range.clone(),
|
||||
on_click,
|
||||
range.start,
|
||||
cx,
|
||||
),
|
||||
);
|
||||
rendered_path_reference_block = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
let language = match kind {
|
||||
CodeBlockKind::Fenced => None,
|
||||
CodeBlockKind::FencedLang(language) => {
|
||||
|
|
@ -2420,6 +2462,44 @@ fn apply_heading_style(
|
|||
heading
|
||||
}
|
||||
|
||||
fn render_path_reference_pill(
|
||||
path_range: PathWithRange,
|
||||
on_click: PathReferenceClickCallback,
|
||||
element_id_seed: usize,
|
||||
cx: &App,
|
||||
) -> AnyElement {
|
||||
let display: SharedString = path_range.path.to_string().into();
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
h_flex()
|
||||
.id(("markdown-path-ref", element_id_seed))
|
||||
.my_1()
|
||||
.flex_none()
|
||||
.gap_1p5()
|
||||
.px_1p5()
|
||||
.py_0p5()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(colors.border_variant)
|
||||
.bg(colors.element_background)
|
||||
.hover(|this| this.bg(colors.element_hover))
|
||||
.cursor_pointer()
|
||||
.child(
|
||||
Icon::new(IconName::File)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new(display)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.on_click(move |_event, window, cx| {
|
||||
on_click(&path_range, window, cx);
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_copy_code_block_button(
|
||||
id: usize,
|
||||
code: String,
|
||||
|
|
|
|||
Loading…
Reference in a new issue