1use std::path::Path;
2use std::sync::Arc;
3use std::time::Duration;
4
5use anyhow::{Result, anyhow};
6use dap::StackFrameId;
7use gpui::{
8 AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, MouseButton, Stateful,
9 Subscription, Task, WeakEntity, list,
10};
11
12use language::PointUtf16;
13use project::debugger::breakpoint_store::ActiveStackFrame;
14use project::debugger::session::{Session, SessionEvent, StackFrame};
15use project::{ProjectItem, ProjectPath};
16use ui::{Scrollbar, ScrollbarState, Tooltip, prelude::*};
17use util::ResultExt;
18use workspace::{ItemHandle, Workspace};
19
20use crate::StackTraceView;
21
22use super::RunningState;
23
24#[derive(Debug)]
25pub enum StackFrameListEvent {
26 SelectedStackFrameChanged(StackFrameId),
27 BuiltEntries,
28}
29
30pub struct StackFrameList {
31 list: ListState,
32 focus_handle: FocusHandle,
33 _subscription: Subscription,
34 session: Entity<Session>,
35 state: WeakEntity<RunningState>,
36 entries: Vec<StackFrameEntry>,
37 workspace: WeakEntity<Workspace>,
38 selected_stack_frame_id: Option<StackFrameId>,
39 scrollbar_state: ScrollbarState,
40 _refresh_task: Task<()>,
41}
42
43#[allow(clippy::large_enum_variant)]
44#[derive(Debug, PartialEq, Eq)]
45pub enum StackFrameEntry {
46 Normal(dap::StackFrame),
47 Collapsed(Vec<dap::StackFrame>),
48}
49
50impl StackFrameList {
51 pub fn new(
52 workspace: WeakEntity<Workspace>,
53 session: Entity<Session>,
54 state: WeakEntity<RunningState>,
55 window: &mut Window,
56 cx: &mut Context<Self>,
57 ) -> Self {
58 let weak_entity = cx.weak_entity();
59 let focus_handle = cx.focus_handle();
60
61 let list = ListState::new(
62 0,
63 gpui::ListAlignment::Top,
64 px(1000.),
65 move |ix, _window, cx| {
66 weak_entity
67 .upgrade()
68 .map(|stack_frame_list| {
69 stack_frame_list.update(cx, |this, cx| this.render_entry(ix, cx))
70 })
71 .unwrap_or(div().into_any())
72 },
73 );
74
75 let _subscription =
76 cx.subscribe_in(&session, window, |this, _, event, window, cx| match event {
77 SessionEvent::Threads => {
78 this.schedule_refresh(false, window, cx);
79 }
80 SessionEvent::Stopped(..) | SessionEvent::StackTrace => {
81 this.schedule_refresh(true, window, cx);
82 }
83 _ => {}
84 });
85
86 let mut this = Self {
87 scrollbar_state: ScrollbarState::new(list.clone()),
88 list,
89 session,
90 workspace,
91 focus_handle,
92 state,
93 _subscription,
94 entries: Default::default(),
95 selected_stack_frame_id: None,
96 _refresh_task: Task::ready(()),
97 };
98 this.schedule_refresh(true, window, cx);
99 this
100 }
101
102 #[cfg(test)]
103 pub(crate) fn entries(&self) -> &Vec<StackFrameEntry> {
104 &self.entries
105 }
106
107 pub(crate) fn flatten_entries(&self, show_collapsed: bool) -> Vec<dap::StackFrame> {
108 self.entries
109 .iter()
110 .flat_map(|frame| match frame {
111 StackFrameEntry::Normal(frame) => vec![frame.clone()],
112 StackFrameEntry::Collapsed(frames) => {
113 if show_collapsed {
114 frames.clone()
115 } else {
116 vec![]
117 }
118 }
119 })
120 .collect::<Vec<_>>()
121 }
122
123 fn stack_frames(&self, cx: &mut App) -> Vec<StackFrame> {
124 self.state
125 .read_with(cx, |state, _| state.thread_id)
126 .log_err()
127 .flatten()
128 .map(|thread_id| {
129 self.session
130 .update(cx, |this, cx| this.stack_frames(thread_id, cx))
131 })
132 .unwrap_or_default()
133 }
134
135 #[cfg(test)]
136 pub(crate) fn dap_stack_frames(&self, cx: &mut App) -> Vec<dap::StackFrame> {
137 self.stack_frames(cx)
138 .into_iter()
139 .map(|stack_frame| stack_frame.dap.clone())
140 .collect()
141 }
142
143 pub fn selected_stack_frame_id(&self) -> Option<StackFrameId> {
144 self.selected_stack_frame_id
145 }
146
147 pub(crate) fn select_stack_frame_id(
148 &mut self,
149 id: StackFrameId,
150 window: &Window,
151 cx: &mut Context<Self>,
152 ) {
153 if !self.entries.iter().any(|entry| match entry {
154 StackFrameEntry::Normal(entry) => entry.id == id,
155 StackFrameEntry::Collapsed(stack_frames) => {
156 stack_frames.iter().any(|frame| frame.id == id)
157 }
158 }) {
159 return;
160 }
161
162 self.selected_stack_frame_id = Some(id);
163 self.go_to_selected_stack_frame(window, cx);
164 }
165
166 pub(super) fn schedule_refresh(
167 &mut self,
168 select_first: bool,
169 window: &mut Window,
170 cx: &mut Context<Self>,
171 ) {
172 const REFRESH_DEBOUNCE: Duration = Duration::from_millis(20);
173
174 self._refresh_task = cx.spawn_in(window, async move |this, cx| {
175 let debounce = this
176 .update(cx, |this, cx| {
177 let new_stack_frames = this.stack_frames(cx);
178 new_stack_frames.is_empty() && !this.entries.is_empty()
179 })
180 .ok()
181 .unwrap_or_default();
182
183 if debounce {
184 cx.background_executor().timer(REFRESH_DEBOUNCE).await;
185 }
186 this.update_in(cx, |this, window, cx| {
187 this.build_entries(select_first, window, cx);
188 cx.notify();
189 })
190 .ok();
191 })
192 }
193
194 pub fn build_entries(
195 &mut self,
196 select_first_stack_frame: bool,
197 window: &mut Window,
198 cx: &mut Context<Self>,
199 ) {
200 let mut entries = Vec::new();
201 let mut collapsed_entries = Vec::new();
202 let mut current_stack_frame = None;
203
204 let stack_frames = self.stack_frames(cx);
205 for stack_frame in &stack_frames {
206 match stack_frame.dap.presentation_hint {
207 Some(dap::StackFramePresentationHint::Deemphasize) => {
208 collapsed_entries.push(stack_frame.dap.clone());
209 }
210 _ => {
211 let collapsed_entries = std::mem::take(&mut collapsed_entries);
212 if !collapsed_entries.is_empty() {
213 entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
214 }
215
216 current_stack_frame.get_or_insert(&stack_frame.dap);
217 entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));
218 }
219 }
220 }
221
222 let collapsed_entries = std::mem::take(&mut collapsed_entries);
223 if !collapsed_entries.is_empty() {
224 entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
225 }
226
227 std::mem::swap(&mut self.entries, &mut entries);
228 self.list.reset(self.entries.len());
229
230 if let Some(current_stack_frame) = current_stack_frame.filter(|_| select_first_stack_frame)
231 {
232 self.select_stack_frame(current_stack_frame, true, window, cx)
233 .detach_and_log_err(cx);
234 }
235
236 cx.emit(StackFrameListEvent::BuiltEntries);
237 cx.notify();
238 }
239
240 pub fn go_to_selected_stack_frame(&mut self, window: &Window, cx: &mut Context<Self>) {
241 if let Some(selected_stack_frame_id) = self.selected_stack_frame_id {
242 let frame = self
243 .entries
244 .iter()
245 .find_map(|entry| match entry {
246 StackFrameEntry::Normal(dap) => {
247 if dap.id == selected_stack_frame_id {
248 Some(dap)
249 } else {
250 None
251 }
252 }
253 StackFrameEntry::Collapsed(daps) => {
254 daps.iter().find(|dap| dap.id == selected_stack_frame_id)
255 }
256 })
257 .cloned();
258
259 if let Some(frame) = frame.as_ref() {
260 self.select_stack_frame(frame, true, window, cx)
261 .detach_and_log_err(cx);
262 }
263 }
264 }
265
266 pub fn select_stack_frame(
267 &mut self,
268 stack_frame: &dap::StackFrame,
269 go_to_stack_frame: bool,
270 window: &Window,
271 cx: &mut Context<Self>,
272 ) -> Task<Result<()>> {
273 self.selected_stack_frame_id = Some(stack_frame.id);
274
275 cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
276 stack_frame.id,
277 ));
278 cx.notify();
279
280 if !go_to_stack_frame {
281 return Task::ready(Ok(()));
282 };
283
284 let row = (stack_frame.line.saturating_sub(1)) as u32;
285
286 let Some(abs_path) = Self::abs_path_from_stack_frame(&stack_frame) else {
287 return Task::ready(Err(anyhow!("Project path not found")));
288 };
289
290 let stack_frame_id = stack_frame.id;
291 cx.spawn_in(window, async move |this, cx| {
292 let (worktree, relative_path) = this
293 .update(cx, |this, cx| {
294 this.workspace.update(cx, |workspace, cx| {
295 workspace.project().update(cx, |this, cx| {
296 this.find_or_create_worktree(&abs_path, false, cx)
297 })
298 })
299 })??
300 .await?;
301 let buffer = this
302 .update(cx, |this, cx| {
303 this.workspace.update(cx, |this, cx| {
304 this.project().update(cx, |this, cx| {
305 let worktree_id = worktree.read(cx).id();
306 this.open_buffer(
307 ProjectPath {
308 worktree_id,
309 path: relative_path.into(),
310 },
311 cx,
312 )
313 })
314 })
315 })??
316 .await?;
317 let position = buffer.update(cx, |this, _| {
318 this.snapshot().anchor_after(PointUtf16::new(row, 0))
319 })?;
320 this.update_in(cx, |this, window, cx| {
321 this.workspace.update(cx, |workspace, cx| {
322 let project_path = buffer.read(cx).project_path(cx).ok_or_else(|| {
323 anyhow!("Could not select a stack frame for unnamed buffer")
324 })?;
325
326 let open_preview = !workspace
327 .item_of_type::<StackTraceView>(cx)
328 .map(|viewer| {
329 workspace
330 .active_item(cx)
331 .is_some_and(|item| item.item_id() == viewer.item_id())
332 })
333 .unwrap_or_default();
334
335 anyhow::Ok(workspace.open_path_preview(
336 project_path,
337 None,
338 true,
339 true,
340 open_preview,
341 window,
342 cx,
343 ))
344 })
345 })???
346 .await?;
347
348 this.update(cx, |this, cx| {
349 let Some(thread_id) = this.state.read_with(cx, |state, _| state.thread_id)? else {
350 return Err(anyhow!("No selected thread ID found"));
351 };
352
353 this.workspace.update(cx, |workspace, cx| {
354 let breakpoint_store = workspace.project().read(cx).breakpoint_store();
355
356 breakpoint_store.update(cx, |store, cx| {
357 store.set_active_position(
358 ActiveStackFrame {
359 session_id: this.session.read(cx).session_id(),
360 thread_id,
361 stack_frame_id,
362 path: abs_path,
363 position,
364 },
365 cx,
366 );
367 })
368 })
369 })?
370 })
371 }
372
373 pub(crate) fn abs_path_from_stack_frame(stack_frame: &dap::StackFrame) -> Option<Arc<Path>> {
374 stack_frame.source.as_ref().and_then(|s| {
375 s.path
376 .as_deref()
377 .map(|path| Arc::<Path>::from(Path::new(path)))
378 })
379 }
380
381 pub fn restart_stack_frame(&mut self, stack_frame_id: u64, cx: &mut Context<Self>) {
382 self.session.update(cx, |state, cx| {
383 state.restart_stack_frame(stack_frame_id, cx)
384 });
385 }
386
387 fn render_normal_entry(
388 &self,
389 stack_frame: &dap::StackFrame,
390 cx: &mut Context<Self>,
391 ) -> AnyElement {
392 let source = stack_frame.source.clone();
393 let is_selected_frame = Some(stack_frame.id) == self.selected_stack_frame_id;
394
395 let path = source.clone().and_then(|s| s.path.or(s.name));
396 let formatted_path = path.map(|path| format!("{}:{}", path, stack_frame.line,));
397 let formatted_path = formatted_path.map(|path| {
398 Label::new(path)
399 .size(LabelSize::XSmall)
400 .line_height_style(LineHeightStyle::UiLabel)
401 .truncate()
402 .color(Color::Muted)
403 });
404
405 let supports_frame_restart = self
406 .session
407 .read(cx)
408 .capabilities()
409 .supports_restart_frame
410 .unwrap_or_default();
411
412 let should_deemphasize = matches!(
413 stack_frame.presentation_hint,
414 Some(
415 dap::StackFramePresentationHint::Subtle
416 | dap::StackFramePresentationHint::Deemphasize
417 )
418 );
419 h_flex()
420 .rounded_md()
421 .justify_between()
422 .w_full()
423 .group("")
424 .id(("stack-frame", stack_frame.id))
425 .p_1()
426 .when(is_selected_frame, |this| {
427 this.bg(cx.theme().colors().element_hover)
428 })
429 .on_click(cx.listener({
430 let stack_frame = stack_frame.clone();
431 move |this, _, window, cx| {
432 this.select_stack_frame(&stack_frame, true, window, cx)
433 .detach_and_log_err(cx);
434 }
435 }))
436 .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
437 .child(
438 v_flex()
439 .gap_0p5()
440 .child(
441 Label::new(stack_frame.name.clone())
442 .size(LabelSize::Small)
443 .truncate()
444 .when(should_deemphasize, |this| this.color(Color::Muted)),
445 )
446 .children(formatted_path),
447 )
448 .when(
449 supports_frame_restart && stack_frame.can_restart.unwrap_or(true),
450 |this| {
451 this.child(
452 h_flex()
453 .id(("restart-stack-frame", stack_frame.id))
454 .visible_on_hover("")
455 .absolute()
456 .right_2()
457 .overflow_hidden()
458 .rounded_md()
459 .border_1()
460 .border_color(cx.theme().colors().element_selected)
461 .bg(cx.theme().colors().element_background)
462 .hover(|style| {
463 style
464 .bg(cx.theme().colors().ghost_element_hover)
465 .cursor_pointer()
466 })
467 .child(
468 IconButton::new(
469 ("restart-stack-frame", stack_frame.id),
470 IconName::DebugRestart,
471 )
472 .icon_size(IconSize::Small)
473 .on_click(cx.listener({
474 let stack_frame_id = stack_frame.id;
475 move |this, _, _window, cx| {
476 this.restart_stack_frame(stack_frame_id, cx);
477 }
478 }))
479 .tooltip(move |window, cx| {
480 Tooltip::text("Restart Stack Frame")(window, cx)
481 }),
482 ),
483 )
484 },
485 )
486 .into_any()
487 }
488
489 pub fn expand_collapsed_entry(
490 &mut self,
491 ix: usize,
492 stack_frames: &Vec<dap::StackFrame>,
493 cx: &mut Context<Self>,
494 ) {
495 self.entries.splice(
496 ix..ix + 1,
497 stack_frames
498 .iter()
499 .map(|frame| StackFrameEntry::Normal(frame.clone())),
500 );
501 self.list.reset(self.entries.len());
502 cx.notify();
503 }
504
505 fn render_collapsed_entry(
506 &self,
507 ix: usize,
508 stack_frames: &Vec<dap::StackFrame>,
509 cx: &mut Context<Self>,
510 ) -> AnyElement {
511 let first_stack_frame = &stack_frames[0];
512
513 h_flex()
514 .rounded_md()
515 .justify_between()
516 .w_full()
517 .group("")
518 .id(("stack-frame", first_stack_frame.id))
519 .p_1()
520 .on_click(cx.listener({
521 let stack_frames = stack_frames.clone();
522 move |this, _, _window, cx| {
523 this.expand_collapsed_entry(ix, &stack_frames, cx);
524 }
525 }))
526 .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
527 .child(
528 v_flex()
529 .text_ui_sm(cx)
530 .truncate()
531 .text_color(cx.theme().colors().text_muted)
532 .child(format!(
533 "Show {} more{}",
534 stack_frames.len(),
535 first_stack_frame
536 .source
537 .as_ref()
538 .and_then(|source| source.origin.as_ref())
539 .map_or(String::new(), |origin| format!(": {}", origin))
540 )),
541 )
542 .into_any()
543 }
544
545 fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
546 match &self.entries[ix] {
547 StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(stack_frame, cx),
548 StackFrameEntry::Collapsed(stack_frames) => {
549 self.render_collapsed_entry(ix, stack_frames, cx)
550 }
551 }
552 }
553
554 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
555 div()
556 .occlude()
557 .id("stack-frame-list-vertical-scrollbar")
558 .on_mouse_move(cx.listener(|_, _, _, cx| {
559 cx.notify();
560 cx.stop_propagation()
561 }))
562 .on_hover(|_, _, cx| {
563 cx.stop_propagation();
564 })
565 .on_any_mouse_down(|_, _, cx| {
566 cx.stop_propagation();
567 })
568 .on_mouse_up(
569 MouseButton::Left,
570 cx.listener(|_, _, _, cx| {
571 cx.stop_propagation();
572 }),
573 )
574 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
575 cx.notify();
576 }))
577 .h_full()
578 .absolute()
579 .right_1()
580 .top_1()
581 .bottom_0()
582 .w(px(12.))
583 .cursor_default()
584 .children(Scrollbar::vertical(self.scrollbar_state.clone()))
585 }
586}
587
588impl Render for StackFrameList {
589 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
590 div()
591 .track_focus(&self.focus_handle)
592 .size_full()
593 .p_1()
594 .child(list(self.list.clone()).size_full())
595 .child(self.render_vertical_scrollbar(cx))
596 }
597}
598
599impl Focusable for StackFrameList {
600 fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
601 self.focus_handle.clone()
602 }
603}
604
605impl EventEmitter<StackFrameListEvent> for StackFrameList {}