1use anyhow::anyhow;
2use dap::Module;
3use gpui::{
4 AnyElement, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Stateful,
5 Subscription, Task, UniformListScrollHandle, WeakEntity, uniform_list,
6};
7use project::{
8 ProjectItem as _, ProjectPath,
9 debugger::session::{Session, SessionEvent},
10};
11use std::{ops::Range, path::Path, sync::Arc};
12use ui::{Scrollbar, ScrollbarState, prelude::*};
13use workspace::Workspace;
14
15pub struct ModuleList {
16 scroll_handle: UniformListScrollHandle,
17 selected_ix: Option<usize>,
18 session: Entity<Session>,
19 workspace: WeakEntity<Workspace>,
20 focus_handle: FocusHandle,
21 scrollbar_state: ScrollbarState,
22 entries: Vec<Module>,
23 _rebuild_task: Option<Task<()>>,
24 _subscription: Subscription,
25}
26
27impl ModuleList {
28 pub fn new(
29 session: Entity<Session>,
30 workspace: WeakEntity<Workspace>,
31 cx: &mut Context<Self>,
32 ) -> Self {
33 let focus_handle = cx.focus_handle();
34
35 let _subscription = cx.subscribe(&session, |this, _, event, cx| match event {
36 SessionEvent::Stopped(_) | SessionEvent::Modules => {
37 if this._rebuild_task.is_some() {
38 this.schedule_rebuild(cx);
39 }
40 }
41 _ => {}
42 });
43
44 let scroll_handle = UniformListScrollHandle::new();
45
46 Self {
47 scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
48 scroll_handle,
49 session,
50 workspace,
51 focus_handle,
52 entries: Vec::new(),
53 selected_ix: None,
54 _subscription,
55 _rebuild_task: None,
56 }
57 }
58
59 fn schedule_rebuild(&mut self, cx: &mut Context<Self>) {
60 self._rebuild_task = Some(cx.spawn(async move |this, cx| {
61 this.update(cx, |this, cx| {
62 let modules = this
63 .session
64 .update(cx, |session, cx| session.modules(cx).to_owned());
65 this.entries = modules;
66 cx.notify();
67 })
68 .ok();
69 }));
70 }
71
72 fn open_module(&mut self, path: Arc<Path>, window: &mut Window, cx: &mut Context<Self>) {
73 cx.spawn_in(window, async move |this, cx| {
74 let (worktree, relative_path) = this
75 .update(cx, |this, cx| {
76 this.workspace.update(cx, |workspace, cx| {
77 workspace.project().update(cx, |this, cx| {
78 this.find_or_create_worktree(&path, false, cx)
79 })
80 })
81 })??
82 .await?;
83
84 let buffer = this
85 .update(cx, |this, cx| {
86 this.workspace.update(cx, |this, cx| {
87 this.project().update(cx, |this, cx| {
88 let worktree_id = worktree.read(cx).id();
89 this.open_buffer(
90 ProjectPath {
91 worktree_id,
92 path: relative_path.into(),
93 },
94 cx,
95 )
96 })
97 })
98 })??
99 .await?;
100
101 this.update_in(cx, |this, window, cx| {
102 this.workspace.update(cx, |workspace, cx| {
103 let project_path = buffer.read(cx).project_path(cx).ok_or_else(|| {
104 anyhow!("Could not select a stack frame for unnamed buffer")
105 })?;
106 anyhow::Ok(workspace.open_path_preview(
107 project_path,
108 None,
109 false,
110 true,
111 true,
112 window,
113 cx,
114 ))
115 })
116 })???
117 .await?;
118
119 anyhow::Ok(())
120 })
121 .detach();
122 }
123
124 fn render_entry(&mut self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
125 let module = self.entries[ix].clone();
126
127 v_flex()
128 .rounded_md()
129 .w_full()
130 .group("")
131 .id(("module-list", ix))
132 .on_any_mouse_down(|_, _, cx| {
133 cx.stop_propagation();
134 })
135 .when(module.path.is_some(), |this| {
136 this.on_click({
137 let path = module
138 .path
139 .as_deref()
140 .map(|path| Arc::<Path>::from(Path::new(path)));
141 cx.listener(move |this, _, window, cx| {
142 this.selected_ix = Some(ix);
143 if let Some(path) = path.as_ref() {
144 this.open_module(path.clone(), window, cx);
145 }
146 cx.notify();
147 })
148 })
149 })
150 .p_1()
151 .hover(|s| s.bg(cx.theme().colors().element_hover))
152 .when(Some(ix) == self.selected_ix, |s| {
153 s.bg(cx.theme().colors().element_hover)
154 })
155 .child(h_flex().gap_0p5().text_ui_sm(cx).child(module.name.clone()))
156 .child(
157 h_flex()
158 .text_ui_xs(cx)
159 .text_color(cx.theme().colors().text_muted)
160 .when_some(module.path, |this, path| this.child(path)),
161 )
162 .into_any()
163 }
164
165 #[cfg(test)]
166 pub(crate) fn modules(&self, cx: &mut Context<Self>) -> Vec<dap::Module> {
167 self.session
168 .update(cx, |session, cx| session.modules(cx).to_vec())
169 }
170 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
171 div()
172 .occlude()
173 .id("module-list-vertical-scrollbar")
174 .on_mouse_move(cx.listener(|_, _, _, cx| {
175 cx.notify();
176 cx.stop_propagation()
177 }))
178 .on_hover(|_, _, cx| {
179 cx.stop_propagation();
180 })
181 .on_any_mouse_down(|_, _, cx| {
182 cx.stop_propagation();
183 })
184 .on_mouse_up(
185 MouseButton::Left,
186 cx.listener(|_, _, _, cx| {
187 cx.stop_propagation();
188 }),
189 )
190 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
191 cx.notify();
192 }))
193 .h_full()
194 .absolute()
195 .right_1()
196 .top_1()
197 .bottom_0()
198 .w(px(12.))
199 .cursor_default()
200 .children(Scrollbar::vertical(self.scrollbar_state.clone()))
201 }
202
203 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
204 let Some(ix) = self.selected_ix else { return };
205 let Some(entry) = self.entries.get(ix) else {
206 return;
207 };
208 let Some(path) = entry.path.as_deref() else {
209 return;
210 };
211 let path = Arc::from(Path::new(path));
212 self.open_module(path, window, cx);
213 }
214
215 fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
216 self.selected_ix = ix;
217 if let Some(ix) = ix {
218 self.scroll_handle
219 .scroll_to_item(ix, ScrollStrategy::Center);
220 }
221 cx.notify();
222 }
223
224 fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
225 let ix = match self.selected_ix {
226 _ if self.entries.is_empty() => None,
227 None => Some(0),
228 Some(ix) => {
229 if ix == self.entries.len() - 1 {
230 Some(0)
231 } else {
232 Some(ix + 1)
233 }
234 }
235 };
236 self.select_ix(ix, cx);
237 }
238
239 fn select_previous(
240 &mut self,
241 _: &menu::SelectPrevious,
242 _window: &mut Window,
243 cx: &mut Context<Self>,
244 ) {
245 let ix = match self.selected_ix {
246 _ if self.entries.is_empty() => None,
247 None => Some(self.entries.len() - 1),
248 Some(ix) => {
249 if ix == 0 {
250 Some(self.entries.len() - 1)
251 } else {
252 Some(ix - 1)
253 }
254 }
255 };
256 self.select_ix(ix, cx);
257 }
258
259 fn select_first(
260 &mut self,
261 _: &menu::SelectFirst,
262 _window: &mut Window,
263 cx: &mut Context<Self>,
264 ) {
265 let ix = if !self.entries.is_empty() {
266 Some(0)
267 } else {
268 None
269 };
270 self.select_ix(ix, cx);
271 }
272
273 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
274 let ix = if !self.entries.is_empty() {
275 Some(self.entries.len() - 1)
276 } else {
277 None
278 };
279 self.select_ix(ix, cx);
280 }
281
282 fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
283 uniform_list(
284 "module-list",
285 self.entries.len(),
286 cx.processor(|this, range: Range<usize>, _window, cx| {
287 range.map(|ix| this.render_entry(ix, cx)).collect()
288 }),
289 )
290 .track_scroll(self.scroll_handle.clone())
291 .size_full()
292 }
293}
294
295impl Focusable for ModuleList {
296 fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
297 self.focus_handle.clone()
298 }
299}
300
301impl Render for ModuleList {
302 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
303 if self._rebuild_task.is_none() {
304 self.schedule_rebuild(cx);
305 }
306 div()
307 .track_focus(&self.focus_handle)
308 .on_action(cx.listener(Self::select_last))
309 .on_action(cx.listener(Self::select_first))
310 .on_action(cx.listener(Self::select_next))
311 .on_action(cx.listener(Self::select_previous))
312 .on_action(cx.listener(Self::confirm))
313 .size_full()
314 .p_1()
315 .child(self.render_list(window, cx))
316 .child(self.render_vertical_scrollbar(cx))
317 }
318}