file_finder.rs

  1use crate::{
  2    editor::{buffer_view, BufferView},
  3    settings::Settings,
  4    util, watch,
  5    workspace::{Workspace, WorkspaceView},
  6    worktree::{match_paths, PathMatch, Worktree},
  7};
  8use gpui::{
  9    color::{ColorF, ColorU},
 10    elements::*,
 11    fonts::{Properties, Weight},
 12    geometry::vector::vec2f,
 13    keymap::{self, Binding},
 14    App, AppContext, Axis, Border, Entity, ModelHandle, View, ViewContext, ViewHandle,
 15    WeakViewHandle,
 16};
 17use std::cmp;
 18
 19pub struct FileFinder {
 20    handle: WeakViewHandle<Self>,
 21    settings: watch::Receiver<Settings>,
 22    workspace: ModelHandle<Workspace>,
 23    query_buffer: ViewHandle<BufferView>,
 24    search_count: usize,
 25    latest_search_id: usize,
 26    matches: Vec<PathMatch>,
 27    selected: usize,
 28    list_state: UniformListState,
 29}
 30
 31pub fn init(app: &mut App) {
 32    app.add_action("file_finder:toggle", FileFinder::toggle);
 33    app.add_action("file_finder:confirm", FileFinder::confirm);
 34    app.add_action("file_finder:select", FileFinder::select);
 35    app.add_action("buffer:move_up", FileFinder::select_prev);
 36    app.add_action("buffer:move_down", FileFinder::select_next);
 37    app.add_action("uniform_list:scroll", FileFinder::scroll);
 38
 39    app.add_bindings(vec![
 40        Binding::new("cmd-p", "file_finder:toggle", None),
 41        Binding::new("escape", "file_finder:toggle", Some("FileFinder")),
 42        Binding::new("enter", "file_finder:confirm", Some("FileFinder")),
 43    ]);
 44}
 45
 46pub enum Event {
 47    Selected(usize, usize),
 48    Dismissed,
 49}
 50
 51impl Entity for FileFinder {
 52    type Event = Event;
 53}
 54
 55impl View for FileFinder {
 56    fn ui_name() -> &'static str {
 57        "FileFinder"
 58    }
 59
 60    fn render(&self, _: &AppContext) -> Box<dyn Element> {
 61        Align::new(
 62            ConstrainedBox::new(
 63                Container::new(
 64                    Flex::new(Axis::Vertical)
 65                        .with_child(ChildView::new(self.query_buffer.id()).boxed())
 66                        .with_child(Expanded::new(1.0, self.render_matches()).boxed())
 67                        .boxed(),
 68                )
 69                .with_margin_top(12.0)
 70                .with_uniform_padding(6.0)
 71                .with_corner_radius(6.0)
 72                .with_background_color(ColorU::new(0xff, 0xf2, 0xf2, 0xff))
 73                // .with_background_color(ColorU::new(0xf2, 0xf2, 0xf2, 0xff))
 74                .with_shadow(
 75                    vec2f(0.0, 4.0),
 76                    12.0,
 77                    ColorF::new(0.0, 0.0, 0.0, 0.25).to_u8(),
 78                )
 79                .boxed(),
 80            )
 81            .with_max_width(600.0)
 82            .with_max_height(400.0)
 83            .boxed(),
 84        )
 85        .top_center()
 86        .boxed()
 87    }
 88
 89    fn on_focus(&mut self, ctx: &mut ViewContext<Self>) {
 90        ctx.focus(&self.query_buffer);
 91    }
 92
 93    fn keymap_context(&self, _: &AppContext) -> keymap::Context {
 94        let mut ctx = Self::default_keymap_context();
 95        ctx.set.insert("menu".into());
 96        ctx
 97    }
 98}
 99
