1use std::path::Path;
2use std::sync::Arc;
3use std::time::Duration;
4
5use anyhow::{Context as _, Result, anyhow};
6use dap::StackFrameId;
7use dap::adapters::DebugAdapterName;
8use db::kvp::KEY_VALUE_STORE;
9use gpui::{
10 Action, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState,
11 Subscription, Task, WeakEntity, list,
12};
13use util::{
14 debug_panic,
15 paths::{PathStyle, is_absolute},
16};
17
18use crate::{StackTraceView, ToggleUserFrames};
19use language::PointUtf16;
20use project::debugger::breakpoint_store::ActiveStackFrame;
21use project::debugger::session::{Session, SessionEvent, StackFrame, ThreadStatus};
22use project::{ProjectItem, ProjectPath};
23use ui::{Tooltip, WithScrollbar, prelude::*};
24use workspace::{ItemHandle, Workspace, WorkspaceId};
25
26use super::RunningState;
27
28#[derive(Debug)]
29pub enum StackFrameListEvent {
30 SelectedStackFrameChanged(StackFrameId),
31 BuiltEntries,
32}
33
34/// Represents the filter applied to the stack frame list
35#[derive(PartialEq, Eq, Copy, Clone, Debug)]
36pub(crate) enum StackFrameFilter {
37 /// Show all frames
38 All,
39 /// Show only frames from the user's code
40 OnlyUserFrames,
41}
42
43impl StackFrameFilter {
44 fn from_str_or_default(s: impl AsRef<str>) -> Self {
45 match s.as_ref() {
46 "user" => StackFrameFilter::OnlyUserFrames,
47 "all" => StackFrameFilter::All,
48 _ => StackFrameFilter::All,
49 }
50 }
51}
52
53impl From<StackFrameFilter> for String {
54 fn from(filter: StackFrameFilter) -> Self {
55 match filter {
56 StackFrameFilter::All => "all".to_string(),
57 StackFrameFilter::OnlyUserFrames => "user".to_string(),
58 }
59 }
60}
61
62pub(crate) fn stack_frame_filter_key(
63 adapter_name: &DebugAdapterName,
64 workspace_id: WorkspaceId,
65) -> String {
66 let database_id: i64 = workspace_id.into();
67 format!("stack-frame-list-filter-{}-{}", adapter_name.0, database_id)
68}
69
70pub struct StackFrameList {
71 focus_handle: FocusHandle,
72 _subscription: Subscription,
73 session: Entity<Session>,
74 state: WeakEntity<RunningState>,
75 entries: Vec<StackFrameEntry>,
76 workspace: WeakEntity<Workspace>,
77 selected_ix: Option<usize>,
78 opened_stack_frame_id: Option<StackFrameId>,
79 list_state: ListState,
80 list_filter: StackFrameFilter,
81 filter_entries_indices: Vec<usize>,
82 error: Option<SharedString>,
83 _refresh_task: Task<()>,
84}
85
86#[derive(Debug, PartialEq, Eq)]
87pub enum StackFrameEntry {
88 Normal(dap::StackFrame),
89 /// Used to indicate that the frame is artificial and is a visual label or separator
90 Label(dap::StackFrame),
91 Collapsed(Vec<dap::StackFrame>),
92}
93
94impl StackFrameList {
95 pub fn new(
96 workspace: WeakEntity<Workspace>,
97 session: Entity<Session>,
98 state: WeakEntity<RunningState>,
99 window: &mut Window,
100 cx: &mut Context<Self>,
101 ) -> Self {
102 let focus_handle = cx.focus_handle();
103
104 let _subscription =
105 cx.subscribe_in(&session, window, |this, _, event, window, cx| match event {
106 SessionEvent::Threads => {
107 this.schedule_refresh(false, window, cx);
108 }
109 SessionEvent::Stopped(..)
110 | SessionEvent::StackTrace
111 | SessionEvent::HistoricSnapshotSelected => {
112 this.schedule_refresh(true, window, cx);
113 }
114 _ => {}
115 });
116
117 let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.));
118
119 let list_filter = workspace
120 .read_with(cx, |workspace, _| workspace.database_id())
121 .ok()
122 .flatten()
123 .and_then(|database_id| {
124 let key = stack_frame_filter_key(&session.read(cx).adapter(), database_id);
125 KEY_VALUE_STORE
126 .read_kvp(&key)
127 .ok()
128 .flatten()
129 .map(StackFrameFilter::from_str_or_default)
130 })
131 .unwrap_or(StackFrameFilter::All);
132
133 let mut this = Self {
134 session,
135 workspace,
136 focus_handle,
137 state,
138 _subscription,
139 entries: Default::default(),
140 filter_entries_indices: Vec::default(),
141 error: None,
142 selected_ix: None,
143 opened_stack_frame_id: None,
144 list_filter,
145 list_state,
146 _refresh_task: Task::ready(()),
147 };
148 this.schedule_refresh(true, window, cx);
149 this
150 }
151
152 #[cfg(test)]
153 pub(crate) fn entries(&self) -> &Vec<StackFrameEntry> {
154 &self.entries
155 }
156
157 pub(crate) fn flatten_entries(
158 &self,
159 show_collapsed: bool,
160 show_labels: bool,
161 ) -> Vec<dap::StackFrame> {
162 self.entries
163 .iter()
164 .enumerate()
165 .filter(|(ix, _)| {
166 self.list_filter == StackFrameFilter::All
167 || self
168 .filter_entries_indices
169 .binary_search_by_key(&ix, |ix| ix)
170 .is_ok()
171 })
172 .flat_map(|(_, frame)| match frame {
173 StackFrameEntry::Normal(frame) => vec![frame.clone()],
174 StackFrameEntry::Label(frame) if show_labels => vec![frame.clone()],
175 StackFrameEntry::Collapsed(frames) if show_collapsed => frames.clone(),
176 _ => vec![],
177 })
178 .collect::<Vec<_>>()
179 }
180
181 fn stack_frames(&self, cx: &mut App) -> Result<Vec<StackFrame>> {
182 if let Ok(Some(thread_id)) = self.state.read_with(cx, |state, _| state.thread_id) {
183 self.session
184 .update(cx, |this, cx| this.stack_frames(thread_id, cx))
185 } else {
186 Ok(Vec::default())
187 }
188 }
189
190 #[cfg(test)]
191 pub(crate) fn dap_stack_frames(&self, cx: &mut App) -> Vec<dap::StackFrame> {
192 match self.list_filter {
193 StackFrameFilter::All => self
194 .stack_frames(cx)
195 .unwrap_or_default()
196 .into_iter()
197 .map(|stack_frame| stack_frame.dap)
198 .collect(),
199 StackFrameFilter::OnlyUserFrames => self
200 .filter_entries_indices
201 .iter()
202 .map(|ix| match &self.entries[*ix] {
203 StackFrameEntry::Label(label) => label,
204 StackFrameEntry::Collapsed(_) => panic!("Collapsed tabs should not be visible"),
205 StackFrameEntry::Normal(frame) => frame,
206 })
207 .cloned()
208 .collect(),
209 }
210 }
211
212 #[cfg(test)]
213 pub(crate) fn list_filter(&self) -> StackFrameFilter {
214 self.list_filter
215 }
216
217 pub fn opened_stack_frame_id(&self) -> Option<StackFrameId> {
218 self.opened_stack_frame_id
219 }
220
221 pub(super) fn schedule_refresh(
222 &mut self,
223 select_first: bool,
224 window: &mut Window,
225 cx: &mut Context<Self>,
226 ) {
227 const REFRESH_DEBOUNCE: Duration = Duration::from_millis(20);
228
229 self._refresh_task = cx.spawn_in(window, async move |this, cx| {
230 let debounce = this
231 .update(cx, |this, cx| {
232 let new_stack_frames = this.stack_frames(cx);
233 new_stack_frames.unwrap_or_default().is_empty() && !this.entries.is_empty()
234 })
235 .ok()
236 .unwrap_or_default();
237
238 if debounce {
239 cx.background_executor().timer(REFRESH_DEBOUNCE).await;
240 }
241 this.update_in(cx, |this, window, cx| {
242 this.build_entries(select_first, window, cx);
243 })
244 .ok();
245 })
246 }
247
248 pub fn build_entries(
249 &mut self,
250 open_first_stack_frame: bool,
251 window: &mut Window,
252 cx: &mut Context<Self>,
253 ) {
254 let old_selected_frame_id = self
255 .selected_ix
256 .and_then(|ix| self.entries.get(ix))
257 .and_then(|entry| match entry {
258 StackFrameEntry::Normal(stack_frame) => Some(stack_frame.id),
259 StackFrameEntry::Collapsed(_) | StackFrameEntry::Label(_) => None,
260 });
261 let mut entries = Vec::new();
262 let mut collapsed_entries = Vec::new();
263 let mut first_stack_frame = None;
264 let mut first_stack_frame_with_path = None;
265
266 let stack_frames = match self.stack_frames(cx) {
267 Ok(stack_frames) => stack_frames,
268 Err(e) => {
269 self.error = Some(format!("{}", e).into());
270 self.entries.clear();
271 self.selected_ix = None;
272 self.list_state.reset(0);
273 self.filter_entries_indices.clear();
274 cx.emit(StackFrameListEvent::BuiltEntries);
275 cx.notify();
276 return;
277 }
278 };
279
280 let worktree_prefixes: Vec<_> = self
281 .workspace
282 .read_with(cx, |workspace, cx| {
283 workspace
284 .visible_worktrees(cx)
285 .map(|tree| tree.read(cx).abs_path())
286 .collect()
287 })
288 .unwrap_or_default();
289
290 let mut filter_entries_indices = Vec::default();
291 for stack_frame in stack_frames.iter() {
292 let frame_in_visible_worktree = stack_frame.dap.source.as_ref().is_some_and(|source| {
293 source.path.as_ref().is_some_and(|path| {
294 worktree_prefixes
295 .iter()
296 .filter_map(|tree| tree.to_str())
297 .any(|tree| path.starts_with(tree))
298 })
299 });
300
301 match stack_frame.dap.presentation_hint {
302 Some(dap::StackFramePresentationHint::Deemphasize)
303 | Some(dap::StackFramePresentationHint::Subtle) => {
304 collapsed_entries.push(stack_frame.dap.clone());
305 }
306 Some(dap::StackFramePresentationHint::Label) => {
307 entries.push(StackFrameEntry::Label(stack_frame.dap.clone()));
308 }
309 _ => {
310 let collapsed_entries = std::mem::take(&mut collapsed_entries);
311 if !collapsed_entries.is_empty() {
312 entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
313 }
314
315 first_stack_frame.get_or_insert(entries.len());
316
317 if stack_frame
318 .dap
319 .source
320 .as_ref()
321 .is_some_and(|source| source.path.is_some())
322 {
323 first_stack_frame_with_path.get_or_insert(entries.len());
324 }
325 entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));
326 if frame_in_visible_worktree {
327 filter_entries_indices.push(entries.len() - 1);
328 }
329 }
330 }
331 }
332
333 let collapsed_entries = std::mem::take(&mut collapsed_entries);
334 if !collapsed_entries.is_empty() {
335 entries.push(StackFrameEntry::Collapsed(collapsed_entries));
336 }
337 self.entries = entries;
338 self.filter_entries_indices = filter_entries_indices;
339
340 if let Some(ix) = first_stack_frame_with_path
341 .or(first_stack_frame)
342 .filter(|_| open_first_stack_frame)
343 {
344 self.select_ix(Some(ix), cx);
345 self.activate_selected_entry(window, cx);
346 } else if let Some(old_selected_frame_id) = old_selected_frame_id {
347 let ix = self.entries.iter().position(|entry| match entry {
348 StackFrameEntry::Normal(frame) => frame.id == old_selected_frame_id,
349 StackFrameEntry::Collapsed(_) | StackFrameEntry::Label(_) => false,
350 });
351 self.selected_ix = ix;
352 }
353
354 match self.list_filter {
355 StackFrameFilter::All => {
356 self.list_state.reset(self.entries.len());
357 }
358 StackFrameFilter::OnlyUserFrames => {
359 self.list_state.reset(self.filter_entries_indices.len());
360 }
361 }
362 cx.emit(StackFrameListEvent::BuiltEntries);
363 cx.notify();
364 }
365
366 pub fn go_to_stack_frame(
367 &mut self,
368 stack_frame_id: StackFrameId,
369 window: &mut Window,
370 cx: &mut Context<Self>,
371 ) -> Task<Result<()>> {
372 let Some(stack_frame) = self
373 .entries
374 .iter()
375 .flat_map(|entry| match entry {
376 StackFrameEntry::Label(stack_frame) => std::slice::from_ref(stack_frame),
377 StackFrameEntry::Normal(stack_frame) => std::slice::from_ref(stack_frame),
378 StackFrameEntry::Collapsed(stack_frames) => stack_frames.as_slice(),
379 })
380 .find(|stack_frame| stack_frame.id == stack_frame_id)
381 .cloned()
382 else {
383 return Task::ready(Err(anyhow!("No stack frame for ID")));
384 };
385 self.go_to_stack_frame_inner(stack_frame, window, cx)
386 }
387
388 fn go_to_stack_frame_inner(
389 &mut self,
390 stack_frame: dap::StackFrame,
391 window: &mut Window,
392 cx: &mut Context<Self>,
393 ) -> Task<Result<()>> {
394 let stack_frame_id = stack_frame.id;
395 self.opened_stack_frame_id = Some(stack_frame_id);
396 let Some(abs_path) = Self::abs_path_from_stack_frame(&stack_frame) else {
397 return Task::ready(Err(anyhow!("Project path not found")));
398 };
399 let row = stack_frame.line.saturating_sub(1) as u32;
400 cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
401 stack_frame_id,
402 ));
403 cx.spawn_in(window, async move |this, cx| {
404 let (worktree, relative_path) = this
405 .update(cx, |this, cx| {
406 this.workspace.update(cx, |workspace, cx| {
407 workspace.project().update(cx, |this, cx| {
408 this.find_or_create_worktree(&abs_path, false, cx)
409 })
410 })
411 })??
412 .await?;
413 let buffer = this
414 .update(cx, |this, cx| {
415 this.workspace.update(cx, |this, cx| {
416 this.project().update(cx, |this, cx| {
417 let worktree_id = worktree.read(cx).id();
418 this.open_buffer(
419 ProjectPath {
420 worktree_id,
421 path: relative_path,
422 },
423 cx,
424 )
425 })
426 })
427 })??
428 .await?;
429 let position = buffer.read_with(cx, |this, _| {
430 this.snapshot().anchor_after(PointUtf16::new(row, 0))
431 });
432 let opened_item = this
433 .update_in(cx, |this, window, cx| {
434 this.workspace.update(cx, |workspace, cx| {
435 let project_path = buffer
436 .read(cx)
437 .project_path(cx)
438 .context("Could not select a stack frame for unnamed buffer")?;
439
440 let open_preview = !workspace
441 .item_of_type::<StackTraceView>(cx)
442 .map(|viewer| {
443 workspace
444 .active_item(cx)
445 .is_some_and(|item| item.item_id() == viewer.item_id())
446 })
447 .unwrap_or_default();
448
449 let active_debug_line_pane = workspace
450 .project()
451 .read(cx)
452 .breakpoint_store()
453 .read(cx)
454 .active_debug_line_pane_id()
455 .and_then(|id| workspace.pane_for_entity_id(id));
456
457 let debug_pane = if let Some(pane) = active_debug_line_pane {
458 Some(pane.downgrade())
459 } else {
460 // No debug pane set yet. Find a pane where the target file
461 // is already the active tab so we don't disrupt other panes.
462 let pane_with_active_file = workspace.panes().iter().find(|pane| {
463 pane.read(cx)
464 .active_item()
465 .and_then(|item| item.project_path(cx))
466 .is_some_and(|path| path == project_path)
467 });
468
469 pane_with_active_file.map(|pane| pane.downgrade())
470 };
471
472 anyhow::Ok(workspace.open_path_preview(
473 project_path,
474 debug_pane,
475 true,
476 true,
477 open_preview,
478 window,
479 cx,
480 ))
481 })
482 })???
483 .await?;
484
485 this.update(cx, |this, cx| {
486 let thread_id = this.state.read_with(cx, |state, _| {
487 state.thread_id.context("No selected thread ID found")
488 })??;
489
490 this.workspace.update(cx, |workspace, cx| {
491 if let Some(pane_id) = workspace
492 .pane_for(&*opened_item)
493 .map(|pane| pane.entity_id())
494 {
495 workspace
496 .project()
497 .read(cx)
498 .breakpoint_store()
499 .update(cx, |store, _cx| {
500 store.set_active_debug_pane_id(pane_id);
501 });
502 }
503
504 let breakpoint_store = workspace.project().read(cx).breakpoint_store();
505
506 breakpoint_store.update(cx, |store, cx| {
507 store.set_active_position(
508 ActiveStackFrame {
509 session_id: this.session.read(cx).session_id(),
510 thread_id,
511 stack_frame_id,
512 path: abs_path,
513 position,
514 },
515 cx,
516 );
517 })
518 })
519 })?
520 })
521 }
522
523 pub(crate) fn abs_path_from_stack_frame(stack_frame: &dap::StackFrame) -> Option<Arc<Path>> {
524 stack_frame.source.as_ref().and_then(|s| {
525 s.path
526 .as_deref()
527 .filter(|path| {
528 // Since we do not know if we are debugging on the host or (a remote/WSL) target,
529 // we need to check if either the path is absolute as Posix or Windows.
530 is_absolute(path, PathStyle::Posix) || is_absolute(path, PathStyle::Windows)
531 })
532 .map(|path| Arc::<Path>::from(Path::new(path)))
533 })
534 }
535
536 pub fn restart_stack_frame(&mut self, stack_frame_id: u64, cx: &mut Context<Self>) {
537 self.session.update(cx, |state, cx| {
538 state.restart_stack_frame(stack_frame_id, cx)
539 });
540 }
541
542 fn render_label_entry(
543 &self,
544 stack_frame: &dap::StackFrame,
545 _cx: &mut Context<Self>,
546 ) -> AnyElement {
547 h_flex()
548 .rounded_md()
549 .justify_between()
550 .w_full()
551 .group("")
552 .id(("label-stack-frame", stack_frame.id))
553 .p_1()
554 .on_any_mouse_down(|_, _, cx| {
555 cx.stop_propagation();
556 })
557 .child(
558 v_flex().justify_center().gap_0p5().child(
559 Label::new(stack_frame.name.clone())
560 .size(LabelSize::Small)
561 .weight(FontWeight::BOLD)
562 .truncate()
563 .color(Color::Info),
564 ),
565 )
566 .into_any()
567 }
568
569 fn render_normal_entry(
570 &self,
571 ix: usize,
572 stack_frame: &dap::StackFrame,
573 cx: &mut Context<Self>,
574 ) -> AnyElement {
575 let source = stack_frame.source.clone();
576 let is_selected_frame = Some(ix) == self.selected_ix;
577
578 let path = source.and_then(|s| s.path.or(s.name));
579 let formatted_path = path.map(|path| format!("{}:{}", path, stack_frame.line,));
580 let formatted_path = formatted_path.map(|path| {
581 Label::new(path)
582 .size(LabelSize::XSmall)
583 .line_height_style(LineHeightStyle::UiLabel)
584 .truncate()
585 .color(Color::Muted)
586 });
587
588 let supports_frame_restart = self
589 .session
590 .read(cx)
591 .capabilities()
592 .supports_restart_frame
593 .unwrap_or_default();
594
595 let should_deemphasize = matches!(
596 stack_frame.presentation_hint,
597 Some(
598 dap::StackFramePresentationHint::Subtle
599 | dap::StackFramePresentationHint::Deemphasize
600 )
601 );
602 h_flex()
603 .rounded_md()
604 .justify_between()
605 .w_full()
606 .group("")
607 .id(("stack-frame", stack_frame.id))
608 .p_1()
609 .when(is_selected_frame, |this| {
610 this.bg(cx.theme().colors().element_hover)
611 })
612 .on_any_mouse_down(|_, _, cx| {
613 cx.stop_propagation();
614 })
615 .on_click(cx.listener(move |this, _, window, cx| {
616 this.selected_ix = Some(ix);
617 this.activate_selected_entry(window, cx);
618 }))
619 .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
620 .overflow_x_scroll()
621 .child(
622 v_flex()
623 .gap_0p5()
624 .child(
625 Label::new(stack_frame.name.clone())
626 .size(LabelSize::Small)
627 .truncate()
628 .when(should_deemphasize, |this| this.color(Color::Muted)),
629 )
630 .children(formatted_path),
631 )
632 .when(
633 supports_frame_restart && stack_frame.can_restart.unwrap_or(true),
634 |this| {
635 this.child(
636 h_flex()
637 .id(("restart-stack-frame", stack_frame.id))
638 .visible_on_hover("")
639 .absolute()
640 .right_2()
641 .overflow_hidden()
642 .rounded_md()
643 .border_1()
644 .border_color(cx.theme().colors().element_selected)
645 .bg(cx.theme().colors().element_background)
646 .hover(|style| {
647 style
648 .bg(cx.theme().colors().ghost_element_hover)
649 .cursor_pointer()
650 })
651 .child(
652 IconButton::new(
653 ("restart-stack-frame", stack_frame.id),
654 IconName::RotateCcw,
655 )
656 .icon_size(IconSize::Small)
657 .on_click(cx.listener({
658 let stack_frame_id = stack_frame.id;
659 move |this, _, _window, cx| {
660 this.restart_stack_frame(stack_frame_id, cx);
661 }
662 }))
663 .tooltip(move |window, cx| {
664 Tooltip::text("Restart Stack Frame")(window, cx)
665 }),
666 ),
667 )
668 },
669 )
670 .into_any()
671 }
672
673 pub(crate) fn expand_collapsed_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
674 let Some(StackFrameEntry::Collapsed(stack_frames)) = self.entries.get_mut(ix) else {
675 return;
676 };
677 let entries = std::mem::take(stack_frames)
678 .into_iter()
679 .map(StackFrameEntry::Normal);
680 // HERE
681 let entries_len = entries.len();
682 self.entries.splice(ix..ix + 1, entries);
683 let (Ok(filtered_indices_start) | Err(filtered_indices_start)) =
684 self.filter_entries_indices.binary_search(&ix);
685
686 for idx in &mut self.filter_entries_indices[filtered_indices_start..] {
687 *idx += entries_len - 1;
688 }
689
690 self.selected_ix = Some(ix);
691 self.list_state.reset(self.entries.len());
692 cx.emit(StackFrameListEvent::BuiltEntries);
693 cx.notify();
694 }
695
696 fn render_collapsed_entry(
697 &self,
698 ix: usize,
699 stack_frames: &Vec<dap::StackFrame>,
700 cx: &mut Context<Self>,
701 ) -> AnyElement {
702 let first_stack_frame = &stack_frames[0];
703 let is_selected = Some(ix) == self.selected_ix;
704
705 h_flex()
706 .rounded_md()
707 .justify_between()
708 .w_full()
709 .group("")
710 .id(("stack-frame", first_stack_frame.id))
711 .p_1()
712 .when(is_selected, |this| {
713 this.bg(cx.theme().colors().element_hover)
714 })
715 .on_any_mouse_down(|_, _, cx| {
716 cx.stop_propagation();
717 })
718 .on_click(cx.listener(move |this, _, window, cx| {
719 this.selected_ix = Some(ix);
720 this.activate_selected_entry(window, cx);
721 }))
722 .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
723 .child(
724 v_flex()
725 .text_ui_sm(cx)
726 .truncate()
727 .text_color(cx.theme().colors().text_muted)
728 .child(format!(
729 "Show {} more{}",
730 stack_frames.len(),
731 first_stack_frame
732 .source
733 .as_ref()
734 .and_then(|source| source.origin.as_ref())
735 .map_or(String::new(), |origin| format!(": {}", origin))
736 )),
737 )
738 .into_any()
739 }
740
741 fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
742 let ix = match self.list_filter {
743 StackFrameFilter::All => ix,
744 StackFrameFilter::OnlyUserFrames => self.filter_entries_indices[ix],
745 };
746
747 match &self.entries[ix] {
748 StackFrameEntry::Label(stack_frame) => self.render_label_entry(stack_frame, cx),
749 StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(ix, stack_frame, cx),
750 StackFrameEntry::Collapsed(stack_frames) => {
751 self.render_collapsed_entry(ix, stack_frames, cx)
752 }
753 }
754 }
755
756 fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
757 self.selected_ix = ix;
758 cx.notify();
759 }
760
761 fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
762 let ix = match self.selected_ix {
763 _ if self.entries.is_empty() => None,
764 None => Some(0),
765 Some(ix) => {
766 if ix == self.entries.len() - 1 {
767 Some(0)
768 } else {
769 Some(ix + 1)
770 }
771 }
772 };
773 self.select_ix(ix, cx);
774 }
775
776 fn select_previous(
777 &mut self,
778 _: &menu::SelectPrevious,
779 _window: &mut Window,
780 cx: &mut Context<Self>,
781 ) {
782 let ix = match self.selected_ix {
783 _ if self.entries.is_empty() => None,
784 None => Some(self.entries.len() - 1),
785 Some(ix) => {
786 if ix == 0 {
787 Some(self.entries.len() - 1)
788 } else {
789 Some(ix - 1)
790 }
791 }
792 };
793 self.select_ix(ix, cx);
794 }
795
796 fn select_first(
797 &mut self,
798 _: &menu::SelectFirst,
799 _window: &mut Window,
800 cx: &mut Context<Self>,
801 ) {
802 let ix = if !self.entries.is_empty() {
803 Some(0)
804 } else {
805 None
806 };
807 self.select_ix(ix, cx);
808 }
809
810 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
811 let ix = if !self.entries.is_empty() {
812 Some(self.entries.len() - 1)
813 } else {
814 None
815 };
816 self.select_ix(ix, cx);
817 }
818
819 fn activate_selected_entry(&mut self, window: &mut Window, cx: &mut Context<Self>) {
820 let Some(ix) = self.selected_ix else {
821 return;
822 };
823 let Some(entry) = self.entries.get_mut(ix) else {
824 return;
825 };
826 match entry {
827 StackFrameEntry::Normal(stack_frame) => {
828 let stack_frame = stack_frame.clone();
829 self.go_to_stack_frame_inner(stack_frame, window, cx)
830 .detach_and_log_err(cx)
831 }
832 StackFrameEntry::Label(_) => {
833 debug_panic!("You should not be able to select a label stack frame")
834 }
835 StackFrameEntry::Collapsed(_) => self.expand_collapsed_entry(ix, cx),
836 }
837 cx.notify();
838 }
839
840 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
841 self.activate_selected_entry(window, cx);
842 }
843
844 pub(crate) fn toggle_frame_filter(
845 &mut self,
846 thread_status: Option<ThreadStatus>,
847 cx: &mut Context<Self>,
848 ) {
849 self.list_filter = match self.list_filter {
850 StackFrameFilter::All => StackFrameFilter::OnlyUserFrames,
851 StackFrameFilter::OnlyUserFrames => StackFrameFilter::All,
852 };
853
854 if let Some(database_id) = self
855 .workspace
856 .read_with(cx, |workspace, _| workspace.database_id())
857 .ok()
858 .flatten()
859 {
860 let key = stack_frame_filter_key(&self.session.read(cx).adapter(), database_id);
861 let save_task = KEY_VALUE_STORE.write_kvp(key, self.list_filter.into());
862 cx.background_spawn(save_task).detach();
863 }
864
865 if let Some(ThreadStatus::Stopped) = thread_status {
866 match self.list_filter {
867 StackFrameFilter::All => {
868 self.list_state.reset(self.entries.len());
869 }
870 StackFrameFilter::OnlyUserFrames => {
871 self.list_state.reset(self.filter_entries_indices.len());
872 if !self
873 .selected_ix
874 .map(|ix| self.filter_entries_indices.contains(&ix))
875 .unwrap_or_default()
876 {
877 self.selected_ix = None;
878 }
879 }
880 }
881
882 if let Some(ix) = self.selected_ix {
883 let scroll_to = match self.list_filter {
884 StackFrameFilter::All => ix,
885 StackFrameFilter::OnlyUserFrames => self
886 .filter_entries_indices
887 .binary_search_by_key(&ix, |ix| *ix)
888 .expect("This index will always exist"),
889 };
890 self.list_state.scroll_to_reveal_item(scroll_to);
891 }
892
893 cx.emit(StackFrameListEvent::BuiltEntries);
894 cx.notify();
895 }
896 }
897
898 fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
899 div().p_1().size_full().child(
900 list(
901 self.list_state.clone(),
902 cx.processor(|this, ix, _window, cx| this.render_entry(ix, cx)),
903 )
904 .size_full(),
905 )
906 }
907
908 pub(crate) fn render_control_strip(&self) -> AnyElement {
909 let tooltip_title = match self.list_filter {
910 StackFrameFilter::All => "Show stack frames from your project",
911 StackFrameFilter::OnlyUserFrames => "Show all stack frames",
912 };
913
914 h_flex()
915 .child(
916 IconButton::new(
917 "filter-by-visible-worktree-stack-frame-list",
918 IconName::ListFilter,
919 )
920 .tooltip(move |_window, cx| {
921 Tooltip::for_action(tooltip_title, &ToggleUserFrames, cx)
922 })
923 .toggle_state(self.list_filter == StackFrameFilter::OnlyUserFrames)
924 .icon_size(IconSize::Small)
925 .on_click(|_, window, cx| {
926 window.dispatch_action(ToggleUserFrames.boxed_clone(), cx)
927 }),
928 )
929 .into_any_element()
930 }
931}
932
933impl Render for StackFrameList {
934 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
935 div()
936 .track_focus(&self.focus_handle)
937 .size_full()
938 .on_action(cx.listener(Self::select_next))
939 .on_action(cx.listener(Self::select_previous))
940 .on_action(cx.listener(Self::select_first))
941 .on_action(cx.listener(Self::select_last))
942 .on_action(cx.listener(Self::confirm))
943 .when_some(self.error.clone(), |el, error| {
944 el.child(
945 h_flex()
946 .bg(cx.theme().status().warning_background)
947 .border_b_1()
948 .border_color(cx.theme().status().warning_border)
949 .pl_1()
950 .child(Icon::new(IconName::Warning).color(Color::Warning))
951 .gap_2()
952 .child(
953 Label::new(error)
954 .size(LabelSize::Small)
955 .color(Color::Warning),
956 ),
957 )
958 })
959 .child(self.render_list(window, cx))
960 .vertical_scrollbar_for(&self.list_state, window, cx)
961 }
962}
963
964impl Focusable for StackFrameList {
965 fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
966 self.focus_handle.clone()
967 }
968}
969
970impl EventEmitter<StackFrameListEvent> for StackFrameList {}