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