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(_) | SessionEvent::Modules => {
36 if this._rebuild_task.is_some() {
37 this.schedule_rebuild(cx);
38 }
39 }
40 _ => {}
41 });
42
43 let scroll_handle = UniformListScrollHandle::new();
44
45 Self {
46 scroll_handle,
47 session,
48 workspace,
49 focus_handle,
50 entries: Vec::new(),
51 selected_ix: None,
52 _subscription,
53 _rebuild_task: None,
54 }
55 }
56
57 fn schedule_rebuild(&mut self, cx: &mut Context<Self>) {
58 self._rebuild_task = Some(cx.spawn(async move |this, cx| {
59 this.update(cx, |this, cx| {
60 let modules = this
61 .session
62 .update(cx, |session, cx| session.modules(cx).to_owned());
63 this.entries = modules;
64 cx.notify();
65 })
66 .ok();
67 }));
68 }
69
70 fn open_module(&mut self, path: Arc<Path>, window: &mut Window, cx: &mut Context<Self>) {
71 cx.spawn_in(window, async move |this, cx| {
72 let (worktree, relative_path) = this
73 .update(cx, |this, cx| {
74 this.workspace.update(cx, |workspace, cx| {
75 workspace.project().update(cx, |this, cx| {
76 this.find_or_create_worktree(&path, false, cx)
77 })
78 })
79 })??
80 .await?;
81
82 let buffer = this
83 .update(cx, |this, cx| {
84 this.workspace.update(cx, |this, cx| {
85 this.project().update(cx, |this, cx| {
86 let worktree_id = worktree.read(cx).id();
87 this.open_buffer(
88 ProjectPath {
89 worktree_id,
90 path: relative_path.into(),
91 },
92 cx,
93 )
94 })
95 })
96 })??
97 .await?;
98
99 this.update_in(cx, |this, window, cx| {
100 this.workspace.update(cx, |workspace, cx| {
101 let project_path = buffer.read(cx).project_path(cx).ok_or_else(|| {
102 anyhow!("Could not select a stack frame for unnamed buffer")
103 })?;
104 anyhow::Ok(workspace.open_path_preview(
105 project_path,
106 None,
107 false,
108 true,
109 true,
110 window,
111 cx,
112 ))
113 })
114 })???
115 .await?;
116
117 anyhow::Ok(())
118 })
119 .detach();
120 }
121
122 fn render_entry(&mut self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
123 let module = self.entries[ix].clone();
124
125 v_flex()
126 .rounded_md()
127 .w_full()
128 .group("")
129 .id(("module-list", ix))
130 .on_any_mouse_down(|_, _, cx| {
131 cx.stop_propagation();
132 })
133 .when(module.path.is_some(), |this| {
134 this.on_click({
135 let path = module
136 .path
137 .as_deref()
138 .map(|path| Arc::<Path>::from(Path::new(path)));
139 cx.listener(move |this, _, window, cx| {
140 this.selected_ix = Some(ix);
141 if let Some(path) = path.as_ref() {
142 this.open_module(path.clone(), window, cx);
143 }
144 cx.notify();
145 })
146 })
147 })
148 .p_1()
149 .hover(|s| s.bg(cx.theme().colors().element_hover))
150 .when(Some(ix) == self.selected_ix, |s| {
151 s.bg(cx.theme().colors().element_hover)
152 })
153 .child(h_flex().gap_0p5().text_ui_sm(cx).child(module.name.clone()))
154 .child(
155 h_flex()
156 .text_ui_xs(cx)
157 .text_color(cx.theme().colors().text_muted)
158 .when_some(module.path, |this, path| this.child(path)),
159 )
160 .into_any()
161 }
162
163 #[cfg(test)]
164 pub(crate) fn modules(&self, cx: &mut Context<Self>) -> Vec<dap::Module> {
165 self.session
166 .update(cx, |session, cx| session.modules(cx).to_vec())
167 }
168
169 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
170 let Some(ix) = self.selected_ix else { return };
171 let Some(entry) = self.entries.get(ix) else {
172 return;
173 };
174 let Some(path) = entry.path.as_deref() else {
175 return;
176 };
177 let path = Arc::from(Path::new(path));
178 self.open_module(path, window, cx);
179 }
180
181 fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
182 self.selected_ix = ix;
183 if let Some(ix) = ix {
184 self.scroll_handle
185 .scroll_to_item(ix, ScrollStrategy::Center);
186 }
187 cx.notify();
188 }
189
190 fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
191 let ix = match self.selected_ix {
192 _ if self.entries.is_empty() => None,
193 None => Some(0),
194 Some(ix) => {
195 if ix == self.entries.len() - 1 {
196 Some(0)
197 } else {
198 Some(ix + 1)
199 }
200 }
201 };
202 self.select_ix(ix, cx);
203 }
204
205 fn select_previous(
206 &mut self,
207 _: &menu::SelectPrevious,
208 _window: &mut Window,
209 cx: &mut Context<Self>,
210 ) {
211 let ix = match self.selected_ix {
212 _ if self.entries.is_empty() => None,
213 None => Some(self.entries.len() - 1),
214 Some(ix) => {
215 if ix == 0 {
216 Some(self.entries.len() - 1)
217 } else {
218 Some(ix - 1)
219 }
220 }
221 };
222 self.select_ix(ix, cx);
223 }
224
225 fn select_first(
226 &mut self,
227 _: &menu::SelectFirst,
228 _window: &mut Window,
229 cx: &mut Context<Self>,
230 ) {
231 let ix = if !self.entries.is_empty() {
232 Some(0)
233 } else {
234 None
235 };
236 self.select_ix(ix, cx);
237 }
238
239 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
240 let ix = if !self.entries.is_empty() {
241 Some(self.entries.len() - 1)
242 } else {
243 None
244 };
245 self.select_ix(ix, cx);
246 }
247
248 fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
249 uniform_list(
250 "module-list",
251 self.entries.len(),
252 cx.processor(|this, range: Range<usize>, _window, cx| {
253 range.map(|ix| this.render_entry(ix, cx)).collect()
254 }),
255 )
256 .track_scroll(self.scroll_handle.clone())
257 .size_full()
258 }
259}
260
261impl Focusable for ModuleList {
262 fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
263 self.focus_handle.clone()
264 }
265}
266
267impl Render for ModuleList {
268 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
269 if self._rebuild_task.is_none() {
270 self.schedule_rebuild(cx);
271 }
272 div()
273 .track_focus(&self.focus_handle)
274 .on_action(cx.listener(Self::select_last))
275 .on_action(cx.listener(Self::select_first))
276 .on_action(cx.listener(Self::select_next))
277 .on_action(cx.listener(Self::select_previous))
278 .on_action(cx.listener(Self::confirm))
279 .size_full()
280 .p_1()
281 .child(self.render_list(window, cx))
282 .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
283 }
284}