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