file_finder_tests.rs

   1use std::{assert_eq, future::IntoFuture, path::Path, time::Duration};
   2
   3use super::*;
   4use editor::Editor;
   5use gpui::{Entity, TestAppContext, VisualTestContext};
   6use menu::{Confirm, SelectNext, SelectPrev};
   7use project::{RemoveOptions, FS_WATCH_LATENCY};
   8use serde_json::json;
   9use util::path;
  10use workspace::{AppState, ToggleFileFinder, Workspace};
  11
  12#[ctor::ctor]
  13fn init_logger() {
  14    if std::env::var("RUST_LOG").is_ok() {
  15        env_logger::init();
  16    }
  17}
  18
  19#[gpui::test]
  20async fn test_matching_paths(cx: &mut TestAppContext) {
  21    let app_state = init_test(cx);
  22    app_state
  23        .fs
  24        .as_fake()
  25        .insert_tree(
  26            path!("/root"),
  27            json!({
  28                "a": {
  29                    "banana": "",
  30                    "bandana": "",
  31                }
  32            }),
  33        )
  34        .await;
  35
  36    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
  37
  38    let (picker, workspace, cx) = build_find_picker(project, cx);
  39
  40    cx.simulate_input("bna");
  41    picker.update(cx, |picker, _| {
  42        assert_eq!(picker.delegate.matches.len(), 2);
  43    });
  44    cx.dispatch_action(SelectNext);
  45    cx.dispatch_action(Confirm);
  46    cx.read(|cx| {
  47        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
  48        assert_eq!(active_editor.read(cx).title(cx), "bandana");
  49    });
  50
  51    for bandana_query in [
  52        "bandana",
  53        " bandana",
  54        "bandana ",
  55        " bandana ",
  56        " ndan ",
  57        " band ",
  58        "a bandana",
  59    ] {
  60        picker
  61            .update_in(cx, |picker, window, cx| {
  62                picker
  63                    .delegate
  64                    .update_matches(bandana_query.to_string(), window, cx)
  65            })
  66            .await;
  67        picker.update(cx, |picker, _| {
  68            assert_eq!(
  69                picker.delegate.matches.len(),
  70                1,
  71                "Wrong number of matches for bandana query '{bandana_query}'"
  72            );
  73        });
  74        cx.dispatch_action(SelectNext);
  75        cx.dispatch_action(Confirm);
  76        cx.read(|cx| {
  77            let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
  78            assert_eq!(
  79                active_editor.read(cx).title(cx),
  80                "bandana",
  81                "Wrong match for bandana query '{bandana_query}'"
  82            );
  83        });
  84    }
  85}
  86
  87#[gpui::test]
  88async fn test_absolute_paths(cx: &mut TestAppContext) {
  89    let app_state = init_test(cx);
  90    app_state
  91        .fs
  92        .as_fake()
  93        .insert_tree(
  94            path!("/root"),
  95            json!({
  96                "a": {
  97                    "file1.txt": "",
  98                    "b": {
  99                        "file2.txt": "",
 100                    },
 101                }
 102            }),
 103        )
 104        .await;
 105
 106    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 107
 108    let (picker, workspace, cx) = build_find_picker(project, cx);
 109
 110    let matching_abs_path = path!("/root/a/b/file2.txt").to_string();
 111    picker
 112        .update_in(cx, |picker, window, cx| {
 113            picker
 114                .delegate
 115                .update_matches(matching_abs_path, window, cx)
 116        })
 117        .await;
 118    picker.update(cx, |picker, _| {
 119        assert_eq!(
 120            collect_search_matches(picker).search_paths_only(),
 121            vec![PathBuf::from("a/b/file2.txt")],
 122            "Matching abs path should be the only match"
 123        )
 124    });
 125    cx.dispatch_action(SelectNext);
 126    cx.dispatch_action(Confirm);
 127    cx.read(|cx| {
 128        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
 129        assert_eq!(active_editor.read(cx).title(cx), "file2.txt");
 130    });
 131
 132    let mismatching_abs_path = path!("/root/a/b/file1.txt").to_string();
 133    picker
 134        .update_in(cx, |picker, window, cx| {
 135            picker
 136                .delegate
 137                .update_matches(mismatching_abs_path, window, cx)
 138        })
 139        .await;
 140    picker.update(cx, |picker, _| {
 141        assert_eq!(
 142            collect_search_matches(picker).search_paths_only(),
 143            Vec::<PathBuf>::new(),
 144            "Mismatching abs path should produce no matches"
 145        )
 146    });
 147}
 148
 149#[gpui::test]
 150async fn test_complex_path(cx: &mut TestAppContext) {
 151    let app_state = init_test(cx);
 152    app_state
 153        .fs
 154        .as_fake()
 155        .insert_tree(
 156            path!("/root"),
 157            json!({
 158                "其他": {
 159                    "S数据表格": {
 160                        "task.xlsx": "some content",
 161                    },
 162                }
 163            }),
 164        )
 165        .await;
 166
 167    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 168
 169    let (picker, workspace, cx) = build_find_picker(project, cx);
 170
 171    cx.simulate_input("t");
 172    picker.update(cx, |picker, _| {
 173        assert_eq!(picker.delegate.matches.len(), 1);
 174        assert_eq!(
 175            collect_search_matches(picker).search_paths_only(),
 176            vec![PathBuf::from("其他/S数据表格/task.xlsx")],
 177        )
 178    });
 179    cx.dispatch_action(SelectNext);
 180    cx.dispatch_action(Confirm);
 181    cx.read(|cx| {
 182        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
 183        assert_eq!(active_editor.read(cx).title(cx), "task.xlsx");
 184    });
 185}
 186
 187#[gpui::test]
 188async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
 189    let app_state = init_test(cx);
 190
 191    let first_file_name = "first.rs";
 192    let first_file_contents = "// First Rust file";
 193    app_state
 194        .fs
 195        .as_fake()
 196        .insert_tree(
 197            path!("/src"),
 198            json!({
 199                "test": {
 200                    first_file_name: first_file_contents,
 201                    "second.rs": "// Second Rust file",
 202                }
 203            }),
 204        )
 205        .await;
 206
 207    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
 208
 209    let (picker, workspace, cx) = build_find_picker(project, cx);
 210
 211    let file_query = &first_file_name[..3];
 212    let file_row = 1;
 213    let file_column = 3;
 214    assert!(file_column <= first_file_contents.len());
 215    let query_inside_file = format!("{file_query}:{file_row}:{file_column}");
 216    picker
 217        .update_in(cx, |finder, window, cx| {
 218            finder
 219                .delegate
 220                .update_matches(query_inside_file.to_string(), window, cx)
 221        })
 222        .await;
 223    picker.update(cx, |finder, _| {
 224        let finder = &finder.delegate;
 225        assert_eq!(finder.matches.len(), 1);
 226        let latest_search_query = finder
 227            .latest_search_query
 228            .as_ref()
 229            .expect("Finder should have a query after the update_matches call");
 230        assert_eq!(latest_search_query.raw_query, query_inside_file);
 231        assert_eq!(latest_search_query.file_query_end, Some(file_query.len()));
 232        assert_eq!(latest_search_query.path_position.row, Some(file_row));
 233        assert_eq!(
 234            latest_search_query.path_position.column,
 235            Some(file_column as u32)
 236        );
 237    });
 238
 239    cx.dispatch_action(SelectNext);
 240    cx.dispatch_action(Confirm);
 241
 242    let editor = cx.update(|_, cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
 243    cx.executor().advance_clock(Duration::from_secs(2));
 244
 245    editor.update(cx, |editor, cx| {
 246            let all_selections = editor.selections.all_adjusted(cx);
 247            assert_eq!(
 248                all_selections.len(),
 249                1,
 250                "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
 251            );
 252            let caret_selection = all_selections.into_iter().next().unwrap();
 253            assert_eq!(caret_selection.start, caret_selection.end,
 254                "Caret selection should have its start and end at the same position");
 255            assert_eq!(file_row, caret_selection.start.row + 1,
 256                "Query inside file should get caret with the same focus row");
 257            assert_eq!(file_column, caret_selection.start.column as usize + 1,
 258                "Query inside file should get caret with the same focus column");
 259        });
 260}
 261
 262#[gpui::test]
 263async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) {
 264    let app_state = init_test(cx);
 265
 266    let first_file_name = "first.rs";
 267    let first_file_contents = "// First Rust file";
 268    app_state
 269        .fs
 270        .as_fake()
 271        .insert_tree(
 272            path!("/src"),
 273            json!({
 274                "test": {
 275                    first_file_name: first_file_contents,
 276                    "second.rs": "// Second Rust file",
 277                }
 278            }),
 279        )
 280        .await;
 281
 282    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
 283
 284    let (picker, workspace, cx) = build_find_picker(project, cx);
 285
 286    let file_query = &first_file_name[..3];
 287    let file_row = 200;
 288    let file_column = 300;
 289    assert!(file_column > first_file_contents.len());
 290    let query_outside_file = format!("{file_query}:{file_row}:{file_column}");
 291    picker
 292        .update_in(cx, |picker, window, cx| {
 293            picker
 294                .delegate
 295                .update_matches(query_outside_file.to_string(), window, cx)
 296        })
 297        .await;
 298    picker.update(cx, |finder, _| {
 299        let delegate = &finder.delegate;
 300        assert_eq!(delegate.matches.len(), 1);
 301        let latest_search_query = delegate
 302            .latest_search_query
 303            .as_ref()
 304            .expect("Finder should have a query after the update_matches call");
 305        assert_eq!(latest_search_query.raw_query, query_outside_file);
 306        assert_eq!(latest_search_query.file_query_end, Some(file_query.len()));
 307        assert_eq!(latest_search_query.path_position.row, Some(file_row));
 308        assert_eq!(
 309            latest_search_query.path_position.column,
 310            Some(file_column as u32)
 311        );
 312    });
 313
 314    cx.dispatch_action(SelectNext);
 315    cx.dispatch_action(Confirm);
 316
 317    let editor = cx.update(|_, cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
 318    cx.executor().advance_clock(Duration::from_secs(2));
 319
 320    editor.update(cx, |editor, cx| {
 321            let all_selections = editor.selections.all_adjusted(cx);
 322            assert_eq!(
 323                all_selections.len(),
 324                1,
 325                "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
 326            );
 327            let caret_selection = all_selections.into_iter().next().unwrap();
 328            assert_eq!(caret_selection.start, caret_selection.end,
 329                "Caret selection should have its start and end at the same position");
 330            assert_eq!(0, caret_selection.start.row,
 331                "Excessive rows (as in query outside file borders) should get trimmed to last file row");
 332            assert_eq!(first_file_contents.len(), caret_selection.start.column as usize,
 333                "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column");
 334        });
 335}
 336
 337#[gpui::test]
 338async fn test_matching_cancellation(cx: &mut TestAppContext) {
 339    let app_state = init_test(cx);
 340    app_state
 341        .fs
 342        .as_fake()
 343        .insert_tree(
 344            "/dir",
 345            json!({
 346                "hello": "",
 347                "goodbye": "",
 348                "halogen-light": "",
 349                "happiness": "",
 350                "height": "",
 351                "hi": "",
 352                "hiccup": "",
 353            }),
 354        )
 355        .await;
 356
 357    let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
 358
 359    let (picker, _, cx) = build_find_picker(project, cx);
 360
 361    let query = test_path_position("hi");
 362    picker
 363        .update_in(cx, |picker, window, cx| {
 364            picker.delegate.spawn_search(query.clone(), window, cx)
 365        })
 366        .await;
 367
 368    picker.update(cx, |picker, _cx| {
 369        assert_eq!(picker.delegate.matches.len(), 5)
 370    });
 371
 372    picker.update_in(cx, |picker, window, cx| {
 373        let matches = collect_search_matches(picker).search_matches_only();
 374        let delegate = &mut picker.delegate;
 375
 376        // Simulate a search being cancelled after the time limit,
 377        // returning only a subset of the matches that would have been found.
 378        drop(delegate.spawn_search(query.clone(), window, cx));
 379        delegate.set_search_matches(
 380            delegate.latest_search_id,
 381            true, // did-cancel
 382            query.clone(),
 383            vec![
 384                ProjectPanelOrdMatch(matches[1].clone()),
 385                ProjectPanelOrdMatch(matches[3].clone()),
 386            ],
 387            cx,
 388        );
 389
 390        // Simulate another cancellation.
 391        drop(delegate.spawn_search(query.clone(), window, cx));
 392        delegate.set_search_matches(
 393            delegate.latest_search_id,
 394            true, // did-cancel
 395            query.clone(),
 396            vec![
 397                ProjectPanelOrdMatch(matches[0].clone()),
 398                ProjectPanelOrdMatch(matches[2].clone()),
 399                ProjectPanelOrdMatch(matches[3].clone()),
 400            ],
 401            cx,
 402        );
 403
 404        assert_eq!(
 405            collect_search_matches(picker)
 406                .search_matches_only()
 407                .as_slice(),
 408            &matches[0..4]
 409        );
 410    });
 411}
 412
 413#[gpui::test]
 414async fn test_ignored_root(cx: &mut TestAppContext) {
 415    let app_state = init_test(cx);
 416    app_state
 417        .fs
 418        .as_fake()
 419        .insert_tree(
 420            "/ancestor",
 421            json!({
 422                ".gitignore": "ignored-root",
 423                "ignored-root": {
 424                    "happiness": "",
 425                    "height": "",
 426                    "hi": "",
 427                    "hiccup": "",
 428                },
 429                "tracked-root": {
 430                    ".gitignore": "height",
 431                    "happiness": "",
 432                    "height": "",
 433                    "hi": "",
 434                    "hiccup": "",
 435                },
 436            }),
 437        )
 438        .await;
 439
 440    let project = Project::test(
 441        app_state.fs.clone(),
 442        [
 443            "/ancestor/tracked-root".as_ref(),
 444            "/ancestor/ignored-root".as_ref(),
 445        ],
 446        cx,
 447    )
 448    .await;
 449
 450    let (picker, _, cx) = build_find_picker(project, cx);
 451
 452    picker
 453        .update_in(cx, |picker, window, cx| {
 454            picker
 455                .delegate
 456                .spawn_search(test_path_position("hi"), window, cx)
 457        })
 458        .await;
 459    picker.update(cx, |picker, _| assert_eq!(picker.delegate.matches.len(), 7));
 460}
 461
 462#[gpui::test]
 463async fn test_single_file_worktrees(cx: &mut TestAppContext) {
 464    let app_state = init_test(cx);
 465    app_state
 466        .fs
 467        .as_fake()
 468        .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } }))
 469        .await;
 470
 471    let project = Project::test(
 472        app_state.fs.clone(),
 473        ["/root/the-parent-dir/the-file".as_ref()],
 474        cx,
 475    )
 476    .await;
 477
 478    let (picker, _, cx) = build_find_picker(project, cx);
 479
 480    // Even though there is only one worktree, that worktree's filename
 481    // is included in the matching, because the worktree is a single file.
 482    picker
 483        .update_in(cx, |picker, window, cx| {
 484            picker
 485                .delegate
 486                .spawn_search(test_path_position("thf"), window, cx)
 487        })
 488        .await;
 489    cx.read(|cx| {
 490        let picker = picker.read(cx);
 491        let delegate = &picker.delegate;
 492        let matches = collect_search_matches(picker).search_matches_only();
 493        assert_eq!(matches.len(), 1);
 494
 495        let (file_name, file_name_positions, full_path, full_path_positions) =
 496            delegate.labels_for_path_match(&matches[0]);
 497        assert_eq!(file_name, "the-file");
 498        assert_eq!(file_name_positions, &[0, 1, 4]);
 499        assert_eq!(full_path, "");
 500        assert_eq!(full_path_positions, &[0; 0]);
 501    });
 502
 503    // Since the worktree root is a file, searching for its name followed by a slash does
 504    // not match anything.
 505    picker
 506        .update_in(cx, |picker, window, cx| {
 507            picker
 508                .delegate
 509                .spawn_search(test_path_position("thf/"), window, cx)
 510        })
 511        .await;
 512    picker.update(cx, |f, _| assert_eq!(f.delegate.matches.len(), 0));
 513}
 514
 515#[gpui::test]
 516async fn test_path_distance_ordering(cx: &mut TestAppContext) {
 517    let app_state = init_test(cx);
 518    app_state
 519        .fs
 520        .as_fake()
 521        .insert_tree(
 522            path!("/root"),
 523            json!({
 524                "dir1": { "a.txt": "" },
 525                "dir2": {
 526                    "a.txt": "",
 527                    "b.txt": ""
 528                }
 529            }),
 530        )
 531        .await;
 532
 533    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 534    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
 535
 536    let worktree_id = cx.read(|cx| {
 537        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
 538        assert_eq!(worktrees.len(), 1);
 539        WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
 540    });
 541
 542    // When workspace has an active item, sort items which are closer to that item
 543    // first when they have the same name. In this case, b.txt is closer to dir2's a.txt
 544    // so that one should be sorted earlier
 545    let b_path = ProjectPath {
 546        worktree_id,
 547        path: Arc::from(Path::new("dir2/b.txt")),
 548    };
 549    workspace
 550        .update_in(cx, |workspace, window, cx| {
 551            workspace.open_path(b_path, None, true, window, cx)
 552        })
 553        .await
 554        .unwrap();
 555    let finder = open_file_picker(&workspace, cx);
 556    finder
 557        .update_in(cx, |f, window, cx| {
 558            f.delegate
 559                .spawn_search(test_path_position("a.txt"), window, cx)
 560        })
 561        .await;
 562
 563    finder.update(cx, |picker, _| {
 564        let matches = collect_search_matches(picker).search_paths_only();
 565        assert_eq!(matches[0].as_path(), Path::new("dir2/a.txt"));
 566        assert_eq!(matches[1].as_path(), Path::new("dir1/a.txt"));
 567    });
 568}
 569
 570#[gpui::test]
 571async fn test_search_worktree_without_files(cx: &mut TestAppContext) {
 572    let app_state = init_test(cx);
 573    app_state
 574        .fs
 575        .as_fake()
 576        .insert_tree(
 577            "/root",
 578            json!({
 579                "dir1": {},
 580                "dir2": {
 581                    "dir3": {}
 582                }
 583            }),
 584        )
 585        .await;
 586
 587    let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
 588    let (picker, _workspace, cx) = build_find_picker(project, cx);
 589
 590    picker
 591        .update_in(cx, |f, window, cx| {
 592            f.delegate
 593                .spawn_search(test_path_position("dir"), window, cx)
 594        })
 595        .await;
 596    cx.read(|cx| {
 597        let finder = picker.read(cx);
 598        assert_eq!(finder.delegate.matches.len(), 0);
 599    });
 600}
 601
 602#[gpui::test]
 603async fn test_query_history(cx: &mut gpui::TestAppContext) {
 604    let app_state = init_test(cx);
 605
 606    app_state
 607        .fs
 608        .as_fake()
 609        .insert_tree(
 610            path!("/src"),
 611            json!({
 612                "test": {
 613                    "first.rs": "// First Rust file",
 614                    "second.rs": "// Second Rust file",
 615                    "third.rs": "// Third Rust file",
 616                }
 617            }),
 618        )
 619        .await;
 620
 621    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
 622    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
 623    let worktree_id = cx.read(|cx| {
 624        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
 625        assert_eq!(worktrees.len(), 1);
 626        WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
 627    });
 628
 629    // Open and close panels, getting their history items afterwards.
 630    // Ensure history items get populated with opened items, and items are kept in a certain order.
 631    // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen.
 632    //
 633    // TODO: without closing, the opened items do not propagate their history changes for some reason
 634    // it does work in real app though, only tests do not propagate.
 635    workspace.update_in(cx, |_workspace, window, cx| window.focused(cx));
 636
 637    let initial_history = open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
 638    assert!(
 639        initial_history.is_empty(),
 640        "Should have no history before opening any files"
 641    );
 642
 643    let history_after_first =
 644        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
 645    assert_eq!(
 646        history_after_first,
 647        vec![FoundPath::new(
 648            ProjectPath {
 649                worktree_id,
 650                path: Arc::from(Path::new("test/first.rs")),
 651            },
 652            Some(PathBuf::from(path!("/src/test/first.rs")))
 653        )],
 654        "Should show 1st opened item in the history when opening the 2nd item"
 655    );
 656
 657    let history_after_second =
 658        open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
 659    assert_eq!(
 660        history_after_second,
 661        vec![
 662            FoundPath::new(
 663                ProjectPath {
 664                    worktree_id,
 665                    path: Arc::from(Path::new("test/second.rs")),
 666                },
 667                Some(PathBuf::from(path!("/src/test/second.rs")))
 668            ),
 669            FoundPath::new(
 670                ProjectPath {
 671                    worktree_id,
 672                    path: Arc::from(Path::new("test/first.rs")),
 673                },
 674                Some(PathBuf::from(path!("/src/test/first.rs")))
 675            ),
 676        ],
 677        "Should show 1st and 2nd opened items in the history when opening the 3rd item. \
 678    2nd item should be the first in the history, as the last opened."
 679    );
 680
 681    let history_after_third =
 682        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
 683    assert_eq!(
 684                history_after_third,
 685                vec![
 686                    FoundPath::new(
 687                        ProjectPath {
 688                            worktree_id,
 689                            path: Arc::from(Path::new("test/third.rs")),
 690                        },
 691                        Some(PathBuf::from(path!("/src/test/third.rs")))
 692                    ),
 693                    FoundPath::new(
 694                        ProjectPath {
 695                            worktree_id,
 696                            path: Arc::from(Path::new("test/second.rs")),
 697                        },
 698                        Some(PathBuf::from(path!("/src/test/second.rs")))
 699                    ),
 700                    FoundPath::new(
 701                        ProjectPath {
 702                            worktree_id,
 703                            path: Arc::from(Path::new("test/first.rs")),
 704                        },
 705                        Some(PathBuf::from(path!("/src/test/first.rs")))
 706                    ),
 707                ],
 708                "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
 709    3rd item should be the first in the history, as the last opened."
 710            );
 711
 712    let history_after_second_again =
 713        open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
 714    assert_eq!(
 715                history_after_second_again,
 716                vec![
 717                    FoundPath::new(
 718                        ProjectPath {
 719                            worktree_id,
 720                            path: Arc::from(Path::new("test/second.rs")),
 721                        },
 722                        Some(PathBuf::from(path!("/src/test/second.rs")))
 723                    ),
 724                    FoundPath::new(
 725                        ProjectPath {
 726                            worktree_id,
 727                            path: Arc::from(Path::new("test/third.rs")),
 728                        },
 729                        Some(PathBuf::from(path!("/src/test/third.rs")))
 730                    ),
 731                    FoundPath::new(
 732                        ProjectPath {
 733                            worktree_id,
 734                            path: Arc::from(Path::new("test/first.rs")),
 735                        },
 736                        Some(PathBuf::from(path!("/src/test/first.rs")))
 737                    ),
 738                ],
 739                "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
 740    2nd item, as the last opened, 3rd item should go next as it was opened right before."
 741            );
 742}
 743
 744#[gpui::test]
 745async fn test_external_files_history(cx: &mut gpui::TestAppContext) {
 746    let app_state = init_test(cx);
 747
 748    app_state
 749        .fs
 750        .as_fake()
 751        .insert_tree(
 752            path!("/src"),
 753            json!({
 754                "test": {
 755                    "first.rs": "// First Rust file",
 756                    "second.rs": "// Second Rust file",
 757                }
 758            }),
 759        )
 760        .await;
 761
 762    app_state
 763        .fs
 764        .as_fake()
 765        .insert_tree(
 766            path!("/external-src"),
 767            json!({
 768                "test": {
 769                    "third.rs": "// Third Rust file",
 770                    "fourth.rs": "// Fourth Rust file",
 771                }
 772            }),
 773        )
 774        .await;
 775
 776    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
 777    cx.update(|cx| {
 778        project.update(cx, |project, cx| {
 779            project.find_or_create_worktree(path!("/external-src"), false, cx)
 780        })
 781    })
 782    .detach();
 783    cx.background_executor.run_until_parked();
 784
 785    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
 786    let worktree_id = cx.read(|cx| {
 787        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
 788        assert_eq!(worktrees.len(), 1,);
 789
 790        WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
 791    });
 792    workspace
 793        .update_in(cx, |workspace, window, cx| {
 794            workspace.open_abs_path(
 795                PathBuf::from(path!("/external-src/test/third.rs")),
 796                false,
 797                window,
 798                cx,
 799            )
 800        })
 801        .detach();
 802    cx.background_executor.run_until_parked();
 803    let external_worktree_id = cx.read(|cx| {
 804        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
 805        assert_eq!(
 806            worktrees.len(),
 807            2,
 808            "External file should get opened in a new worktree"
 809        );
 810
 811        WorktreeId::from_usize(
 812            worktrees
 813                .into_iter()
 814                .find(|worktree| worktree.entity_id().as_u64() as usize != worktree_id.to_usize())
 815                .expect("New worktree should have a different id")
 816                .entity_id()
 817                .as_u64() as usize,
 818        )
 819    });
 820    cx.dispatch_action(workspace::CloseActiveItem {
 821        save_intent: None,
 822        close_pinned: false,
 823    });
 824
 825    let initial_history_items =
 826        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
 827    assert_eq!(
 828        initial_history_items,
 829        vec![FoundPath::new(
 830            ProjectPath {
 831                worktree_id: external_worktree_id,
 832                path: Arc::from(Path::new("")),
 833            },
 834            Some(PathBuf::from(path!("/external-src/test/third.rs")))
 835        )],
 836        "Should show external file with its full path in the history after it was open"
 837    );
 838
 839    let updated_history_items =
 840        open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
 841    assert_eq!(
 842        updated_history_items,
 843        vec![
 844            FoundPath::new(
 845                ProjectPath {
 846                    worktree_id,
 847                    path: Arc::from(Path::new("test/second.rs")),
 848                },
 849                Some(PathBuf::from(path!("/src/test/second.rs")))
 850            ),
 851            FoundPath::new(
 852                ProjectPath {
 853                    worktree_id: external_worktree_id,
 854                    path: Arc::from(Path::new("")),
 855                },
 856                Some(PathBuf::from(path!("/external-src/test/third.rs")))
 857            ),
 858        ],
 859        "Should keep external file with history updates",
 860    );
 861}
 862
 863#[gpui::test]
 864async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) {
 865    let app_state = init_test(cx);
 866
 867    app_state
 868        .fs
 869        .as_fake()
 870        .insert_tree(
 871            path!("/src"),
 872            json!({
 873                "test": {
 874                    "first.rs": "// First Rust file",
 875                    "second.rs": "// Second Rust file",
 876                    "third.rs": "// Third Rust file",
 877                }
 878            }),
 879        )
 880        .await;
 881
 882    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
 883    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
 884
 885    // generate some history to select from
 886    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
 887    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
 888    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
 889    let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
 890
 891    for expected_selected_index in 0..current_history.len() {
 892        cx.dispatch_action(ToggleFileFinder::default());
 893        let picker = active_file_picker(&workspace, cx);
 894        let selected_index = picker.update(cx, |picker, _| picker.delegate.selected_index());
 895        assert_eq!(
 896            selected_index, expected_selected_index,
 897            "Should select the next item in the history"
 898        );
 899    }
 900
 901    cx.dispatch_action(ToggleFileFinder::default());
 902    let selected_index = workspace.update(cx, |workspace, cx| {
 903        workspace
 904            .active_modal::<FileFinder>(cx)
 905            .unwrap()
 906            .read(cx)
 907            .picker
 908            .read(cx)
 909            .delegate
 910            .selected_index()
 911    });
 912    assert_eq!(
 913        selected_index, 0,
 914        "Should wrap around the history and start all over"
 915    );
 916}
 917
 918#[gpui::test]
 919async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
 920    let app_state = init_test(cx);
 921
 922    app_state
 923        .fs
 924        .as_fake()
 925        .insert_tree(
 926            path!("/src"),
 927            json!({
 928                "test": {
 929                    "first.rs": "// First Rust file",
 930                    "second.rs": "// Second Rust file",
 931                    "third.rs": "// Third Rust file",
 932                    "fourth.rs": "// Fourth Rust file",
 933                }
 934            }),
 935        )
 936        .await;
 937
 938    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
 939    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
 940    let worktree_id = cx.read(|cx| {
 941        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
 942        assert_eq!(worktrees.len(), 1,);
 943
 944        WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
 945    });
 946
 947    // generate some history to select from
 948    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
 949    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
 950    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
 951    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
 952
 953    let finder = open_file_picker(&workspace, cx);
 954    let first_query = "f";
 955    finder
 956        .update_in(cx, |finder, window, cx| {
 957            finder
 958                .delegate
 959                .update_matches(first_query.to_string(), window, cx)
 960        })
 961        .await;
 962    finder.update(cx, |picker, _| {
 963            let matches = collect_search_matches(picker);
 964            assert_eq!(matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out");
 965            let history_match = matches.history_found_paths.first().expect("Should have path matches for history items after querying");
 966            assert_eq!(history_match, &FoundPath::new(
 967                ProjectPath {
 968                    worktree_id,
 969                    path: Arc::from(Path::new("test/first.rs")),
 970                },
 971                Some(PathBuf::from(path!("/src/test/first.rs")))
 972            ));
 973            assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
 974            assert_eq!(matches.search.first().unwrap(), Path::new("test/fourth.rs"));
 975        });
 976
 977    let second_query = "fsdasdsa";
 978    let finder = active_file_picker(&workspace, cx);
 979    finder
 980        .update_in(cx, |finder, window, cx| {
 981            finder
 982                .delegate
 983                .update_matches(second_query.to_string(), window, cx)
 984        })
 985        .await;
 986    finder.update(cx, |picker, _| {
 987        assert!(
 988            collect_search_matches(picker)
 989                .search_paths_only()
 990                .is_empty(),
 991            "No search entries should match {second_query}"
 992        );
 993    });
 994
 995    let first_query_again = first_query;
 996
 997    let finder = active_file_picker(&workspace, cx);
 998    finder
 999        .update_in(cx, |finder, window, cx| {
1000            finder
1001                .delegate
1002                .update_matches(first_query_again.to_string(), window, cx)
1003        })
1004        .await;
1005    finder.update(cx, |picker, _| {
1006            let matches = collect_search_matches(picker);
1007            assert_eq!(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");
1008            let history_match = matches.history_found_paths.first().expect("Should have path matches for history items after querying");
1009            assert_eq!(history_match, &FoundPath::new(
1010                ProjectPath {
1011                    worktree_id,
1012                    path: Arc::from(Path::new("test/first.rs")),
1013                },
1014                Some(PathBuf::from(path!("/src/test/first.rs")))
1015            ));
1016            assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
1017            assert_eq!(matches.search.first().unwrap(), Path::new("test/fourth.rs"));
1018        });
1019}
1020
1021#[gpui::test]
1022async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) {
1023    let app_state = init_test(cx);
1024
1025    app_state
1026        .fs
1027        .as_fake()
1028        .insert_tree(
1029            path!("/root"),
1030            json!({
1031                "test": {
1032                    "1_qw": "// First file that matches the query",
1033                    "2_second": "// Second file",
1034                    "3_third": "// Third file",
1035                    "4_fourth": "// Fourth file",
1036                    "5_qwqwqw": "// A file with 3 more matches than the first one",
1037                    "6_qwqwqw": "// Same query matches as above, but closer to the end of the list due to the name",
1038                    "7_qwqwqw": "// One more, same amount of query matches as above",
1039                }
1040            }),
1041        )
1042        .await;
1043
1044    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
1045    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1046    // generate some history to select from
1047    open_close_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
1048    open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
1049    open_close_queried_buffer("3", 1, "3_third", &workspace, cx).await;
1050    open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
1051    open_close_queried_buffer("6", 1, "6_qwqwqw", &workspace, cx).await;
1052
1053    let finder = open_file_picker(&workspace, cx);
1054    let query = "qw";
1055    finder
1056        .update_in(cx, |finder, window, cx| {
1057            finder
1058                .delegate
1059                .update_matches(query.to_string(), window, cx)
1060        })
1061        .await;
1062    finder.update(cx, |finder, _| {
1063        let search_matches = collect_search_matches(finder);
1064        assert_eq!(
1065            search_matches.history,
1066            vec![PathBuf::from("test/1_qw"), PathBuf::from("test/6_qwqwqw"),],
1067        );
1068        assert_eq!(
1069            search_matches.search,
1070            vec![
1071                PathBuf::from("test/5_qwqwqw"),
1072                PathBuf::from("test/7_qwqwqw"),
1073            ],
1074        );
1075    });
1076}
1077
1078#[gpui::test]
1079async fn test_select_current_open_file_when_no_history(cx: &mut gpui::TestAppContext) {
1080    let app_state = init_test(cx);
1081
1082    app_state
1083        .fs
1084        .as_fake()
1085        .insert_tree(
1086            path!("/root"),
1087            json!({
1088                "test": {
1089                    "1_qw": "",
1090                }
1091            }),
1092        )
1093        .await;
1094
1095    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
1096    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1097    // Open new buffer
1098    open_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
1099
1100    let picker = open_file_picker(&workspace, cx);
1101    picker.update(cx, |finder, _| {
1102        assert_match_selection(&finder, 0, "1_qw");
1103    });
1104}
1105
1106#[gpui::test]
1107async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
1108    cx: &mut TestAppContext,
1109) {
1110    let app_state = init_test(cx);
1111
1112    app_state
1113        .fs
1114        .as_fake()
1115        .insert_tree(
1116            path!("/src"),
1117            json!({
1118                "test": {
1119                    "bar.rs": "// Bar file",
1120                    "lib.rs": "// Lib file",
1121                    "maaa.rs": "// Maaaaaaa",
1122                    "main.rs": "// Main file",
1123                    "moo.rs": "// Moooooo",
1124                }
1125            }),
1126        )
1127        .await;
1128
1129    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1130    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1131
1132    open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
1133    open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
1134    open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
1135
1136    // main.rs is on top, previously used is selected
1137    let picker = open_file_picker(&workspace, cx);
1138    picker.update(cx, |finder, _| {
1139        assert_eq!(finder.delegate.matches.len(), 3);
1140        assert_match_selection(finder, 0, "main.rs");
1141        assert_match_at_position(finder, 1, "lib.rs");
1142        assert_match_at_position(finder, 2, "bar.rs");
1143    });
1144
1145    // all files match, main.rs is still on top, but the second item is selected
1146    picker
1147        .update_in(cx, |finder, window, cx| {
1148            finder
1149                .delegate
1150                .update_matches(".rs".to_string(), window, cx)
1151        })
1152        .await;
1153    picker.update(cx, |finder, _| {
1154        assert_eq!(finder.delegate.matches.len(), 5);
1155        assert_match_at_position(finder, 0, "main.rs");
1156        assert_match_selection(finder, 1, "bar.rs");
1157        assert_match_at_position(finder, 2, "lib.rs");
1158        assert_match_at_position(finder, 3, "moo.rs");
1159        assert_match_at_position(finder, 4, "maaa.rs");
1160    });
1161
1162    // main.rs is not among matches, select top item
1163    picker
1164        .update_in(cx, |finder, window, cx| {
1165            finder.delegate.update_matches("b".to_string(), window, cx)
1166        })
1167        .await;
1168    picker.update(cx, |finder, _| {
1169        assert_eq!(finder.delegate.matches.len(), 2);
1170        assert_match_at_position(finder, 0, "bar.rs");
1171        assert_match_at_position(finder, 1, "lib.rs");
1172    });
1173
1174    // main.rs is back, put it on top and select next item
1175    picker
1176        .update_in(cx, |finder, window, cx| {
1177            finder.delegate.update_matches("m".to_string(), window, cx)
1178        })
1179        .await;
1180    picker.update(cx, |finder, _| {
1181        assert_eq!(finder.delegate.matches.len(), 3);
1182        assert_match_at_position(finder, 0, "main.rs");
1183        assert_match_selection(finder, 1, "moo.rs");
1184        assert_match_at_position(finder, 2, "maaa.rs");
1185    });
1186
1187    // get back to the initial state
1188    picker
1189        .update_in(cx, |finder, window, cx| {
1190            finder.delegate.update_matches("".to_string(), window, cx)
1191        })
1192        .await;
1193    picker.update(cx, |finder, _| {
1194        assert_eq!(finder.delegate.matches.len(), 3);
1195        assert_match_selection(finder, 0, "main.rs");
1196        assert_match_at_position(finder, 1, "lib.rs");
1197        assert_match_at_position(finder, 2, "bar.rs");
1198    });
1199}
1200
1201#[gpui::test]
1202async fn test_non_separate_history_items(cx: &mut TestAppContext) {
1203    let app_state = init_test(cx);
1204
1205    app_state
1206        .fs
1207        .as_fake()
1208        .insert_tree(
1209            path!("/src"),
1210            json!({
1211                "test": {
1212                    "bar.rs": "// Bar file",
1213                    "lib.rs": "// Lib file",
1214                    "maaa.rs": "// Maaaaaaa",
1215                    "main.rs": "// Main file",
1216                    "moo.rs": "// Moooooo",
1217                }
1218            }),
1219        )
1220        .await;
1221
1222    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1223    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1224
1225    open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
1226    open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
1227    open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
1228
1229    cx.dispatch_action(ToggleFileFinder::default());
1230    let picker = active_file_picker(&workspace, cx);
1231    // main.rs is on top, previously used is selected
1232    picker.update(cx, |finder, _| {
1233        assert_eq!(finder.delegate.matches.len(), 3);
1234        assert_match_selection(finder, 0, "main.rs");
1235        assert_match_at_position(finder, 1, "lib.rs");
1236        assert_match_at_position(finder, 2, "bar.rs");
1237    });
1238
1239    // all files match, main.rs is still on top, but the second item is selected
1240    picker
1241        .update_in(cx, |finder, window, cx| {
1242            finder
1243                .delegate
1244                .update_matches(".rs".to_string(), window, cx)
1245        })
1246        .await;
1247    picker.update(cx, |finder, _| {
1248        assert_eq!(finder.delegate.matches.len(), 5);
1249        assert_match_at_position(finder, 0, "main.rs");
1250        assert_match_selection(finder, 1, "moo.rs");
1251        assert_match_at_position(finder, 2, "bar.rs");
1252        assert_match_at_position(finder, 3, "lib.rs");
1253        assert_match_at_position(finder, 4, "maaa.rs");
1254    });
1255
1256    // main.rs is not among matches, select top item
1257    picker
1258        .update_in(cx, |finder, window, cx| {
1259            finder.delegate.update_matches("b".to_string(), window, cx)
1260        })
1261        .await;
1262    picker.update(cx, |finder, _| {
1263        assert_eq!(finder.delegate.matches.len(), 2);
1264        assert_match_at_position(finder, 0, "bar.rs");
1265        assert_match_at_position(finder, 1, "lib.rs");
1266    });
1267
1268    // main.rs is back, put it on top and select next item
1269    picker
1270        .update_in(cx, |finder, window, cx| {
1271            finder.delegate.update_matches("m".to_string(), window, cx)
1272        })
1273        .await;
1274    picker.update(cx, |finder, _| {
1275        assert_eq!(finder.delegate.matches.len(), 3);
1276        assert_match_at_position(finder, 0, "main.rs");
1277        assert_match_selection(finder, 1, "moo.rs");
1278        assert_match_at_position(finder, 2, "maaa.rs");
1279    });
1280
1281    // get back to the initial state
1282    picker
1283        .update_in(cx, |finder, window, cx| {
1284            finder.delegate.update_matches("".to_string(), window, cx)
1285        })
1286        .await;
1287    picker.update(cx, |finder, _| {
1288        assert_eq!(finder.delegate.matches.len(), 3);
1289        assert_match_selection(finder, 0, "main.rs");
1290        assert_match_at_position(finder, 1, "lib.rs");
1291        assert_match_at_position(finder, 2, "bar.rs");
1292    });
1293}
1294
1295#[gpui::test]
1296async fn test_history_items_shown_in_order_of_open(cx: &mut TestAppContext) {
1297    let app_state = init_test(cx);
1298
1299    app_state
1300        .fs
1301        .as_fake()
1302        .insert_tree(
1303            path!("/test"),
1304            json!({
1305                "test": {
1306                    "1.txt": "// One",
1307                    "2.txt": "// Two",
1308                    "3.txt": "// Three",
1309                }
1310            }),
1311        )
1312        .await;
1313
1314    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
1315    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1316
1317    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1318    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
1319    open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
1320
1321    let picker = open_file_picker(&workspace, cx);
1322    picker.update(cx, |finder, _| {
1323        assert_eq!(finder.delegate.matches.len(), 3);
1324        assert_match_selection(finder, 0, "3.txt");
1325        assert_match_at_position(finder, 1, "2.txt");
1326        assert_match_at_position(finder, 2, "1.txt");
1327    });
1328
1329    cx.dispatch_action(SelectNext);
1330    cx.dispatch_action(Confirm); // Open 2.txt
1331
1332    let picker = open_file_picker(&workspace, cx);
1333    picker.update(cx, |finder, _| {
1334        assert_eq!(finder.delegate.matches.len(), 3);
1335        assert_match_selection(finder, 0, "2.txt");
1336        assert_match_at_position(finder, 1, "3.txt");
1337        assert_match_at_position(finder, 2, "1.txt");
1338    });
1339
1340    cx.dispatch_action(SelectNext);
1341    cx.dispatch_action(SelectNext);
1342    cx.dispatch_action(Confirm); // Open 1.txt
1343
1344    let picker = open_file_picker(&workspace, cx);
1345    picker.update(cx, |finder, _| {
1346        assert_eq!(finder.delegate.matches.len(), 3);
1347        assert_match_selection(finder, 0, "1.txt");
1348        assert_match_at_position(finder, 1, "2.txt");
1349        assert_match_at_position(finder, 2, "3.txt");
1350    });
1351}
1352
1353#[gpui::test]
1354async fn test_selected_history_item_stays_selected_on_worktree_updated(cx: &mut TestAppContext) {
1355    let app_state = init_test(cx);
1356
1357    app_state
1358        .fs
1359        .as_fake()
1360        .insert_tree(
1361            path!("/test"),
1362            json!({
1363                "test": {
1364                    "1.txt": "// One",
1365                    "2.txt": "// Two",
1366                    "3.txt": "// Three",
1367                }
1368            }),
1369        )
1370        .await;
1371
1372    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
1373    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1374
1375    open_close_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1376    open_close_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
1377    open_close_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
1378
1379    let picker = open_file_picker(&workspace, cx);
1380    picker.update(cx, |finder, _| {
1381        assert_eq!(finder.delegate.matches.len(), 3);
1382        assert_match_selection(finder, 0, "3.txt");
1383        assert_match_at_position(finder, 1, "2.txt");
1384        assert_match_at_position(finder, 2, "1.txt");
1385    });
1386
1387    cx.dispatch_action(SelectNext);
1388
1389    // Add more files to the worktree to trigger update matches
1390    for i in 0..5 {
1391        let filename = if cfg!(windows) {
1392            format!("C:/test/{}.txt", 4 + i)
1393        } else {
1394            format!("/test/{}.txt", 4 + i)
1395        };
1396        app_state
1397            .fs
1398            .create_file(Path::new(&filename), Default::default())
1399            .await
1400            .expect("unable to create file");
1401    }
1402
1403    cx.executor().advance_clock(FS_WATCH_LATENCY);
1404
1405    picker.update(cx, |finder, _| {
1406        assert_eq!(finder.delegate.matches.len(), 3);
1407        assert_match_at_position(finder, 0, "3.txt");
1408        assert_match_selection(finder, 1, "2.txt");
1409        assert_match_at_position(finder, 2, "1.txt");
1410    });
1411}
1412
1413#[gpui::test]
1414async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
1415    let app_state = init_test(cx);
1416
1417    app_state
1418        .fs
1419        .as_fake()
1420        .insert_tree(
1421            path!("/src"),
1422            json!({
1423                "collab_ui": {
1424                    "first.rs": "// First Rust file",
1425                    "second.rs": "// Second Rust file",
1426                    "third.rs": "// Third Rust file",
1427                    "collab_ui.rs": "// Fourth Rust file",
1428                }
1429            }),
1430        )
1431        .await;
1432
1433    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1434    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1435    // generate some history to select from
1436    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1437    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1438    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1439    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1440
1441    let finder = open_file_picker(&workspace, cx);
1442    let query = "collab_ui";
1443    cx.simulate_input(query);
1444    finder.update(cx, |picker, _| {
1445            let search_entries = collect_search_matches(picker).search_paths_only();
1446            assert_eq!(
1447                search_entries,
1448                vec![
1449                    PathBuf::from("collab_ui/collab_ui.rs"),
1450                    PathBuf::from("collab_ui/first.rs"),
1451                    PathBuf::from("collab_ui/third.rs"),
1452                    PathBuf::from("collab_ui/second.rs"),
1453                ],
1454                "Despite all search results having the same directory name, the most matching one should be on top"
1455            );
1456        });
1457}
1458
1459#[gpui::test]
1460async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) {
1461    let app_state = init_test(cx);
1462
1463    app_state
1464        .fs
1465        .as_fake()
1466        .insert_tree(
1467            path!("/src"),
1468            json!({
1469                "test": {
1470                    "first.rs": "// First Rust file",
1471                    "nonexistent.rs": "// Second Rust file",
1472                    "third.rs": "// Third Rust file",
1473                }
1474            }),
1475        )
1476        .await;
1477
1478    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1479    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); // generate some history to select from
1480    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1481    open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await;
1482    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1483    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1484    app_state
1485        .fs
1486        .remove_file(
1487            Path::new(path!("/src/test/nonexistent.rs")),
1488            RemoveOptions::default(),
1489        )
1490        .await
1491        .unwrap();
1492    cx.run_until_parked();
1493
1494    let picker = open_file_picker(&workspace, cx);
1495    cx.simulate_input("rs");
1496
1497    picker.update(cx, |picker, _| {
1498            assert_eq!(
1499                collect_search_matches(picker).history,
1500                vec![
1501                    PathBuf::from("test/first.rs"),
1502                    PathBuf::from("test/third.rs"),
1503                ],
1504                "Should have all opened files in the history, except the ones that do not exist on disk"
1505            );
1506        });
1507}
1508
1509#[gpui::test]
1510async fn test_search_results_refreshed_on_worktree_updates(cx: &mut gpui::TestAppContext) {
1511    let app_state = init_test(cx);
1512
1513    app_state
1514        .fs
1515        .as_fake()
1516        .insert_tree(
1517            "/src",
1518            json!({
1519                "lib.rs": "// Lib file",
1520                "main.rs": "// Bar file",
1521                "read.me": "// Readme file",
1522            }),
1523        )
1524        .await;
1525
1526    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1527    let (workspace, cx) =
1528        cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1529
1530    // Initial state
1531    let picker = open_file_picker(&workspace, cx);
1532    cx.simulate_input("rs");
1533    picker.update(cx, |finder, _| {
1534        assert_eq!(finder.delegate.matches.len(), 2);
1535        assert_match_at_position(finder, 0, "lib.rs");
1536        assert_match_at_position(finder, 1, "main.rs");
1537    });
1538
1539    // Delete main.rs
1540    app_state
1541        .fs
1542        .remove_file("/src/main.rs".as_ref(), Default::default())
1543        .await
1544        .expect("unable to remove file");
1545    cx.executor().advance_clock(FS_WATCH_LATENCY);
1546
1547    // main.rs is in not among search results anymore
1548    picker.update(cx, |finder, _| {
1549        assert_eq!(finder.delegate.matches.len(), 1);
1550        assert_match_at_position(finder, 0, "lib.rs");
1551    });
1552
1553    // Create util.rs
1554    app_state
1555        .fs
1556        .create_file("/src/util.rs".as_ref(), Default::default())
1557        .await
1558        .expect("unable to create file");
1559    cx.executor().advance_clock(FS_WATCH_LATENCY);
1560
1561    // util.rs is among search results
1562    picker.update(cx, |finder, _| {
1563        assert_eq!(finder.delegate.matches.len(), 2);
1564        assert_match_at_position(finder, 0, "lib.rs");
1565        assert_match_at_position(finder, 1, "util.rs");
1566    });
1567}
1568
1569#[gpui::test]
1570async fn test_search_results_refreshed_on_adding_and_removing_worktrees(
1571    cx: &mut gpui::TestAppContext,
1572) {
1573    let app_state = init_test(cx);
1574
1575    app_state
1576        .fs
1577        .as_fake()
1578        .insert_tree(
1579            "/test",
1580            json!({
1581                "project_1": {
1582                    "bar.rs": "// Bar file",
1583                    "lib.rs": "// Lib file",
1584                },
1585                "project_2": {
1586                    "Cargo.toml": "// Cargo file",
1587                    "main.rs": "// Main file",
1588                }
1589            }),
1590        )
1591        .await;
1592
1593    let project = Project::test(app_state.fs.clone(), ["/test/project_1".as_ref()], cx).await;
1594    let (workspace, cx) =
1595        cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1596    let worktree_1_id = project.update(cx, |project, cx| {
1597        let worktree = project.worktrees(cx).last().expect("worktree not found");
1598        worktree.read(cx).id()
1599    });
1600
1601    // Initial state
1602    let picker = open_file_picker(&workspace, cx);
1603    cx.simulate_input("rs");
1604    picker.update(cx, |finder, _| {
1605        assert_eq!(finder.delegate.matches.len(), 2);
1606        assert_match_at_position(finder, 0, "bar.rs");
1607        assert_match_at_position(finder, 1, "lib.rs");
1608    });
1609
1610    // Add new worktree
1611    project
1612        .update(cx, |project, cx| {
1613            project
1614                .find_or_create_worktree("/test/project_2", true, cx)
1615                .into_future()
1616        })
1617        .await
1618        .expect("unable to create workdir");
1619    cx.executor().advance_clock(FS_WATCH_LATENCY);
1620
1621    // main.rs is among search results
1622    picker.update(cx, |finder, _| {
1623        assert_eq!(finder.delegate.matches.len(), 3);
1624        assert_match_at_position(finder, 0, "bar.rs");
1625        assert_match_at_position(finder, 1, "lib.rs");
1626        assert_match_at_position(finder, 2, "main.rs");
1627    });
1628
1629    // Remove the first worktree
1630    project.update(cx, |project, cx| {
1631        project.remove_worktree(worktree_1_id, cx);
1632    });
1633    cx.executor().advance_clock(FS_WATCH_LATENCY);
1634
1635    // Files from the first worktree are not in the search results anymore
1636    picker.update(cx, |finder, _| {
1637        assert_eq!(finder.delegate.matches.len(), 1);
1638        assert_match_at_position(finder, 0, "main.rs");
1639    });
1640}
1641
1642#[gpui::test]
1643async fn test_selected_match_stays_selected_after_matches_refreshed(cx: &mut gpui::TestAppContext) {
1644    let app_state = init_test(cx);
1645
1646    app_state.fs.as_fake().insert_tree("/src", json!({})).await;
1647
1648    app_state
1649        .fs
1650        .create_dir("/src/even".as_ref())
1651        .await
1652        .expect("unable to create dir");
1653
1654    let initial_files_num = 5;
1655    for i in 0..initial_files_num {
1656        let filename = format!("/src/even/file_{}.txt", 10 + i);
1657        app_state
1658            .fs
1659            .create_file(Path::new(&filename), Default::default())
1660            .await
1661            .expect("unable to create file");
1662    }
1663
1664    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1665    let (workspace, cx) =
1666        cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1667
1668    // Initial state
1669    let picker = open_file_picker(&workspace, cx);
1670    cx.simulate_input("file");
1671    let selected_index = 3;
1672    // Checking only the filename, not the whole path
1673    let selected_file = format!("file_{}.txt", 10 + selected_index);
1674    // Select even/file_13.txt
1675    for _ in 0..selected_index {
1676        cx.dispatch_action(SelectNext);
1677    }
1678
1679    picker.update(cx, |finder, _| {
1680        assert_match_selection(finder, selected_index, &selected_file)
1681    });
1682
1683    // Add more matches to the search results
1684    let files_to_add = 10;
1685    for i in 0..files_to_add {
1686        let filename = format!("/src/file_{}.txt", 20 + i);
1687        app_state
1688            .fs
1689            .create_file(Path::new(&filename), Default::default())
1690            .await
1691            .expect("unable to create file");
1692    }
1693    cx.executor().advance_clock(FS_WATCH_LATENCY);
1694
1695    // file_13.txt is still selected
1696    picker.update(cx, |finder, _| {
1697        let expected_selected_index = selected_index + files_to_add;
1698        assert_match_selection(finder, expected_selected_index, &selected_file);
1699    });
1700}
1701
1702#[gpui::test]
1703async fn test_first_match_selected_if_previous_one_is_not_in_the_match_list(
1704    cx: &mut gpui::TestAppContext,
1705) {
1706    let app_state = init_test(cx);
1707
1708    app_state
1709        .fs
1710        .as_fake()
1711        .insert_tree(
1712            "/src",
1713            json!({
1714                "file_1.txt": "// file_1",
1715                "file_2.txt": "// file_2",
1716                "file_3.txt": "// file_3",
1717            }),
1718        )
1719        .await;
1720
1721    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1722    let (workspace, cx) =
1723        cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1724
1725    // Initial state
1726    let picker = open_file_picker(&workspace, cx);
1727    cx.simulate_input("file");
1728    // Select even/file_2.txt
1729    cx.dispatch_action(SelectNext);
1730
1731    // Remove the selected entry
1732    app_state
1733        .fs
1734        .remove_file("/src/file_2.txt".as_ref(), Default::default())
1735        .await
1736        .expect("unable to remove file");
1737    cx.executor().advance_clock(FS_WATCH_LATENCY);
1738
1739    // file_1.txt is now selected
1740    picker.update(cx, |finder, _| {
1741        assert_match_selection(finder, 0, "file_1.txt");
1742    });
1743}
1744
1745#[gpui::test]
1746async fn test_keeps_file_finder_open_after_modifier_keys_release(cx: &mut gpui::TestAppContext) {
1747    let app_state = init_test(cx);
1748
1749    app_state
1750        .fs
1751        .as_fake()
1752        .insert_tree(
1753            path!("/test"),
1754            json!({
1755                "1.txt": "// One",
1756            }),
1757        )
1758        .await;
1759
1760    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
1761    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1762
1763    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1764
1765    cx.simulate_modifiers_change(Modifiers::secondary_key());
1766    open_file_picker(&workspace, cx);
1767
1768    cx.simulate_modifiers_change(Modifiers::none());
1769    active_file_picker(&workspace, cx);
1770}
1771
1772#[gpui::test]
1773async fn test_opens_file_on_modifier_keys_release(cx: &mut gpui::TestAppContext) {
1774    let app_state = init_test(cx);
1775
1776    app_state
1777        .fs
1778        .as_fake()
1779        .insert_tree(
1780            path!("/test"),
1781            json!({
1782                "1.txt": "// One",
1783                "2.txt": "// Two",
1784            }),
1785        )
1786        .await;
1787
1788    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
1789    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1790
1791    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1792    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
1793
1794    cx.simulate_modifiers_change(Modifiers::secondary_key());
1795    let picker = open_file_picker(&workspace, cx);
1796    picker.update(cx, |finder, _| {
1797        assert_eq!(finder.delegate.matches.len(), 2);
1798        assert_match_selection(finder, 0, "2.txt");
1799        assert_match_at_position(finder, 1, "1.txt");
1800    });
1801
1802    cx.dispatch_action(SelectNext);
1803    cx.simulate_modifiers_change(Modifiers::none());
1804    cx.read(|cx| {
1805        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1806        assert_eq!(active_editor.read(cx).title(cx), "1.txt");
1807    });
1808}
1809
1810#[gpui::test]
1811async fn test_switches_between_release_norelease_modes_on_forward_nav(
1812    cx: &mut gpui::TestAppContext,
1813) {
1814    let app_state = init_test(cx);
1815
1816    app_state
1817        .fs
1818        .as_fake()
1819        .insert_tree(
1820            path!("/test"),
1821            json!({
1822                "1.txt": "// One",
1823                "2.txt": "// Two",
1824            }),
1825        )
1826        .await;
1827
1828    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
1829    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1830
1831    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1832    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
1833
1834    // Open with a shortcut
1835    cx.simulate_modifiers_change(Modifiers::secondary_key());
1836    let picker = open_file_picker(&workspace, cx);
1837    picker.update(cx, |finder, _| {
1838        assert_eq!(finder.delegate.matches.len(), 2);
1839        assert_match_selection(finder, 0, "2.txt");
1840        assert_match_at_position(finder, 1, "1.txt");
1841    });
1842
1843    // Switch to navigating with other shortcuts
1844    // Don't open file on modifiers release
1845    cx.simulate_modifiers_change(Modifiers::control());
1846    cx.dispatch_action(SelectNext);
1847    cx.simulate_modifiers_change(Modifiers::none());
1848    picker.update(cx, |finder, _| {
1849        assert_eq!(finder.delegate.matches.len(), 2);
1850        assert_match_at_position(finder, 0, "2.txt");
1851        assert_match_selection(finder, 1, "1.txt");
1852    });
1853
1854    // Back to navigation with initial shortcut
1855    // Open file on modifiers release
1856    cx.simulate_modifiers_change(Modifiers::secondary_key());
1857    cx.dispatch_action(ToggleFileFinder::default());
1858    cx.simulate_modifiers_change(Modifiers::none());
1859    cx.read(|cx| {
1860        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1861        assert_eq!(active_editor.read(cx).title(cx), "2.txt");
1862    });
1863}
1864
1865#[gpui::test]
1866async fn test_switches_between_release_norelease_modes_on_backward_nav(
1867    cx: &mut gpui::TestAppContext,
1868) {
1869    let app_state = init_test(cx);
1870
1871    app_state
1872        .fs
1873        .as_fake()
1874        .insert_tree(
1875            path!("/test"),
1876            json!({
1877                "1.txt": "// One",
1878                "2.txt": "// Two",
1879                "3.txt": "// Three"
1880            }),
1881        )
1882        .await;
1883
1884    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
1885    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1886
1887    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1888    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
1889    open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
1890
1891    // Open with a shortcut
1892    cx.simulate_modifiers_change(Modifiers::secondary_key());
1893    let picker = open_file_picker(&workspace, cx);
1894    picker.update(cx, |finder, _| {
1895        assert_eq!(finder.delegate.matches.len(), 3);
1896        assert_match_selection(finder, 0, "3.txt");
1897        assert_match_at_position(finder, 1, "2.txt");
1898        assert_match_at_position(finder, 2, "1.txt");
1899    });
1900
1901    // Switch to navigating with other shortcuts
1902    // Don't open file on modifiers release
1903    cx.simulate_modifiers_change(Modifiers::control());
1904    cx.dispatch_action(menu::SelectPrev);
1905    cx.simulate_modifiers_change(Modifiers::none());
1906    picker.update(cx, |finder, _| {
1907        assert_eq!(finder.delegate.matches.len(), 3);
1908        assert_match_at_position(finder, 0, "3.txt");
1909        assert_match_at_position(finder, 1, "2.txt");
1910        assert_match_selection(finder, 2, "1.txt");
1911    });
1912
1913    // Back to navigation with initial shortcut
1914    // Open file on modifiers release
1915    cx.simulate_modifiers_change(Modifiers::secondary_key());
1916    cx.dispatch_action(SelectPrev); // <-- File Finder's SelectPrev, not menu's
1917    cx.simulate_modifiers_change(Modifiers::none());
1918    cx.read(|cx| {
1919        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1920        assert_eq!(active_editor.read(cx).title(cx), "3.txt");
1921    });
1922}
1923
1924#[gpui::test]
1925async fn test_extending_modifiers_does_not_confirm_selection(cx: &mut gpui::TestAppContext) {
1926    let app_state = init_test(cx);
1927
1928    app_state
1929        .fs
1930        .as_fake()
1931        .insert_tree(
1932            path!("/test"),
1933            json!({
1934                "1.txt": "// One",
1935            }),
1936        )
1937        .await;
1938
1939    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
1940    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1941
1942    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1943
1944    cx.simulate_modifiers_change(Modifiers::secondary_key());
1945    open_file_picker(&workspace, cx);
1946
1947    cx.simulate_modifiers_change(Modifiers::command_shift());
1948    active_file_picker(&workspace, cx);
1949}
1950
1951#[gpui::test]
1952async fn test_repeat_toggle_action(cx: &mut gpui::TestAppContext) {
1953    let app_state = init_test(cx);
1954    app_state
1955        .fs
1956        .as_fake()
1957        .insert_tree(
1958            "/test",
1959            json!({
1960                "00.txt": "",
1961                "01.txt": "",
1962                "02.txt": "",
1963                "03.txt": "",
1964                "04.txt": "",
1965                "05.txt": "",
1966            }),
1967        )
1968        .await;
1969
1970    let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
1971    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1972
1973    cx.dispatch_action(ToggleFileFinder::default());
1974    let picker = active_file_picker(&workspace, cx);
1975    picker.update(cx, |picker, _| {
1976        assert_eq!(picker.delegate.selected_index, 0);
1977        assert_eq!(picker.logical_scroll_top_index(), 0);
1978    });
1979
1980    // When toggling repeatedly, the picker scrolls to reveal the selected item.
1981    cx.dispatch_action(ToggleFileFinder::default());
1982    cx.dispatch_action(ToggleFileFinder::default());
1983    cx.dispatch_action(ToggleFileFinder::default());
1984    picker.update(cx, |picker, _| {
1985        assert_eq!(picker.delegate.selected_index, 3);
1986        assert_eq!(picker.logical_scroll_top_index(), 3);
1987    });
1988}
1989
1990async fn open_close_queried_buffer(
1991    input: &str,
1992    expected_matches: usize,
1993    expected_editor_title: &str,
1994    workspace: &Entity<Workspace>,
1995    cx: &mut gpui::VisualTestContext,
1996) -> Vec<FoundPath> {
1997    let history_items = open_queried_buffer(
1998        input,
1999        expected_matches,
2000        expected_editor_title,
2001        workspace,
2002        cx,
2003    )
2004    .await;
2005
2006    cx.dispatch_action(workspace::CloseActiveItem {
2007        save_intent: None,
2008        close_pinned: false,
2009    });
2010
2011    history_items
2012}
2013
2014async fn open_queried_buffer(
2015    input: &str,
2016    expected_matches: usize,
2017    expected_editor_title: &str,
2018    workspace: &Entity<Workspace>,
2019    cx: &mut gpui::VisualTestContext,
2020) -> Vec<FoundPath> {
2021    let picker = open_file_picker(&workspace, cx);
2022    cx.simulate_input(input);
2023
2024    let history_items = picker.update(cx, |finder, _| {
2025        assert_eq!(
2026            finder.delegate.matches.len(),
2027            expected_matches,
2028            "Unexpected number of matches found for query `{input}`, matches: {:?}",
2029            finder.delegate.matches
2030        );
2031        finder.delegate.history_items.clone()
2032    });
2033
2034    cx.dispatch_action(Confirm);
2035
2036    cx.read(|cx| {
2037        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
2038        let active_editor_title = active_editor.read(cx).title(cx);
2039        assert_eq!(
2040            expected_editor_title, active_editor_title,
2041            "Unexpected editor title for query `{input}`"
2042        );
2043    });
2044
2045    history_items
2046}
2047
2048fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
2049    cx.update(|cx| {
2050        let state = AppState::test(cx);
2051        theme::init(theme::LoadThemes::JustBase, cx);
2052        language::init(cx);
2053        super::init(cx);
2054        editor::init(cx);
2055        workspace::init_settings(cx);
2056        Project::init_settings(cx);
2057        state
2058    })
2059}
2060
2061fn test_path_position(test_str: &str) -> FileSearchQuery {
2062    let path_position = PathWithPosition::parse_str(test_str);
2063
2064    FileSearchQuery {
2065        raw_query: test_str.to_owned(),
2066        file_query_end: if path_position.path.to_str().unwrap() == test_str {
2067            None
2068        } else {
2069            Some(path_position.path.to_str().unwrap().len())
2070        },
2071        path_position,
2072    }
2073}
2074
2075fn build_find_picker(
2076    project: Entity<Project>,
2077    cx: &mut TestAppContext,
2078) -> (
2079    Entity<Picker<FileFinderDelegate>>,
2080    Entity<Workspace>,
2081    &mut VisualTestContext,
2082) {
2083    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2084    let picker = open_file_picker(&workspace, cx);
2085    (picker, workspace, cx)
2086}
2087
2088#[track_caller]
2089fn open_file_picker(
2090    workspace: &Entity<Workspace>,
2091    cx: &mut VisualTestContext,
2092) -> Entity<Picker<FileFinderDelegate>> {
2093    cx.dispatch_action(ToggleFileFinder {
2094        separate_history: true,
2095    });
2096    active_file_picker(workspace, cx)
2097}
2098
2099#[track_caller]
2100fn active_file_picker(
2101    workspace: &Entity<Workspace>,
2102    cx: &mut VisualTestContext,
2103) -> Entity<Picker<FileFinderDelegate>> {
2104    workspace.update(cx, |workspace, cx| {
2105        workspace
2106            .active_modal::<FileFinder>(cx)
2107            .expect("file finder is not open")
2108            .read(cx)
2109            .picker
2110            .clone()
2111    })
2112}
2113
2114#[derive(Debug, Default)]
2115struct SearchEntries {
2116    history: Vec<PathBuf>,
2117    history_found_paths: Vec<FoundPath>,
2118    search: Vec<PathBuf>,
2119    search_matches: Vec<PathMatch>,
2120}
2121
2122impl SearchEntries {
2123    #[track_caller]
2124    fn search_paths_only(self) -> Vec<PathBuf> {
2125        assert!(
2126            self.history.is_empty(),
2127            "Should have no history matches, but got: {:?}",
2128            self.history
2129        );
2130        self.search
2131    }
2132
2133    #[track_caller]
2134    fn search_matches_only(self) -> Vec<PathMatch> {
2135        assert!(
2136            self.history.is_empty(),
2137            "Should have no history matches, but got: {:?}",
2138            self.history
2139        );
2140        self.search_matches
2141    }
2142}
2143
2144fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries {
2145    let mut search_entries = SearchEntries::default();
2146    for m in &picker.delegate.matches.matches {
2147        match &m {
2148            Match::History {
2149                path: history_path,
2150                panel_match: path_match,
2151            } => {
2152                search_entries.history.push(
2153                    path_match
2154                        .as_ref()
2155                        .map(|path_match| {
2156                            Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path)
2157                        })
2158                        .unwrap_or_else(|| {
2159                            history_path
2160                                .absolute
2161                                .as_deref()
2162                                .unwrap_or_else(|| &history_path.project.path)
2163                                .to_path_buf()
2164                        }),
2165                );
2166                search_entries
2167                    .history_found_paths
2168                    .push(history_path.clone());
2169            }
2170            Match::Search(path_match) => {
2171                search_entries
2172                    .search
2173                    .push(Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path));
2174                search_entries.search_matches.push(path_match.0.clone());
2175            }
2176        }
2177    }
2178    search_entries
2179}
2180
2181#[track_caller]
2182fn assert_match_selection(
2183    finder: &Picker<FileFinderDelegate>,
2184    expected_selection_index: usize,
2185    expected_file_name: &str,
2186) {
2187    assert_eq!(
2188        finder.delegate.selected_index(),
2189        expected_selection_index,
2190        "Match is not selected"
2191    );
2192    assert_match_at_position(finder, expected_selection_index, expected_file_name);
2193}
2194
2195#[track_caller]
2196fn assert_match_at_position(
2197    finder: &Picker<FileFinderDelegate>,
2198    match_index: usize,
2199    expected_file_name: &str,
2200) {
2201    let match_item = finder
2202        .delegate
2203        .matches
2204        .get(match_index)
2205        .unwrap_or_else(|| panic!("Finder has no match for index {match_index}"));
2206    let match_file_name = match &match_item {
2207        Match::History { path, .. } => path.absolute.as_deref().unwrap().file_name(),
2208        Match::Search(path_match) => path_match.0.path.file_name(),
2209    }
2210    .unwrap()
2211    .to_string_lossy();
2212    assert_eq!(match_file_name, expected_file_name);
2213}