1use anyhow::anyhow;
2use gpui::{
3 AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, MouseButton, Stateful,
4 Subscription, WeakEntity, list,
5};
6use project::{
7 ProjectItem as _, ProjectPath,
8 debugger::session::{Session, SessionEvent},
9};
10use std::{path::Path, sync::Arc};
11use ui::{Scrollbar, ScrollbarState, prelude::*};
12use util::maybe;
13use workspace::Workspace;
14
15pub struct ModuleList {
16 list: ListState,
17 invalidate: bool,
18 session: Entity<Session>,
19 workspace: WeakEntity<Workspace>,
20 focus_handle: FocusHandle,
21 scrollbar_state: ScrollbarState,
22 _subscription: Subscription,
23}
24
25impl ModuleList {
26 pub fn new(
27 session: Entity<Session>,
28 workspace: WeakEntity<Workspace>,
29 cx: &mut Context<Self>,
30 ) -> Self {
31 let weak_entity = cx.weak_entity();
32 let focus_handle = cx.focus_handle();
33
34 let list = ListState::new(
35 0,
36 gpui::ListAlignment::Top,
37 px(1000.),
38 move |ix, _window, cx| {
39 weak_entity
40 .upgrade()
41 .map(|module_list| module_list.update(cx, |this, cx| this.render_entry(ix, cx)))
42 .unwrap_or(div().into_any())
43 },
44 );
45
46 let _subscription = cx.subscribe(&session, |this, _, event, cx| match event {
47 SessionEvent::Stopped(_) | SessionEvent::Modules => {
48 this.invalidate = true;
49 cx.notify();
50 }
51 _ => {}
52 });
53
54 Self {
55 scrollbar_state: ScrollbarState::new(list.clone()),
56 list,
57 session,
58 workspace,
59 focus_handle,
60 _subscription,
61 invalidate: true,
62 }
63 }
64
65 fn open_module(&mut self, path: Arc<Path>, window: &mut Window, cx: &mut Context<Self>) {
66 cx.spawn_in(window, async move |this, cx| {
67 let (worktree, relative_path) = this
68 .update(cx, |this, cx| {
69 this.workspace.update(cx, |workspace, cx| {
70 workspace.project().update(cx, |this, cx| {
71 this.find_or_create_worktree(&path, false, cx)
72 })
73 })
74 })??
75 .await?;
76
77 let buffer = this
78 .update(cx, |this, cx| {
79 this.workspace.update(cx, |this, cx| {
80 this.project().update(cx, |this, cx| {
81 let worktree_id = worktree.read(cx).id();
82 this.open_buffer(
83 ProjectPath {
84 worktree_id,
85 path: relative_path.into(),
86 },
87 cx,
88 )
89 })
90 })
91 })??
92 .await?;
93
94 this.update_in(cx, |this, window, cx| {
95 this.workspace.update(cx, |workspace, cx| {
96 let project_path = buffer.read(cx).project_path(cx).ok_or_else(|| {
97 anyhow!("Could not select a stack frame for unnamed buffer")
98 })?;
99 anyhow::Ok(workspace.open_path_preview(
100 project_path,
101 None,
102 false,
103 true,
104 true,
105 window,
106 cx,
107 ))
108 })
109 })???
110 .await?;
111
112 anyhow::Ok(())
113 })
114 .detach_and_log_err(cx);
115 }
116
117 fn render_entry(&mut self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
118 let Some(module) = maybe!({
119 self.session
120 .update(cx, |state, cx| state.modules(cx).get(ix).cloned())
121 }) else {
122 return Empty.into_any();
123 };
124
125 v_flex()
126 .rounded_md()
127 .w_full()
128 .group("")
129 .id(("module-list", ix))
130 .when(module.path.is_some(), |this| {
131 this.on_click({
132 let path = module.path.as_deref().map(|path| Arc::<Path>::from(Path::new(path)));
133 cx.listener(move |this, _, window, cx| {
134 if let Some(path) = path.as_ref() {
135 this.open_module(path.clone(), window, cx);
136 } else {
137 log::error!("Wasn't able to find module path, but was still able to click on module list entry");
138 }
139 })
140 })
141 })
142 .p_1()
143 .hover(|s| s.bg(cx.theme().colors().element_hover))
144 .child(h_flex().gap_0p5().text_ui_sm(cx).child(module.name.clone()))
145 .child(
146 h_flex()
147 .text_ui_xs(cx)
148 .text_color(cx.theme().colors().text_muted)
149 .when_some(module.path.clone(), |this, path| this.child(path)),
150 )
151 .into_any()
152 }
153
154 #[cfg(test)]
155 pub(crate) fn modules(&self, cx: &mut Context<Self>) -> Vec<dap::Module> {
156 self.session
157 .update(cx, |session, cx| session.modules(cx).to_vec())
158 }
159 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
160 div()
161 .occlude()
162 .id("module-list-vertical-scrollbar")
163 .on_mouse_move(cx.listener(|_, _, _, cx| {
164 cx.notify();
165 cx.stop_propagation()
166 }))
167 .on_hover(|_, _, cx| {
168 cx.stop_propagation();
169 })
170 .on_any_mouse_down(|_, _, cx| {
171 cx.stop_propagation();
172 })
173 .on_mouse_up(
174 MouseButton::Left,
175 cx.listener(|_, _, _, cx| {
176 cx.stop_propagation();
177 }),
178 )
179 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
180 cx.notify();
181 }))
182 .h_full()
183 .absolute()
184 .right_1()
185 .top_1()
186 .bottom_0()
187 .w(px(12.))
188 .cursor_default()
189 .children(Scrollbar::vertical(self.scrollbar_state.clone()))
190 }
191}
192
193impl Focusable for ModuleList {
194 fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
195 self.focus_handle.clone()
196 }
197}
198
199impl Render for ModuleList {
200 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
201 if self.invalidate {
202 let len = self
203 .session
204 .update(cx, |session, cx| session.modules(cx).len());
205 self.list.reset(len);
206 self.invalidate = false;
207 cx.notify();
208 }
209
210 div()
211 .track_focus(&self.focus_handle)
212 .size_full()
213 .p_1()
214 .child(list(self.list.clone()).size_full())
215 .child(self.render_vertical_scrollbar(cx))
216 }
217}