step_view.rs

  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}