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}