1use super::WorkflowStep;
2use crate::{Assist, Context};
3use editor::{
4 display_map::{BlockDisposition, BlockProperties, BlockStyle},
5 Editor, EditorEvent, ExcerptRange, MultiBuffer,
6};
7use gpui::{
8 div, AnyElement, AppContext, Context as _, Empty, EventEmitter, FocusableView, IntoElement,
9 Model, ParentElement as _, Render, SharedString, Styled as _, View, ViewContext,
10 VisualContext as _, WeakModel, WindowContext,
11};
12use language::{language_settings::SoftWrap, Anchor, Buffer, LanguageRegistry};
13use std::{ops::DerefMut, sync::Arc};
14use text::OffsetRangeExt;
15use theme::ActiveTheme as _;
16use ui::{
17 h_flex, v_flex, ButtonCommon as _, ButtonLike, ButtonStyle, Color, Icon, IconName,
18 InteractiveElement as _, Label, LabelCommon as _,
19};
20use workspace::{
21 item::{self, Item},
22 pane,
23 searchable::SearchableItemHandle,
24};
25
26pub struct WorkflowStepView {
27 step: WeakModel<WorkflowStep>,
28 tool_output_buffer: Model<Buffer>,
29 editor: View<Editor>,
30}
31
32impl WorkflowStepView {
33 pub fn new(
34 context: Model<Context>,
35 step: Model<WorkflowStep>,
36 language_registry: Arc<LanguageRegistry>,
37 cx: &mut ViewContext<Self>,
38 ) -> Self {
39 let tool_output_buffer =
40 cx.new_model(|cx| Buffer::local(step.read(cx).tool_output.clone(), cx));
41 let buffer = cx.new_model(|cx| {
42 let mut buffer = MultiBuffer::without_headers(0, language::Capability::ReadWrite);
43 buffer.push_excerpts(
44 context.read(cx).buffer().clone(),
45 [ExcerptRange {
46 context: step.read(cx).context_buffer_range.clone(),
47 primary: None,
48 }],
49 cx,
50 );
51 buffer.push_excerpts(
52 tool_output_buffer.clone(),
53 [ExcerptRange {
54 context: Anchor::MIN..Anchor::MAX,
55 primary: None,
56 }],
57 cx,
58 );
59 buffer
60 });
61
62 let buffer_snapshot = buffer.read(cx).snapshot(cx);
63 let output_excerpt = buffer_snapshot.excerpts().skip(1).next().unwrap().0;
64 let input_start_anchor = multi_buffer::Anchor::min();
65 let output_start_anchor = buffer_snapshot
66 .anchor_in_excerpt(output_excerpt, Anchor::MIN)
67 .unwrap();
68 let output_end_anchor = multi_buffer::Anchor::max();
69
70 let handle = cx.view().downgrade();
71 let editor = cx.new_view(|cx| {
72 let mut editor = Editor::for_multibuffer(buffer.clone(), None, false, cx);
73 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
74 editor.set_show_line_numbers(false, cx);
75 editor.set_show_git_diff_gutter(false, cx);
76 editor.set_show_code_actions(false, cx);
77 editor.set_show_runnables(false, cx);
78 editor.set_show_wrap_guides(false, cx);
79 editor.set_show_indent_guides(false, cx);
80 editor.set_read_only(true);
81 editor.set_show_inline_completions(false);
82 editor.insert_blocks(
83 [
84 BlockProperties {
85 position: input_start_anchor,
86 height: 1,
87 style: BlockStyle::Fixed,
88 render: Box::new(|cx| section_header("Step Input", cx)),
89 disposition: BlockDisposition::Above,
90 priority: 0,
91 },
92 BlockProperties {
93 position: output_start_anchor,
94 height: 1,
95 style: BlockStyle::Fixed,
96 render: Box::new(|cx| section_header("Tool Output", cx)),
97 disposition: BlockDisposition::Above,
98 priority: 0,
99 },
100 BlockProperties {
101 position: output_end_anchor,
102 height: 1,
103 style: BlockStyle::Fixed,
104 render: Box::new(move |cx| {
105 if let Some(result) = handle.upgrade().and_then(|this| {
106 this.update(cx.deref_mut(), |this, cx| this.render_result(cx))
107 }) {
108 v_flex()
109 .child(section_header("Output", cx))
110 .child(
111 div().pl(cx.gutter_dimensions.full_width()).child(result),
112 )
113 .into_any_element()
114 } else {
115 Empty.into_any_element()
116 }
117 }),
118 disposition: BlockDisposition::Below,
119 priority: 0,
120 },
121 ],
122 None,
123 cx,
124 );
125 editor
126 });
127
128 cx.observe(&step, Self::step_updated).detach();
129 cx.observe_release(&step, Self::step_released).detach();
130
131 cx.spawn(|this, mut cx| async move {
132 if let Ok(language) = language_registry.language_for_name("JSON").await {
133 this.update(&mut cx, |this, cx| {
134 this.tool_output_buffer.update(cx, |buffer, cx| {
135 buffer.set_language(Some(language), cx);
136 });
137 })
138 .ok();
139 }
140 })
141 .detach();
142
143 Self {
144 tool_output_buffer,
145 step: step.downgrade(),
146 editor,
147 }
148 }
149
150 pub fn step(&self) -> &WeakModel<WorkflowStep> {
151 &self.step
152 }
153
154 fn render_result(&mut self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
155 let step = self.step.upgrade()?;
156 let result = step.read(cx).resolution.as_ref()?;
157 match result {
158 Ok(result) => {
159 Some(
160 v_flex()
161 .child(result.title.clone())
162 .children(result.suggestion_groups.iter().filter_map(
163 |(buffer, suggestion_groups)| {
164 let buffer = buffer.read(cx);
165 let path = buffer.file().map(|f| f.path());
166 let snapshot = buffer.snapshot();
167 v_flex()
168 .mb_2()
169 .border_b_1()
170 .children(path.map(|path| format!("path: {}", path.display())))
171 .children(suggestion_groups.iter().map(|group| {
172 v_flex().pt_2().pl_2().children(
173 group.suggestions.iter().map(|suggestion| {
174 let range = suggestion.range().to_point(&snapshot);
175 v_flex()
176 .children(
177 suggestion.description().map(|desc| {
178 format!("description: {desc}")
179 }),
180 )
181 .child(format!("kind: {}", suggestion.kind()))
182 .children(suggestion.symbol_path().map(
183 |path| format!("symbol path: {}", path.0),
184 ))
185 .child(format!(
186 "lines: {} - {}",
187 range.start.row + 1,
188 range.end.row + 1
189 ))
190 }),
191 )
192 }))
193 .into()
194 },
195 ))
196 .into_any_element(),
197 )
198 }
199 Err(error) => Some(format!("{:?}", error).into_any_element()),
200 }
201 }
202
203 fn step_updated(&mut self, step: Model<WorkflowStep>, cx: &mut ViewContext<Self>) {
204 self.tool_output_buffer.update(cx, |buffer, cx| {
205 let text = step.read(cx).tool_output.clone();
206 buffer.set_text(text, cx);
207 });
208 cx.notify();
209 }
210
211 fn step_released(&mut self, _: &mut WorkflowStep, cx: &mut ViewContext<Self>) {
212 cx.emit(EditorEvent::Closed);
213 }
214
215 fn resolve(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
216 self.step
217 .update(cx, |step, cx| {
218 step.resolve(cx);
219 })
220 .ok();
221 }
222}
223
224fn section_header(
225 name: &'static str,
226 cx: &mut editor::display_map::BlockContext,
227) -> gpui::AnyElement {
228 h_flex()
229 .pl(cx.gutter_dimensions.full_width())
230 .h_11()
231 .w_full()
232 .relative()
233 .gap_1()
234 .child(
235 ButtonLike::new("role")
236 .style(ButtonStyle::Filled)
237 .child(Label::new(name).color(Color::Default)),
238 )
239 .into_any_element()
240}
241
242impl Render for WorkflowStepView {
243 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
244 div()
245 .key_context("ContextEditor")
246 .on_action(cx.listener(Self::resolve))
247 .flex_grow()
248 .bg(cx.theme().colors().editor_background)
249 .child(self.editor.clone())
250 }
251}
252
253impl EventEmitter<EditorEvent> for WorkflowStepView {}
254
255impl FocusableView for WorkflowStepView {
256 fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
257 self.editor.read(cx).focus_handle(cx)
258 }
259}
260
261impl Item for WorkflowStepView {
262 type Event = EditorEvent;
263
264 fn tab_content_text(&self, cx: &WindowContext) -> Option<SharedString> {
265 let step = self.step.upgrade()?.read(cx);
266 let context = step.context.upgrade()?.read(cx);
267 let buffer = context.buffer().read(cx);
268 let index = context
269 .workflow_step_index_for_range(&step.context_buffer_range, buffer)
270 .ok()?
271 + 1;
272 Some(format!("Step {index}").into())
273 }
274
275 fn tab_icon(&self, _cx: &WindowContext) -> Option<ui::Icon> {
276 Some(Icon::new(IconName::Pencil))
277 }
278
279 fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
280 match event {
281 EditorEvent::Edited { .. } => {
282 f(item::ItemEvent::Edit);
283 }
284 EditorEvent::TitleChanged => {
285 f(item::ItemEvent::UpdateTab);
286 }
287 EditorEvent::Closed => f(item::ItemEvent::CloseItem),
288 _ => {}
289 }
290 }
291
292 fn tab_tooltip_text(&self, _cx: &AppContext) -> Option<SharedString> {
293 None
294 }
295
296 fn as_searchable(&self, _handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
297 None
298 }
299
300 fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext<Self>) {
301 self.editor.update(cx, |editor, cx| {
302 Item::set_nav_history(editor, nav_history, cx)
303 })
304 }
305
306 fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
307 self.editor
308 .update(cx, |editor, cx| Item::navigate(editor, data, cx))
309 }
310
311 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
312 self.editor
313 .update(cx, |editor, cx| Item::deactivated(editor, cx))
314 }
315}