1use anyhow::anyhow;
2use dap::Module;
3use gpui::{
4 AnyElement, Entity, FocusHandle, Focusable, ScrollStrategy, Subscription, Task,
5 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::{WithScrollbar, 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 entries: Vec<Module>,
22 _rebuild_task: Option<Task<()>>,
23 _subscription: Subscription,
24}
25
26impl ModuleList {
27 pub fn new(
28 session: Entity<Session>,
29 workspace: WeakEntity<Workspace>,
30 cx: &mut Context<Self>,
31 ) -> Self {
32 let focus_handle = cx.focus_handle();
33
34 let _subscription = cx.subscribe(&session, |this, _, event, cx| match event {
35 SessionEvent::Stopped(_)
36 | SessionEvent::HistoricSnapshotSelected
37 | SessionEvent::Modules => {
38 if this._rebuild_task.is_some() {
39 this.schedule_rebuild(cx);
40 }
41 }
42 _ => {}
43 });
44
45 let scroll_handle = UniformListScrollHandle::new();
46
47 Self {
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,
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
171 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
172 let Some(ix) = self.selected_ix else { return };
173 let Some(entry) = self.entries.get(ix) else {
174 return;
175 };
176 let Some(path) = entry.path.as_deref() else {
177 return;
178 };
179 let path = Arc::from(Path::new(path));
180 self.open_module(path, window, cx);
181 }
182
183 fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
184 self.selected_ix = ix;
185 if let Some(ix) = ix {
186 self.scroll_handle
187 .scroll_to_item(ix, ScrollStrategy::Center);
188 }
189 cx.notify();
190 }
191
192 fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
193 let ix = match self.selected_ix {
194 _ if self.entries.is_empty() => None,
195 None => Some(0),
196 Some(ix) => {
197 if ix == self.entries.len() - 1 {
198 Some(0)
199 } else {
200 Some(ix + 1)
201 }
202 }
203 };
204 self.select_ix(ix, cx);
205 }
206
207 fn select_previous(
208 &mut self,
209 _: &menu::SelectPrevious,
210 _window: &mut Window,
211 cx: &mut Context<Self>,
212 ) {
213 let ix = match self.selected_ix {
214 _ if self.entries.is_empty() => None,
215 None => Some(self.entries.len() - 1),
216 Some(ix) => {
217 if ix == 0 {
218 Some(self.entries.len() - 1)
219 } else {
220 Some(ix - 1)
221 }
222 }
223 };
224 self.select_ix(ix, cx);
225 }
226
227 fn select_first(
228 &mut self,
229 _: &menu::SelectFirst,
230 _window: &mut Window,
231 cx: &mut Context<Self>,
232 ) {
233 let ix = if !self.entries.is_empty() {
234 Some(0)
235 } else {
236 None
237 };
238 self.select_ix(ix, cx);
239 }
240
241 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
242 let ix = if !self.entries.is_empty() {
243 Some(self.entries.len() - 1)
244 } else {
245 None
246 };
247 self.select_ix(ix, cx);
248 }
249
250 fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
251 uniform_list(
252 "module-list",
253 self.entries.len(),
254 cx.processor(|this, range: Range<usize>, _window, cx| {
255 range.map(|ix| this.render_entry(ix, cx)).collect()
256 }),
257 )
258 .track_scroll(&self.scroll_handle)
259 .size_full()
260 }
261}
262
263impl Focusable for ModuleList {
264 fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
265 self.focus_handle.clone()
266 }
267}
268
269impl Render for ModuleList {
270 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
271 if self._rebuild_task.is_none() {
272 self.schedule_rebuild(cx);
273 }
274 div()
275 .track_focus(&self.focus_handle)
276 .on_action(cx.listener(Self::select_last))
277 .on_action(cx.listener(Self::select_first))
278 .on_action(cx.listener(Self::select_next))
279 .on_action(cx.listener(Self::select_previous))
280 .on_action(cx.listener(Self::confirm))
281 .size_full()
282 .p_1()
283 .child(self.render_list(window, cx))
284 .vertical_scrollbar_for(&self.scroll_handle, window, cx)
285 }
286}