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}