project_index_debug_view.rs

  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}