Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:37:26.838Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

stack_frame_list.rs

970 lines · 35.1 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.