context_inspector.rs

  1use std::{ops::Range, sync::Arc};
  2
  3use collections::{HashMap, HashSet};
  4use editor::{
  5    display_map::{BlockDisposition, BlockProperties, BlockStyle, CustomBlockId},
  6    Editor,
  7};
  8use gpui::{AppContext, Model, View};
  9use text::{Bias, ToOffset, ToPoint};
 10use ui::{
 11    div, h_flex, px, Color, Element as _, ParentElement as _, Styled, ViewContext, WindowContext,
 12};
 13
 14use crate::{Context, ResolvedWorkflowStep, WorkflowSuggestion, WorkflowSuggestionKind};
 15
 16type StepRange = Range<language::Anchor>;
 17
 18struct DebugInfo {
 19    range: Range<editor::Anchor>,
 20    block_id: CustomBlockId,
 21}
 22
 23pub(crate) struct ContextInspector {
 24    active_debug_views: HashMap<Range<language::Anchor>, DebugInfo>,
 25    context: Model<Context>,
 26    editor: View<Editor>,
 27}
 28
 29impl ContextInspector {
 30    pub(crate) fn new(editor: View<Editor>, context: Model<Context>) -> Self {
 31        Self {
 32            editor,
 33            context,
 34            active_debug_views: Default::default(),
 35        }
 36    }
 37
 38    pub(crate) fn is_active(&self, range: &StepRange) -> bool {
 39        self.active_debug_views.contains_key(range)
 40    }
 41
 42    pub(crate) fn refresh(&mut self, range: &StepRange, cx: &mut WindowContext<'_>) {
 43        if self.deactivate_for(range, cx) {
 44            self.activate_for_step(range.clone(), cx);
 45        }
 46    }
 47
 48    fn crease_content(
 49        context: &Model<Context>,
 50        range: StepRange,
 51        cx: &mut AppContext,
 52    ) -> Option<Arc<str>> {
 53        use std::fmt::Write;
 54        let step = context.read(cx).workflow_step_for_range(range)?;
 55        let mut output = String::from("\n\n");
 56        match &step.resolution.read(cx).result {
 57            Some(Ok(ResolvedWorkflowStep {
 58                title,
 59                suggestion_groups: suggestions,
 60            })) => {
 61                writeln!(output, "Resolution:").ok()?;
 62                writeln!(output, "  {title:?}").ok()?;
 63                if suggestions.is_empty() {
 64                    writeln!(output, "  No suggestions").ok()?;
 65                }
 66
 67                for (buffer, suggestion_groups) in suggestions {
 68                    let buffer = buffer.read(cx);
 69                    let buffer_path = buffer
 70                        .file()
 71                        .and_then(|file| file.path().to_str())
 72                        .unwrap_or("untitled");
 73                    let snapshot = buffer.text_snapshot();
 74                    writeln!(output, "Path: {buffer_path}:").ok()?;
 75                    for group in suggestion_groups {
 76                        for suggestion in &group.suggestions {
 77                            pretty_print_workflow_suggestion(&mut output, suggestion, &snapshot);
 78                        }
 79                    }
 80                }
 81            }
 82            Some(Err(error)) => {
 83                writeln!(output, "Resolution: Error").ok()?;
 84                writeln!(output, "{error:?}").ok()?;
 85            }
 86            None => {
 87                writeln!(output, "Resolution: Pending").ok()?;
 88            }
 89        }
 90
 91        Some(output.into())
 92    }
 93
 94    pub(crate) fn activate_for_step(&mut self, range: StepRange, cx: &mut WindowContext<'_>) {
 95        let text = Self::crease_content(&self.context, range.clone(), cx)
 96            .unwrap_or_else(|| Arc::from("Error fetching debug info"));
 97        self.editor.update(cx, |editor, cx| {
 98            let buffer = editor.buffer().read(cx).as_singleton()?;
 99            let snapshot = buffer.read(cx).text_snapshot();
100            let start_offset = range.end.to_offset(&snapshot) + 1;
101            let start_offset = snapshot.clip_offset(start_offset, Bias::Right);
102            let text_len = text.len();
103            buffer.update(cx, |this, cx| {
104                this.edit([(start_offset..start_offset, text)], None, cx);
105            });
106
107            let end_offset = start_offset + text_len;
108            let multibuffer_snapshot = editor.buffer().read(cx).snapshot(cx);
109            let anchor_before = multibuffer_snapshot.anchor_after(start_offset);
110            let anchor_after = multibuffer_snapshot.anchor_before(end_offset);
111
112            let block_id = editor
113                .insert_blocks(
114                    [BlockProperties {
115                        position: anchor_after,
116                        height: 0,
117                        style: BlockStyle::Sticky,
118                        render: Box::new(move |cx| {
119                            div()
120                                .w_full()
121                                .px(cx.gutter_dimensions.full_width())
122                                .child(h_flex().h(px(1.)).bg(Color::Warning.color(cx)))
123                                .into_any()
124                        }),
125                        disposition: BlockDisposition::Below,
126                        priority: 0,
127                    }],
128                    None,
129                    cx,
130                )
131                .into_iter()
132                .next()?;
133            let info = DebugInfo {
134                range: anchor_before..anchor_after,
135                block_id,
136            };
137            self.active_debug_views.insert(range, info);
138            Some(())
139        });
140    }
141
142    fn deactivate_impl(editor: &mut Editor, debug_data: DebugInfo, cx: &mut ViewContext<Editor>) {
143        editor.remove_blocks(HashSet::from_iter([debug_data.block_id]), None, cx);
144        editor.edit([(debug_data.range, Arc::<str>::default())], cx)
145    }
146    pub(crate) fn deactivate_for(&mut self, range: &StepRange, cx: &mut WindowContext<'_>) -> bool {
147        if let Some(debug_data) = self.active_debug_views.remove(range) {
148            self.editor.update(cx, |this, cx| {
149                Self::deactivate_impl(this, debug_data, cx);
150            });
151            true
152        } else {
153            false
154        }
155    }
156
157    pub(crate) fn deactivate(&mut self, cx: &mut WindowContext<'_>) {
158        let steps_to_disable = std::mem::take(&mut self.active_debug_views);
159
160        self.editor.update(cx, move |editor, cx| {
161            for (_, debug_data) in steps_to_disable {
162                Self::deactivate_impl(editor, debug_data, cx);
163            }
164        });
165    }
166}
167fn pretty_print_anchor(
168    out: &mut String,
169    anchor: language::Anchor,
170    snapshot: &text::BufferSnapshot,
171) {
172    use std::fmt::Write;
173    let point = anchor.to_point(snapshot);
174    write!(out, "{}:{}", point.row, point.column).ok();
175}
176fn pretty_print_range(
177    out: &mut String,
178    range: &Range<language::Anchor>,
179    snapshot: &text::BufferSnapshot,
180) {
181    use std::fmt::Write;
182    write!(out, "    Range: ").ok();
183    pretty_print_anchor(out, range.start, snapshot);
184    write!(out, "..").ok();
185    pretty_print_anchor(out, range.end, snapshot);
186}
187
188fn pretty_print_workflow_suggestion(
189    out: &mut String,
190    suggestion: &WorkflowSuggestion,
191    snapshot: &text::BufferSnapshot,
192) {
193    use std::fmt::Write;
194    let (position, description, range) = match &suggestion.kind {
195        WorkflowSuggestionKind::Update {
196            range, description, ..
197        } => (None, Some(description), Some(range)),
198        WorkflowSuggestionKind::CreateFile { description } => (None, Some(description), None),
199        WorkflowSuggestionKind::AppendChild {
200            position,
201            description,
202            ..
203        } => (Some(position), Some(description), None),
204        WorkflowSuggestionKind::InsertSiblingBefore {
205            position,
206            description,
207            ..
208        } => (Some(position), Some(description), None),
209        WorkflowSuggestionKind::InsertSiblingAfter {
210            position,
211            description,
212            ..
213        } => (Some(position), Some(description), None),
214        WorkflowSuggestionKind::PrependChild {
215            position,
216            description,
217            ..
218        } => (Some(position), Some(description), None),
219        WorkflowSuggestionKind::Delete { range, .. } => (None, None, Some(range)),
220    };
221    writeln!(out, "    Tool input: {}", suggestion.tool_input).ok();
222    writeln!(
223        out,
224        "    Tool output: {}",
225        serde_json::to_string_pretty(&suggestion.tool_output)
226            .expect("Should not fail on valid struct serialization")
227    )
228    .ok();
229    if let Some(description) = description {
230        writeln!(out, "    Description: {description}").ok();
231    }
232    if let Some(range) = range {
233        pretty_print_range(out, &range, snapshot);
234    }
235    if let Some(position) = position {
236        write!(out, "    Position: ").ok();
237        pretty_print_anchor(out, *position, snapshot);
238        write!(out, "\n").ok();
239    }
240    write!(out, "\n").ok();
241}