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