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 formatted_path = format!(
325 "{}:{}",
326 source.clone().and_then(|s| s.name).unwrap_or_default(),
327 stack_frame.line,
328 );
329
330 let supports_frame_restart = self
331 .session
332 .read(cx)
333 .capabilities()
334 .supports_restart_frame
335 .unwrap_or_default();
336
337 let origin = stack_frame
338 .source
339 .to_owned()
340 .and_then(|source| source.origin);
341
342 h_flex()
343 .rounded_md()
344 .justify_between()
345 .w_full()
346 .group("")
347 .id(("stack-frame", stack_frame.id))
348 .tooltip({
349 let formatted_path = formatted_path.clone();
350 move |_window, app| {
351 app.new(|_| {
352 let mut tooltip = Tooltip::new(formatted_path.clone());
353
354 if let Some(origin) = &origin {
355 tooltip = tooltip.meta(origin);
356 }
357
358 tooltip
359 })
360 .into()
361 }
362 })
363 .p_1()
364 .when(is_selected_frame, |this| {
365 this.bg(cx.theme().colors().element_hover)
366 })
367 .on_click(cx.listener({
368 let stack_frame = stack_frame.clone();
369 move |this, _, window, cx| {
370 this.select_stack_frame(&stack_frame, true, window, cx)
371 .detach_and_log_err(cx);
372 }
373 }))
374 .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
375 .child(
376 v_flex()
377 .child(
378 h_flex()
379 .gap_0p5()
380 .text_ui_sm(cx)
381 .truncate()
382 .child(stack_frame.name.clone())
383 .child(formatted_path),
384 )
385 .child(
386 h_flex()
387 .text_ui_xs(cx)
388 .truncate()
389 .text_color(cx.theme().colors().text_muted)
390 .when_some(source.and_then(|s| s.path), |this, path| this.child(path)),
391 ),
392 )
393 .when(
394 supports_frame_restart && stack_frame.can_restart.unwrap_or(true),
395 |this| {
396 this.child(
397 h_flex()
398 .id(("restart-stack-frame", stack_frame.id))
399 .visible_on_hover("")
400 .absolute()
401 .right_2()
402 .overflow_hidden()
403 .rounded_md()
404 .border_1()
405 .border_color(cx.theme().colors().element_selected)
406 .bg(cx.theme().colors().element_background)
407 .hover(|style| {
408 style
409 .bg(cx.theme().colors().ghost_element_hover)
410 .cursor_pointer()
411 })
412 .child(
413 IconButton::new(
414 ("restart-stack-frame", stack_frame.id),
415 IconName::DebugRestart,
416 )
417 .icon_size(IconSize::Small)
418 .on_click(cx.listener({
419 let stack_frame_id = stack_frame.id;
420 move |this, _, _window, cx| {
421 this.restart_stack_frame(stack_frame_id, cx);
422 }
423 }))
424 .tooltip(move |window, cx| {
425 Tooltip::text("Restart Stack Frame")(window, cx)
426 }),
427 ),
428 )
429 },
430 )
431 .into_any()
432 }
433
434 pub fn expand_collapsed_entry(
435 &mut self,
436 ix: usize,
437 stack_frames: &Vec<dap::StackFrame>,
438 cx: &mut Context<Self>,
439 ) {
440 self.entries.splice(
441 ix..ix + 1,
442 stack_frames
443 .iter()
444 .map(|frame| StackFrameEntry::Normal(frame.clone())),
445 );
446 self.list.reset(self.entries.len());
447 cx.notify();
448 }
449
450 fn render_collapsed_entry(
451 &self,
452 ix: usize,
453 stack_frames: &Vec<dap::StackFrame>,
454 cx: &mut Context<Self>,
455 ) -> AnyElement {
456 let first_stack_frame = &stack_frames[0];
457
458 h_flex()
459 .rounded_md()
460 .justify_between()
461 .w_full()
462 .group("")
463 .id(("stack-frame", first_stack_frame.id))
464 .p_1()
465 .on_click(cx.listener({
466 let stack_frames = stack_frames.clone();
467 move |this, _, _window, cx| {
468 this.expand_collapsed_entry(ix, &stack_frames, cx);
469 }
470 }))
471 .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
472 .child(
473 v_flex()
474 .text_ui_sm(cx)
475 .truncate()
476 .text_color(cx.theme().colors().text_muted)
477 .child(format!(
478 "Show {} more{}",
479 stack_frames.len(),
480 first_stack_frame
481 .source
482 .as_ref()
483 .and_then(|source| source.origin.as_ref())
484 .map_or(String::new(), |origin| format!(": {}", origin))
485 )),
486 )
487 .into_any()
488 }
489
490 fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
491 match &self.entries[ix] {
492 StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(stack_frame, cx),
493 StackFrameEntry::Collapsed(stack_frames) => {
494 self.render_collapsed_entry(ix, stack_frames, cx)
495 }
496 }
497 }
498
499 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
500 div()
501 .occlude()
502 .id("stack-frame-list-vertical-scrollbar")
503 .on_mouse_move(cx.listener(|_, _, _, cx| {
504 cx.notify();
505 cx.stop_propagation()
506 }))
507 .on_hover(|_, _, cx| {
508 cx.stop_propagation();
509 })
510 .on_any_mouse_down(|_, _, cx| {
511 cx.stop_propagation();
512 })
513 .on_mouse_up(
514 MouseButton::Left,
515 cx.listener(|_, _, _, cx| {
516 cx.stop_propagation();
517 }),
518 )
519 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
520 cx.notify();
521 }))
522 .h_full()
523 .absolute()
524 .right_1()
525 .top_1()
526 .bottom_0()
527 .w(px(12.))
528 .cursor_default()
529 .children(Scrollbar::vertical(self.scrollbar_state.clone()))
530 }
531}
532
533impl Render for StackFrameList {
534 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
535 if self.invalidate {
536 self.build_entries(self.entries.is_empty(), window, cx);
537 self.invalidate = false;
538 cx.notify();
539 }
540
541 div()
542 .size_full()
543 .p_1()
544 .child(list(self.list.clone()).size_full())
545 .child(self.render_vertical_scrollbar(cx))
546 }
547}
548
549impl Focusable for StackFrameList {
550 fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
551 self.focus_handle.clone()
552 }
553}
554
555impl EventEmitter<StackFrameListEvent> for StackFrameList {}