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