1use std::any::{Any, TypeId};
2
3use collections::HashMap;
4use dap::StackFrameId;
5use editor::{
6 Anchor, Bias, DebugStackFrameLine, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer,
7 RowHighlightOptions, ToPoint, scroll::Autoscroll,
8};
9use gpui::{
10 AnyView, App, AppContext, Entity, EventEmitter, Focusable, IntoElement, Render, SharedString,
11 Subscription, Task, WeakEntity, Window,
12};
13use language::{BufferSnapshot, Capability, Point, Selection, SelectionGoal, TreeSitterOptions};
14use project::{Project, ProjectPath};
15use ui::{ActiveTheme as _, Context, ParentElement as _, Styled as _, div};
16use util::ResultExt as _;
17use workspace::{
18 Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace,
19 item::{BreadcrumbText, ItemEvent},
20 searchable::SearchableItemHandle,
21};
22
23use crate::session::running::stack_frame_list::{StackFrameList, StackFrameListEvent};
24use anyhow::Result;
25
26pub(crate) struct StackTraceView {
27 editor: Entity<Editor>,
28 multibuffer: Entity<MultiBuffer>,
29 workspace: WeakEntity<Workspace>,
30 project: Entity<Project>,
31 stack_frame_list: Entity<StackFrameList>,
32 selected_stack_frame_id: Option<StackFrameId>,
33 highlights: Vec<(StackFrameId, Anchor)>,
34 excerpt_for_frames: collections::HashMap<ExcerptId, StackFrameId>,
35 refresh_task: Option<Task<Result<()>>>,
36 _subscription: Option<Subscription>,
37}
38
39impl StackTraceView {
40 pub(crate) fn new(
41 workspace: WeakEntity<Workspace>,
42 project: Entity<Project>,
43 stack_frame_list: Entity<StackFrameList>,
44 window: &mut Window,
45 cx: &mut Context<Self>,
46 ) -> Self {
47 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
48 let editor = cx.new(|cx| {
49 let mut editor =
50 Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
51 editor.set_vertical_scroll_margin(5, cx);
52 editor
53 });
54
55 cx.subscribe_in(&editor, window, |this, editor, event, window, cx| {
56 if let EditorEvent::SelectionsChanged { local: true } = event {
57 let excerpt_id = editor.update(cx, |editor, cx| {
58 let position: Point = editor.selections.newest(cx).head();
59
60 editor
61 .snapshot(window, cx)
62 .buffer_snapshot
63 .excerpt_containing(position..position)
64 .map(|excerpt| excerpt.id())
65 });
66
67 if let Some(stack_frame_id) = excerpt_id
68 .and_then(|id| this.excerpt_for_frames.get(&id))
69 .filter(|id| Some(**id) != this.selected_stack_frame_id)
70 {
71 this.stack_frame_list.update(cx, |list, cx| {
72 list.go_to_stack_frame(*stack_frame_id, window, cx).detach();
73 });
74 }
75 }
76 })
77 .detach();
78
79 cx.subscribe_in(
80 &stack_frame_list,
81 window,
82 |this, stack_frame_list, event, window, cx| match event {
83 StackFrameListEvent::BuiltEntries => {
84 this.selected_stack_frame_id =
85 stack_frame_list.read(cx).opened_stack_frame_id();
86 this.update_excerpts(window, cx);
87 }
88 StackFrameListEvent::SelectedStackFrameChanged(selected_frame_id) => {
89 this.selected_stack_frame_id = Some(*selected_frame_id);
90 this.update_highlights(window, cx);
91
92 if let Some(frame_anchor) = this
93 .highlights
94 .iter()
95 .find(|(frame_id, _)| frame_id == selected_frame_id)
96 .map(|highlight| highlight.1)
97 {
98 this.editor.update(cx, |editor, cx| {
99 if frame_anchor.excerpt_id
100 != editor.selections.newest_anchor().head().excerpt_id
101 {
102 let auto_scroll =
103 Some(Autoscroll::center().for_anchor(frame_anchor));
104
105 editor.change_selections(auto_scroll, window, cx, |selections| {
106 let selection_id = selections.new_selection_id();
107
108 let selection = Selection {
109 id: selection_id,
110 start: frame_anchor,
111 end: frame_anchor,
112 goal: SelectionGoal::None,
113 reversed: false,
114 };
115
116 selections.select_anchors(vec![selection]);
117 })
118 }
119 });
120 }
121 }
122 },
123 )
124 .detach();
125
126 let mut this = Self {
127 editor,
128 multibuffer,
129 workspace,
130 project,
131 excerpt_for_frames: HashMap::default(),
132 highlights: Vec::default(),
133 stack_frame_list,
134 selected_stack_frame_id: None,
135 refresh_task: None,
136 _subscription: None,
137 };
138
139 this.update_excerpts(window, cx);
140 this
141 }
142
143 fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
144 self.refresh_task.take();
145 self.editor.update(cx, |editor, cx| {
146 editor.clear_highlights::<DebugStackFrameLine>(cx)
147 });
148
149 let stack_frames = self
150 .stack_frame_list
151 .read_with(cx, |list, _| list.flatten_entries(false));
152
153 let frames_to_open: Vec<_> = stack_frames
154 .into_iter()
155 .filter_map(|frame| {
156 Some((
157 frame.id,
158 frame.line as u32 - 1,
159 StackFrameList::abs_path_from_stack_frame(&frame)?,
160 ))
161 })
162 .collect();
163
164 self.multibuffer
165 .update(cx, |multi_buffer, cx| multi_buffer.clear(cx));
166
167 let task = cx.spawn_in(window, async move |this, cx| {
168 let mut to_highlights = Vec::default();
169
170 for (stack_frame_id, line, abs_path) in frames_to_open {
171 let (worktree, relative_path) = this
172 .update(cx, |this, cx| {
173 this.workspace.update(cx, |workspace, cx| {
174 workspace.project().update(cx, |this, cx| {
175 this.find_or_create_worktree(&abs_path, false, cx)
176 })
177 })
178 })??
179 .await?;
180
181 let project_path = ProjectPath {
182 worktree_id: worktree.read_with(cx, |tree, _| tree.id())?,
183 path: relative_path.into(),
184 };
185
186 if let Some(buffer) = this
187 .read_with(cx, |this, _| this.project.clone())?
188 .update(cx, |project, cx| project.open_buffer(project_path, cx))?
189 .await
190 .log_err()
191 {
192 this.update(cx, |this, cx| {
193 this.multibuffer.update(cx, |multi_buffer, cx| {
194 let line_point = Point::new(line, 0);
195 let start_context = Self::heuristic_syntactic_expand(
196 &buffer.read(cx).snapshot(),
197 line_point,
198 );
199
200 // Users will want to see what happened before an active debug line in most cases
201 let range = ExcerptRange {
202 context: start_context..Point::new(line.saturating_add(1), 0),
203 primary: line_point..line_point,
204 };
205 multi_buffer.push_excerpts(buffer.clone(), vec![range], cx);
206
207 let line_anchor =
208 multi_buffer.buffer_point_to_anchor(&buffer, line_point, cx);
209
210 if let Some(line_anchor) = line_anchor {
211 this.excerpt_for_frames
212 .insert(line_anchor.excerpt_id, stack_frame_id);
213 to_highlights.push((stack_frame_id, line_anchor));
214 }
215 });
216 })
217 .ok();
218 }
219 }
220
221 this.update_in(cx, |this, window, cx| {
222 this.highlights = to_highlights;
223 this.update_highlights(window, cx);
224 })
225 .ok();
226
227 anyhow::Ok(())
228 });
229
230 self.refresh_task = Some(task);
231 }
232
233 fn update_highlights(&mut self, window: &mut Window, cx: &mut Context<Self>) {
234 self.editor.update(cx, |editor, _| {
235 editor.clear_row_highlights::<DebugStackFrameLine>()
236 });
237
238 let stack_frames = self
239 .stack_frame_list
240 .read_with(cx, |session, _| session.flatten_entries(false));
241
242 let active_idx = self
243 .selected_stack_frame_id
244 .and_then(|id| {
245 stack_frames
246 .iter()
247 .enumerate()
248 .find_map(|(idx, frame)| if frame.id == id { Some(idx) } else { None })
249 })
250 .unwrap_or(0);
251
252 self.editor.update(cx, |editor, cx| {
253 let snapshot = editor.snapshot(window, cx).display_snapshot;
254 let first_color = cx.theme().colors().editor_debugger_active_line_background;
255
256 let color = first_color.opacity(0.5);
257
258 let mut is_first = true;
259
260 for (_, highlight) in self.highlights.iter().skip(active_idx) {
261 let position = highlight.to_point(&snapshot.buffer_snapshot);
262 let color = if is_first {
263 is_first = false;
264 first_color
265 } else {
266 color
267 };
268
269 let start = snapshot
270 .buffer_snapshot
271 .clip_point(Point::new(position.row, 0), Bias::Left);
272 let end = start + Point::new(1, 0);
273 let start = snapshot.buffer_snapshot.anchor_before(start);
274 let end = snapshot.buffer_snapshot.anchor_before(end);
275 editor.highlight_rows::<DebugStackFrameLine>(
276 start..end,
277 color,
278 RowHighlightOptions::default(),
279 cx,
280 );
281 }
282 })
283 }
284
285 fn heuristic_syntactic_expand(snapshot: &BufferSnapshot, selected_point: Point) -> Point {
286 let mut text_objects = snapshot.text_object_ranges(
287 selected_point..selected_point,
288 TreeSitterOptions::max_start_depth(4),
289 );
290
291 let mut start_position = text_objects
292 .find(|(_, obj)| matches!(obj, language::TextObject::AroundFunction))
293 .map(|(range, _)| snapshot.offset_to_point(range.start))
294 .map(|point| Point::new(point.row.max(selected_point.row.saturating_sub(8)), 0))
295 .unwrap_or(selected_point);
296
297 if start_position.row == selected_point.row {
298 start_position.row = start_position.row.saturating_sub(1);
299 }
300
301 start_position
302 }
303}
304
305impl Render for StackTraceView {
306 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
307 div().size_full().child(self.editor.clone())
308 }
309}
310
311impl EventEmitter<EditorEvent> for StackTraceView {}
312impl Focusable for StackTraceView {
313 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
314 self.editor.focus_handle(cx)
315 }
316}
317
318impl Item for StackTraceView {
319 type Event = EditorEvent;
320
321 fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
322 Editor::to_item_events(event, f)
323 }
324
325 fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
326 self.editor
327 .update(cx, |editor, cx| editor.deactivated(window, cx));
328 }
329
330 fn navigate(
331 &mut self,
332 data: Box<dyn Any>,
333 window: &mut Window,
334 cx: &mut Context<Self>,
335 ) -> bool {
336 self.editor
337 .update(cx, |editor, cx| editor.navigate(data, window, cx))
338 }
339
340 fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
341 Some("Stack Frame Viewer".into())
342 }
343
344 fn tab_content_text(&self, _detail: usize, _: &App) -> SharedString {
345 "Stack Frames".into()
346 }
347
348 fn for_each_project_item(
349 &self,
350 cx: &App,
351 f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
352 ) {
353 self.editor.for_each_project_item(cx, f)
354 }
355
356 fn is_singleton(&self, _: &App) -> bool {
357 false
358 }
359
360 fn set_nav_history(
361 &mut self,
362 nav_history: ItemNavHistory,
363 _: &mut Window,
364 cx: &mut Context<Self>,
365 ) {
366 self.editor.update(cx, |editor, _| {
367 editor.set_nav_history(Some(nav_history));
368 });
369 }
370
371 fn is_dirty(&self, cx: &App) -> bool {
372 self.multibuffer.read(cx).is_dirty(cx)
373 }
374
375 fn has_deleted_file(&self, cx: &App) -> bool {
376 self.multibuffer.read(cx).has_deleted_file(cx)
377 }
378
379 fn has_conflict(&self, cx: &App) -> bool {
380 self.multibuffer.read(cx).has_conflict(cx)
381 }
382
383 fn can_save(&self, _: &App) -> bool {
384 true
385 }
386
387 fn save(
388 &mut self,
389 format: bool,
390 project: Entity<Project>,
391 window: &mut Window,
392 cx: &mut Context<Self>,
393 ) -> Task<Result<()>> {
394 self.editor.save(format, project, window, cx)
395 }
396
397 fn save_as(
398 &mut self,
399 _: Entity<Project>,
400 _: ProjectPath,
401 _window: &mut Window,
402 _: &mut Context<Self>,
403 ) -> Task<Result<()>> {
404 unreachable!()
405 }
406
407 fn reload(
408 &mut self,
409 project: Entity<Project>,
410 window: &mut Window,
411 cx: &mut Context<Self>,
412 ) -> Task<Result<()>> {
413 self.editor.reload(project, window, cx)
414 }
415
416 fn act_as_type<'a>(
417 &'a self,
418 type_id: TypeId,
419 self_handle: &'a Entity<Self>,
420 _: &'a App,
421 ) -> Option<AnyView> {
422 if type_id == TypeId::of::<Self>() {
423 Some(self_handle.to_any())
424 } else if type_id == TypeId::of::<Editor>() {
425 Some(self.editor.to_any())
426 } else {
427 None
428 }
429 }
430
431 fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
432 Some(Box::new(self.editor.clone()))
433 }
434
435 fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
436 ToolbarItemLocation::PrimaryLeft
437 }
438
439 fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
440 self.editor.breadcrumbs(theme, cx)
441 }
442
443 fn added_to_workspace(
444 &mut self,
445 workspace: &mut Workspace,
446 window: &mut Window,
447 cx: &mut Context<Self>,
448 ) {
449 self.editor.update(cx, |editor, cx| {
450 editor.added_to_workspace(workspace, window, cx)
451 });
452 }
453}