1use crate::ProjectIndex;
2use gpui::{
3 canvas, div, list, uniform_list, AnyElement, AppContext, CursorStyle, EventEmitter,
4 FocusHandle, FocusableView, IntoElement, ListOffset, ListState, Model, MouseMoveEvent, Render,
5 UniformListScrollHandle, View,
6};
7use project::WorktreeId;
8use settings::Settings;
9use std::{path::Path, sync::Arc};
10use theme::ThemeSettings;
11use ui::prelude::*;
12use workspace::item::Item;
13
14pub struct ProjectIndexDebugView {
15 index: Model<ProjectIndex>,
16 rows: Vec<Row>,
17 selected_path: Option<PathState>,
18 hovered_row_ix: Option<usize>,
19 focus_handle: FocusHandle,
20 list_scroll_handle: UniformListScrollHandle,
21 _subscription: gpui::Subscription,
22}
23
24struct PathState {
25 path: Arc<Path>,
26 chunks: Vec<SharedString>,
27 list_state: ListState,
28}
29
30enum Row {
31 Worktree(Arc<Path>),
32 Entry(WorktreeId, Arc<Path>),
33}
34
35impl ProjectIndexDebugView {
36 pub fn new(index: Model<ProjectIndex>, cx: &mut ViewContext<Self>) -> Self {
37 let mut this = Self {
38 rows: Vec::new(),
39 list_scroll_handle: UniformListScrollHandle::new(),
40 selected_path: None,
41 hovered_row_ix: None,
42 focus_handle: cx.focus_handle(),
43 _subscription: cx.subscribe(&index, |this, _, _, cx| this.update_rows(cx)),
44 index,
45 };
46 this.update_rows(cx);
47 this
48 }
49
50 fn update_rows(&mut self, cx: &mut ViewContext<Self>) {
51 let worktree_indices = self.index.read(cx).worktree_indices(cx);
52 cx.spawn(|this, mut cx| async move {
53 let mut rows = Vec::new();
54
55 for index in worktree_indices {
56 let (root_path, worktree_id, worktree_paths) =
57 index.read_with(&cx, |index, cx| {
58 let worktree = index.worktree.read(cx);
59 (worktree.abs_path(), worktree.id(), index.paths(cx))
60 })?;
61 rows.push(Row::Worktree(root_path));
62 rows.extend(
63 worktree_paths
64 .await?
65 .into_iter()
66 .map(|path| Row::Entry(worktree_id, path)),
67 );
68 }
69
70 this.update(&mut cx, |this, cx| {
71 this.rows = rows;
72 cx.notify();
73 })
74 })
75 .detach();
76 }
77
78 fn handle_path_click(
79 &mut self,
80 worktree_id: WorktreeId,
81 file_path: Arc<Path>,
82 cx: &mut ViewContext<Self>,
83 ) -> Option<()> {
84 let project_index = self.index.read(cx);
85 let fs = project_index.fs.clone();
86 let worktree_index = project_index.worktree_index(worktree_id, cx)?.read(cx);
87 let root_path = worktree_index.worktree.read(cx).abs_path();
88 let chunks = worktree_index.chunks_for_path(file_path.clone(), cx);
89
90 cx.spawn(|this, mut cx| async move {
91 let chunks = chunks.await?;
92 let content = fs.load(&root_path.join(&file_path)).await?;
93 let chunks = chunks
94 .into_iter()
95 .map(|chunk| {
96 let mut start = chunk.chunk.range.start.min(content.len());
97 let mut end = chunk.chunk.range.end.min(content.len());
98 while !content.is_char_boundary(start) {
99 start += 1;
100 }
101 while !content.is_char_boundary(end) {
102 end -= 1;
103 }
104 content[start..end].to_string().into()
105 })
106 .collect::<Vec<_>>();
107
108 this.update(&mut cx, |this, cx| {
109 let view = cx.view().downgrade();
110 this.selected_path = Some(PathState {
111 path: file_path,
112 list_state: ListState::new(
113 chunks.len(),
114 gpui::ListAlignment::Top,
115 px(100.),
116 move |ix, cx| {
117 if let Some(view) = view.upgrade() {
118 view.update(cx, |view, cx| view.render_chunk(ix, cx))
119 } else {
120 div().into_any()
121 }
122 },
123 ),
124 chunks,
125 });
126 cx.notify();
127 })
128 })
129 .detach();
130 None
131 }
132
133 fn render_chunk(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
134 let buffer_font = ThemeSettings::get_global(cx).buffer_font.clone();
135 let Some(state) = &self.selected_path else {
136 return div().into_any();
137 };
138
139 let colors = cx.theme().colors();
140 let chunk = &state.chunks[ix];
141
142 div()
143 .text_ui(cx)
144 .w_full()
145 .font(buffer_font)
146 .child(
147 h_flex()
148 .justify_between()
149 .child(format!(
150 "chunk {} of {}. length: {}",
151 ix + 1,
152 state.chunks.len(),
153 chunk.len(),
154 ))
155 .child(
156 h_flex()
157 .child(
158 Button::new(("prev", ix), "prev")
159 .disabled(ix == 0)
160 .on_click(cx.listener(move |this, _, _| {
161 this.scroll_to_chunk(ix.saturating_sub(1))
162 })),
163 )
164 .child(
165 Button::new(("next", ix), "next")
166 .disabled(ix + 1 == state.chunks.len())
167 .on_click(
168 cx.listener(move |this, _, _| this.scroll_to_chunk(ix + 1)),
169 ),
170 ),
171 ),
172 )
173 .child(
174 div()
175 .bg(colors.editor_background)
176 .text_xs()
177 .child(chunk.clone()),
178 )
179 .into_any_element()
180 }
181
182 fn scroll_to_chunk(&mut self, ix: usize) {
183 if let Some(state) = self.selected_path.as_mut() {
184 state.list_state.scroll_to(ListOffset {
185 item_ix: ix,
186 offset_in_item: px(0.),
187 })
188 }
189 }
190}
191
192impl Render for ProjectIndexDebugView {
193 fn render(&mut self, cx: &mut gpui::ViewContext<'_, Self>) -> impl IntoElement {
194 if let Some(selected_path) = self.selected_path.as_ref() {
195 v_flex()
196 .child(
197 div()
198 .id("selected-path-name")
199 .child(
200 h_flex()
201 .justify_between()
202 .child(selected_path.path.to_string_lossy().to_string())
203 .child("x"),
204 )
205 .border_b_1()
206 .border_color(cx.theme().colors().border)
207 .cursor(CursorStyle::PointingHand)
208 .on_click(cx.listener(|this, _, cx| {
209 this.selected_path.take();
210 cx.notify();
211 })),
212 )
213 .child(list(selected_path.list_state.clone()).size_full())
214 .size_full()
215 .into_any_element()
216 } else {
217 let mut list = uniform_list(
218 cx.view().clone(),
219 "ProjectIndexDebugView",
220 self.rows.len(),
221 move |this, range, cx| {
222 this.rows[range]
223 .iter()
224 .enumerate()
225 .map(|(ix, row)| match row {
226 Row::Worktree(root_path) => div()
227 .id(ix)
228 .child(Label::new(root_path.to_string_lossy().to_string())),
229 Row::Entry(worktree_id, file_path) => div()
230 .id(ix)
231 .pl_8()
232 .child(Label::new(file_path.to_string_lossy().to_string()))
233 .on_mouse_move(cx.listener(move |this, _: &MouseMoveEvent, cx| {
234 if this.hovered_row_ix != Some(ix) {
235 this.hovered_row_ix = Some(ix);
236 cx.notify();
237 }
238 }))
239 .cursor(CursorStyle::PointingHand)
240 .on_click(cx.listener({
241 let worktree_id = *worktree_id;
242 let file_path = file_path.clone();
243 move |this, _, cx| {
244 this.handle_path_click(worktree_id, file_path.clone(), cx);
245 }
246 })),
247 })
248 .collect()
249 },
250 )
251 .track_scroll(self.list_scroll_handle.clone())
252 .size_full()
253 .text_bg(cx.theme().colors().background)
254 .into_any_element();
255
256 canvas(
257 move |bounds, cx| {
258 list.prepaint_as_root(bounds.origin, bounds.size.into(), cx);
259 list
260 },
261 |_, mut list, cx| {
262 list.paint(cx);
263 },
264 )
265 .size_full()
266 .into_any_element()
267 }
268 }
269}
270
271impl EventEmitter<()> for ProjectIndexDebugView {}
272
273impl Item for ProjectIndexDebugView {
274 type Event = ();
275
276 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
277 Some("Project Index (Debug)".into())
278 }
279
280 fn clone_on_split(
281 &self,
282 _: Option<workspace::WorkspaceId>,
283 cx: &mut ViewContext<Self>,
284 ) -> Option<View<Self>>
285 where
286 Self: Sized,
287 {
288 Some(cx.new_view(|cx| Self::new(self.index.clone(), cx)))
289 }
290}
291
292impl FocusableView for ProjectIndexDebugView {
293 fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
294 self.focus_handle.clone()
295 }
296}