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