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