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::{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: 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 this.schedule_rebuild(cx);
38 }
39 _ => {}
40 });
41
42 let scroll_handle = UniformListScrollHandle::new();
43
44 let mut this = Self {
45 scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
46 scroll_handle,
47 session,
48 workspace,
49 focus_handle,
50 entries: Vec::new(),
51 selected_ix: None,
52 _subscription,
53 _rebuild_task: Task::ready(()),
54 };
55 this.schedule_rebuild(cx);
56 this
57 }
58
59 fn schedule_rebuild(&mut self, cx: &mut Context<Self>) {
60 self._rebuild_task = 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 .when(module.path.is_some(), |this| {
133 this.on_click({
134 let path = module
135 .path
136 .as_deref()
137 .map(|path| Arc::<Path>::from(Path::new(path)));
138 cx.listener(move |this, _, window, cx| {
139 this.selected_ix = Some(ix);
140 if let Some(path) = path.as_ref() {
141 this.open_module(path.clone(), window, cx);
142 }
143 cx.notify();
144 })
145 })
146 })
147 .p_1()
148 .hover(|s| s.bg(cx.theme().colors().element_hover))
149 .when(Some(ix) == self.selected_ix, |s| {
150 s.bg(cx.theme().colors().element_hover)
151 })
152 .child(h_flex().gap_0p5().text_ui_sm(cx).child(module.name.clone()))
153 .child(
154 h_flex()
155 .text_ui_xs(cx)
156 .text_color(cx.theme().colors().text_muted)
157 .when_some(module.path.clone(), |this, path| this.child(path)),
158 )
159 .into_any()
160 }
161
162 #[cfg(test)]
163 pub(crate) fn modules(&self, cx: &mut Context<Self>) -> Vec<dap::Module> {
164 self.session
165 .update(cx, |session, cx| session.modules(cx).to_vec())
166 }
167 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
168 div()
169 .occlude()
170 .id("module-list-vertical-scrollbar")
171 .on_mouse_move(cx.listener(|_, _, _, cx| {
172 cx.notify();
173 cx.stop_propagation()
174 }))
175 .on_hover(|_, _, cx| {
176 cx.stop_propagation();
177 })
178 .on_any_mouse_down(|_, _, cx| {
179 cx.stop_propagation();
180 })
181 .on_mouse_up(
182 MouseButton::Left,
183 cx.listener(|_, _, _, cx| {
184 cx.stop_propagation();
185 }),
186 )
187 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
188 cx.notify();
189 }))
190 .h_full()
191 .absolute()
192 .right_1()
193 .top_1()
194 .bottom_0()
195 .w(px(12.))
196 .cursor_default()
197 .children(Scrollbar::vertical(self.scrollbar_state.clone()))
198 }
199
200 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
201 let Some(ix) = self.selected_ix else { return };
202 let Some(entry) = self.entries.get(ix) else {
203 return;
204 };
205 let Some(path) = entry.path.as_deref() else {
206 return;
207 };
208 let path = Arc::from(Path::new(path));
209 self.open_module(path, window, cx);
210 }
211
212 fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
213 self.selected_ix = ix;
214 if let Some(ix) = ix {
215 self.scroll_handle
216 .scroll_to_item(ix, ScrollStrategy::Center);
217 }
218 cx.notify();
219 }
220
221 fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
222 let ix = match self.selected_ix {
223 _ if self.entries.len() == 0 => None,
224 None => Some(0),
225 Some(ix) => {
226 if ix == self.entries.len() - 1 {
227 Some(0)
228 } else {
229 Some(ix + 1)
230 }
231 }
232 };
233 self.select_ix(ix, cx);
234 }
235
236 fn select_previous(
237 &mut self,
238 _: &menu::SelectPrevious,
239 _window: &mut Window,
240 cx: &mut Context<Self>,
241 ) {
242 let ix = match self.selected_ix {
243 _ if self.entries.len() == 0 => None,
244 None => Some(self.entries.len() - 1),
245 Some(ix) => {
246 if ix == 0 {
247 Some(self.entries.len() - 1)
248 } else {
249 Some(ix - 1)
250 }
251 }
252 };
253 self.select_ix(ix, cx);
254 }
255
256 fn select_first(
257 &mut self,
258 _: &menu::SelectFirst,
259 _window: &mut Window,
260 cx: &mut Context<Self>,
261 ) {
262 let ix = if self.entries.len() > 0 {
263 Some(0)
264 } else {
265 None
266 };
267 self.select_ix(ix, cx);
268 }
269
270 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
271 let ix = if self.entries.len() > 0 {
272 Some(self.entries.len() - 1)
273 } else {
274 None
275 };
276 self.select_ix(ix, cx);
277 }
278
279 fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
280 uniform_list(
281 cx.entity(),
282 "module-list",
283 self.entries.len(),
284 |this, range, _window, cx| range.map(|ix| this.render_entry(ix, cx)).collect(),
285 )
286 .track_scroll(self.scroll_handle.clone())
287 .size_full()
288 }
289}
290
291impl Focusable for ModuleList {
292 fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
293 self.focus_handle.clone()
294 }
295}
296
297impl Render for ModuleList {
298 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
299 div()
300 .track_focus(&self.focus_handle)
301 .on_action(cx.listener(Self::select_last))
302 .on_action(cx.listener(Self::select_first))
303 .on_action(cx.listener(Self::select_next))
304 .on_action(cx.listener(Self::select_previous))
305 .on_action(cx.listener(Self::confirm))
306 .size_full()
307 .p_1()
308 .child(self.render_list(window, cx))
309 .child(self.render_vertical_scrollbar(cx))
310 }
311}