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 let chunk_count = state.chunks.len();
139
140 let prev_button = Button::new(("prev", ix), "prev", cx)
141 .disabled(ix == 0)
142 .on_click(cx.listener(move |this, _, _, _| this.scroll_to_chunk(ix.saturating_sub(1))));
143
144 let next_button = Button::new(("next", ix), "next", cx)
145 .disabled(ix + 1 == chunk_count)
146 .on_click(cx.listener(move |this, _, _, _| this.scroll_to_chunk(ix + 1)));
147
148 div()
149 .text_ui(cx)
150 .w_full()
151 .font(buffer_font)
152 .child(
153 h_flex()
154 .justify_between()
155 .child(format!(
156 "chunk {} of {}. length: {}",
157 ix + 1,
158 chunk_count,
159 chunk.len(),
160 ))
161 .child(h_flex().child(prev_button).child(next_button)),
162 )
163 .child(
164 div()
165 .bg(colors.editor_background)
166 .text_xs()
167 .child(chunk.clone()),
168 )
169 .into_any_element()
170 }
171
172 fn scroll_to_chunk(&mut self, ix: usize) {
173 if let Some(state) = self.selected_path.as_mut() {
174 state.list_state.scroll_to(ListOffset {
175 item_ix: ix,
176 offset_in_item: px(0.),
177 })
178 }
179 }
180}
181
182impl Render for ProjectIndexDebugView {
183 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
184 if let Some(selected_path) = self.selected_path.as_ref() {
185 v_flex()
186 .child(
187 div()
188 .id("selected-path-name")
189 .child(
190 h_flex()
191 .justify_between()
192 .child(selected_path.path.to_string_lossy().to_string())
193 .child("x"),
194 )
195 .border_b_1()
196 .border_color(cx.theme().colors().border)
197 .cursor(CursorStyle::PointingHand)
198 .on_click(cx.listener(|this, _, _, cx| {
199 this.selected_path.take();
200 cx.notify();
201 })),
202 )
203 .child(
204 list(
205 selected_path.list_state.clone(),
206 cx.processor(|this, ix, _, cx| this.render_chunk(ix, cx)),
207 )
208 .size_full(),
209 )
210 .size_full()
211 .into_any_element()
212 } else {
213 let mut list = uniform_list(
214 "ProjectIndexDebugView",
215 self.rows.len(),
216 cx.processor(move |this, range: Range<usize>, _, cx| {
217 this.rows[range]
218 .iter()
219 .enumerate()
220 .map(|(ix, row)| match row {
221 Row::Worktree(root_path) => div()
222 .id(ix)
223 .child(Label::new(root_path.to_string_lossy().to_string())),
224 Row::Entry(worktree_id, file_path) => div()
225 .id(ix)
226 .pl_8()
227 .child(Label::new(file_path.to_string_lossy().to_string()))
228 .on_mouse_move(cx.listener(
229 move |this, _: &MouseMoveEvent, _, cx| {
230 if this.hovered_row_ix != Some(ix) {
231 this.hovered_row_ix = Some(ix);
232 cx.notify();
233 }
234 },
235 ))
236 .cursor(CursorStyle::PointingHand)
237 .on_click(cx.listener({
238 let worktree_id = *worktree_id;
239 let file_path = file_path.clone();
240 move |this, _, window, cx| {
241 this.handle_path_click(
242 worktree_id,
243 file_path.clone(),
244 window,
245 cx,
246 );
247 }
248 })),
249 })
250 .collect()
251 }),
252 )
253 .track_scroll(self.list_scroll_handle.clone())
254 .size_full()
255 .text_bg(cx.theme().colors().background)
256 .into_any_element();
257
258 canvas(
259 move |bounds, window, cx| {
260 list.prepaint_as_root(bounds.origin, bounds.size.into(), window, cx);
261 list
262 },
263 |_, mut list, window, cx| {
264 list.paint(window, cx);
265 },
266 )
267 .size_full()
268 .into_any_element()
269 }
270 }
271}
272
273impl EventEmitter<()> for ProjectIndexDebugView {}
274
275impl Item for ProjectIndexDebugView {
276 type Event = ();
277
278 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
279 "Project Index (Debug)".into()
280 }
281
282 fn clone_on_split(
283 &self,
284 _: Option<workspace::WorkspaceId>,
285 window: &mut Window,
286 cx: &mut Context<Self>,
287 ) -> Option<Entity<Self>>
288 where
289 Self: Sized,
290 {
291 Some(cx.new(|cx| Self::new(self.index.clone(), window, cx)))
292 }
293}
294
295impl Focusable for ProjectIndexDebugView {
296 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
297 self.focus_handle.clone()
298 }
299}