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 this.update_in(cx, |this, window, cx| {
433 this.workspace.update(cx, |workspace, cx| {
434 let project_path = buffer
435 .read(cx)
436 .project_path(cx)
437 .context("Could not select a stack frame for unnamed buffer")?;
438
439 let open_preview = !workspace
440 .item_of_type::<StackTraceView>(cx)
441 .map(|viewer| {
442 workspace
443 .active_item(cx)
444 .is_some_and(|item| item.item_id() == viewer.item_id())
445 })
446 .unwrap_or_default();
447
448 anyhow::Ok(workspace.open_path_preview(
449 project_path,
450 None,
451 true,
452 true,
453 open_preview,
454 window,
455 cx,
456 ))
457 })
458 })???
459 .await?;
460
461 this.update(cx, |this, cx| {
462 let thread_id = this.state.read_with(cx, |state, _| {
463 state.thread_id.context("No selected thread ID found")
464 })??;
465
466 this.workspace.update(cx, |workspace, cx| {
467 let breakpoint_store = workspace.project().read(cx).breakpoint_store();
468
469 breakpoint_store.update(cx, |store, cx| {
470 store.set_active_position(
471 ActiveStackFrame {
472 session_id: this.session.read(cx).session_id(),
473 thread_id,
474 stack_frame_id,
475 path: abs_path,
476 position,
477 },
478 cx,
479 );
480 })
481 })
482 })?
483 })
484 }
485
486 pub(crate) fn abs_path_from_stack_frame(stack_frame: &dap::StackFrame) -> Option<Arc<Path>> {
487 stack_frame.source.as_ref().and_then(|s| {
488 s.path
489 .as_deref()
490 .filter(|path| {
491 // Since we do not know if we are debugging on the host or (a remote/WSL) target,
492 // we need to check if either the path is absolute as Posix or Windows.
493 is_absolute(path, PathStyle::Posix) || is_absolute(path, PathStyle::Windows)
494 })
495 .map(|path| Arc::<Path>::from(Path::new(path)))
496 })
497 }
498
499 pub fn restart_stack_frame(&mut self, stack_frame_id: u64, cx: &mut Context<Self>) {
500 self.session.update(cx, |state, cx| {
501 state.restart_stack_frame(stack_frame_id, cx)
502 });
503 }
504
505 fn render_label_entry(
506 &self,
507 stack_frame: &dap::StackFrame,
508 _cx: &mut Context<Self>,
509 ) -> AnyElement {
510 h_flex()
511 .rounded_md()
512 .justify_between()
513 .w_full()
514 .group("")
515 .id(("label-stack-frame", stack_frame.id))
516 .p_1()
517 .on_any_mouse_down(|_, _, cx| {
518 cx.stop_propagation();
519 })
520 .child(
521 v_flex().justify_center().gap_0p5().child(
522 Label::new(stack_frame.name.clone())
523 .size(LabelSize::Small)
524 .weight(FontWeight::BOLD)
525 .truncate()
526 .color(Color::Info),
527 ),
528 )
529 .into_any()
530 }
531
532 fn render_normal_entry(
533 &self,
534 ix: usize,
535 stack_frame: &dap::StackFrame,
536 cx: &mut Context<Self>,
537 ) -> AnyElement {
538 let source = stack_frame.source.clone();
539 let is_selected_frame = Some(ix) == self.selected_ix;
540
541 let path = source.and_then(|s| s.path.or(s.name));
542 let formatted_path = path.map(|path| format!("{}:{}", path, stack_frame.line,));
543 let formatted_path = formatted_path.map(|path| {
544 Label::new(path)
545 .size(LabelSize::XSmall)
546 .line_height_style(LineHeightStyle::UiLabel)
547 .truncate()
548 .color(Color::Muted)
549 });
550
551 let supports_frame_restart = self
552 .session
553 .read(cx)
554 .capabilities()
555 .supports_restart_frame
556 .unwrap_or_default();
557
558 let should_deemphasize = matches!(
559 stack_frame.presentation_hint,
560 Some(
561 dap::StackFramePresentationHint::Subtle
562 | dap::StackFramePresentationHint::Deemphasize
563 )
564 );
565 h_flex()
566 .rounded_md()
567 .justify_between()
568 .w_full()
569 .group("")
570 .id(("stack-frame", stack_frame.id))
571 .p_1()
572 .when(is_selected_frame, |this| {
573 this.bg(cx.theme().colors().element_hover)
574 })
575 .on_any_mouse_down(|_, _, cx| {
576 cx.stop_propagation();
577 })
578 .on_click(cx.listener(move |this, _, window, cx| {
579 this.selected_ix = Some(ix);
580 this.activate_selected_entry(window, cx);
581 }))
582 .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
583 .overflow_x_scroll()
584 .child(
585 v_flex()
586 .gap_0p5()
587 .child(
588 Label::new(stack_frame.name.clone())
589 .size(LabelSize::Small)
590 .truncate()
591 .when(should_deemphasize, |this| this.color(Color::Muted)),
592 )
593 .children(formatted_path),
594 )
595 .when(
596 supports_frame_restart && stack_frame.can_restart.unwrap_or(true),
597 |this| {
598 this.child(
599 h_flex()
600 .id(("restart-stack-frame", stack_frame.id))
601 .visible_on_hover("")
602 .absolute()
603 .right_2()
604 .overflow_hidden()
605 .rounded_md()
606 .border_1()
607 .border_color(cx.theme().colors().element_selected)
608 .bg(cx.theme().colors().element_background)
609 .hover(|style| {
610 style
611 .bg(cx.theme().colors().ghost_element_hover)
612 .cursor_pointer()
613 })
614 .child(
615 IconButton::new(
616 ("restart-stack-frame", stack_frame.id),
617 IconName::RotateCcw,
618 )
619 .icon_size(IconSize::Small)
620 .on_click(cx.listener({
621 let stack_frame_id = stack_frame.id;
622 move |this, _, _window, cx| {
623 this.restart_stack_frame(stack_frame_id, cx);
624 }
625 }))
626 .tooltip(move |window, cx| {
627 Tooltip::text("Restart Stack Frame")(window, cx)
628 }),
629 ),
630 )
631 },
632 )
633 .into_any()
634 }
635
636 pub(crate) fn expand_collapsed_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
637 let Some(StackFrameEntry::Collapsed(stack_frames)) = self.entries.get_mut(ix) else {
638 return;
639 };
640 let entries = std::mem::take(stack_frames)
641 .into_iter()
642 .map(StackFrameEntry::Normal);
643 // HERE
644 let entries_len = entries.len();
645 self.entries.splice(ix..ix + 1, entries);
646 let (Ok(filtered_indices_start) | Err(filtered_indices_start)) =
647 self.filter_entries_indices.binary_search(&ix);
648
649 for idx in &mut self.filter_entries_indices[filtered_indices_start..] {
650 *idx += entries_len - 1;
651 }
652
653 self.selected_ix = Some(ix);
654 self.list_state.reset(self.entries.len());
655 cx.emit(StackFrameListEvent::BuiltEntries);
656 cx.notify();
657 }
658
659 fn render_collapsed_entry(
660 &self,
661 ix: usize,
662 stack_frames: &Vec<dap::StackFrame>,
663 cx: &mut Context<Self>,
664 ) -> AnyElement {
665 let first_stack_frame = &stack_frames[0];
666 let is_selected = Some(ix) == self.selected_ix;
667
668 h_flex()
669 .rounded_md()
670 .justify_between()
671 .w_full()
672 .group("")
673 .id(("stack-frame", first_stack_frame.id))
674 .p_1()
675 .when(is_selected, |this| {
676 this.bg(cx.theme().colors().element_hover)
677 })
678 .on_any_mouse_down(|_, _, cx| {
679 cx.stop_propagation();
680 })
681 .on_click(cx.listener(move |this, _, window, cx| {
682 this.selected_ix = Some(ix);
683 this.activate_selected_entry(window, cx);
684 }))
685 .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
686 .child(
687 v_flex()
688 .text_ui_sm(cx)
689 .truncate()
690 .text_color(cx.theme().colors().text_muted)
691 .child(format!(
692 "Show {} more{}",
693 stack_frames.len(),
694 first_stack_frame
695 .source
696 .as_ref()
697 .and_then(|source| source.origin.as_ref())
698 .map_or(String::new(), |origin| format!(": {}", origin))
699 )),
700 )
701 .into_any()
702 }
703
704 fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
705 let ix = match self.list_filter {
706 StackFrameFilter::All => ix,
707 StackFrameFilter::OnlyUserFrames => self.filter_entries_indices[ix],
708 };
709
710 match &self.entries[ix] {
711 StackFrameEntry::Label(stack_frame) => self.render_label_entry(stack_frame, cx),
712 StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(ix, stack_frame, cx),
713 StackFrameEntry::Collapsed(stack_frames) => {
714 self.render_collapsed_entry(ix, stack_frames, cx)
715 }
716 }
717 }
718
719 fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
720 self.selected_ix = ix;
721 cx.notify();
722 }
723
724 fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
725 let ix = match self.selected_ix {
726 _ if self.entries.is_empty() => None,
727 None => Some(0),
728 Some(ix) => {
729 if ix == self.entries.len() - 1 {
730 Some(0)
731 } else {
732 Some(ix + 1)
733 }
734 }
735 };
736 self.select_ix(ix, cx);
737 }
738
739 fn select_previous(
740 &mut self,
741 _: &menu::SelectPrevious,
742 _window: &mut Window,
743 cx: &mut Context<Self>,
744 ) {
745 let ix = match self.selected_ix {
746 _ if self.entries.is_empty() => None,
747 None => Some(self.entries.len() - 1),
748 Some(ix) => {
749 if ix == 0 {
750 Some(self.entries.len() - 1)
751 } else {
752 Some(ix - 1)
753 }
754 }
755 };
756 self.select_ix(ix, cx);
757 }
758
759 fn select_first(
760 &mut self,
761 _: &menu::SelectFirst,
762 _window: &mut Window,
763 cx: &mut Context<Self>,
764 ) {
765 let ix = if !self.entries.is_empty() {
766 Some(0)
767 } else {
768 None
769 };
770 self.select_ix(ix, cx);
771 }
772
773 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
774 let ix = if !self.entries.is_empty() {
775 Some(self.entries.len() - 1)
776 } else {
777 None
778 };
779 self.select_ix(ix, cx);
780 }
781
782 fn activate_selected_entry(&mut self, window: &mut Window, cx: &mut Context<Self>) {
783 let Some(ix) = self.selected_ix else {
784 return;
785 };
786 let Some(entry) = self.entries.get_mut(ix) else {
787 return;
788 };
789 match entry {
790 StackFrameEntry::Normal(stack_frame) => {
791 let stack_frame = stack_frame.clone();
792 self.go_to_stack_frame_inner(stack_frame, window, cx)
793 .detach_and_log_err(cx)
794 }
795 StackFrameEntry::Label(_) => {
796 debug_panic!("You should not be able to select a label stack frame")
797 }
798 StackFrameEntry::Collapsed(_) => self.expand_collapsed_entry(ix, cx),
799 }
800 cx.notify();
801 }
802
803 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
804 self.activate_selected_entry(window, cx);
805 }
806
807 pub(crate) fn toggle_frame_filter(
808 &mut self,
809 thread_status: Option<ThreadStatus>,
810 cx: &mut Context<Self>,
811 ) {
812 self.list_filter = match self.list_filter {
813 StackFrameFilter::All => StackFrameFilter::OnlyUserFrames,
814 StackFrameFilter::OnlyUserFrames => StackFrameFilter::All,
815 };
816
817 if let Some(database_id) = self
818 .workspace
819 .read_with(cx, |workspace, _| workspace.database_id())
820 .ok()
821 .flatten()
822 {
823 let key = stack_frame_filter_key(&self.session.read(cx).adapter(), database_id);
824 let save_task = KEY_VALUE_STORE.write_kvp(key, self.list_filter.into());
825 cx.background_spawn(save_task).detach();
826 }
827
828 if let Some(ThreadStatus::Stopped) = thread_status {
829 match self.list_filter {
830 StackFrameFilter::All => {
831 self.list_state.reset(self.entries.len());
832 }
833 StackFrameFilter::OnlyUserFrames => {
834 self.list_state.reset(self.filter_entries_indices.len());
835 if !self
836 .selected_ix
837 .map(|ix| self.filter_entries_indices.contains(&ix))
838 .unwrap_or_default()
839 {
840 self.selected_ix = None;
841 }
842 }
843 }
844
845 if let Some(ix) = self.selected_ix {
846 let scroll_to = match self.list_filter {
847 StackFrameFilter::All => ix,
848 StackFrameFilter::OnlyUserFrames => self
849 .filter_entries_indices
850 .binary_search_by_key(&ix, |ix| *ix)
851 .expect("This index will always exist"),
852 };
853 self.list_state.scroll_to_reveal_item(scroll_to);
854 }
855
856 cx.emit(StackFrameListEvent::BuiltEntries);
857 cx.notify();
858 }
859 }
860
861 fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
862 div().p_1().size_full().child(
863 list(
864 self.list_state.clone(),
865 cx.processor(|this, ix, _window, cx| this.render_entry(ix, cx)),
866 )
867 .size_full(),
868 )
869 }
870
871 pub(crate) fn render_control_strip(&self) -> AnyElement {
872 let tooltip_title = match self.list_filter {
873 StackFrameFilter::All => "Show stack frames from your project",
874 StackFrameFilter::OnlyUserFrames => "Show all stack frames",
875 };
876
877 h_flex()
878 .child(
879 IconButton::new(
880 "filter-by-visible-worktree-stack-frame-list",
881 IconName::ListFilter,
882 )
883 .tooltip(move |_window, cx| {
884 Tooltip::for_action(tooltip_title, &ToggleUserFrames, cx)
885 })
886 .toggle_state(self.list_filter == StackFrameFilter::OnlyUserFrames)
887 .icon_size(IconSize::Small)
888 .on_click(|_, window, cx| {
889 window.dispatch_action(ToggleUserFrames.boxed_clone(), cx)
890 }),
891 )
892 .into_any_element()
893 }
894}
895
896impl Render for StackFrameList {
897 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
898 div()
899 .track_focus(&self.focus_handle)
900 .size_full()
901 .on_action(cx.listener(Self::select_next))
902 .on_action(cx.listener(Self::select_previous))
903 .on_action(cx.listener(Self::select_first))
904 .on_action(cx.listener(Self::select_last))
905 .on_action(cx.listener(Self::confirm))
906 .when_some(self.error.clone(), |el, error| {
907 el.child(
908 h_flex()
909 .bg(cx.theme().status().warning_background)
910 .border_b_1()
911 .border_color(cx.theme().status().warning_border)
912 .pl_1()
913 .child(Icon::new(IconName::Warning).color(Color::Warning))
914 .gap_2()
915 .child(
916 Label::new(error)
917 .size(LabelSize::Small)
918 .color(Color::Warning),
919 ),
920 )
921 })
922 .child(self.render_list(window, cx))
923 .vertical_scrollbar_for(&self.list_state, window, cx)
924 }
925}
926
927impl Focusable for StackFrameList {
928 fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
929 self.focus_handle.clone()
930 }
931}
932
933impl EventEmitter<StackFrameListEvent> for StackFrameList {}