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 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<WorkflowStep>,
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<WorkflowStep>,
35 language_registry: Arc<LanguageRegistry>,
36 cx: &mut ViewContext<Self>,
37 ) -> Self {
38 let tool_output_buffer =
39 cx.new_model(|cx| Buffer::local(step.read(cx).tool_output.clone(), cx));
40 let buffer = cx.new_model(|cx| {
41 let mut buffer = MultiBuffer::without_headers(0, language::Capability::ReadWrite);
42 buffer.push_excerpts(
43 context.read(cx).buffer().clone(),
44 [ExcerptRange {
45 context: step.read(cx).context_buffer_range.clone(),
46 primary: None,
47 }],
48 cx,
49 );
50 buffer.push_excerpts(
51 tool_output_buffer.clone(),
52 [ExcerptRange {
53 context: Anchor::MIN..Anchor::MAX,
54 primary: None,
55 }],
56 cx,
57 );
58 buffer
59 });
60
61 let buffer_snapshot = buffer.read(cx).snapshot(cx);
62 let output_excerpt = buffer_snapshot.excerpts().skip(1).next().unwrap().0;
63 let input_start_anchor = multi_buffer::Anchor::min();
64 let output_start_anchor = buffer_snapshot
65 .anchor_in_excerpt(output_excerpt, Anchor::MIN)
66 .unwrap();
67 let output_end_anchor = multi_buffer::Anchor::max();
68
69 let handle = cx.view().downgrade();
70 let editor = cx.new_view(|cx| {
71 let mut editor = Editor::for_multibuffer(buffer.clone(), None, false, cx);
72 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
73 editor.set_show_line_numbers(false, cx);
74 editor.set_show_git_diff_gutter(false, cx);
75 editor.set_show_code_actions(false, cx);
76 editor.set_show_runnables(false, cx);
77 editor.set_show_wrap_guides(false, cx);
78 editor.set_show_indent_guides(false, cx);
79 editor.set_read_only(true);
80 editor.insert_blocks(
81 [
82 BlockProperties {
83 position: input_start_anchor,
84 height: 1,
85 style: BlockStyle::Fixed,
86 render: Box::new(|cx| section_header("Step Input", cx)),
87 disposition: BlockDisposition::Above,
88 priority: 0,
89 },
90 BlockProperties {
91 position: output_start_anchor,
92 height: 1,
93 style: BlockStyle::Fixed,
94 render: Box::new(|cx| section_header("Tool Output", cx)),
95 disposition: BlockDisposition::Above,
96 priority: 0,
97 },
98 BlockProperties {
99 position: output_end_anchor,
100 height: 1,
101 style: BlockStyle::Fixed,
102 render: Box::new(move |cx| {
103 if let Some(result) = handle.upgrade().and_then(|this| {
104 this.update(cx.deref_mut(), |this, cx| this.render_result(cx))
105 }) {
106 v_flex()
107 .child(section_header("Output", cx))
108 .child(
109 div().pl(cx.gutter_dimensions.full_width()).child(result),
110 )
111 .into_any_element()
112 } else {
113 Empty.into_any_element()
114 }
115 }),
116 disposition: BlockDisposition::Below,
117 priority: 0,
118 },
119 ],
120 None,
121 cx,
122 );
123 editor
124 });
125
126 cx.observe(&step, Self::step_updated).detach();
127 cx.observe_release(&step, Self::step_released).detach();
128
129 cx.spawn(|this, mut cx| async move {
130 if let Ok(language) = language_registry.language_for_name("JSON").await {
131 this.update(&mut cx, |this, cx| {
132 this.tool_output_buffer.update(cx, |buffer, cx| {
133 buffer.set_language(Some(language), cx);
134 });
135 })
136 .ok();
137 }
138 })
139 .detach();
140
141 Self {
142 tool_output_buffer,
143 step: step.downgrade(),
144 editor,
145 }
146 }
147
148 fn render_result(&mut self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
149 let step = self.step.upgrade()?;
150 let result = step.read(cx).resolution.as_ref()?;
151 match result {
152 Ok(result) => {
153 Some(
154 v_flex()
155 .child(result.title.clone())
156 .children(result.suggestion_groups.iter().filter_map(
157 |(buffer, suggestion_groups)| {
158 let path = buffer.read(cx).file().map(|f| f.path());
159 v_flex()
160 .mb_2()
161 .border_b_1()
162 .children(path.map(|path| format!("path: {}", path.display())))
163 .children(suggestion_groups.iter().map(|group| {
164 v_flex().pl_2().children(group.suggestions.iter().map(
165 |suggestion| {
166 v_flex()
167 .children(
168 suggestion.description().map(|desc| {
169 format!("description: {desc}")
170 }),
171 )
172 .child(format!("kind: {}", suggestion.kind()))
173 .children(suggestion.symbol_path().map(
174 |path| format!("symbol path: {}", path.0),
175 ))
176 },
177 ))
178 }))
179 .into()
180 },
181 ))
182 .into_any_element(),
183 )
184 }
185 Err(error) => Some(format!("{:?}", error).into_any_element()),
186 }
187 }
188
189 fn step_updated(&mut self, step: Model<WorkflowStep>, cx: &mut ViewContext<Self>) {
190 self.tool_output_buffer.update(cx, |buffer, cx| {
191 let text = step.read(cx).tool_output.clone();
192 buffer.set_text(text, cx);
193 });
194 cx.notify();
195 }
196
197 fn step_released(&mut self, _: &mut WorkflowStep, 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}