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