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.insert_blocks(
82 [
83 BlockProperties {
84 position: input_start_anchor,
85 height: 1,
86 style: BlockStyle::Fixed,
87 render: Box::new(|cx| section_header("Step Input", cx)),
88 disposition: BlockDisposition::Above,
89 priority: 0,
90 },
91 BlockProperties {
92 position: output_start_anchor,
93 height: 1,
94 style: BlockStyle::Fixed,
95 render: Box::new(|cx| section_header("Tool Output", cx)),
96 disposition: BlockDisposition::Above,
97 priority: 0,
98 },
99 BlockProperties {
100 position: output_end_anchor,
101 height: 1,
102 style: BlockStyle::Fixed,
103 render: Box::new(move |cx| {
104 if let Some(result) = handle.upgrade().and_then(|this| {
105 this.update(cx.deref_mut(), |this, cx| this.render_result(cx))
106 }) {
107 v_flex()
108 .child(section_header("Output", cx))
109 .child(
110 div().pl(cx.gutter_dimensions.full_width()).child(result),
111 )
112 .into_any_element()
113 } else {
114 Empty.into_any_element()
115 }
116 }),
117 disposition: BlockDisposition::Below,
118 priority: 0,
119 },
120 ],
121 None,
122 cx,
123 );
124 editor
125 });
126
127 cx.observe(&step, Self::step_updated).detach();
128 cx.observe_release(&step, Self::step_released).detach();
129
130 cx.spawn(|this, mut cx| async move {
131 if let Ok(language) = language_registry.language_for_name("JSON").await {
132 this.update(&mut cx, |this, cx| {
133 this.tool_output_buffer.update(cx, |buffer, cx| {
134 buffer.set_language(Some(language), cx);
135 });
136 })
137 .ok();
138 }
139 })
140 .detach();
141
142 Self {
143 tool_output_buffer,
144 step: step.downgrade(),
145 editor,
146 }
147 }
148
149 pub fn step(&self) -> &WeakModel<WorkflowStep> {
150 &self.step
151 }
152
153 fn render_result(&mut self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
154 let step = self.step.upgrade()?;
155 let result = step.read(cx).resolution.as_ref()?;
156 match result {
157 Ok(result) => {
158 Some(
159 v_flex()
160 .child(result.title.clone())
161 .children(result.suggestion_groups.iter().filter_map(
162 |(buffer, suggestion_groups)| {
163 let buffer = buffer.read(cx);
164 let path = buffer.file().map(|f| f.path());
165 let snapshot = buffer.snapshot();
166 v_flex()
167 .mb_2()
168 .border_b_1()
169 .children(path.map(|path| format!("path: {}", path.display())))
170 .children(suggestion_groups.iter().map(|group| {
171 v_flex().pt_2().pl_2().children(
172 group.suggestions.iter().map(|suggestion| {
173 let range = suggestion.range().to_point(&snapshot);
174 v_flex()
175 .children(
176 suggestion.description().map(|desc| {
177 format!("description: {desc}")
178 }),
179 )
180 .child(format!("kind: {}", suggestion.kind()))
181 .children(suggestion.symbol_path().map(
182 |path| format!("symbol path: {}", path.0),
183 ))
184 .child(format!(
185 "lines: {} - {}",
186 range.start.row + 1,
187 range.end.row + 1
188 ))
189 }),
190 )
191 }))
192 .into()
193 },
194 ))
195 .into_any_element(),
196 )
197 }
198 Err(error) => Some(format!("{:?}", error).into_any_element()),
199 }
200 }
201
202 fn step_updated(&mut self, step: Model<WorkflowStep>, cx: &mut ViewContext<Self>) {
203 self.tool_output_buffer.update(cx, |buffer, cx| {
204 let text = step.read(cx).tool_output.clone();
205 buffer.set_text(text, cx);
206 });
207 cx.notify();
208 }
209
210 fn step_released(&mut self, _: &mut WorkflowStep, cx: &mut ViewContext<Self>) {
211 cx.emit(EditorEvent::Closed);
212 }
213
214 fn resolve(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
215 self.step
216 .update(cx, |step, cx| {
217 step.resolve(cx);
218 })
219 .ok();
220 }
221}
222
223fn section_header(
224 name: &'static str,
225 cx: &mut editor::display_map::BlockContext,
226) -> gpui::AnyElement {
227 h_flex()
228 .pl(cx.gutter_dimensions.full_width())
229 .h_11()
230 .w_full()
231 .relative()
232 .gap_1()
233 .child(
234 ButtonLike::new("role")
235 .style(ButtonStyle::Filled)
236 .child(Label::new(name).color(Color::Default)),
237 )
238 .into_any_element()
239}
240
241impl Render for WorkflowStepView {
242 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
243 div()
244 .key_context("ContextEditor")
245 .on_action(cx.listener(Self::resolve))
246 .flex_grow()
247 .bg(cx.theme().colors().editor_background)
248 .child(self.editor.clone())
249 }
250}
251
252impl EventEmitter<EditorEvent> for WorkflowStepView {}
253
254impl FocusableView for WorkflowStepView {
255 fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
256 self.editor.read(cx).focus_handle(cx)
257 }
258}
259
260impl Item for WorkflowStepView {
261 type Event = EditorEvent;
262
263 fn tab_content_text(&self, cx: &WindowContext) -> Option<SharedString> {
264 let step = self.step.upgrade()?.read(cx);
265 let context = step.context.upgrade()?.read(cx);
266 let buffer = context.buffer().read(cx);
267 let index = context
268 .workflow_step_index_for_range(&step.context_buffer_range, buffer)
269 .ok()?
270 + 1;
271 Some(format!("Step {index}").into())
272 }
273
274 fn tab_icon(&self, _cx: &WindowContext) -> Option<ui::Icon> {
275 Some(Icon::new(IconName::Pencil))
276 }
277
278 fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
279 match event {
280 EditorEvent::Edited { .. } => {
281 f(item::ItemEvent::Edit);
282 }
283 EditorEvent::TitleChanged => {
284 f(item::ItemEvent::UpdateTab);
285 }
286 EditorEvent::Closed => f(item::ItemEvent::CloseItem),
287 _ => {}
288 }
289 }
290
291 fn tab_tooltip_text(&self, _cx: &AppContext) -> Option<SharedString> {
292 None
293 }
294
295 fn as_searchable(&self, _handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
296 None
297 }
298
299 fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext<Self>) {
300 self.editor.update(cx, |editor, cx| {
301 Item::set_nav_history(editor, nav_history, cx)
302 })
303 }
304
305 fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
306 self.editor
307 .update(cx, |editor, cx| Item::navigate(editor, data, cx))
308 }
309
310 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
311 self.editor
312 .update(cx, |editor, cx| Item::deactivated(editor, cx))
313 }
314}