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 cx.notify();
231 })
232 .ok();
233 })
234 }
235
236 pub fn build_entries(
237 &mut self,
238 open_first_stack_frame: bool,
239 window: &mut Window,
240 cx: &mut Context<Self>,
241 ) {
242 let old_selected_frame_id = self
243 .selected_ix
244 .and_then(|ix| self.entries.get(ix))
245 .and_then(|entry| match entry {
246 StackFrameEntry::Normal(stack_frame) => Some(stack_frame.id),
247 StackFrameEntry::Collapsed(_) | StackFrameEntry::Label(_) => None,
248 });
249 let mut entries = Vec::new();
250 let mut collapsed_entries = Vec::new();
251 let mut first_stack_frame = None;
252 let mut first_stack_frame_with_path = None;
253
254 let stack_frames = match self.stack_frames(cx) {
255 Ok(stack_frames) => stack_frames,
256 Err(e) => {
257 self.error = Some(format!("{}", e).into());
258 self.entries.clear();
259 self.selected_ix = None;
260 self.list_state.reset(0);
261 self.filter_entries_indices.clear();
262 cx.emit(StackFrameListEvent::BuiltEntries);
263 cx.notify();
264 return;
265 }
266 };
267
268 let worktree_prefixes: Vec<_> = self
269 .workspace
270 .read_with(cx, |workspace, cx| {
271 workspace
272 .visible_worktrees(cx)
273 .map(|tree| tree.read(cx).abs_path())
274 .collect()
275 })
276 .unwrap_or_default();
277
278 let mut filter_entries_indices = Vec::default();
279 for stack_frame in stack_frames.iter() {
280 let frame_in_visible_worktree = stack_frame.dap.source.as_ref().is_some_and(|source| {
281 source.path.as_ref().is_some_and(|path| {
282 worktree_prefixes
283 .iter()
284 .filter_map(|tree| tree.to_str())
285 .any(|tree| path.starts_with(tree))
286 })
287 });
288
289 match stack_frame.dap.presentation_hint {
290 Some(dap::StackFramePresentationHint::Deemphasize)
291 | Some(dap::StackFramePresentationHint::Subtle) => {
292 collapsed_entries.push(stack_frame.dap.clone());
293 }
294 Some(dap::StackFramePresentationHint::Label) => {
295 entries.push(StackFrameEntry::Label(stack_frame.dap.clone()));
296 }
297 _ => {
298 let collapsed_entries = std::mem::take(&mut collapsed_entries);
299 if !collapsed_entries.is_empty() {
300 entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
301 }
302
303 first_stack_frame.get_or_insert(entries.len());
304
305 if stack_frame
306 .dap
307 .source
308 .as_ref()
309 .is_some_and(|source| source.path.is_some())
310 {
311 first_stack_frame_with_path.get_or_insert(entries.len());
312 }
313 entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));
314 if frame_in_visible_worktree {
315 filter_entries_indices.push(entries.len() - 1);
316 }
317 }
318 }
319 }
320
321 let collapsed_entries = std::mem::take(&mut collapsed_entries);
322 if !collapsed_entries.is_empty() {
323 entries.push(StackFrameEntry::Collapsed(collapsed_entries));
324 }
325 self.entries = entries;
326 self.filter_entries_indices = filter_entries_indices;
327
328 if let Some(ix) = first_stack_frame_with_path
329 .or(first_stack_frame)
330 .filter(|_| open_first_stack_frame)
331 {
332 self.select_ix(Some(ix), cx);
333 self.activate_selected_entry(window, cx);
334 } else if let Some(old_selected_frame_id) = old_selected_frame_id {
335 let ix = self.entries.iter().position(|entry| match entry {
336 StackFrameEntry::Normal(frame) => frame.id == old_selected_frame_id,
337 StackFrameEntry::Collapsed(_) | StackFrameEntry::Label(_) => false,
338 });
339 self.selected_ix = ix;
340 }
341
342 match self.list_filter {
343 StackFrameFilter::All => {
344 self.list_state.reset(self.entries.len());
345 }
346 StackFrameFilter::OnlyUserFrames => {
347 self.list_state.reset(self.filter_entries_indices.len());
348 }
349 }
350 cx.emit(StackFrameListEvent::BuiltEntries);
351 cx.notify();
352 }
353
354 pub fn go_to_stack_frame(
355 &mut self,
356 stack_frame_id: StackFrameId,
357 window: &mut Window,
358 cx: &mut Context<Self>,
359 ) -> Task<Result<()>> {
360 let Some(stack_frame) = self
361 .entries
362 .iter()
363 .flat_map(|entry| match entry {
364 StackFrameEntry::Label(stack_frame) => std::slice::from_ref(stack_frame),
365 StackFrameEntry::Normal(stack_frame) => std::slice::from_ref(stack_frame),
366 StackFrameEntry::Collapsed(stack_frames) => stack_frames.as_slice(),
367 })
368 .find(|stack_frame| stack_frame.id == stack_frame_id)
369 .cloned()
370 else {
371 return Task::ready(Err(anyhow!("No stack frame for ID")));
372 };
373 self.go_to_stack_frame_inner(stack_frame, window, cx)
374 }
375
376 fn go_to_stack_frame_inner(
377 &mut self,
378 stack_frame: dap::StackFrame,
379 window: &mut Window,
380 cx: &mut Context<Self>,
381 ) -> Task<Result<()>> {
382 let stack_frame_id = stack_frame.id;
383 self.opened_stack_frame_id = Some(stack_frame_id);
384 let Some(abs_path) = Self::abs_path_from_stack_frame(&stack_frame) else {
385 return Task::ready(Err(anyhow!("Project path not found")));
386 };
387 let row = stack_frame.line.saturating_sub(1) as u32;
388 cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
389 stack_frame_id,
390 ));
391 cx.spawn_in(window, async move |this, cx| {
392 let (worktree, relative_path) = this
393 .update(cx, |this, cx| {
394 this.workspace.update(cx, |workspace, cx| {
395 workspace.project().update(cx, |this, cx| {
396 this.find_or_create_worktree(&abs_path, false, cx)
397 })
398 })
399 })??
400 .await?;
401 let buffer = this
402 .update(cx, |this, cx| {
403 this.workspace.update(cx, |this, cx| {
404 this.project().update(cx, |this, cx| {
405 let worktree_id = worktree.read(cx).id();
406 this.open_buffer(
407 ProjectPath {
408 worktree_id,
409 path: relative_path,
410 },
411 cx,
412 )
413 })
414 })
415 })??
416 .await?;
417 let position = buffer.read_with(cx, |this, _| {
418 this.snapshot().anchor_after(PointUtf16::new(row, 0))
419 })?;
420 this.update_in(cx, |this, window, cx| {
421 this.workspace.update(cx, |workspace, cx| {
422 let project_path = buffer
423 .read(cx)
424 .project_path(cx)
425 .context("Could not select a stack frame for unnamed buffer")?;
426
427 let open_preview = !workspace
428 .item_of_type::<StackTraceView>(cx)
429 .map(|viewer| {
430 workspace
431 .active_item(cx)
432 .is_some_and(|item| item.item_id() == viewer.item_id())
433 })
434 .unwrap_or_default();
435
436 anyhow::Ok(workspace.open_path_preview(
437 project_path,
438 None,
439 true,
440 true,
441 open_preview,
442 window,
443 cx,
444 ))
445 })
446 })???
447 .await?;
448
449 this.update(cx, |this, cx| {
450 let thread_id = this.state.read_with(cx, |state, _| {
451 state.thread_id.context("No selected thread ID found")
452 })??;
453
454 this.workspace.update(cx, |workspace, cx| {
455 let breakpoint_store = workspace.project().read(cx).breakpoint_store();
456
457 breakpoint_store.update(cx, |store, cx| {
458 store.set_active_position(
459 ActiveStackFrame {
460 session_id: this.session.read(cx).session_id(),
461 thread_id,
462 stack_frame_id,
463 path: abs_path,
464 position,
465 },
466 cx,
467 );
468 })
469 })
470 })?
471 })
472 }
473
474 pub(crate) fn abs_path_from_stack_frame(stack_frame: &dap::StackFrame) -> Option<Arc<Path>> {
475 stack_frame.source.as_ref().and_then(|s| {
476 s.path
477 .as_deref()
478 .filter(|path| {
479 // Since we do not know if we are debugging on the host or (a remote/WSL) target,
480 // we need to check if either the path is absolute as Posix or Windows.
481 is_absolute(path, PathStyle::Posix) || is_absolute(path, PathStyle::Windows)
482 })
483 .map(|path| Arc::<Path>::from(Path::new(path)))
484 })
485 }
486
487 pub fn restart_stack_frame(&mut self, stack_frame_id: u64, cx: &mut Context<Self>) {
488 self.session.update(cx, |state, cx| {
489 state.restart_stack_frame(stack_frame_id, cx)
490 });
491 }
492
493 fn render_label_entry(
494 &self,
495 stack_frame: &dap::StackFrame,
496 _cx: &mut Context<Self>,
497 ) -> AnyElement {
498 h_flex()
499 .rounded_md()
500 .justify_between()
501 .w_full()
502 .group("")
503 .id(("label-stack-frame", stack_frame.id))
504 .p_1()
505 .on_any_mouse_down(|_, _, cx| {
506 cx.stop_propagation();
507 })
508 .child(
509 v_flex().justify_center().gap_0p5().child(
510 Label::new(stack_frame.name.clone())
511 .size(LabelSize::Small)
512 .weight(FontWeight::BOLD)
513 .truncate()
514 .color(Color::Info),
515 ),
516 )
517 .into_any()
518 }
519
520 fn render_normal_entry(
521 &self,
522 ix: usize,
523 stack_frame: &dap::StackFrame,
524 cx: &mut Context<Self>,
525 ) -> AnyElement {
526 let source = stack_frame.source.clone();
527 let is_selected_frame = Some(ix) == self.selected_ix;
528
529 let path = source.and_then(|s| s.path.or(s.name));
530 let formatted_path = path.map(|path| format!("{}:{}", path, stack_frame.line,));
531 let formatted_path = formatted_path.map(|path| {
532 Label::new(path)
533 .size(LabelSize::XSmall)
534 .line_height_style(LineHeightStyle::UiLabel)
535 .truncate()
536 .color(Color::Muted)
537 });
538
539 let supports_frame_restart = self
540 .session
541 .read(cx)
542 .capabilities()
543 .supports_restart_frame
544 .unwrap_or_default();
545
546 let should_deemphasize = matches!(
547 stack_frame.presentation_hint,
548 Some(
549 dap::StackFramePresentationHint::Subtle
550 | dap::StackFramePresentationHint::Deemphasize
551 )
552 );
553 h_flex()
554 .rounded_md()
555 .justify_between()
556 .w_full()
557 .group("")
558 .id(("stack-frame", stack_frame.id))
559 .p_1()
560 .when(is_selected_frame, |this| {
561 this.bg(cx.theme().colors().element_hover)
562 })
563 .on_any_mouse_down(|_, _, cx| {
564 cx.stop_propagation();
565 })
566 .on_click(cx.listener(move |this, _, window, cx| {
567 this.selected_ix = Some(ix);
568 this.activate_selected_entry(window, cx);
569 }))
570 .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
571 .overflow_x_scroll()
572 .child(
573 v_flex()
574 .gap_0p5()
575 .child(
576 Label::new(stack_frame.name.clone())
577 .size(LabelSize::Small)
578 .truncate()
579 .when(should_deemphasize, |this| this.color(Color::Muted)),
580 )
581 .children(formatted_path),
582 )
583 .when(
584 supports_frame_restart && stack_frame.can_restart.unwrap_or(true),
585 |this| {
586 this.child(
587 h_flex()
588 .id(("restart-stack-frame", stack_frame.id))
589 .visible_on_hover("")
590 .absolute()
591 .right_2()
592 .overflow_hidden()
593 .rounded_md()
594 .border_1()
595 .border_color(cx.theme().colors().element_selected)
596 .bg(cx.theme().colors().element_background)
597 .hover(|style| {
598 style
599 .bg(cx.theme().colors().ghost_element_hover)
600 .cursor_pointer()
601 })
602 .child(
603 IconButton::new(
604 ("restart-stack-frame", stack_frame.id),
605 IconName::RotateCcw,
606 )
607 .icon_size(IconSize::Small)
608 .on_click(cx.listener({
609 let stack_frame_id = stack_frame.id;
610 move |this, _, _window, cx| {
611 this.restart_stack_frame(stack_frame_id, cx);
612 }
613 }))
614 .tooltip(move |window, cx| {
615 Tooltip::text("Restart Stack Frame")(window, cx)
616 }),
617 ),
618 )
619 },
620 )
621 .into_any()
622 }
623
624 pub(crate) fn expand_collapsed_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
625 let Some(StackFrameEntry::Collapsed(stack_frames)) = self.entries.get_mut(ix) else {
626 return;
627 };
628 let entries = std::mem::take(stack_frames)
629 .into_iter()
630 .map(StackFrameEntry::Normal);
631 // HERE
632 let entries_len = entries.len();
633 self.entries.splice(ix..ix + 1, entries);
634 let (Ok(filtered_indices_start) | Err(filtered_indices_start)) =
635 self.filter_entries_indices.binary_search(&ix);
636
637 for idx in &mut self.filter_entries_indices[filtered_indices_start..] {
638 *idx += entries_len - 1;
639 }
640
641 self.selected_ix = Some(ix);
642 self.list_state.reset(self.entries.len());
643 cx.emit(StackFrameListEvent::BuiltEntries);
644 cx.notify();
645 }
646
647 fn render_collapsed_entry(
648 &self,
649 ix: usize,
650 stack_frames: &Vec<dap::StackFrame>,
651 cx: &mut Context<Self>,
652 ) -> AnyElement {
653 let first_stack_frame = &stack_frames[0];
654 let is_selected = Some(ix) == self.selected_ix;
655
656 h_flex()
657 .rounded_md()
658 .justify_between()
659 .w_full()
660 .group("")
661 .id(("stack-frame", first_stack_frame.id))
662 .p_1()
663 .when(is_selected, |this| {
664 this.bg(cx.theme().colors().element_hover)
665 })
666 .on_any_mouse_down(|_, _, cx| {
667 cx.stop_propagation();
668 })
669 .on_click(cx.listener(move |this, _, window, cx| {
670 this.selected_ix = Some(ix);
671 this.activate_selected_entry(window, cx);
672 }))
673 .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
674 .child(
675 v_flex()
676 .text_ui_sm(cx)
677 .truncate()
678 .text_color(cx.theme().colors().text_muted)
679 .child(format!(
680 "Show {} more{}",
681 stack_frames.len(),
682 first_stack_frame
683 .source
684 .as_ref()
685 .and_then(|source| source.origin.as_ref())
686 .map_or(String::new(), |origin| format!(": {}", origin))
687 )),
688 )
689 .into_any()
690 }
691
692 fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
693 let ix = match self.list_filter {
694 StackFrameFilter::All => ix,
695 StackFrameFilter::OnlyUserFrames => self.filter_entries_indices[ix],
696 };
697
698 match &self.entries[ix] {
699 StackFrameEntry::Label(stack_frame) => self.render_label_entry(stack_frame, cx),
700 StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(ix, stack_frame, cx),
701 StackFrameEntry::Collapsed(stack_frames) => {
702 self.render_collapsed_entry(ix, stack_frames, cx)
703 }
704 }
705 }
706
707 fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
708 self.selected_ix = ix;
709 cx.notify();
710 }
711
712 fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
713 let ix = match self.selected_ix {
714 _ if self.entries.is_empty() => None,
715 None => Some(0),
716 Some(ix) => {
717 if ix == self.entries.len() - 1 {
718 Some(0)
719 } else {
720 Some(ix + 1)
721 }
722 }
723 };
724 self.select_ix(ix, cx);
725 }
726
727 fn select_previous(
728 &mut self,
729 _: &menu::SelectPrevious,
730 _window: &mut Window,
731 cx: &mut Context<Self>,
732 ) {
733 let ix = match self.selected_ix {
734 _ if self.entries.is_empty() => None,
735 None => Some(self.entries.len() - 1),
736 Some(ix) => {
737 if ix == 0 {
738 Some(self.entries.len() - 1)
739 } else {
740 Some(ix - 1)
741 }
742 }
743 };
744 self.select_ix(ix, cx);
745 }
746
747 fn select_first(
748 &mut self,
749 _: &menu::SelectFirst,
750 _window: &mut Window,
751 cx: &mut Context<Self>,
752 ) {
753 let ix = if !self.entries.is_empty() {
754 Some(0)
755 } else {
756 None
757 };
758 self.select_ix(ix, cx);
759 }
760
761 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
762 let ix = if !self.entries.is_empty() {
763 Some(self.entries.len() - 1)
764 } else {
765 None
766 };
767 self.select_ix(ix, cx);
768 }
769
770 fn activate_selected_entry(&mut self, window: &mut Window, cx: &mut Context<Self>) {
771 let Some(ix) = self.selected_ix else {
772 return;
773 };
774 let Some(entry) = self.entries.get_mut(ix) else {
775 return;
776 };
777 match entry {
778 StackFrameEntry::Normal(stack_frame) => {
779 let stack_frame = stack_frame.clone();
780 self.go_to_stack_frame_inner(stack_frame, window, cx)
781 .detach_and_log_err(cx)
782 }
783 StackFrameEntry::Label(_) => {
784 debug_panic!("You should not be able to select a label stack frame")
785 }
786 StackFrameEntry::Collapsed(_) => self.expand_collapsed_entry(ix, cx),
787 }
788 cx.notify();
789 }
790
791 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
792 self.activate_selected_entry(window, cx);
793 }
794
795 pub(crate) fn toggle_frame_filter(
796 &mut self,
797 thread_status: Option<ThreadStatus>,
798 cx: &mut Context<Self>,
799 ) {
800 self.list_filter = match self.list_filter {
801 StackFrameFilter::All => StackFrameFilter::OnlyUserFrames,
802 StackFrameFilter::OnlyUserFrames => StackFrameFilter::All,
803 };
804
805 if let Some(database_id) = self
806 .workspace
807 .read_with(cx, |workspace, _| workspace.database_id())
808 .ok()
809 .flatten()
810 {
811 let database_id: i64 = database_id.into();
812 let save_task = KEY_VALUE_STORE.write_kvp(
813 format!(
814 "stack-frame-list-filter-{}-{}",
815 self.session.read(cx).adapter().0,
816 database_id,
817 ),
818 self.list_filter.into(),
819 );
820 cx.background_spawn(save_task).detach();
821 }
822
823 if let Some(ThreadStatus::Stopped) = thread_status {
824 match self.list_filter {
825 StackFrameFilter::All => {
826 self.list_state.reset(self.entries.len());
827 }
828 StackFrameFilter::OnlyUserFrames => {
829 self.list_state.reset(self.filter_entries_indices.len());
830 if !self
831 .selected_ix
832 .map(|ix| self.filter_entries_indices.contains(&ix))
833 .unwrap_or_default()
834 {
835 self.selected_ix = None;
836 }
837 }
838 }
839
840 if let Some(ix) = self.selected_ix {
841 let scroll_to = match self.list_filter {
842 StackFrameFilter::All => ix,
843 StackFrameFilter::OnlyUserFrames => self
844 .filter_entries_indices
845 .binary_search_by_key(&ix, |ix| *ix)
846 .expect("This index will always exist"),
847 };
848 self.list_state.scroll_to_reveal_item(scroll_to);
849 }
850
851 cx.emit(StackFrameListEvent::BuiltEntries);
852 cx.notify();
853 }
854 }
855
856 fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
857 div().p_1().size_full().child(
858 list(
859 self.list_state.clone(),
860 cx.processor(|this, ix, _window, cx| this.render_entry(ix, cx)),
861 )
862 .size_full(),
863 )
864 }
865
866 pub(crate) fn render_control_strip(&self) -> AnyElement {
867 let tooltip_title = match self.list_filter {
868 StackFrameFilter::All => "Show stack frames from your project",
869 StackFrameFilter::OnlyUserFrames => "Show all stack frames",
870 };
871
872 h_flex()
873 .child(
874 IconButton::new(
875 "filter-by-visible-worktree-stack-frame-list",
876 IconName::ListFilter,
877 )
878 .tooltip(move |_window, cx| {
879 Tooltip::for_action(tooltip_title, &ToggleUserFrames, cx)
880 })
881 .toggle_state(self.list_filter == StackFrameFilter::OnlyUserFrames)
882 .icon_size(IconSize::Small)
883 .on_click(|_, window, cx| {
884 window.dispatch_action(ToggleUserFrames.boxed_clone(), cx)
885 }),
886 )
887 .into_any_element()
888 }
889}
890
891impl Render for StackFrameList {
892 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
893 div()
894 .track_focus(&self.focus_handle)
895 .size_full()
896 .on_action(cx.listener(Self::select_next))
897 .on_action(cx.listener(Self::select_previous))
898 .on_action(cx.listener(Self::select_first))
899 .on_action(cx.listener(Self::select_last))
900 .on_action(cx.listener(Self::confirm))
901 .when_some(self.error.clone(), |el, error| {
902 el.child(
903 h_flex()
904 .bg(cx.theme().status().warning_background)
905 .border_b_1()
906 .border_color(cx.theme().status().warning_border)
907 .pl_1()
908 .child(Icon::new(IconName::Warning).color(Color::Warning))
909 .gap_2()
910 .child(
911 Label::new(error)
912 .size(LabelSize::Small)
913 .color(Color::Warning),
914 ),
915 )
916 })
917 .child(self.render_list(window, cx))
918 .vertical_scrollbar_for(&self.list_state, window, cx)
919 }
920}
921
922impl Focusable for StackFrameList {
923 fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
924 self.focus_handle.clone()
925 }
926}
927
928impl EventEmitter<StackFrameListEvent> for StackFrameList {}