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.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}