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                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}