ui/components/ai/
diff_block.rs1use std::ops::Range;
2use std::sync::LazyLock;
3
4use gpui::{AnyElement, HighlightStyle, StyledText};
5use language::{DefaultLanguageRegistry, LanguageRegistry};
6
7use crate::prelude::*;
8
9static LANGUAGES: LazyLock<DefaultLanguageRegistry> = LazyLock::new(DefaultLanguageRegistry::new);
14
15const DIFF_FONT_FAMILY: &str = "IBM Plex Mono";
19const DIFF_FONT_SIZE: Pixels = px(12.5);
20
21fn style_for_capture(syntax: &syntax_theme::SyntaxTheme, name: &str) -> Option<HighlightStyle> {
28 syntax.style_for_name(name).or_else(|| {
29 let prefix = name.split('.').next()?;
30 (prefix != name)
31 .then(|| syntax.style_for_name(prefix))
32 .flatten()
33 })
34}
35
36fn highlighted_text(text: SharedString, extension: Option<&str>, cx: &App) -> AnyElement {
37 let language = extension.and_then(|extension| LANGUAGES.language_for_extension(extension));
38 match language {
39 Some(language) => {
40 let syntax = cx.theme().syntax();
41 let highlights: Vec<(Range<usize>, HighlightStyle)> =
42 language::highlighted_spans(language, &text)
43 .into_iter()
44 .filter_map(|(range, name)| {
45 style_for_capture(syntax, &name).map(|style| (range, style))
46 })
47 .collect();
48 StyledText::new(text)
49 .with_highlights(highlights)
50 .into_any_element()
51 }
52 None => StyledText::new(text).into_any_element(),
53 }
54}
55
56fn diff_section(
57 label: &'static str,
58 text: SharedString,
59 extension: Option<&str>,
60 bg: gpui::Hsla,
61 label_color: Color,
62 cx: &App,
63) -> AnyElement {
64 v_flex()
65 .gap_1()
66 .p_2()
67 .rounded_md()
68 .bg(bg)
69 .child(Label::new(label).size(LabelSize::Small).color(label_color))
70 .child(
71 div()
72 .font_family(DIFF_FONT_FAMILY)
73 .text_size(DIFF_FONT_SIZE)
74 .child(highlighted_text(text, extension, cx)),
75 )
76 .into_any_element()
77}
78
79#[derive(IntoElement, RegisterComponent)]
85pub struct DiffBlock {
86 id: ElementId,
87 old_text: SharedString,
88 new_text: SharedString,
89 language: Option<SharedString>,
90}
91
92impl DiffBlock {
93 pub fn new(
94 id: impl Into<ElementId>,
95 old_text: impl Into<SharedString>,
96 new_text: impl Into<SharedString>,
97 ) -> Self {
98 Self {
99 id: id.into(),
100 old_text: old_text.into(),
101 new_text: new_text.into(),
102 language: None,
103 }
104 }
105
106 pub fn language(mut self, extension: impl Into<SharedString>) -> Self {
110 self.language = Some(extension.into());
111 self
112 }
113}
114
115impl RenderOnce for DiffBlock {
116 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
117 let extension = self.language.as_deref();
118 let status = cx.theme().status();
119
120 v_flex()
121 .id(self.id)
122 .w_full()
123 .gap_1()
124 .child(diff_section(
125 "− Old",
126 self.old_text,
127 extension,
128 status.deleted_background,
129 Color::Error,
130 cx,
131 ))
132 .child(diff_section(
133 "+ New",
134 self.new_text,
135 extension,
136 status.created_background,
137 Color::Success,
138 cx,
139 ))
140 }
141}
142
143impl Component for DiffBlock {
144 fn scope() -> ComponentScope {
145 ComponentScope::Agent
146 }
147
148 fn description() -> Option<&'static str> {
149 Some(
150 "Renders a tool-call diff as a plain unified old/new text block, \
151 syntax-highlighted via the same tree-sitter path CodeEditor uses.",
152 )
153 }
154
155 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
156 Some(
157 v_flex()
158 .w_96()
159 .gap_4()
160 .child(single_example(
161 "Rust diff",
162 DiffBlock::new(
163 "diff-preview-1",
164 "fn greet() {\n println!(\"hi\");\n}",
165 "fn greet(name: &str) {\n println!(\"hi, {name}\");\n}",
166 )
167 .language("rs")
168 .into_any_element(),
169 ))
170 .into_any_element(),
171 )
172 }
173}