100impl FileFinder {
101    fn render_matches(&self) -> Box<dyn Element> {
102        if self.matches.is_empty() {
103            let settings = smol::block_on(self.settings.read());
104            return Container::new(
105                Label::new(
106                    "No matches".into(),
107                    settings.ui_font_family,
108                    settings.ui_font_size,
109                )
110                .boxed(),
111            )
112            .with_margin_top(6.0)
113            .boxed();
114        }
115
116        let handle = self.handle.clone();
117        let list = UniformList::new(
118            self.list_state.clone(),
119            self.matches.len(),
120            move |mut range, items, app| {
121                let finder = handle.upgrade(app).unwrap();
122                let finder = finder.as_ref(app);
123                let start = range.start;
124                range.end = cmp::min(range.end, finder.matches.len());
125                items.extend(finder.matches[range].iter().enumerate().filter_map(
126                    move |(i, path_match)| finder.render_match(path_match, start + i, app),
127                ));
128            },
129        );
130
131        Container::new(list.boxed())
132            .with_background_color(ColorU::new(0xf7, 0xf7, 0xf7, 0xff))
133            .with_border(Border::all(1.0, ColorU::new(0xdb, 0xdb, 0xdc, 0xff)))
134            .with_margin_top(6.0)
135            .boxed()
136    }
137
138    fn render_match(
139        &self,
140        path_match: &PathMatch,
141        index: usize,
142        app: &AppContext,
143    ) -> Option<Box<dyn Element>> {
144        let tree_id = path_match.tree_id;
145        let entry_id = path_match.entry_id;
146
147        self.worktree(tree_id, app).map(|tree| {
148            let path = tree.entry_path(entry_id).unwrap();
149            let file_name = path
150                .file_name()
151                .unwrap_or_default()
152                .to_string_lossy()
153                .to_string();
154
155            let mut path = path.to_string_lossy().to_string();
156            if path_match.skipped_prefix_len > 0 {
157                let mut i = 0;
158                path.retain(|_| util::post_inc(&mut i) >= path_match.skipped_prefix_len)
159            }
160
161            let path_positions = path_match.positions.clone();
162            let file_name_start = path.chars().count() - file_name.chars().count();
163            let mut file_name_positions = Vec::new();
164            file_name_positions.extend(path_positions.iter().filter_map(|pos| {
165                if pos >= &file_name_start {
166                    Some(pos - file_name_start)
167                } else {
168                    None
169                }
170            }));
171
172            let settings = smol::block_on(self.settings.read());
173            let highlight_color = ColorU::new(0x30, 0x4e, 0xe2, 0xff);
174            let bold = *Properties::new().weight(Weight::BOLD);
175
176            let mut container = Container::new(
177                Flex::row()
178                    .with_child(
179                        Container::new(
180                            LineBox::new(
181                                settings.ui_font_family,
182                                settings.ui_font_size,
183                                Svg::new("icons/file-16.svg".into()).boxed(),
184                            )
185                            .boxed(),
186                        )
187                        .with_padding_right(6.0)
188                        .boxed(),
189                    )
190                    .with_child(
191                        Expanded::new(
192                            1.0,
193                            Flex::column()
194                                .with_child(
195                                    Label::new(
196                                        file_name,
197                                        settings.ui_font_family,
198                                        settings.ui_font_size,
199                                    )
200                                    .with_highlights(highlight_color, bold, file_name_positions)
201                                    .boxed(),
202                                )
203                                .with_child(
204                                    Label::new(
205                                        path.into(),
206                                        settings.ui_font_family,
207                                        settings.ui_font_size,
208                                    )
209                                    .with_highlights(highlight_color, bold, path_positions)
210                                    .boxed(),
211                                )
212                                .boxed(),
213                        )
214                        .boxed(),
215                    )
216                    .boxed(),
217            )
218            .with_uniform_padding(6.0);
219
220            if index == self.selected || index < self.matches.len() - 1 {
221                container =
222                    container.with_border(Border::bottom(1.0, ColorU::new(0xdb, 0xdb, 0xdc, 0xff)));
223            }
224
225            if index == self.selected {
226                container = container.with_background_color(ColorU::new(0xdb, 0xdb, 0xdc, 0xff));
227            }
228
229            EventHandler::new(container.boxed())
230                .on_mouse_down(move |ctx, _| {
231                    ctx.dispatch_action("file_finder:select", (tree_id, entry_id));
232                    true
233                })
234                .boxed()
235        })
236    }
237
238    fn toggle(workspace_view: &mut WorkspaceView, _: &(), ctx: &mut ViewContext<WorkspaceView>) {
239        workspace_view.toggle_modal(ctx, |ctx, workspace_view| {
240            let handle = ctx.add_view(|ctx| {
241                Self::new(
242                    workspace_view.settings.clone(),
243                    workspace_view.workspace.clone(),
244                    ctx,
245                )
246            });
247            ctx.subscribe_to_view(&handle, Self::on_event);
248            handle
249        });
250    }
251
252    fn on_event(
253        workspace_view: &mut WorkspaceView,
254        _: ViewHandle<FileFinder>,
255        event: &Event,
256        ctx: &mut ViewContext<WorkspaceView>,
257    ) {
258        match event {
259            Event::Selected(tree_id, entry_id) => {
260                workspace_view.open_entry((*tree_id, *entry_id), ctx);
261                workspace_view.dismiss_modal(ctx);
262            }
263            Event::Dismissed => {
264                workspace_view.dismiss_modal(ctx);
265            }
266        }
267    }
268
269    pub fn new(
270        settings: watch::Receiver<Settings>,
271        workspace: ModelHandle<Workspace>,
272        ctx: &mut ViewContext<Self>,
273    ) -> Self {
274        ctx.observe(&workspace, Self::workspace_updated);
275
276        let query_buffer = ctx.add_view(|ctx| BufferView::single_line(settings.clone(), ctx));
277        ctx.subscribe_to_view(&query_buffer, Self::on_query_buffer_event);
278
279        settings.notify_view_on_change(ctx);
280
281        Self {
282            handle: ctx.handle(),
283            settings,
284            workspace,
285            query_buffer,
286            search_count: 0,
287            latest_search_id: 0,
288            matches: Vec::new(),
289            selected: 0,
290            list_state: UniformListState::new(),
291        }
292    }
293
294    fn workspace_updated(&mut self, _: ModelHandle<Workspace>, ctx: &mut ViewContext<Self>) {
295        self.spawn_search(self.query_buffer.as_ref(ctx).text(ctx.app()), ctx);
296    }
297
298    fn on_query_buffer_event(
299        &mut self,
300        _: ViewHandle<BufferView>,
301        event: &buffer_view::Event,
302        ctx: &mut ViewContext<Self>,
303    ) {
304        use buffer_view::Event::*;
305        match event {
306            Edited => {
307                let query = self.query_buffer.as_ref(ctx).text(ctx.app());
308                if query.is_empty() {
309                    self.latest_search_id = util::post_inc(&mut self.search_count);
310                    self.matches.clear();
311                    ctx.notify();
312                } else {
313                    self.spawn_search(query, ctx);
314                }
315            }
316            Blurred => ctx.emit(Event::Dismissed),
317            Activate => {}
318        }
319    }
320
321    fn select_prev(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
322        if self.selected > 0 {
323            self.selected -= 1;
324        }
325        self.list_state.scroll_to(self.selected);
326        ctx.notify();
327    }
328
329    fn select_next(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
330        if self.selected + 1 < self.matches.len() {
331            self.selected += 1;
332        }
333        self.list_state.scroll_to(self.selected);
334        ctx.notify();
335    }
336
337    fn scroll(&mut self, _: &f32, ctx: &mut ViewContext<Self>) {
338        ctx.notify();
339    }
340
341    fn confirm(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
342        if let Some(m) = self.matches.get(self.selected) {
343            ctx.emit(Event::Selected(m.tree_id, m.entry_id));
344        }
345    }
346
347    fn select(&mut self, entry: &(usize, usize), ctx: &mut ViewContext<Self>) {
348        let (tree_id, entry_id) = *entry;
349        log::info!("selected item! {} {}", tree_id, entry_id);
350        ctx.emit(Event::Selected(tree_id, entry_id));
351    }
352
353    fn spawn_search(&mut self, query: String, ctx: &mut ViewContext<Self>) {
354        log::info!("spawn search!");
355
356        let worktrees = self.worktrees(ctx.app());
357        let search_id = util::post_inc(&mut self.search_count);
358        let task = ctx.background_executor().spawn(async move {
359            let matches = match_paths(worktrees.as_slice(), &query, false, false, 100);
360            (search_id, matches)
361        });
362
363        ctx.spawn(task, Self::update_matches).detach();
364    }
365
366    fn update_matches(
367        &mut self,
368        (search_id, matches): (usize, Vec<PathMatch>),
369        ctx: &mut ViewContext<Self>,
370    ) {
371        if search_id >= self.latest_search_id {
372            self.latest_search_id = search_id;
373            self.matches = matches;
374            self.selected = 0;
375            self.list_state.scroll_to(0);
376            ctx.notify();
377        }
378    }
379
380    fn worktree<'a>(&'a self, tree_id: usize, app: &'a AppContext) -> Option<&'a Worktree> {
381        self.workspace
382            .as_ref(app)
383            .worktrees()
384            .get(&tree_id)
385            .map(|worktree| worktree.as_ref(app))
386    }
387
388    fn worktrees(&self, app: &AppContext) -> Vec<Worktree> {
389        self.workspace
390            .as_ref(app)
391            .worktrees()
392            .iter()
393            .map(|worktree| worktree.as_ref(app).clone())
394            .collect()
395    }
396}
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401    use crate::{
402        editor, settings,
403        workspace::{Workspace, WorkspaceView},
404    };
405    use anyhow::Result;
406    use gpui::App;
407    use smol::fs;
408    use tempdir::TempDir;
409
410    #[test]
411    fn test_matching_paths() -> Result<()> {
412        App::test((), |mut app| async move {
413            let tmp_dir = TempDir::new("example")?;
414            fs::create_dir(tmp_dir.path().join("a")).await?;
415            fs::write(tmp_dir.path().join("a/banana"), "banana").await?;
416            fs::write(tmp_dir.path().join("a/bandana"), "bandana").await?;
417            super::init(&mut app);
418            editor::init(&mut app);
419
420            let settings = settings::channel(&app.fonts()).unwrap().1;
421            let workspace = app.add_model(|ctx| Workspace::new(vec![tmp_dir.path().into()], ctx));
422            let (window_id, workspace_view) =
423                app.add_window(|ctx| WorkspaceView::new(workspace.clone(), settings, ctx));
424            app.finish_pending_tasks().await; // Open and populate worktree.
425            app.dispatch_action(
426                window_id,
427                vec![workspace_view.id()],
428                "file_finder:toggle".into(),
429                (),
430            );
431            let (finder, query_buffer) = workspace_view.read(&app, |view, ctx| {
432                let finder = view
433                    .modal()
434                    .cloned()
435                    .unwrap()
436                    .downcast::<FileFinder>()
437                    .unwrap();
438                let query_buffer = finder.as_ref(ctx).query_buffer.clone();
439                (finder, query_buffer)
440            });
441
442            let chain = vec![finder.id(), query_buffer.id()];
443            app.dispatch_action(window_id, chain.clone(), "buffer:insert", "b".to_string());
444            app.dispatch_action(window_id, chain.clone(), "buffer:insert", "n".to_string());
445            app.dispatch_action(window_id, chain.clone(), "buffer:insert", "a".to_string());
446            app.finish_pending_tasks().await; // Complete path search.
447
448            // let view_state = finder.state(&app);
449            // assert!(view_state.matches.len() > 1);
450            // app.dispatch_action(
451            //     window_id,
452            //     vec![workspace_view.id(), finder.id()],
453            //     "menu:select_next",
454            //     (),
455            // );
456            // app.dispatch_action(
457            //     window_id,
458            //     vec![workspace_view.id(), finder.id()],
459            //     "file_finder:confirm",
460            //     (),
461            // );
462            // app.finish_pending_tasks().await; // Load Buffer and open BufferView.
463            // let active_pane = workspace_view.read(&app, |view, _| view.active_pane().clone());
464            // assert_eq!(
465            //     active_pane.state(&app),
466            //     pane::State {
467            //         tabs: vec![pane::TabState {
468            //             title: "bandana".into(),
469            //             active: true,
470            //         }]
471            //     }
472            // );
473            Ok(())
474        })
475    }
476}