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, SelectionEffects, 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, SaveOptions},
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
59 .selections
60 .newest(&editor.display_snapshot(cx))
61 .head();
62
63 editor
64 .snapshot(window, cx)
65 .buffer_snapshot()
66 .excerpt_containing(position..position)
67 .map(|excerpt| excerpt.id())
68 });
69
70 if let Some(stack_frame_id) = excerpt_id
71 .and_then(|id| this.excerpt_for_frames.get(&id))
72 .filter(|id| Some(**id) != this.selected_stack_frame_id)
73 {
74 this.stack_frame_list.update(cx, |list, cx| {
75 list.go_to_stack_frame(*stack_frame_id, window, cx).detach();
76 });
77 }
78 }
79 })
80 .detach();
81
82 cx.subscribe_in(
83 &stack_frame_list,
84 window,
85 |this, stack_frame_list, event, window, cx| match event {
86 StackFrameListEvent::BuiltEntries => {
87 this.selected_stack_frame_id =
88 stack_frame_list.read(cx).opened_stack_frame_id();
89 this.update_excerpts(window, cx);
90 }
91 StackFrameListEvent::SelectedStackFrameChanged(selected_frame_id) => {
92 this.selected_stack_frame_id = Some(*selected_frame_id);
93 this.update_highlights(window, cx);
94
95 if let Some(frame_anchor) = this
96 .highlights
97 .iter()
98 .find(|(frame_id, _)| frame_id == selected_frame_id)
99 .map(|highlight| highlight.1)
100 {
101 this.editor.update(cx, |editor, cx| {
102 if frame_anchor.excerpt_id
103 != editor.selections.newest_anchor().head().excerpt_id
104 {
105 let effects = SelectionEffects::scroll(
106 Autoscroll::center().for_anchor(frame_anchor),
107 );
108
109 editor.change_selections(effects, window, cx, |selections| {
110 let selection_id = selections.new_selection_id();
111
112 let selection = Selection {
113 id: selection_id,
114 start: frame_anchor,
115 end: frame_anchor,
116 goal: SelectionGoal::None,
117 reversed: false,
118 };
119
120 selections.select_anchors(vec![selection]);
121 })
122 }
123 });
124 }
125 }
126 },
127 )
128 .detach();
129
130 let mut this = Self {
131 editor,
132 multibuffer,
133 workspace,
134 project,
135 excerpt_for_frames: HashMap::default(),
136 highlights: Vec::default(),
137 stack_frame_list,
138 selected_stack_frame_id: None,
139 refresh_task: None,
140 _subscription: None,
141 };
142
143 this.update_excerpts(window, cx);
144 this
145 }
146
147 fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
148 self.refresh_task.take();
149 self.editor.update(cx, |editor, cx| {
150 editor.clear_highlights::<DebugStackFrameLine>(cx)
151 });
152
153 let stack_frames = self
154 .stack_frame_list
155 .read_with(cx, |list, _| list.flatten_entries(false, false));
156
157 let frames_to_open: Vec<_> = stack_frames
158 .into_iter()
159 .filter_map(|frame| {
160 Some((
161 frame.id,
162 frame.line as u32 - 1,
163 StackFrameList::abs_path_from_stack_frame(&frame)?,
164 ))
165 })
166 .collect();
167
168 self.multibuffer
169 .update(cx, |multi_buffer, cx| multi_buffer.clear(cx));
170
171 let task = cx.spawn_in(window, async move |this, cx| {
172 let mut to_highlights = Vec::default();
173
174 for (stack_frame_id, line, abs_path) in frames_to_open {
175 let (worktree, relative_path) = this
176 .update(cx, |this, cx| {
177 this.workspace.update(cx, |workspace, cx| {
178 workspace.project().update(cx, |this, cx| {
179 this.find_or_create_worktree(&abs_path, false, cx)
180 })
181 })
182 })??
183 .await?;
184
185 let project_path = ProjectPath {
186 worktree_id: worktree.read_with(cx, |tree, _| tree.id())?,
187 path: relative_path,
188 };
189
190 if let Some(buffer) = this
191 .read_with(cx, |this, _| this.project.clone())?
192 .update(cx, |project, cx| project.open_buffer(project_path, cx))?
193 .await
194 .log_err()
195 {
196 this.update(cx, |this, cx| {
197 this.multibuffer.update(cx, |multi_buffer, cx| {
198 let line_point = Point::new(line, 0);
199 let start_context = Self::heuristic_syntactic_expand(
200 &buffer.read(cx).snapshot(),
201 line_point,
202 );
203
204 // Users will want to see what happened before an active debug line in most cases
205 let range = ExcerptRange {
206 context: start_context..Point::new(line.saturating_add(1), 0),
207 primary: line_point..line_point,
208 };
209 multi_buffer.push_excerpts(buffer.clone(), vec![range], cx);
210
211 let line_anchor =
212 multi_buffer.buffer_point_to_anchor(&buffer, line_point, cx);
213
214 if let Some(line_anchor) = line_anchor {
215 this.excerpt_for_frames
216 .insert(line_anchor.excerpt_id, stack_frame_id);
217 to_highlights.push((stack_frame_id, line_anchor));
218 }
219 });
220 })
221 .ok();
222 }
223 }
224
225 this.update_in(cx, |this, window, cx| {
226 this.highlights = to_highlights;
227 this.update_highlights(window, cx);
228 })
229 .ok();
230
231 anyhow::Ok(())
232 });
233
234 self.refresh_task = Some(task);
235 }
236
237 fn update_highlights(&mut self, window: &mut Window, cx: &mut Context<Self>) {
238 self.editor.update(cx, |editor, _| {
239 editor.clear_row_highlights::<DebugStackFrameLine>()
240 });
241
242 let stack_frames = self
243 .stack_frame_list
244 .read_with(cx, |session, _| session.flatten_entries(false, false));
245
246 let active_idx = self
247 .selected_stack_frame_id
248 .and_then(|id| {
249 stack_frames
250 .iter()
251 .enumerate()
252 .find_map(|(idx, frame)| if frame.id == id { Some(idx) } else { None })
253 })
254 .unwrap_or(0);
255
256 self.editor.update(cx, |editor, cx| {
257 let snapshot = editor.snapshot(window, cx).display_snapshot;
258 let first_color = cx.theme().colors().editor_debugger_active_line_background;
259
260 let color = first_color.opacity(0.5);
261
262 let mut is_first = true;
263
264 for (_, highlight) in self.highlights.iter().skip(active_idx) {
265 let position = highlight.to_point(&snapshot.buffer_snapshot());
266 let color = if is_first {
267 is_first = false;
268 first_color
269 } else {
270 color
271 };
272
273 let start = snapshot
274 .buffer_snapshot()
275 .clip_point(Point::new(position.row, 0), Bias::Left);
276 let end = start + Point::new(1, 0);
277 let start = snapshot.buffer_snapshot().anchor_before(start);
278 let end = snapshot.buffer_snapshot().anchor_before(end);
279 editor.highlight_rows::<DebugStackFrameLine>(
280 start..end,
281 color,
282 RowHighlightOptions::default(),
283 cx,
284 );
285 }
286 })
287 }
288
289 fn heuristic_syntactic_expand(snapshot: &BufferSnapshot, selected_point: Point) -> Point {
290 let mut text_objects = snapshot.text_object_ranges(
291 selected_point..selected_point,
292 TreeSitterOptions::max_start_depth(4),
293 );
294
295 let mut start_position = text_objects
296 .find(|(_, obj)| matches!(obj, language::TextObject::AroundFunction))
297 .map(|(range, _)| snapshot.offset_to_point(range.start))
298 .map(|point| Point::new(point.row.max(selected_point.row.saturating_sub(8)), 0))
299 .unwrap_or(selected_point);
300
301 if start_position.row == selected_point.row {
302 start_position.row = start_position.row.saturating_sub(1);
303 }
304
305 start_position
306 }
307}
308
309impl Render for StackTraceView {
310 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
311 div().size_full().child(self.editor.clone())
312 }
313}
314
315impl EventEmitter<EditorEvent> for StackTraceView {}
316impl Focusable for StackTraceView {
317 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
318 self.editor.focus_handle(cx)
319 }
320}
321
322impl Item for StackTraceView {
323 type Event = EditorEvent;
324
325 fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
326 Editor::to_item_events(event, f)
327 }
328
329 fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
330 self.editor
331 .update(cx, |editor, cx| editor.deactivated(window, cx));
332 }
333
334 fn navigate(
335 &mut self,
336 data: Box<dyn Any>,
337 window: &mut Window,
338 cx: &mut Context<Self>,
339 ) -> bool {
340 self.editor
341 .update(cx, |editor, cx| editor.navigate(data, window, cx))
342 }
343
344 fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
345 Some("Stack Frame Viewer".into())
346 }
347
348 fn tab_content_text(&self, _detail: usize, _: &App) -> SharedString {
349 "Stack Frames".into()
350 }
351
352 fn for_each_project_item(
353 &self,
354 cx: &App,
355 f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
356 ) {
357 self.editor.for_each_project_item(cx, f)
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 options: SaveOptions,
390 project: Entity<Project>,
391 window: &mut Window,
392 cx: &mut Context<Self>,
393 ) -> Task<Result<()>> {
394 self.editor.save(options, 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}