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