agent: Cleanup message_editor (#28614)

This PR splits up the rendering of the message editor into multiple
functions. Previously we had a single `h_flex()...` expression which
spanned across 550 lines, `cargo fmt` stopped working.

Release Notes:

- N/A
This commit is contained in:
Bennet Bo Fenner 2025-04-11 18:06:49 -06:00 committed by GitHub
parent 62ebae96e3
commit 429d4580cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,6 +1,8 @@
use std::collections::BTreeMap;
use std::sync::Arc;
use crate::assistant_model_selector::ModelType;
use buffer_diff::BufferDiff;
use collections::HashSet;
use editor::actions::MoveUp;
use editor::{
@ -336,21 +338,23 @@ impl MessageEditor {
diff.update(cx, |diff, cx| diff.move_to_path(path_key, window, cx));
}
}
}
impl Focusable for MessageEditor {
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
self.editor.focus_handle(cx)
}
}
impl Render for MessageEditor {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let font_size = TextSize::Small.rems(cx);
let line_height = font_size.to_pixels(window.rem_size()) * 1.5;
fn render_editor(
&self,
font_size: Rems,
line_height: Pixels,
window: &mut Window,
cx: &mut Context<Self>,
) -> Div {
let thread = self.thread.read(cx);
let editor_bg_color = cx.theme().colors().editor_background;
let is_generating = thread.is_generating();
let focus_handle = self.editor.focus_handle(cx);
let is_model_selected = self.is_model_selected(cx);
let is_editor_empty = self.is_editor_empty(cx);
let is_editor_expanded = self.editor_is_expanded;
let expand_icon = if is_editor_expanded {
IconName::Minimize
@ -358,295 +362,6 @@ impl Render for MessageEditor {
IconName::Maximize
};
let thread = self.thread.read(cx);
let is_generating = thread.is_generating();
let total_token_usage = thread.total_token_usage(cx);
let is_model_selected = self.is_model_selected(cx);
let is_editor_empty = self.is_editor_empty(cx);
let is_edit_changes_expanded = self.edits_expanded;
let action_log = self.thread.read(cx).action_log();
let changed_buffers = action_log.read(cx).changed_buffers(cx);
let changed_buffers_count = changed_buffers.len();
let editor_bg_color = cx.theme().colors().editor_background;
let border_color = cx.theme().colors().border;
let active_color = cx.theme().colors().element_selected;
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
v_flex()
.size_full()
.when(self.waiting_for_summaries_to_send, |parent| {
parent.child(
h_flex().py_3().w_full().justify_center().child(
h_flex()
.flex_none()
.px_2()
.py_2()
.bg(editor_bg_color)
.border_1()
.border_color(cx.theme().colors().border_variant)
.rounded_lg()
.shadow_md()
.gap_1()
.child(
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Muted)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(gpui::Transformation::rotate(
gpui::percentage(delta),
))
},
),
)
.child(
Label::new("Summarizing context…")
.size(LabelSize::XSmall)
.color(Color::Muted),
),
),
)
})
.when(changed_buffers_count > 0, |parent| {
parent.child(
v_flex()
.mx_2()
.bg(bg_edit_files_disclosure)
.border_1()
.border_b_0()
.border_color(border_color)
.rounded_t_md()
.shadow(smallvec::smallvec![gpui::BoxShadow {
color: gpui::black().opacity(0.15),
offset: point(px(1.), px(-1.)),
blur_radius: px(3.),
spread_radius: px(0.),
}])
.child(
h_flex()
.id("edits-container")
.cursor_pointer()
.p_1p5()
.justify_between()
.when(is_edit_changes_expanded, |this| {
this.border_b_1().border_color(border_color)
})
.on_click(cx.listener(|this, _, window, cx| {
this.handle_review_click(window, cx)
}))
.child(
h_flex()
.gap_1()
.child(
Disclosure::new(
"edits-disclosure",
is_edit_changes_expanded,
)
.on_click(
cx.listener(|this, _ev, _window, cx| {
this.edits_expanded = !this.edits_expanded;
cx.notify();
}),
),
)
.child(
Label::new("Edits")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
Label::new("")
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.child(
Label::new(format!(
"{} {}",
changed_buffers_count,
if changed_buffers_count == 1 {
"file"
} else {
"files"
}
))
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.child(
Button::new("review", "Review Changes")
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&OpenAgentDiff,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(cx.listener(|this, _, window, cx| {
this.handle_review_click(window, cx)
})),
),
)
.when(is_edit_changes_expanded, |parent| {
parent.child(
v_flex().children(
changed_buffers.into_iter().enumerate().flat_map(
|(index, (buffer, _diff))| {
let file = buffer.read(cx).file()?;
let path = file.path();
let parent_label = path.parent().and_then(|parent| {
let parent_str = parent.to_string_lossy();
if parent_str.is_empty() {
None
} else {
Some(
Label::new(format!(
"/{}{}",
parent_str,
std::path::MAIN_SEPARATOR_STR
))
.color(Color::Muted)
.size(LabelSize::XSmall)
.buffer_font(cx),
)
}
});
let name_label = path.file_name().map(|name| {
Label::new(name.to_string_lossy().to_string())
.size(LabelSize::XSmall)
.buffer_font(cx)
});
let file_icon = FileIcons::get_icon(&path, cx)
.map(Icon::from_path)
.map(|icon| {
icon.color(Color::Muted).size(IconSize::Small)
})
.unwrap_or_else(|| {
Icon::new(IconName::File)
.color(Color::Muted)
.size(IconSize::Small)
});
let hover_color = cx.theme()
.colors()
.element_background
.blend(cx.theme().colors().editor_foreground.opacity(0.025));
let overlay_gradient = linear_gradient(
90.,
linear_color_stop(
editor_bg_color,
1.,
),
linear_color_stop(
editor_bg_color
.opacity(0.2),
0.,
),
);
let overlay_gradient_hover = linear_gradient(
90.,
linear_color_stop(
hover_color,
1.,
),
linear_color_stop(
hover_color
.opacity(0.2),
0.,
),
);
let element = h_flex()
.group("edited-code")
.id(("file-container", index))
.cursor_pointer()
.relative()
.py_1()
.pl_2()
.pr_1()
.gap_2()
.justify_between()
.bg(cx.theme().colors().editor_background)
.hover(|style| style.bg(hover_color))
.when(index + 1 < changed_buffers_count, |parent| {
parent.border_color(border_color).border_b_1()
})
.child(
h_flex()
.id("file-name")
.pr_8()
.gap_1p5()
.max_w_full()
.overflow_x_scroll()
.child(file_icon)
.child(
h_flex()
.gap_0p5()
.children(name_label)
.children(parent_label)
) // TODO: show lines changed
.child(
Label::new("+")
.color(Color::Created),
)
.child(
Label::new("-")
.color(Color::Deleted),
),
)
.child(
div().visible_on_hover("edited-code").child(
Button::new("review", "Review")
.label_size(LabelSize::Small)
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.handle_file_click(buffer.clone(), window, cx);
})
})
)
)
.child(
div()
.id("gradient-overlay")
.absolute()
.h_5_6()
.w_12()
.bottom_0()
.right(px(52.))
.bg(overlay_gradient)
.group_hover("edited-code", |style| style.bg(overlay_gradient_hover))
,
)
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.handle_file_click(buffer.clone(), window, cx);
})
});
Some(element)
},
),
),
)
}),
)
})
.child(
v_flex()
.key_context("MessageEditor")
.on_action(cx.listener(Self::chat))
@ -695,17 +410,20 @@ impl Render for MessageEditor {
window,
cx,
)
}})
}
})
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(Box::new(ExpandMessageEditor), cx);
}))
)
})),
),
)
.child(
v_flex()
.size_full()
.gap_4()
.when(is_editor_expanded, |this| this.h(vh(0.8, window)).justify_between())
.when(is_editor_expanded, |this| {
this.h(vh(0.8, window)).justify_between()
})
.child(div().when(is_editor_expanded, |this| this.h_full()).child({
let settings = ThemeSettings::get_global(cx);
@ -728,22 +446,23 @@ impl Render for MessageEditor {
syntax: cx.theme().syntax().clone(),
..Default::default()
},
).into_any()
)
.into_any()
}))
.child(
h_flex()
.flex_none()
.justify_between()
.child(h_flex().gap_2().child(self.profile_selector.clone()))
.child(
h_flex().gap_1()
.child(self.model_selector.clone())
.map({
.child(h_flex().gap_1().child(self.model_selector.clone()).map({
let focus_handle = focus_handle.clone();
move |parent| {
if is_generating {
parent.child(
IconButton::new("stop-generation", IconName::StopFilled)
IconButton::new(
"stop-generation",
IconName::StopFilled,
)
.icon_color(Color::Error)
.style(ButtonStyle::Tinted(ui::TintColor::Error))
.tooltip(move |window, cx| {
@ -780,24 +499,25 @@ impl Render for MessageEditor {
.disabled(
is_editor_empty
|| !is_model_selected
|| self.waiting_for_summaries_to_send
|| self.waiting_for_summaries_to_send,
)
.on_click({
let focus_handle = focus_handle.clone();
move |_event, window, cx| {
focus_handle.dispatch_action(&Chat, window, cx);
focus_handle
.dispatch_action(&Chat, window, cx);
}
})
.when(!is_editor_empty && is_model_selected, |button| {
.when(
!is_editor_empty && is_model_selected,
|button| {
button.tooltip(move |window, cx| {
Tooltip::for_action(
"Send",
&Chat,
window,
cx,
"Send", &Chat, window, cx,
)
})
})
},
)
.when(is_editor_empty, |button| {
button.tooltip(Tooltip::text(
"Type a message to submit",
@ -807,18 +527,236 @@ impl Render for MessageEditor {
button.tooltip(Tooltip::text(
"Select a model to continue",
))
}),
)
}
}
})),
),
)
}
fn render_changed_buffers(
&self,
changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Div {
let focus_handle = self.editor.focus_handle(cx);
let editor_bg_color = cx.theme().colors().editor_background;
let border_color = cx.theme().colors().border;
let active_color = cx.theme().colors().element_selected;
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
let is_edit_changes_expanded = self.edits_expanded;
v_flex()
.mx_2()
.bg(bg_edit_files_disclosure)
.border_1()
.border_b_0()
.border_color(border_color)
.rounded_t_md()
.shadow(smallvec::smallvec![gpui::BoxShadow {
color: gpui::black().opacity(0.15),
offset: point(px(1.), px(-1.)),
blur_radius: px(3.),
spread_radius: px(0.),
}])
.child(
h_flex()
.id("edits-container")
.cursor_pointer()
.p_1p5()
.justify_between()
.when(is_edit_changes_expanded, |this| {
this.border_b_1().border_color(border_color)
})
.on_click(
cx.listener(|this, _, window, cx| this.handle_review_click(window, cx)),
)
}
}
}
.child(
h_flex()
.gap_1()
.child(
Disclosure::new("edits-disclosure", is_edit_changes_expanded)
.on_click(cx.listener(|this, _ev, _window, cx| {
this.edits_expanded = !this.edits_expanded;
cx.notify();
})),
)
),
.child(
Label::new("Edits")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(Label::new("").size(LabelSize::XSmall).color(Color::Muted))
.child(
Label::new(format!(
"{} {}",
changed_buffers.len(),
if changed_buffers.len() == 1 {
"file"
} else {
"files"
}
))
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.child(
Button::new("review", "Review Changes")
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&OpenAgentDiff,
&focus_handle,
window,
cx,
)
.when(total_token_usage.ratio != TokenUsageRatio::Normal, |parent| {
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(cx.listener(|this, _, window, cx| {
this.handle_review_click(window, cx)
})),
),
)
.when(is_edit_changes_expanded, |parent| {
parent.child(
v_flex().children(changed_buffers.into_iter().enumerate().flat_map(
|(index, (buffer, _diff))| {
let file = buffer.read(cx).file()?;
let path = file.path();
let parent_label = path.parent().and_then(|parent| {
let parent_str = parent.to_string_lossy();
if parent_str.is_empty() {
None
} else {
Some(
Label::new(format!(
"/{}{}",
parent_str,
std::path::MAIN_SEPARATOR_STR
))
.color(Color::Muted)
.size(LabelSize::XSmall)
.buffer_font(cx),
)
}
});
let name_label = path.file_name().map(|name| {
Label::new(name.to_string_lossy().to_string())
.size(LabelSize::XSmall)
.buffer_font(cx)
});
let file_icon = FileIcons::get_icon(&path, cx)
.map(Icon::from_path)
.map(|icon| icon.color(Color::Muted).size(IconSize::Small))
.unwrap_or_else(|| {
Icon::new(IconName::File)
.color(Color::Muted)
.size(IconSize::Small)
});
let hover_color = cx
.theme()
.colors()
.element_background
.blend(cx.theme().colors().editor_foreground.opacity(0.025));
let overlay_gradient = linear_gradient(
90.,
linear_color_stop(editor_bg_color, 1.),
linear_color_stop(editor_bg_color.opacity(0.2), 0.),
);
let overlay_gradient_hover = linear_gradient(
90.,
linear_color_stop(hover_color, 1.),
linear_color_stop(hover_color.opacity(0.2), 0.),
);
let element = h_flex()
.group("edited-code")
.id(("file-container", index))
.cursor_pointer()
.relative()
.py_1()
.pl_2()
.pr_1()
.gap_2()
.justify_between()
.bg(cx.theme().colors().editor_background)
.hover(|style| style.bg(hover_color))
.when(index + 1 < changed_buffers.len(), |parent| {
parent.border_color(border_color).border_b_1()
})
.child(
h_flex()
.id("file-name")
.pr_8()
.gap_1p5()
.max_w_full()
.overflow_x_scroll()
.child(file_icon)
.child(
h_flex()
.gap_0p5()
.children(name_label)
.children(parent_label),
) // TODO: show lines changed
.child(Label::new("+").color(Color::Created))
.child(Label::new("-").color(Color::Deleted)),
)
.child(
div().visible_on_hover("edited-code").child(
Button::new("review", "Review")
.label_size(LabelSize::Small)
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.handle_file_click(
buffer.clone(),
window,
cx,
);
})
}),
),
)
.child(
div()
.id("gradient-overlay")
.absolute()
.h_5_6()
.w_12()
.bottom_0()
.right(px(52.))
.bg(overlay_gradient)
.group_hover("edited-code", |style| {
style.bg(overlay_gradient_hover)
}),
)
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.handle_file_click(buffer.clone(), window, cx);
})
});
Some(element)
},
)),
)
})
}
fn render_reaching_token_limit(&self, line_height: Pixels, cx: &mut Context<Self>) -> Div {
h_flex()
.p_2()
.gap_2()
@ -868,8 +806,71 @@ impl Render for MessageEditor {
.icon_size(IconSize::Small)
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.label_size(LabelSize::Small),
)
}
}
impl Focusable for MessageEditor {
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
self.editor.focus_handle(cx)
}
}
impl Render for MessageEditor {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let thread = self.thread.read(cx);
let total_token_usage = thread.total_token_usage(cx);
let action_log = self.thread.read(cx).action_log();
let changed_buffers = action_log.read(cx).changed_buffers(cx);
let font_size = TextSize::Small.rems(cx);
let line_height = font_size.to_pixels(window.rem_size()) * 1.5;
v_flex()
.size_full()
.when(self.waiting_for_summaries_to_send, |parent| {
parent.child(
h_flex().py_3().w_full().justify_center().child(
h_flex()
.flex_none()
.px_2()
.py_2()
.bg(cx.theme().colors().editor_background)
.border_1()
.border_color(cx.theme().colors().border_variant)
.rounded_lg()
.shadow_md()
.gap_1()
.child(
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Muted)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(gpui::Transformation::rotate(
gpui::percentage(delta),
))
},
),
)
.child(
Label::new("Summarizing context…")
.size(LabelSize::XSmall)
.color(Color::Muted),
),
),
)
})
.when(changed_buffers.len() > 0, |parent| {
parent.child(self.render_changed_buffers(&changed_buffers, window, cx))
})
.child(self.render_editor(font_size, line_height, window, cx))
.when(
total_token_usage.ratio != TokenUsageRatio::Normal,
|parent| parent.child(self.render_reaching_token_limit(line_height, cx)),
)
}
}