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