module_list.rs

  1use anyhow::anyhow;
  2use gpui::{
  3    AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, MouseButton, Stateful,
  4    Subscription, WeakEntity, list,
  5};
  6use project::{
  7    ProjectItem as _, ProjectPath,
  8    debugger::session::{Session, SessionEvent},
  9};
 10use std::{path::Path, sync::Arc};
 11use ui::{Scrollbar, ScrollbarState, prelude::*};
 12use util::maybe;
 13use workspace::Workspace;
 14
 15pub struct ModuleList {
 16    list: ListState,
 17    invalidate: bool,
 18    session: Entity<Session>,
 19    workspace: WeakEntity<Workspace>,
 20    focus_handle: FocusHandle,
 21    scrollbar_state: ScrollbarState,
 22    _subscription: Subscription,
 23}
 24
 25impl ModuleList {
 26    pub fn new(
 27        session: Entity<Session>,
 28        workspace: WeakEntity<Workspace>,
 29        cx: &mut Context<Self>,
 30    ) -> Self {
 31        let weak_entity = cx.weak_entity();
 32        let focus_handle = cx.focus_handle();
 33
 34        let list = ListState::new(
 35            0,
 36            gpui::ListAlignment::Top,
 37            px(1000.),
 38            move |ix, _window, cx| {
 39                weak_entity
 40                    .upgrade()
 41                    .map(|module_list| module_list.update(cx, |this, cx| this.render_entry(ix, cx)))
 42                    .unwrap_or(div().into_any())
 43            },
 44        );
 45
 46        let _subscription = cx.subscribe(&session, |this, _, event, cx| match event {
 47            SessionEvent::Stopped(_) | SessionEvent::Modules => {
 48                this.invalidate = true;
 49                cx.notify();
 50            }
 51            _ => {}
 52        });
 53
 54        Self {
 55            scrollbar_state: ScrollbarState::new(list.clone()),
 56            list,
 57            session,
 58            workspace,
 59            focus_handle,
 60            _subscription,
 61            invalidate: true,
 62        }
 63    }
 64
 65    fn open_module(&mut self, path: Arc<Path>, window: &mut Window, cx: &mut Context<Self>) {
 66        cx.spawn_in(window, async move |this, cx| {
 67            let (worktree, relative_path) = this
 68                .update(cx, |this, cx| {
 69                    this.workspace.update(cx, |workspace, cx| {
 70                        workspace.project().update(cx, |this, cx| {
 71                            this.find_or_create_worktree(&path, false, cx)
 72                        })
 73                    })
 74                })??
 75                .await?;
 76
 77            let buffer = this
 78                .update(cx, |this, cx| {
 79                    this.workspace.update(cx, |this, cx| {
 80                        this.project().update(cx, |this, cx| {
 81                            let worktree_id = worktree.read(cx).id();
 82                            this.open_buffer(
 83                                ProjectPath {
 84                                    worktree_id,
 85                                    path: relative_path.into(),
 86                                },
 87                                cx,
 88                            )
 89                        })
 90                    })
 91                })??
 92                .await?;
 93
 94            this.update_in(cx, |this, window, cx| {
 95                this.workspace.update(cx, |workspace, cx| {
 96                    let project_path = buffer.read(cx).project_path(cx).ok_or_else(|| {
 97                        anyhow!("Could not select a stack frame for unnamed buffer")
 98                    })?;
 99                    anyhow::Ok(workspace.open_path_preview(
100                        project_path,
101                        None,
102                        false,
103                        true,
104                        true,
105                        window,
106                        cx,
107                    ))
108                })
109            })???
110            .await?;
111
112            anyhow::Ok(())
113        })
114        .detach_and_log_err(cx);
115    }
116
117    fn render_entry(&mut self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
118        let Some(module) = maybe!({
119            self.session
120                .update(cx, |state, cx| state.modules(cx).get(ix).cloned())
121        }) else {
122            return Empty.into_any();
123        };
124
125        v_flex()
126            .rounded_md()
127            .w_full()
128            .group("")
129            .id(("module-list", ix))
130            .when(module.path.is_some(), |this| {
131                this.on_click({
132                    let path = module.path.as_deref().map(|path| Arc::<Path>::from(Path::new(path)));
133                    cx.listener(move |this, _, window, cx| {
134                        if let Some(path) = path.as_ref() {
135                            this.open_module(path.clone(), window, cx);
136                        } else {
137                            log::error!("Wasn't able to find module path, but was still able to click on module list entry");
138                        }
139                    })
140                })
141            })
142            .p_1()
143            .hover(|s| s.bg(cx.theme().colors().element_hover))
144            .child(h_flex().gap_0p5().text_ui_sm(cx).child(module.name.clone()))
145            .child(
146                h_flex()
147                    .text_ui_xs(cx)
148                    .text_color(cx.theme().colors().text_muted)
149                    .when_some(module.path.clone(), |this, path| this.child(path)),
150            )
151            .into_any()
152    }
153
154    #[cfg(test)]
155    pub(crate) fn modules(&self, cx: &mut Context<Self>) -> Vec<dap::Module> {
156        self.session
157            .update(cx, |session, cx| session.modules(cx).to_vec())
158    }
159    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
160        div()
161            .occlude()
162            .id("module-list-vertical-scrollbar")
163            .on_mouse_move(cx.listener(|_, _, _, cx| {
164                cx.notify();
165                cx.stop_propagation()
166            }))
167            .on_hover(|_, _, cx| {
168                cx.stop_propagation();
169            })
170            .on_any_mouse_down(|_, _, cx| {
171                cx.stop_propagation();
172            })
173            .on_mouse_up(
174                MouseButton::Left,
175                cx.listener(|_, _, _, cx| {
176                    cx.stop_propagation();
177                }),
178            )
179            .on_scroll_wheel(cx.listener(|_, _, _, cx| {
180                cx.notify();
181            }))
182            .h_full()
183            .absolute()
184            .right_1()
185            .top_1()
186            .bottom_0()
187            .w(px(12.))
188            .cursor_default()
189            .children(Scrollbar::vertical(self.scrollbar_state.clone()))
190    }
191}
192
193impl Focusable for ModuleList {
194    fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
195        self.focus_handle.clone()
196    }
197}
198
199impl Render for ModuleList {
200    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
201        if self.invalidate {
202            let len = self
203                .session
204                .update(cx, |session, cx| session.modules(cx).len());
205            self.list.reset(len);
206            self.invalidate = false;
207            cx.notify();
208        }
209
210        div()
211            .track_focus(&self.focus_handle)
212            .size_full()
213            .p_1()
214            .child(list(self.list.clone()).size_full())
215            .child(self.render_vertical_scrollbar(cx))
216    }
217}