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