module_list.rs

  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}