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