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