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