project_index_debug_view.rs

  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}