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};
 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    fn crease_content(
 48        context: &Model<Context>,
 49        range: StepRange,
 50        cx: &mut AppContext,
 51    ) -> Option<Arc<str>> {
 52        use std::fmt::Write;
 53        let step = context.read(cx).workflow_step_for_range(range)?;
 54        let mut output = String::from("\n\n");
 55        match &step.status {
 56            crate::WorkflowStepStatus::Resolved(ResolvedWorkflowStep { title, suggestions }) => {
 57                writeln!(output, "Resolution:").ok()?;
 58                writeln!(output, "  {title:?}").ok()?;
 59                if suggestions.is_empty() {
 60                    writeln!(output, "  No suggestions").ok()?;
 61                }
 62
 63                for (buffer, suggestion_groups) in suggestions {
 64                    let buffer = buffer.read(cx);
 65                    let buffer_path = buffer
 66                        .file()
 67                        .and_then(|file| file.path().to_str())
 68                        .unwrap_or("untitled");
 69                    let snapshot = buffer.text_snapshot();
 70                    writeln!(output, "  {buffer_path}:").ok()?;
 71                    for group in suggestion_groups {
 72                        for suggestion in &group.suggestions {
 73                            pretty_print_workflow_suggestion(&mut output, suggestion, &snapshot);
 74                        }
 75                    }
 76                }
 77            }
 78            crate::WorkflowStepStatus::Pending(_) => {
 79                writeln!(output, "Resolution: Pending").ok()?;
 80            }
 81            crate::WorkflowStepStatus::Error(error) => {
 82                writeln!(output, "Resolution: Error").ok()?;
 83                writeln!(output, "{error:?}").ok()?;
 84            }
 85        }
 86
 87        Some(output.into())
 88    }
 89    pub(crate) fn activate_for_step(&mut self, range: StepRange, cx: &mut WindowContext<'_>) {
 90        let text = Self::crease_content(&self.context, range.clone(), cx)
 91            .unwrap_or_else(|| Arc::from("Error fetching debug info"));
 92        self.editor.update(cx, |editor, cx| {
 93            let buffer = editor.buffer().read(cx).as_singleton()?;
 94            let snapshot = buffer.read(cx).text_snapshot();
 95            let start_offset = range.end.to_offset(&snapshot) + 1;
 96            let start_offset = snapshot.clip_offset(start_offset, Bias::Right);
 97            let text_len = text.len();
 98            buffer.update(cx, |this, cx| {
 99                this.edit([(start_offset..start_offset, text)], None, cx);
100            });
101
102            let end_offset = start_offset + text_len;
103            let multibuffer_snapshot = editor.buffer().read(cx).snapshot(cx);
104            let anchor_before = multibuffer_snapshot.anchor_after(start_offset);
105            let anchor_after = multibuffer_snapshot.anchor_before(end_offset);
106
107            let block_id = editor
108                .insert_blocks(
109                    [BlockProperties {
110                        position: anchor_after,
111                        height: 0,
112                        style: BlockStyle::Sticky,
113                        render: Box::new(move |cx| {
114                            div()
115                                .w_full()
116                                .px(cx.gutter_dimensions.full_width())
117                                .child(h_flex().h(px(1.)).bg(Color::Warning.color(cx)))
118                                .into_any()
119                        }),
120                        disposition: BlockDisposition::Below,
121                        priority: 0,
122                    }],
123                    None,
124                    cx,
125                )
126                .into_iter()
127                .next()?;
128            let info = DebugInfo {
129                range: anchor_before..anchor_after,
130                block_id,
131            };
132            self.active_debug_views.insert(range, info);
133            Some(())
134        });
135    }
136
137    fn deactivate_impl(editor: &mut Editor, debug_data: DebugInfo, cx: &mut ViewContext<Editor>) {
138        editor.remove_blocks(HashSet::from_iter([debug_data.block_id]), None, cx);
139        editor.edit([(debug_data.range, Arc::<str>::default())], cx)
140    }
141    pub(crate) fn deactivate_for(&mut self, range: &StepRange, cx: &mut WindowContext<'_>) -> bool {
142        if let Some(debug_data) = self.active_debug_views.remove(range) {
143            self.editor.update(cx, |this, cx| {
144                Self::deactivate_impl(this, debug_data, cx);
145            });
146            true
147        } else {
148            false
149        }
150    }
151
152    pub(crate) fn deactivate(&mut self, cx: &mut WindowContext<'_>) {
153        let steps_to_disable = std::mem::take(&mut self.active_debug_views);
154
155        self.editor.update(cx, move |editor, cx| {
156            for (_, debug_data) in steps_to_disable {
157                Self::deactivate_impl(editor, debug_data, cx);
158            }
159        });
160    }
161}
162fn pretty_print_anchor(
163    out: &mut String,
164    anchor: &language::Anchor,
165    snapshot: &text::BufferSnapshot,
166) {
167    use std::fmt::Write;
168    let point = anchor.to_point(snapshot);
169    write!(out, "{}:{}", point.row, point.column).ok();
170}
171fn pretty_print_range(
172    out: &mut String,
173    range: &Range<language::Anchor>,
174    snapshot: &text::BufferSnapshot,
175) {
176    use std::fmt::Write;
177    write!(out, "    Range: ").ok();
178    pretty_print_anchor(out, &range.start, snapshot);
179    write!(out, "..").ok();
180    pretty_print_anchor(out, &range.end, snapshot);
181}
182
183fn pretty_print_workflow_suggestion(
184    out: &mut String,
185    suggestion: &WorkflowSuggestion,
186    snapshot: &text::BufferSnapshot,
187) {
188    use std::fmt::Write;
189    let (range, description, position) = match suggestion {
190        WorkflowSuggestion::Update { range, description } => (Some(range), Some(description), None),
191        WorkflowSuggestion::CreateFile { description } => (None, Some(description), None),
192        WorkflowSuggestion::AppendChild {
193            position,
194            description,
195        }
196        | WorkflowSuggestion::InsertSiblingBefore {
197            position,
198            description,
199        }
200        | WorkflowSuggestion::InsertSiblingAfter {
201            position,
202            description,
203        }
204        | WorkflowSuggestion::PrependChild {
205            position,
206            description,
207        } => (None, Some(description), Some(position)),
208
209        WorkflowSuggestion::Delete { range } => (Some(range), None, None),
210    };
211    if let Some(description) = description {
212        writeln!(out, "    Description: {description}").ok();
213    }
214    if let Some(range) = range {
215        pretty_print_range(out, range, snapshot);
216    }
217    if let Some(position) = position {
218        write!(out, "    Position: ").ok();
219        pretty_print_anchor(out, position, snapshot);
220        write!(out, "\n").ok();
221    }
222    write!(out, "\n").ok();
223}