1use collections::HashMap;
2use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
3use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
4use gpui::{
5 actions, div, AppContext, Component, Div, EventEmitter, InteractiveComponent, Model,
6 ParentComponent, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
7 WindowContext,
8};
9use picker::{Picker, PickerDelegate};
10use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
11use std::{
12 path::{Path, PathBuf},
13 sync::{
14 atomic::{self, AtomicBool},
15 Arc,
16 },
17};
18use text::Point;
19use theme::ActiveTheme;
20use ui::{v_stack, HighlightedLabel, StyledExt};
21use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
22use workspace::{Modal, ModalEvent, Workspace};
23
24actions!(Toggle);
25
26pub struct FileFinder {
27 picker: View<Picker<FileFinderDelegate>>,
28}
29
30pub fn init(cx: &mut AppContext) {
31 cx.observe_new_views(FileFinder::register).detach();
32}
33
34impl FileFinder {
35 fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
36 workspace.register_action(|workspace, _: &Toggle, cx| {
37 let Some(file_finder) = workspace.current_modal::<Self>(cx) else {
38 Self::open(workspace, cx);
39 return;
40 };
41
42 file_finder.update(cx, |file_finder, cx| {
43 file_finder
44 .picker
45 .update(cx, |picker, cx| picker.cycle_selection(cx))
46 });
47 });
48 }
49
50 fn open(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
51 let project = workspace.project().read(cx);
52
53 let currently_opened_path = workspace
54 .active_item(cx)
55 .and_then(|item| item.project_path(cx))
56 .map(|project_path| {
57 let abs_path = project
58 .worktree_for_id(project_path.worktree_id, cx)
59 .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path));
60 FoundPath::new(project_path, abs_path)
61 });
62
63 // if exists, bubble the currently opened path to the top
64 let history_items = currently_opened_path
65 .clone()
66 .into_iter()
67 .chain(
68 workspace
69 .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
70 .into_iter()
71 .filter(|(history_path, _)| {
72 Some(history_path)
73 != currently_opened_path
74 .as_ref()
75 .map(|found_path| &found_path.project)
76 })
77 .filter(|(_, history_abs_path)| {
78 history_abs_path.as_ref()
79 != currently_opened_path
80 .as_ref()
81 .and_then(|found_path| found_path.absolute.as_ref())
82 })
83 .filter(|(_, history_abs_path)| match history_abs_path {
84 Some(abs_path) => history_file_exists(abs_path),
85 None => true,
86 })
87 .map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
88 )
89 .collect();
90
91 let project = workspace.project().clone();
92 let weak_workspace = cx.view().downgrade();
93 workspace.toggle_modal(cx, |cx| {
94 let delegate = FileFinderDelegate::new(
95 cx.view().downgrade(),
96 weak_workspace,
97 project,
98 currently_opened_path,
99 history_items,
100 cx,
101 );
102
103 FileFinder::new(delegate, cx)
104 });
105 }
106
107 fn new(delegate: FileFinderDelegate, cx: &mut ViewContext<Self>) -> Self {
108 Self {
109 picker: cx.build_view(|cx| Picker::new(delegate, cx)),
110 }
111 }
112}
113
114impl EventEmitter<ModalEvent> for FileFinder {}
115impl Modal for FileFinder {
116 fn focus(&self, cx: &mut WindowContext) {
117 self.picker.update(cx, |picker, cx| picker.focus(cx))
118 }
119}
120impl Render for FileFinder {
121 type Element = Div<Self>;
122
123 fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
124 v_stack().w_96().child(self.picker.clone())
125 }
126}
127
128pub struct FileFinderDelegate {
129 file_finder: WeakView<FileFinder>,
130 workspace: WeakView<Workspace>,
131 project: Model<Project>,
132 search_count: usize,
133 latest_search_id: usize,
134 latest_search_did_cancel: bool,
135 latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
136 currently_opened_path: Option<FoundPath>,
137 matches: Matches,
138 selected_index: Option<usize>,
139 cancel_flag: Arc<AtomicBool>,
140 history_items: Vec<FoundPath>,
141}
142
143#[derive(Debug, Default)]
144struct Matches {
145 history: Vec<(FoundPath, Option<PathMatch>)>,
146 search: Vec<PathMatch>,
147}
148
149#[derive(Debug)]
150enum Match<'a> {
151 History(&'a FoundPath, Option<&'a PathMatch>),
152 Search(&'a PathMatch),
153}
154
155impl Matches {
156 fn len(&self) -> usize {
157 self.history.len() + self.search.len()
158 }
159
160 fn get(&self, index: usize) -> Option<Match<'_>> {
161 if index < self.history.len() {
162 self.history
163 .get(index)
164 .map(|(path, path_match)| Match::History(path, path_match.as_ref()))
165 } else {
166 self.search
167 .get(index - self.history.len())
168 .map(Match::Search)
169 }
170 }
171
172 fn push_new_matches(
173 &mut self,
174 history_items: &Vec<FoundPath>,
175 query: &PathLikeWithPosition<FileSearchQuery>,
176 mut new_search_matches: Vec<PathMatch>,
177 extend_old_matches: bool,
178 ) {
179 let matching_history_paths = matching_history_item_paths(history_items, query);
180 new_search_matches
181 .retain(|path_match| !matching_history_paths.contains_key(&path_match.path));
182 let history_items_to_show = history_items
183 .iter()
184 .filter_map(|history_item| {
185 Some((
186 history_item.clone(),
187 Some(
188 matching_history_paths
189 .get(&history_item.project.path)?
190 .clone(),
191 ),
192 ))
193 })
194 .collect::<Vec<_>>();
195 self.history = history_items_to_show;
196 if extend_old_matches {
197 self.search
198 .retain(|path_match| !matching_history_paths.contains_key(&path_match.path));
199 util::extend_sorted(
200 &mut self.search,
201 new_search_matches.into_iter(),
202 100,
203 |a, b| b.cmp(a),
204 )
205 } else {
206 self.search = new_search_matches;
207 }
208 }
209}
210
211fn matching_history_item_paths(
212 history_items: &Vec<FoundPath>,
213 query: &PathLikeWithPosition<FileSearchQuery>,
214) -> HashMap<Arc<Path>, PathMatch> {
215 let history_items_by_worktrees = history_items
216 .iter()
217 .filter_map(|found_path| {
218 let candidate = PathMatchCandidate {
219 path: &found_path.project.path,
220 // Only match history items names, otherwise their paths may match too many queries, producing false positives.
221 // E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item,
222 // it would be shown first always, despite the latter being a better match.
223 char_bag: CharBag::from_iter(
224 found_path
225 .project
226 .path
227 .file_name()?
228 .to_string_lossy()
229 .to_lowercase()
230 .chars(),
231 ),
232 };
233 Some((found_path.project.worktree_id, candidate))
234 })
235 .fold(
236 HashMap::default(),
237 |mut candidates, (worktree_id, new_candidate)| {
238 candidates
239 .entry(worktree_id)
240 .or_insert_with(Vec::new)
241 .push(new_candidate);
242 candidates
243 },
244 );
245 let mut matching_history_paths = HashMap::default();
246 for (worktree, candidates) in history_items_by_worktrees {
247 let max_results = candidates.len() + 1;
248 matching_history_paths.extend(
249 fuzzy::match_fixed_path_set(
250 candidates,
251 worktree.to_usize(),
252 query.path_like.path_query(),
253 false,
254 max_results,
255 )
256 .into_iter()
257 .map(|path_match| (Arc::clone(&path_match.path), path_match)),
258 );
259 }
260 matching_history_paths
261}
262
263#[derive(Debug, Clone, PartialEq, Eq)]
264struct FoundPath {
265 project: ProjectPath,
266 absolute: Option<PathBuf>,
267}
268
269impl FoundPath {
270 fn new(project: ProjectPath, absolute: Option<PathBuf>) -> Self {
271 Self { project, absolute }
272 }
273}
274
275const MAX_RECENT_SELECTIONS: usize = 20;
276
277#[cfg(not(test))]
278fn history_file_exists(abs_path: &PathBuf) -> bool {
279 abs_path.exists()
280}
281
282#[cfg(test)]
283fn history_file_exists(abs_path: &PathBuf) -> bool {
284 !abs_path.ends_with("nonexistent.rs")
285}
286
287pub enum Event {
288 Selected(ProjectPath),
289 Dismissed,
290}
291
292#[derive(Debug, Clone)]
293struct FileSearchQuery {
294 raw_query: String,
295 file_query_end: Option<usize>,
296}
297
298impl FileSearchQuery {
299 fn path_query(&self) -> &str {
300 match self.file_query_end {
301 Some(file_path_end) => &self.raw_query[..file_path_end],
302 None => &self.raw_query,
303 }
304 }
305}
306
307impl FileFinderDelegate {
308 fn new(
309 file_finder: WeakView<FileFinder>,
310 workspace: WeakView<Workspace>,
311 project: Model<Project>,
312 currently_opened_path: Option<FoundPath>,
313 history_items: Vec<FoundPath>,
314 cx: &mut ViewContext<FileFinder>,
315 ) -> Self {
316 cx.observe(&project, |file_finder, _, cx| {
317 //todo!() We should probably not re-render on every project anything
318 file_finder
319 .picker
320 .update(cx, |picker, cx| picker.refresh(cx))
321 })
322 .detach();
323
324 Self {
325 file_finder,
326 workspace,
327 project,
328 search_count: 0,
329 latest_search_id: 0,
330 latest_search_did_cancel: false,
331 latest_search_query: None,
332 currently_opened_path,
333 matches: Matches::default(),
334 selected_index: None,
335 cancel_flag: Arc::new(AtomicBool::new(false)),
336 history_items,
337 }
338 }
339
340 fn spawn_search(
341 &mut self,
342 query: PathLikeWithPosition<FileSearchQuery>,
343 cx: &mut ViewContext<Picker<Self>>,
344 ) -> Task<()> {
345 let relative_to = self
346 .currently_opened_path
347 .as_ref()
348 .map(|found_path| Arc::clone(&found_path.project.path));
349 let worktrees = self
350 .project
351 .read(cx)
352 .visible_worktrees(cx)
353 .collect::<Vec<_>>();
354 let include_root_name = worktrees.len() > 1;
355 let candidate_sets = worktrees
356 .into_iter()
357 .map(|worktree| {
358 let worktree = worktree.read(cx);
359 PathMatchCandidateSet {
360 snapshot: worktree.snapshot(),
361 include_ignored: worktree
362 .root_entry()
363 .map_or(false, |entry| entry.is_ignored),
364 include_root_name,
365 }
366 })
367 .collect::<Vec<_>>();
368
369 let search_id = util::post_inc(&mut self.search_count);
370 self.cancel_flag.store(true, atomic::Ordering::Relaxed);
371 self.cancel_flag = Arc::new(AtomicBool::new(false));
372 let cancel_flag = self.cancel_flag.clone();
373 cx.spawn(|picker, mut cx| async move {
374 let matches = fuzzy::match_path_sets(
375 candidate_sets.as_slice(),
376 query.path_like.path_query(),
377 relative_to,
378 false,
379 100,
380 &cancel_flag,
381 cx.background_executor().clone(),
382 )
383 .await;
384 let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
385 picker
386 .update(&mut cx, |picker, cx| {
387 picker
388 .delegate
389 .set_search_matches(search_id, did_cancel, query, matches, cx)
390 })
391 .log_err();
392 })
393 }
394
395 fn set_search_matches(
396 &mut self,
397 search_id: usize,
398 did_cancel: bool,
399 query: PathLikeWithPosition<FileSearchQuery>,
400 matches: Vec<PathMatch>,
401 cx: &mut ViewContext<Picker<Self>>,
402 ) {
403 if search_id >= self.latest_search_id {
404 self.latest_search_id = search_id;
405 let extend_old_matches = self.latest_search_did_cancel
406 && Some(query.path_like.path_query())
407 == self
408 .latest_search_query
409 .as_ref()
410 .map(|query| query.path_like.path_query());
411 self.matches
412 .push_new_matches(&self.history_items, &query, matches, extend_old_matches);
413 self.latest_search_query = Some(query);
414 self.latest_search_did_cancel = did_cancel;
415 cx.notify();
416 }
417 }
418
419 fn labels_for_match(
420 &self,
421 path_match: Match,
422 cx: &AppContext,
423 ix: usize,
424 ) -> (String, Vec<usize>, String, Vec<usize>) {
425 let (file_name, file_name_positions, full_path, full_path_positions) = match path_match {
426 Match::History(found_path, found_path_match) => {
427 let worktree_id = found_path.project.worktree_id;
428 let project_relative_path = &found_path.project.path;
429 let has_worktree = self
430 .project
431 .read(cx)
432 .worktree_for_id(worktree_id, cx)
433 .is_some();
434
435 if !has_worktree {
436 if let Some(absolute_path) = &found_path.absolute {
437 return (
438 absolute_path
439 .file_name()
440 .map_or_else(
441 || project_relative_path.to_string_lossy(),
442 |file_name| file_name.to_string_lossy(),
443 )
444 .to_string(),
445 Vec::new(),
446 absolute_path.to_string_lossy().to_string(),
447 Vec::new(),
448 );
449 }
450 }
451
452 let mut path = Arc::clone(project_relative_path);
453 if project_relative_path.as_ref() == Path::new("") {
454 if let Some(absolute_path) = &found_path.absolute {
455 path = Arc::from(absolute_path.as_path());
456 }
457 }
458
459 let mut path_match = PathMatch {
460 score: ix as f64,
461 positions: Vec::new(),
462 worktree_id: worktree_id.to_usize(),
463 path,
464 path_prefix: "".into(),
465 distance_to_relative_ancestor: usize::MAX,
466 };
467 if let Some(found_path_match) = found_path_match {
468 path_match
469 .positions
470 .extend(found_path_match.positions.iter())
471 }
472
473 self.labels_for_path_match(&path_match)
474 }
475 Match::Search(path_match) => self.labels_for_path_match(path_match),
476 };
477
478 if file_name_positions.is_empty() {
479 if let Some(user_home_path) = std::env::var("HOME").ok() {
480 let user_home_path = user_home_path.trim();
481 if !user_home_path.is_empty() {
482 if (&full_path).starts_with(user_home_path) {
483 return (
484 file_name,
485 file_name_positions,
486 full_path.replace(user_home_path, "~"),
487 full_path_positions,
488 );
489 }
490 }
491 }
492 }
493
494 (
495 file_name,
496 file_name_positions,
497 full_path,
498 full_path_positions,
499 )
500 }
501
502 fn labels_for_path_match(
503 &self,
504 path_match: &PathMatch,
505 ) -> (String, Vec<usize>, String, Vec<usize>) {
506 let path = &path_match.path;
507 let path_string = path.to_string_lossy();
508 let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
509 let path_positions = path_match.positions.clone();
510
511 let file_name = path.file_name().map_or_else(
512 || path_match.path_prefix.to_string(),
513 |file_name| file_name.to_string_lossy().to_string(),
514 );
515 let file_name_start = path_match.path_prefix.chars().count() + path_string.chars().count()
516 - file_name.chars().count();
517 let file_name_positions = path_positions
518 .iter()
519 .filter_map(|pos| {
520 if pos >= &file_name_start {
521 Some(pos - file_name_start)
522 } else {
523 None
524 }
525 })
526 .collect();
527
528 (file_name, file_name_positions, full_path, path_positions)
529 }
530}
531
532impl PickerDelegate for FileFinderDelegate {
533 type ListItem = Div<Picker<Self>>;
534
535 fn placeholder_text(&self) -> Arc<str> {
536 "Search project files...".into()
537 }
538
539 fn match_count(&self) -> usize {
540 self.matches.len()
541 }
542
543 fn selected_index(&self) -> usize {
544 self.selected_index.unwrap_or(0)
545 }
546
547 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
548 self.selected_index = Some(ix);
549 cx.notify();
550 }
551
552 fn update_matches(
553 &mut self,
554 raw_query: String,
555 cx: &mut ViewContext<Picker<Self>>,
556 ) -> Task<()> {
557 if raw_query.is_empty() {
558 let project = self.project.read(cx);
559 self.latest_search_id = post_inc(&mut self.search_count);
560 self.matches = Matches {
561 history: self
562 .history_items
563 .iter()
564 .filter(|history_item| {
565 project
566 .worktree_for_id(history_item.project.worktree_id, cx)
567 .is_some()
568 || (project.is_local() && history_item.absolute.is_some())
569 })
570 .cloned()
571 .map(|p| (p, None))
572 .collect(),
573 search: Vec::new(),
574 };
575 cx.notify();
576 Task::ready(())
577 } else {
578 let raw_query = &raw_query;
579 let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| {
580 Ok::<_, std::convert::Infallible>(FileSearchQuery {
581 raw_query: raw_query.to_owned(),
582 file_query_end: if path_like_str == raw_query {
583 None
584 } else {
585 Some(path_like_str.len())
586 },
587 })
588 })
589 .expect("infallible");
590 self.spawn_search(query, cx)
591 }
592 }
593
594 fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
595 if let Some(m) = self.matches.get(self.selected_index()) {
596 if let Some(workspace) = self.workspace.upgrade() {
597 let open_task = workspace.update(cx, move |workspace, cx| {
598 let split_or_open = |workspace: &mut Workspace, project_path, cx| {
599 if secondary {
600 workspace.split_path(project_path, cx)
601 } else {
602 workspace.open_path(project_path, None, true, cx)
603 }
604 };
605 match m {
606 Match::History(history_match, _) => {
607 let worktree_id = history_match.project.worktree_id;
608 if workspace
609 .project()
610 .read(cx)
611 .worktree_for_id(worktree_id, cx)
612 .is_some()
613 {
614 split_or_open(
615 workspace,
616 ProjectPath {
617 worktree_id,
618 path: Arc::clone(&history_match.project.path),
619 },
620 cx,
621 )
622 } else {
623 match history_match.absolute.as_ref() {
624 Some(abs_path) => {
625 if secondary {
626 workspace.split_abs_path(
627 abs_path.to_path_buf(),
628 false,
629 cx,
630 )
631 } else {
632 workspace.open_abs_path(
633 abs_path.to_path_buf(),
634 false,
635 cx,
636 )
637 }
638 }
639 None => split_or_open(
640 workspace,
641 ProjectPath {
642 worktree_id,
643 path: Arc::clone(&history_match.project.path),
644 },
645 cx,
646 ),
647 }
648 }
649 }
650 Match::Search(m) => split_or_open(
651 workspace,
652 ProjectPath {
653 worktree_id: WorktreeId::from_usize(m.worktree_id),
654 path: m.path.clone(),
655 },
656 cx,
657 ),
658 }
659 });
660
661 let row = self
662 .latest_search_query
663 .as_ref()
664 .and_then(|query| query.row)
665 .map(|row| row.saturating_sub(1));
666 let col = self
667 .latest_search_query
668 .as_ref()
669 .and_then(|query| query.column)
670 .unwrap_or(0)
671 .saturating_sub(1);
672 let finder = self.file_finder.clone();
673
674 cx.spawn(|_, mut cx| async move {
675 let item = open_task.await.log_err()?;
676 if let Some(row) = row {
677 if let Some(active_editor) = item.downcast::<Editor>() {
678 active_editor
679 .downgrade()
680 .update(&mut cx, |editor, cx| {
681 let snapshot = editor.snapshot(cx).display_snapshot;
682 let point = snapshot
683 .buffer_snapshot
684 .clip_point(Point::new(row, col), Bias::Left);
685 editor.change_selections(Some(Autoscroll::center()), cx, |s| {
686 s.select_ranges([point..point])
687 });
688 })
689 .log_err();
690 }
691 }
692 finder
693 .update(&mut cx, |_, cx| cx.emit(ModalEvent::Dismissed))
694 .ok()?;
695
696 Some(())
697 })
698 .detach();
699 }
700 }
701 }
702
703 fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
704 self.file_finder
705 .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed))
706 .log_err();
707 }
708
709 fn render_match(
710 &self,
711 ix: usize,
712 selected: bool,
713 cx: &mut ViewContext<Picker<Self>>,
714 ) -> Self::ListItem {
715 let path_match = self
716 .matches
717 .get(ix)
718 .expect("Invalid matches state: no element for index {ix}");
719 let theme = cx.theme();
720 let colors = theme.colors();
721
722 let (file_name, file_name_positions, full_path, full_path_positions) =
723 self.labels_for_match(path_match, cx, ix);
724
725 div()
726 .px_1()
727 .text_color(colors.text)
728 .text_ui()
729 .bg(colors.ghost_element_background)
730 .rounded_md()
731 .when(selected, |this| this.bg(colors.ghost_element_selected))
732 .hover(|this| this.bg(colors.ghost_element_hover))
733 .child(
734 v_stack()
735 .child(HighlightedLabel::new(file_name, file_name_positions))
736 .child(HighlightedLabel::new(full_path, full_path_positions)),
737 )
738 }
739}
740
741// #[cfg(test)]
742// mod tests {
743// use std::{assert_eq, collections::HashMap, path::Path, time::Duration};
744
745// use super::*;
746// use editor::Editor;
747// use gpui::{Entity, TestAppContext, VisualTestContext};
748// use menu::{Confirm, SelectNext};
749// use serde_json::json;
750// use workspace::{AppState, Workspace};
751
752// #[ctor::ctor]
753// fn init_logger() {
754// if std::env::var("RUST_LOG").is_ok() {
755// env_logger::init();
756// }
757// }
758
759// #[gpui::test]
760// async fn test_matching_paths(cx: &mut TestAppContext) {
761// let app_state = init_test(cx);
762// app_state
763// .fs
764// .as_fake()
765// .insert_tree(
766// "/root",
767// json!({
768// "a": {
769// "banana": "",
770// "bandana": "",
771// }
772// }),
773// )
774// .await;
775
776// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
777
778// let (picker, workspace, mut cx) = build_find_picker(project, cx);
779// let cx = &mut cx;
780
781// picker
782// .update(cx, |picker, cx| {
783// picker.delegate.update_matches("bna".to_string(), cx)
784// })
785// .await;
786
787// picker.update(cx, |picker, _| {
788// assert_eq!(picker.delegate.matches.len(), 2);
789// });
790
791// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
792// cx.dispatch_action(SelectNext);
793// cx.dispatch_action(Confirm);
794// active_pane
795// .condition(cx, |pane, _| pane.active_item().is_some())
796// .await;
797// cx.read(|cx| {
798// let active_item = active_pane.read(cx).active_item().unwrap();
799// assert_eq!(
800// active_item
801// .to_any()
802// .downcast::<Editor>()
803// .unwrap()
804// .read(cx)
805// .title(cx),
806// "bandana"
807// );
808// });
809// }
810
811// #[gpui::test]
812// async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
813// let app_state = init_test(cx);
814
815// let first_file_name = "first.rs";
816// let first_file_contents = "// First Rust file";
817// app_state
818// .fs
819// .as_fake()
820// .insert_tree(
821// "/src",
822// json!({
823// "test": {
824// first_file_name: first_file_contents,
825// "second.rs": "// Second Rust file",
826// }
827// }),
828// )
829// .await;
830
831// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
832
833// let (picker, workspace, mut cx) = build_find_picker(project, cx);
834// let cx = &mut cx;
835
836// let file_query = &first_file_name[..3];
837// let file_row = 1;
838// let file_column = 3;
839// assert!(file_column <= first_file_contents.len());
840// let query_inside_file = format!("{file_query}:{file_row}:{file_column}");
841// picker
842// .update(cx, |finder, cx| {
843// finder
844// .delegate
845// .update_matches(query_inside_file.to_string(), cx)
846// })
847// .await;
848// picker.update(cx, |finder, _| {
849// let finder = &finder.delegate;
850// assert_eq!(finder.matches.len(), 1);
851// let latest_search_query = finder
852// .latest_search_query
853// .as_ref()
854// .expect("Finder should have a query after the update_matches call");
855// assert_eq!(latest_search_query.path_like.raw_query, query_inside_file);
856// assert_eq!(
857// latest_search_query.path_like.file_query_end,
858// Some(file_query.len())
859// );
860// assert_eq!(latest_search_query.row, Some(file_row));
861// assert_eq!(latest_search_query.column, Some(file_column as u32));
862// });
863
864// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
865// cx.dispatch_action(SelectNext);
866// cx.dispatch_action(Confirm);
867// active_pane
868// .condition(cx, |pane, _| pane.active_item().is_some())
869// .await;
870// let editor = cx.update(|cx| {
871// let active_item = active_pane.read(cx).active_item().unwrap();
872// active_item.downcast::<Editor>().unwrap()
873// });
874// cx.executor().advance_clock(Duration::from_secs(2));
875
876// editor.update(cx, |editor, cx| {
877// let all_selections = editor.selections.all_adjusted(cx);
878// assert_eq!(
879// all_selections.len(),
880// 1,
881// "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
882// );
883// let caret_selection = all_selections.into_iter().next().unwrap();
884// assert_eq!(caret_selection.start, caret_selection.end,
885// "Caret selection should have its start and end at the same position");
886// assert_eq!(file_row, caret_selection.start.row + 1,
887// "Query inside file should get caret with the same focus row");
888// assert_eq!(file_column, caret_selection.start.column as usize + 1,
889// "Query inside file should get caret with the same focus column");
890// });
891// }
892
893// #[gpui::test]
894// async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) {
895// let app_state = init_test(cx);
896
897// let first_file_name = "first.rs";
898// let first_file_contents = "// First Rust file";
899// app_state
900// .fs
901// .as_fake()
902// .insert_tree(
903// "/src",
904// json!({
905// "test": {
906// first_file_name: first_file_contents,
907// "second.rs": "// Second Rust file",
908// }
909// }),
910// )
911// .await;
912
913// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
914
915// let (picker, workspace, mut cx) = build_find_picker(project, cx);
916// let cx = &mut cx;
917
918// let file_query = &first_file_name[..3];
919// let file_row = 200;
920// let file_column = 300;
921// assert!(file_column > first_file_contents.len());
922// let query_outside_file = format!("{file_query}:{file_row}:{file_column}");
923// picker
924// .update(cx, |picker, cx| {
925// picker
926// .delegate
927// .update_matches(query_outside_file.to_string(), cx)
928// })
929// .await;
930// picker.update(cx, |finder, _| {
931// let delegate = &finder.delegate;
932// assert_eq!(delegate.matches.len(), 1);
933// let latest_search_query = delegate
934// .latest_search_query
935// .as_ref()
936// .expect("Finder should have a query after the update_matches call");
937// assert_eq!(latest_search_query.path_like.raw_query, query_outside_file);
938// assert_eq!(
939// latest_search_query.path_like.file_query_end,
940// Some(file_query.len())
941// );
942// assert_eq!(latest_search_query.row, Some(file_row));
943// assert_eq!(latest_search_query.column, Some(file_column as u32));
944// });
945
946// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
947// cx.dispatch_action(SelectNext);
948// cx.dispatch_action(Confirm);
949// active_pane
950// .condition(cx, |pane, _| pane.active_item().is_some())
951// .await;
952// let editor = cx.update(|cx| {
953// let active_item = active_pane.read(cx).active_item().unwrap();
954// active_item.downcast::<Editor>().unwrap()
955// });
956// cx.executor().advance_clock(Duration::from_secs(2));
957
958// editor.update(cx, |editor, cx| {
959// let all_selections = editor.selections.all_adjusted(cx);
960// assert_eq!(
961// all_selections.len(),
962// 1,
963// "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
964// );
965// let caret_selection = all_selections.into_iter().next().unwrap();
966// assert_eq!(caret_selection.start, caret_selection.end,
967// "Caret selection should have its start and end at the same position");
968// assert_eq!(0, caret_selection.start.row,
969// "Excessive rows (as in query outside file borders) should get trimmed to last file row");
970// assert_eq!(first_file_contents.len(), caret_selection.start.column as usize,
971// "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column");
972// });
973// }
974
975// #[gpui::test]
976// async fn test_matching_cancellation(cx: &mut TestAppContext) {
977// let app_state = init_test(cx);
978// app_state
979// .fs
980// .as_fake()
981// .insert_tree(
982// "/dir",
983// json!({
984// "hello": "",
985// "goodbye": "",
986// "halogen-light": "",
987// "happiness": "",
988// "height": "",
989// "hi": "",
990// "hiccup": "",
991// }),
992// )
993// .await;
994
995// let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
996
997// let (picker, _, mut cx) = build_find_picker(project, cx);
998// let cx = &mut cx;
999
1000// let query = test_path_like("hi");
1001// picker
1002// .update(cx, |picker, cx| {
1003// picker.delegate.spawn_search(query.clone(), cx)
1004// })
1005// .await;
1006
1007// picker.update(cx, |picker, _cx| {
1008// assert_eq!(picker.delegate.matches.len(), 5)
1009// });
1010
1011// picker.update(cx, |picker, cx| {
1012// let delegate = &mut picker.delegate;
1013// assert!(
1014// delegate.matches.history.is_empty(),
1015// "Search matches expected"
1016// );
1017// let matches = delegate.matches.search.clone();
1018
1019// // Simulate a search being cancelled after the time limit,
1020// // returning only a subset of the matches that would have been found.
1021// drop(delegate.spawn_search(query.clone(), cx));
1022// delegate.set_search_matches(
1023// delegate.latest_search_id,
1024// true, // did-cancel
1025// query.clone(),
1026// vec![matches[1].clone(), matches[3].clone()],
1027// cx,
1028// );
1029
1030// // Simulate another cancellation.
1031// drop(delegate.spawn_search(query.clone(), cx));
1032// delegate.set_search_matches(
1033// delegate.latest_search_id,
1034// true, // did-cancel
1035// query.clone(),
1036// vec![matches[0].clone(), matches[2].clone(), matches[3].clone()],
1037// cx,
1038// );
1039
1040// assert!(
1041// delegate.matches.history.is_empty(),
1042// "Search matches expected"
1043// );
1044// assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]);
1045// });
1046// }
1047
1048// #[gpui::test]
1049// async fn test_ignored_files(cx: &mut TestAppContext) {
1050// let app_state = init_test(cx);
1051// app_state
1052// .fs
1053// .as_fake()
1054// .insert_tree(
1055// "/ancestor",
1056// json!({
1057// ".gitignore": "ignored-root",
1058// "ignored-root": {
1059// "happiness": "",
1060// "height": "",
1061// "hi": "",
1062// "hiccup": "",
1063// },
1064// "tracked-root": {
1065// ".gitignore": "height",
1066// "happiness": "",
1067// "height": "",
1068// "hi": "",
1069// "hiccup": "",
1070// },
1071// }),
1072// )
1073// .await;
1074
1075// let project = Project::test(
1076// app_state.fs.clone(),
1077// [
1078// "/ancestor/tracked-root".as_ref(),
1079// "/ancestor/ignored-root".as_ref(),
1080// ],
1081// cx,
1082// )
1083// .await;
1084
1085// let (picker, _, mut cx) = build_find_picker(project, cx);
1086// let cx = &mut cx;
1087
1088// picker
1089// .update(cx, |picker, cx| {
1090// picker.delegate.spawn_search(test_path_like("hi"), cx)
1091// })
1092// .await;
1093// picker.update(cx, |picker, _| assert_eq!(picker.delegate.matches.len(), 7));
1094// }
1095
1096// #[gpui::test]
1097// async fn test_single_file_worktrees(cx: &mut TestAppContext) {
1098// let app_state = init_test(cx);
1099// app_state
1100// .fs
1101// .as_fake()
1102// .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } }))
1103// .await;
1104
1105// let project = Project::test(
1106// app_state.fs.clone(),
1107// ["/root/the-parent-dir/the-file".as_ref()],
1108// cx,
1109// )
1110// .await;
1111
1112// let (picker, _, mut cx) = build_find_picker(project, cx);
1113// let cx = &mut cx;
1114
1115// // Even though there is only one worktree, that worktree's filename
1116// // is included in the matching, because the worktree is a single file.
1117// picker
1118// .update(cx, |picker, cx| {
1119// picker.delegate.spawn_search(test_path_like("thf"), cx)
1120// })
1121// .await;
1122// cx.read(|cx| {
1123// let picker = picker.read(cx);
1124// let delegate = &picker.delegate;
1125// assert!(
1126// delegate.matches.history.is_empty(),
1127// "Search matches expected"
1128// );
1129// let matches = delegate.matches.search.clone();
1130// assert_eq!(matches.len(), 1);
1131
1132// let (file_name, file_name_positions, full_path, full_path_positions) =
1133// delegate.labels_for_path_match(&matches[0]);
1134// assert_eq!(file_name, "the-file");
1135// assert_eq!(file_name_positions, &[0, 1, 4]);
1136// assert_eq!(full_path, "the-file");
1137// assert_eq!(full_path_positions, &[0, 1, 4]);
1138// });
1139
1140// // Since the worktree root is a file, searching for its name followed by a slash does
1141// // not match anything.
1142// picker
1143// .update(cx, |f, cx| {
1144// f.delegate.spawn_search(test_path_like("thf/"), cx)
1145// })
1146// .await;
1147// picker.update(cx, |f, _| assert_eq!(f.delegate.matches.len(), 0));
1148// }
1149
1150// #[gpui::test]
1151// async fn test_path_distance_ordering(cx: &mut TestAppContext) {
1152// let app_state = init_test(cx);
1153// app_state
1154// .fs
1155// .as_fake()
1156// .insert_tree(
1157// "/root",
1158// json!({
1159// "dir1": { "a.txt": "" },
1160// "dir2": {
1161// "a.txt": "",
1162// "b.txt": ""
1163// }
1164// }),
1165// )
1166// .await;
1167
1168// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1169// let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1170// let cx = &mut cx;
1171
1172// let worktree_id = cx.read(|cx| {
1173// let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1174// assert_eq!(worktrees.len(), 1);
1175// WorktreeId::from_usize(worktrees[0].id())
1176// });
1177
1178// // When workspace has an active item, sort items which are closer to that item
1179// // first when they have the same name. In this case, b.txt is closer to dir2's a.txt
1180// // so that one should be sorted earlier
1181// let b_path = Some(dummy_found_path(ProjectPath {
1182// worktree_id,
1183// path: Arc::from(Path::new("/root/dir2/b.txt")),
1184// }));
1185// cx.dispatch_action(Toggle);
1186
1187// let finder = cx
1188// .add_window(|cx| {
1189// Picker::new(
1190// FileFinderDelegate::new(
1191// workspace.downgrade(),
1192// workspace.read(cx).project().clone(),
1193// b_path,
1194// Vec::new(),
1195// cx,
1196// ),
1197// cx,
1198// )
1199// })
1200// .root(cx);
1201
1202// finder
1203// .update(cx, |f, cx| {
1204// f.delegate.spawn_search(test_path_like("a.txt"), cx)
1205// })
1206// .await;
1207
1208// finder.read_with(cx, |f, _| {
1209// let delegate = &f.delegate;
1210// assert!(
1211// delegate.matches.history.is_empty(),
1212// "Search matches expected"
1213// );
1214// let matches = delegate.matches.search.clone();
1215// assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt"));
1216// assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt"));
1217// });
1218// }
1219
1220// #[gpui::test]
1221// async fn test_search_worktree_without_files(cx: &mut TestAppContext) {
1222// let app_state = init_test(cx);
1223// app_state
1224// .fs
1225// .as_fake()
1226// .insert_tree(
1227// "/root",
1228// json!({
1229// "dir1": {},
1230// "dir2": {
1231// "dir3": {}
1232// }
1233// }),
1234// )
1235// .await;
1236
1237// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1238// let workspace = cx
1239// .add_window(|cx| Workspace::test_new(project, cx))
1240// .root(cx);
1241// let finder = cx
1242// .add_window(|cx| {
1243// Picker::new(
1244// FileFinderDelegate::new(
1245// workspace.downgrade(),
1246// workspace.read(cx).project().clone(),
1247// None,
1248// Vec::new(),
1249// cx,
1250// ),
1251// cx,
1252// )
1253// })
1254// .root(cx);
1255// finder
1256// .update(cx, |f, cx| {
1257// f.delegate.spawn_search(test_path_like("dir"), cx)
1258// })
1259// .await;
1260// cx.read(|cx| {
1261// let finder = finder.read(cx);
1262// assert_eq!(finder.delegate.matches.len(), 0);
1263// });
1264// }
1265
1266// #[gpui::test]
1267// async fn test_query_history(cx: &mut gpui::TestAppContext) {
1268// let app_state = init_test(cx);
1269
1270// app_state
1271// .fs
1272// .as_fake()
1273// .insert_tree(
1274// "/src",
1275// json!({
1276// "test": {
1277// "first.rs": "// First Rust file",
1278// "second.rs": "// Second Rust file",
1279// "third.rs": "// Third Rust file",
1280// }
1281// }),
1282// )
1283// .await;
1284
1285// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1286// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1287// let cx = &mut cx;
1288// let worktree_id = cx.read(|cx| {
1289// let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1290// assert_eq!(worktrees.len(), 1);
1291// WorktreeId::from_usize(worktrees[0].id())
1292// });
1293
1294// // Open and close panels, getting their history items afterwards.
1295// // Ensure history items get populated with opened items, and items are kept in a certain order.
1296// // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen.
1297// //
1298// // TODO: without closing, the opened items do not propagate their history changes for some reason
1299// // it does work in real app though, only tests do not propagate.
1300
1301// let initial_history = open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1302// assert!(
1303// initial_history.is_empty(),
1304// "Should have no history before opening any files"
1305// );
1306
1307// let history_after_first =
1308// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1309// assert_eq!(
1310// history_after_first,
1311// vec![FoundPath::new(
1312// ProjectPath {
1313// worktree_id,
1314// path: Arc::from(Path::new("test/first.rs")),
1315// },
1316// Some(PathBuf::from("/src/test/first.rs"))
1317// )],
1318// "Should show 1st opened item in the history when opening the 2nd item"
1319// );
1320
1321// let history_after_second =
1322// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1323// assert_eq!(
1324// history_after_second,
1325// vec![
1326// FoundPath::new(
1327// ProjectPath {
1328// worktree_id,
1329// path: Arc::from(Path::new("test/second.rs")),
1330// },
1331// Some(PathBuf::from("/src/test/second.rs"))
1332// ),
1333// FoundPath::new(
1334// ProjectPath {
1335// worktree_id,
1336// path: Arc::from(Path::new("test/first.rs")),
1337// },
1338// Some(PathBuf::from("/src/test/first.rs"))
1339// ),
1340// ],
1341// "Should show 1st and 2nd opened items in the history when opening the 3rd item. \
1342// 2nd item should be the first in the history, as the last opened."
1343// );
1344
1345// let history_after_third =
1346// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1347// assert_eq!(
1348// history_after_third,
1349// vec![
1350// FoundPath::new(
1351// ProjectPath {
1352// worktree_id,
1353// path: Arc::from(Path::new("test/third.rs")),
1354// },
1355// Some(PathBuf::from("/src/test/third.rs"))
1356// ),
1357// FoundPath::new(
1358// ProjectPath {
1359// worktree_id,
1360// path: Arc::from(Path::new("test/second.rs")),
1361// },
1362// Some(PathBuf::from("/src/test/second.rs"))
1363// ),
1364// FoundPath::new(
1365// ProjectPath {
1366// worktree_id,
1367// path: Arc::from(Path::new("test/first.rs")),
1368// },
1369// Some(PathBuf::from("/src/test/first.rs"))
1370// ),
1371// ],
1372// "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
1373// 3rd item should be the first in the history, as the last opened."
1374// );
1375
1376// let history_after_second_again =
1377// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1378// assert_eq!(
1379// history_after_second_again,
1380// vec![
1381// FoundPath::new(
1382// ProjectPath {
1383// worktree_id,
1384// path: Arc::from(Path::new("test/second.rs")),
1385// },
1386// Some(PathBuf::from("/src/test/second.rs"))
1387// ),
1388// FoundPath::new(
1389// ProjectPath {
1390// worktree_id,
1391// path: Arc::from(Path::new("test/third.rs")),
1392// },
1393// Some(PathBuf::from("/src/test/third.rs"))
1394// ),
1395// FoundPath::new(
1396// ProjectPath {
1397// worktree_id,
1398// path: Arc::from(Path::new("test/first.rs")),
1399// },
1400// Some(PathBuf::from("/src/test/first.rs"))
1401// ),
1402// ],
1403// "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
1404// 2nd item, as the last opened, 3rd item should go next as it was opened right before."
1405// );
1406// }
1407
1408// #[gpui::test]
1409// async fn test_external_files_history(cx: &mut gpui::TestAppContext) {
1410// let app_state = init_test(cx);
1411
1412// app_state
1413// .fs
1414// .as_fake()
1415// .insert_tree(
1416// "/src",
1417// json!({
1418// "test": {
1419// "first.rs": "// First Rust file",
1420// "second.rs": "// Second Rust file",
1421// }
1422// }),
1423// )
1424// .await;
1425
1426// app_state
1427// .fs
1428// .as_fake()
1429// .insert_tree(
1430// "/external-src",
1431// json!({
1432// "test": {
1433// "third.rs": "// Third Rust file",
1434// "fourth.rs": "// Fourth Rust file",
1435// }
1436// }),
1437// )
1438// .await;
1439
1440// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1441// cx.update(|cx| {
1442// project.update(cx, |project, cx| {
1443// project.find_or_create_local_worktree("/external-src", false, cx)
1444// })
1445// })
1446// .detach();
1447// cx.background_executor.run_until_parked();
1448
1449// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1450// let cx = &mut cx;
1451// let worktree_id = cx.read(|cx| {
1452// let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1453// assert_eq!(worktrees.len(), 1,);
1454
1455// WorktreeId::from_usize(worktrees[0].id())
1456// });
1457// workspace
1458// .update(cx, |workspace, cx| {
1459// workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx)
1460// })
1461// .detach();
1462// cx.background_executor.run_until_parked();
1463// let external_worktree_id = cx.read(|cx| {
1464// let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1465// assert_eq!(
1466// worktrees.len(),
1467// 2,
1468// "External file should get opened in a new worktree"
1469// );
1470
1471// WorktreeId::from_usize(
1472// worktrees
1473// .into_iter()
1474// .find(|worktree| worktree.entity_id() != worktree_id.to_usize())
1475// .expect("New worktree should have a different id")
1476// .id(),
1477// )
1478// });
1479// close_active_item(&workspace, cx).await;
1480
1481// let initial_history_items =
1482// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1483// assert_eq!(
1484// initial_history_items,
1485// vec![FoundPath::new(
1486// ProjectPath {
1487// worktree_id: external_worktree_id,
1488// path: Arc::from(Path::new("")),
1489// },
1490// Some(PathBuf::from("/external-src/test/third.rs"))
1491// )],
1492// "Should show external file with its full path in the history after it was open"
1493// );
1494
1495// let updated_history_items =
1496// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1497// assert_eq!(
1498// updated_history_items,
1499// vec![
1500// FoundPath::new(
1501// ProjectPath {
1502// worktree_id,
1503// path: Arc::from(Path::new("test/second.rs")),
1504// },
1505// Some(PathBuf::from("/src/test/second.rs"))
1506// ),
1507// FoundPath::new(
1508// ProjectPath {
1509// worktree_id: external_worktree_id,
1510// path: Arc::from(Path::new("")),
1511// },
1512// Some(PathBuf::from("/external-src/test/third.rs"))
1513// ),
1514// ],
1515// "Should keep external file with history updates",
1516// );
1517// }
1518
1519// #[gpui::test]
1520// async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) {
1521// let app_state = init_test(cx);
1522
1523// app_state
1524// .fs
1525// .as_fake()
1526// .insert_tree(
1527// "/src",
1528// json!({
1529// "test": {
1530// "first.rs": "// First Rust file",
1531// "second.rs": "// Second Rust file",
1532// "third.rs": "// Third Rust file",
1533// }
1534// }),
1535// )
1536// .await;
1537
1538// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1539// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1540// let cx = &mut cx;
1541
1542// // generate some history to select from
1543// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1544// cx.executor().run_until_parked();
1545// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1546// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1547// let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1548
1549// for expected_selected_index in 0..current_history.len() {
1550// cx.dispatch_action(Toggle);
1551// let selected_index = workspace.update(cx, |workspace, cx| {
1552// workspace
1553// .current_modal::<FileFinder>(cx)
1554// .unwrap()
1555// .read(cx)
1556// .picker
1557// .read(cx)
1558// .delegate
1559// .selected_index()
1560// });
1561// assert_eq!(
1562// selected_index, expected_selected_index,
1563// "Should select the next item in the history"
1564// );
1565// }
1566
1567// cx.dispatch_action(Toggle);
1568// let selected_index = workspace.update(cx, |workspace, cx| {
1569// workspace
1570// .current_modal::<FileFinder>(cx)
1571// .unwrap()
1572// .read(cx)
1573// .picker
1574// .read(cx)
1575// .delegate
1576// .selected_index()
1577// });
1578// assert_eq!(
1579// selected_index, 0,
1580// "Should wrap around the history and start all over"
1581// );
1582// }
1583
1584// #[gpui::test]
1585// async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
1586// let app_state = init_test(cx);
1587
1588// app_state
1589// .fs
1590// .as_fake()
1591// .insert_tree(
1592// "/src",
1593// json!({
1594// "test": {
1595// "first.rs": "// First Rust file",
1596// "second.rs": "// Second Rust file",
1597// "third.rs": "// Third Rust file",
1598// "fourth.rs": "// Fourth Rust file",
1599// }
1600// }),
1601// )
1602// .await;
1603
1604// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1605// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1606// let cx = &mut cx;
1607// let worktree_id = cx.read(|cx| {
1608// let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1609// assert_eq!(worktrees.len(), 1,);
1610
1611// WorktreeId::from_usize(worktrees[0].entity_id())
1612// });
1613
1614// // generate some history to select from
1615// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1616// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1617// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1618// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1619
1620// cx.dispatch_action(Toggle);
1621// let first_query = "f";
1622// let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
1623// finder
1624// .update(cx, |finder, cx| {
1625// finder.delegate.update_matches(first_query.to_string(), cx)
1626// })
1627// .await;
1628// finder.read_with(cx, |finder, _| {
1629// let delegate = &finder.delegate;
1630// assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out");
1631// let history_match = delegate.matches.history.first().unwrap();
1632// assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
1633// assert_eq!(history_match.0, FoundPath::new(
1634// ProjectPath {
1635// worktree_id,
1636// path: Arc::from(Path::new("test/first.rs")),
1637// },
1638// Some(PathBuf::from("/src/test/first.rs"))
1639// ));
1640// assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
1641// assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs"));
1642// });
1643
1644// let second_query = "fsdasdsa";
1645// let finder = workspace.update(cx, |workspace, cx| {
1646// workspace
1647// .current_modal::<FileFinder>(cx)
1648// .unwrap()
1649// .read(cx)
1650// .picker
1651// });
1652// finder
1653// .update(cx, |finder, cx| {
1654// finder.delegate.update_matches(second_query.to_string(), cx)
1655// })
1656// .await;
1657// finder.update(cx, |finder, _| {
1658// let delegate = &finder.delegate;
1659// assert!(
1660// delegate.matches.history.is_empty(),
1661// "No history entries should match {second_query}"
1662// );
1663// assert!(
1664// delegate.matches.search.is_empty(),
1665// "No search entries should match {second_query}"
1666// );
1667// });
1668
1669// let first_query_again = first_query;
1670
1671// let finder = workspace.update(cx, |workspace, cx| {
1672// workspace
1673// .current_modal::<FileFinder>(cx)
1674// .unwrap()
1675// .read(cx)
1676// .picker
1677// });
1678// finder
1679// .update(cx, |finder, cx| {
1680// finder
1681// .delegate
1682// .update_matches(first_query_again.to_string(), cx)
1683// })
1684// .await;
1685// finder.read_with(cx, |finder, _| {
1686// let delegate = &finder.delegate;
1687// assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query");
1688// let history_match = delegate.matches.history.first().unwrap();
1689// assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
1690// assert_eq!(history_match.0, FoundPath::new(
1691// ProjectPath {
1692// worktree_id,
1693// path: Arc::from(Path::new("test/first.rs")),
1694// },
1695// Some(PathBuf::from("/src/test/first.rs"))
1696// ));
1697// assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
1698// assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs"));
1699// });
1700// }
1701
1702// #[gpui::test]
1703// async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
1704// let app_state = init_test(cx);
1705
1706// app_state
1707// .fs
1708// .as_fake()
1709// .insert_tree(
1710// "/src",
1711// json!({
1712// "collab_ui": {
1713// "first.rs": "// First Rust file",
1714// "second.rs": "// Second Rust file",
1715// "third.rs": "// Third Rust file",
1716// "collab_ui.rs": "// Fourth Rust file",
1717// }
1718// }),
1719// )
1720// .await;
1721
1722// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1723// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1724// let cx = &mut cx;
1725// // generate some history to select from
1726// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1727// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1728// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1729// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1730
1731// cx.dispatch_action(Toggle);
1732// let query = "collab_ui";
1733// let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
1734// finder
1735// .update(cx, |finder, cx| {
1736// finder.delegate.update_matches(query.to_string(), cx)
1737// })
1738// .await;
1739// finder.read_with(cx, |finder, _| {
1740// let delegate = &finder.delegate;
1741// assert!(
1742// delegate.matches.history.is_empty(),
1743// "History items should not math query {query}, they should be matched by name only"
1744// );
1745
1746// let search_entries = delegate
1747// .matches
1748// .search
1749// .iter()
1750// .map(|path_match| path_match.path.to_path_buf())
1751// .collect::<Vec<_>>();
1752// assert_eq!(
1753// search_entries,
1754// vec![
1755// PathBuf::from("collab_ui/collab_ui.rs"),
1756// PathBuf::from("collab_ui/third.rs"),
1757// PathBuf::from("collab_ui/first.rs"),
1758// PathBuf::from("collab_ui/second.rs"),
1759// ],
1760// "Despite all search results having the same directory name, the most matching one should be on top"
1761// );
1762// });
1763// }
1764
1765// #[gpui::test]
1766// async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) {
1767// let app_state = init_test(cx);
1768
1769// app_state
1770// .fs
1771// .as_fake()
1772// .insert_tree(
1773// "/src",
1774// json!({
1775// "test": {
1776// "first.rs": "// First Rust file",
1777// "nonexistent.rs": "// Second Rust file",
1778// "third.rs": "// Third Rust file",
1779// }
1780// }),
1781// )
1782// .await;
1783
1784// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1785// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1786// let cx = &mut cx;
1787// // generate some history to select from
1788// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1789// open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await;
1790// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1791// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1792
1793// cx.dispatch_action(Toggle);
1794// let query = "rs";
1795// let finder = cx.read(|cx| workspace.read(cx).current_modal::<FileFinder>().unwrap());
1796// finder
1797// .update(cx, |finder, cx| {
1798// finder.picker.update(cx, |picker, cx| {
1799// picker.delegate.update_matches(query.to_string(), cx)
1800// })
1801// })
1802// .await;
1803// finder.update(cx, |finder, _| {
1804// let history_entries = finder.delegate
1805// .matches
1806// .history
1807// .iter()
1808// .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf())
1809// .collect::<Vec<_>>();
1810// assert_eq!(
1811// history_entries,
1812// vec![
1813// PathBuf::from("test/first.rs"),
1814// PathBuf::from("test/third.rs"),
1815// ],
1816// "Should have all opened files in the history, except the ones that do not exist on disk"
1817// );
1818// });
1819// }
1820
1821// async fn open_close_queried_buffer(
1822// input: &str,
1823// expected_matches: usize,
1824// expected_editor_title: &str,
1825// workspace: &View<Workspace>,
1826// cx: &mut gpui::VisualTestContext<'_>,
1827// ) -> Vec<FoundPath> {
1828// cx.dispatch_action(Toggle);
1829// let picker = workspace.update(cx, |workspace, cx| {
1830// workspace
1831// .current_modal::<FileFinder>(cx)
1832// .unwrap()
1833// .read(cx)
1834// .picker
1835// .clone()
1836// });
1837// picker
1838// .update(cx, |finder, cx| {
1839// finder.delegate.update_matches(input.to_string(), cx)
1840// })
1841// .await;
1842// let history_items = picker.update(cx, |finder, _| {
1843// assert_eq!(
1844// finder.delegate.matches.len(),
1845// expected_matches,
1846// "Unexpected number of matches found for query {input}"
1847// );
1848// finder.delegate.history_items.clone()
1849// });
1850
1851// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
1852// cx.dispatch_action(SelectNext);
1853// cx.dispatch_action(Confirm);
1854// cx.background_executor.run_until_parked();
1855// active_pane
1856// .condition(cx, |pane, _| pane.active_item().is_some())
1857// .await;
1858// cx.read(|cx| {
1859// let active_item = active_pane.read(cx).active_item().unwrap();
1860// let active_editor_title = active_item
1861// .to_any()
1862// .downcast::<Editor>()
1863// .unwrap()
1864// .read(cx)
1865// .title(cx);
1866// assert_eq!(
1867// expected_editor_title, active_editor_title,
1868// "Unexpected editor title for query {input}"
1869// );
1870// });
1871
1872// close_active_item(workspace, cx).await;
1873
1874// history_items
1875// }
1876
1877// async fn close_active_item(workspace: &View<Workspace>, cx: &mut VisualTestContext<'_>) {
1878// let mut original_items = HashMap::new();
1879// cx.read(|cx| {
1880// for pane in workspace.read(cx).panes() {
1881// let pane_id = pane.entity_id();
1882// let pane = pane.read(cx);
1883// let insertion_result = original_items.insert(pane_id, pane.items().count());
1884// assert!(insertion_result.is_none(), "Pane id {pane_id} collision");
1885// }
1886// });
1887
1888// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
1889// active_pane
1890// .update(cx, |pane, cx| {
1891// pane.close_active_item(&workspace::CloseActiveItem { save_intent: None }, cx)
1892// .unwrap()
1893// })
1894// .await
1895// .unwrap();
1896// cx.background_executor.run_until_parked();
1897// cx.read(|cx| {
1898// for pane in workspace.read(cx).panes() {
1899// let pane_id = pane.entity_id();
1900// let pane = pane.read(cx);
1901// match original_items.remove(&pane_id) {
1902// Some(original_items) => {
1903// assert_eq!(
1904// pane.items().count(),
1905// original_items.saturating_sub(1),
1906// "Pane id {pane_id} should have item closed"
1907// );
1908// }
1909// None => panic!("Pane id {pane_id} not found in original items"),
1910// }
1911// }
1912// });
1913// assert!(
1914// original_items.len() <= 1,
1915// "At most one panel should got closed"
1916// );
1917// }
1918
1919// fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
1920// cx.update(|cx| {
1921// let state = AppState::test(cx);
1922// theme::init(cx);
1923// language::init(cx);
1924// super::init(cx);
1925// editor::init(cx);
1926// workspace::init_settings(cx);
1927// Project::init_settings(cx);
1928// state
1929// })
1930// }
1931
1932// fn test_path_like(test_str: &str) -> PathLikeWithPosition<FileSearchQuery> {
1933// PathLikeWithPosition::parse_str(test_str, |path_like_str| {
1934// Ok::<_, std::convert::Infallible>(FileSearchQuery {
1935// raw_query: test_str.to_owned(),
1936// file_query_end: if path_like_str == test_str {
1937// None
1938// } else {
1939// Some(path_like_str.len())
1940// },
1941// })
1942// })
1943// .unwrap()
1944// }
1945
1946// fn dummy_found_path(project_path: ProjectPath) -> FoundPath {
1947// FoundPath {
1948// project: project_path,
1949// absolute: None,
1950// }
1951// }
1952
1953// fn build_find_picker(
1954// project: Model<Project>,
1955// cx: &mut TestAppContext,
1956// ) -> (
1957// View<Picker<FileFinderDelegate>>,
1958// View<Workspace>,
1959// VisualTestContext,
1960// ) {
1961// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1962// cx.dispatch_action(Toggle);
1963// let picker = workspace.update(&mut cx, |workspace, cx| {
1964// workspace
1965// .current_modal::<FileFinder>(cx)
1966// .unwrap()
1967// .read(cx)
1968// .picker
1969// .clone()
1970// });
1971// (picker, workspace, cx)
1972// }
1973// }