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