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