module_list.rs

  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}