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}