step_view.rs

  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}