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