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