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::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
1454    let picker = open_file_picker(&workspace, cx);
1455    cx.simulate_input("rs");
1456
1457    picker.update(cx, |picker, _| {
1458            assert_eq!(
1459                collect_search_matches(picker).history,
1460                vec![
1461                    PathBuf::from("test/first.rs"),
1462                    PathBuf::from("test/third.rs"),
1463                ],
1464                "Should have all opened files in the history, except the ones that do not exist on disk"
1465            );
1466        });
1467}
1468
1469#[gpui::test]
1470async fn test_search_results_refreshed_on_worktree_updates(cx: &mut gpui::TestAppContext) {
1471    let app_state = init_test(cx);
1472
1473    app_state
1474        .fs
1475        .as_fake()
1476        .insert_tree(
1477            "/src",
1478            json!({
1479                "lib.rs": "// Lib file",
1480                "main.rs": "// Bar file",
1481                "read.me": "// Readme file",
1482            }),
1483        )
1484        .await;
1485
1486    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1487    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
1488
1489    // Initial state
1490    let picker = open_file_picker(&workspace, cx);
1491    cx.simulate_input("rs");
1492    picker.update(cx, |finder, _| {
1493        assert_eq!(finder.delegate.matches.len(), 2);
1494        assert_match_at_position(finder, 0, "lib.rs");
1495        assert_match_at_position(finder, 1, "main.rs");
1496    });
1497
1498    // Delete main.rs
1499    app_state
1500        .fs
1501        .remove_file("/src/main.rs".as_ref(), Default::default())
1502        .await
1503        .expect("unable to remove file");
1504    cx.executor().advance_clock(FS_WATCH_LATENCY);
1505
1506    // main.rs is in not among search results anymore
1507    picker.update(cx, |finder, _| {
1508        assert_eq!(finder.delegate.matches.len(), 1);
1509        assert_match_at_position(finder, 0, "lib.rs");
1510    });
1511
1512    // Create util.rs
1513    app_state
1514        .fs
1515        .create_file("/src/util.rs".as_ref(), Default::default())
1516        .await
1517        .expect("unable to create file");
1518    cx.executor().advance_clock(FS_WATCH_LATENCY);
1519
1520    // util.rs is among search results
1521    picker.update(cx, |finder, _| {
1522        assert_eq!(finder.delegate.matches.len(), 2);
1523        assert_match_at_position(finder, 0, "lib.rs");
1524        assert_match_at_position(finder, 1, "util.rs");
1525    });
1526}
1527
1528#[gpui::test]
1529async fn test_search_results_refreshed_on_adding_and_removing_worktrees(
1530    cx: &mut gpui::TestAppContext,
1531) {
1532    let app_state = init_test(cx);
1533
1534    app_state
1535        .fs
1536        .as_fake()
1537        .insert_tree(
1538            "/test",
1539            json!({
1540                "project_1": {
1541                    "bar.rs": "// Bar file",
1542                    "lib.rs": "// Lib file",
1543                },
1544                "project_2": {
1545                    "Cargo.toml": "// Cargo file",
1546                    "main.rs": "// Main file",
1547                }
1548            }),
1549        )
1550        .await;
1551
1552    let project = Project::test(app_state.fs.clone(), ["/test/project_1".as_ref()], cx).await;
1553    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
1554    let worktree_1_id = project.update(cx, |project, cx| {
1555        let worktree = project.worktrees(cx).last().expect("worktree not found");
1556        worktree.read(cx).id()
1557    });
1558
1559    // Initial state
1560    let picker = open_file_picker(&workspace, cx);
1561    cx.simulate_input("rs");
1562    picker.update(cx, |finder, _| {
1563        assert_eq!(finder.delegate.matches.len(), 2);
1564        assert_match_at_position(finder, 0, "bar.rs");
1565        assert_match_at_position(finder, 1, "lib.rs");
1566    });
1567
1568    // Add new worktree
1569    project
1570        .update(cx, |project, cx| {
1571            project
1572                .find_or_create_worktree("/test/project_2", true, cx)
1573                .into_future()
1574        })
1575        .await
1576        .expect("unable to create workdir");
1577    cx.executor().advance_clock(FS_WATCH_LATENCY);
1578
1579    // main.rs is among search results
1580    picker.update(cx, |finder, _| {
1581        assert_eq!(finder.delegate.matches.len(), 3);
1582        assert_match_at_position(finder, 0, "bar.rs");
1583        assert_match_at_position(finder, 1, "lib.rs");
1584        assert_match_at_position(finder, 2, "main.rs");
1585    });
1586
1587    // Remove the first worktree
1588    project.update(cx, |project, cx| {
1589        project.remove_worktree(worktree_1_id, cx);
1590    });
1591    cx.executor().advance_clock(FS_WATCH_LATENCY);
1592
1593    // Files from the first worktree are not in the search results anymore
1594    picker.update(cx, |finder, _| {
1595        assert_eq!(finder.delegate.matches.len(), 1);
1596        assert_match_at_position(finder, 0, "main.rs");
1597    });
1598}
1599
1600#[gpui::test]
1601async fn test_selected_match_stays_selected_after_matches_refreshed(cx: &mut gpui::TestAppContext) {
1602    let app_state = init_test(cx);
1603
1604    app_state.fs.as_fake().insert_tree("/src", json!({})).await;
1605
1606    app_state
1607        .fs
1608        .create_dir("/src/even".as_ref())
1609        .await
1610        .expect("unable to create dir");
1611
1612    let initial_files_num = 5;
1613    for i in 0..initial_files_num {
1614        let filename = format!("/src/even/file_{}.txt", 10 + i);
1615        app_state
1616            .fs
1617            .create_file(Path::new(&filename), Default::default())
1618            .await
1619            .expect("unable to create file");
1620    }
1621
1622    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1623    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
1624
1625    // Initial state
1626    let picker = open_file_picker(&workspace, cx);
1627    cx.simulate_input("file");
1628    let selected_index = 3;
1629    // Checking only the filename, not the whole path
1630    let selected_file = format!("file_{}.txt", 10 + selected_index);
1631    // Select even/file_13.txt
1632    for _ in 0..selected_index {
1633        cx.dispatch_action(SelectNext);
1634    }
1635
1636    picker.update(cx, |finder, _| {
1637        assert_match_selection(finder, selected_index, &selected_file)
1638    });
1639
1640    // Add more matches to the search results
1641    let files_to_add = 10;
1642    for i in 0..files_to_add {
1643        let filename = format!("/src/file_{}.txt", 20 + i);
1644        app_state
1645            .fs
1646            .create_file(Path::new(&filename), Default::default())
1647            .await
1648            .expect("unable to create file");
1649    }
1650    cx.executor().advance_clock(FS_WATCH_LATENCY);
1651
1652    // file_13.txt is still selected
1653    picker.update(cx, |finder, _| {
1654        let expected_selected_index = selected_index + files_to_add;
1655        assert_match_selection(finder, expected_selected_index, &selected_file);
1656    });
1657}
1658
1659#[gpui::test]
1660async fn test_first_match_selected_if_previous_one_is_not_in_the_match_list(
1661    cx: &mut gpui::TestAppContext,
1662) {
1663    let app_state = init_test(cx);
1664
1665    app_state
1666        .fs
1667        .as_fake()
1668        .insert_tree(
1669            "/src",
1670            json!({
1671                "file_1.txt": "// file_1",
1672                "file_2.txt": "// file_2",
1673                "file_3.txt": "// file_3",
1674            }),
1675        )
1676        .await;
1677
1678    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1679    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
1680
1681    // Initial state
1682    let picker = open_file_picker(&workspace, cx);
1683    cx.simulate_input("file");
1684    // Select even/file_2.txt
1685    cx.dispatch_action(SelectNext);
1686
1687    // Remove the selected entry
1688    app_state
1689        .fs
1690        .remove_file("/src/file_2.txt".as_ref(), Default::default())
1691        .await
1692        .expect("unable to remove file");
1693    cx.executor().advance_clock(FS_WATCH_LATENCY);
1694
1695    // file_1.txt is now selected
1696    picker.update(cx, |finder, _| {
1697        assert_match_selection(finder, 0, "file_1.txt");
1698    });
1699}
1700
1701#[gpui::test]
1702async fn test_keeps_file_finder_open_after_modifier_keys_release(cx: &mut gpui::TestAppContext) {
1703    let app_state = init_test(cx);
1704
1705    app_state
1706        .fs
1707        .as_fake()
1708        .insert_tree(
1709            "/test",
1710            json!({
1711                "1.txt": "// One",
1712            }),
1713        )
1714        .await;
1715
1716    let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
1717    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1718
1719    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1720
1721    cx.simulate_modifiers_change(Modifiers::secondary_key());
1722    open_file_picker(&workspace, cx);
1723
1724    cx.simulate_modifiers_change(Modifiers::none());
1725    active_file_picker(&workspace, cx);
1726}
1727
1728#[gpui::test]
1729async fn test_opens_file_on_modifier_keys_release(cx: &mut gpui::TestAppContext) {
1730    let app_state = init_test(cx);
1731
1732    app_state
1733        .fs
1734        .as_fake()
1735        .insert_tree(
1736            "/test",
1737            json!({
1738                "1.txt": "// One",
1739                "2.txt": "// Two",
1740            }),
1741        )
1742        .await;
1743
1744    let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
1745    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1746
1747    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1748    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
1749
1750    cx.simulate_modifiers_change(Modifiers::secondary_key());
1751    let picker = open_file_picker(&workspace, cx);
1752    picker.update(cx, |finder, _| {
1753        assert_eq!(finder.delegate.matches.len(), 2);
1754        assert_match_selection(finder, 0, "2.txt");
1755        assert_match_at_position(finder, 1, "1.txt");
1756    });
1757
1758    cx.dispatch_action(SelectNext);
1759    cx.simulate_modifiers_change(Modifiers::none());
1760    cx.read(|cx| {
1761        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1762        assert_eq!(active_editor.read(cx).title(cx), "1.txt");
1763    });
1764}
1765
1766#[gpui::test]
1767async fn test_switches_between_release_norelease_modes_on_forward_nav(
1768    cx: &mut gpui::TestAppContext,
1769) {
1770    let app_state = init_test(cx);
1771
1772    app_state
1773        .fs
1774        .as_fake()
1775        .insert_tree(
1776            "/test",
1777            json!({
1778                "1.txt": "// One",
1779                "2.txt": "// Two",
1780            }),
1781        )
1782        .await;
1783
1784    let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
1785    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1786
1787    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1788    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
1789
1790    // Open with a shortcut
1791    cx.simulate_modifiers_change(Modifiers::secondary_key());
1792    let picker = open_file_picker(&workspace, cx);
1793    picker.update(cx, |finder, _| {
1794        assert_eq!(finder.delegate.matches.len(), 2);
1795        assert_match_selection(finder, 0, "2.txt");
1796        assert_match_at_position(finder, 1, "1.txt");
1797    });
1798
1799    // Switch to navigating with other shortcuts
1800    // Don't open file on modifiers release
1801    cx.simulate_modifiers_change(Modifiers::control());
1802    cx.dispatch_action(SelectNext);
1803    cx.simulate_modifiers_change(Modifiers::none());
1804    picker.update(cx, |finder, _| {
1805        assert_eq!(finder.delegate.matches.len(), 2);
1806        assert_match_at_position(finder, 0, "2.txt");
1807        assert_match_selection(finder, 1, "1.txt");
1808    });
1809
1810    // Back to navigation with initial shortcut
1811    // Open file on modifiers release
1812    cx.simulate_modifiers_change(Modifiers::secondary_key());
1813    cx.dispatch_action(ToggleFileFinder::default());
1814    cx.simulate_modifiers_change(Modifiers::none());
1815    cx.read(|cx| {
1816        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1817        assert_eq!(active_editor.read(cx).title(cx), "2.txt");
1818    });
1819}
1820
1821#[gpui::test]
1822async fn test_switches_between_release_norelease_modes_on_backward_nav(
1823    cx: &mut gpui::TestAppContext,
1824) {
1825    let app_state = init_test(cx);
1826
1827    app_state
1828        .fs
1829        .as_fake()
1830        .insert_tree(
1831            "/test",
1832            json!({
1833                "1.txt": "// One",
1834                "2.txt": "// Two",
1835                "3.txt": "// Three"
1836            }),
1837        )
1838        .await;
1839
1840    let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
1841    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1842
1843    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1844    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
1845    open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
1846
1847    // Open with a shortcut
1848    cx.simulate_modifiers_change(Modifiers::secondary_key());
1849    let picker = open_file_picker(&workspace, cx);
1850    picker.update(cx, |finder, _| {
1851        assert_eq!(finder.delegate.matches.len(), 3);
1852        assert_match_selection(finder, 0, "3.txt");
1853        assert_match_at_position(finder, 1, "2.txt");
1854        assert_match_at_position(finder, 2, "1.txt");
1855    });
1856
1857    // Switch to navigating with other shortcuts
1858    // Don't open file on modifiers release
1859    cx.simulate_modifiers_change(Modifiers::control());
1860    cx.dispatch_action(menu::SelectPrev);
1861    cx.simulate_modifiers_change(Modifiers::none());
1862    picker.update(cx, |finder, _| {
1863        assert_eq!(finder.delegate.matches.len(), 3);
1864        assert_match_at_position(finder, 0, "3.txt");
1865        assert_match_at_position(finder, 1, "2.txt");
1866        assert_match_selection(finder, 2, "1.txt");
1867    });
1868
1869    // Back to navigation with initial shortcut
1870    // Open file on modifiers release
1871    cx.simulate_modifiers_change(Modifiers::secondary_key());
1872    cx.dispatch_action(SelectPrev); // <-- File Finder's SelectPrev, not menu's
1873    cx.simulate_modifiers_change(Modifiers::none());
1874    cx.read(|cx| {
1875        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1876        assert_eq!(active_editor.read(cx).title(cx), "3.txt");
1877    });
1878}
1879
1880#[gpui::test]
1881async fn test_extending_modifiers_does_not_confirm_selection(cx: &mut gpui::TestAppContext) {
1882    let app_state = init_test(cx);
1883
1884    app_state
1885        .fs
1886        .as_fake()
1887        .insert_tree(
1888            "/test",
1889            json!({
1890                "1.txt": "// One",
1891            }),
1892        )
1893        .await;
1894
1895    let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
1896    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1897
1898    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1899
1900    cx.simulate_modifiers_change(Modifiers::secondary_key());
1901    open_file_picker(&workspace, cx);
1902
1903    cx.simulate_modifiers_change(Modifiers::command_shift());
1904    active_file_picker(&workspace, cx);
1905}
1906
1907#[gpui::test]
1908async fn test_repeat_toggle_action(cx: &mut gpui::TestAppContext) {
1909    let app_state = init_test(cx);
1910    app_state
1911        .fs
1912        .as_fake()
1913        .insert_tree(
1914            "/test",
1915            json!({
1916                "00.txt": "",
1917                "01.txt": "",
1918                "02.txt": "",
1919                "03.txt": "",
1920                "04.txt": "",
1921                "05.txt": "",
1922            }),
1923        )
1924        .await;
1925
1926    let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
1927    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1928
1929    cx.dispatch_action(ToggleFileFinder::default());
1930    let picker = active_file_picker(&workspace, cx);
1931    picker.update(cx, |picker, _| {
1932        assert_eq!(picker.delegate.selected_index, 0);
1933        assert_eq!(picker.logical_scroll_top_index(), 0);
1934    });
1935
1936    // When toggling repeatedly, the picker scrolls to reveal the selected item.
1937    cx.dispatch_action(ToggleFileFinder::default());
1938    cx.dispatch_action(ToggleFileFinder::default());
1939    cx.dispatch_action(ToggleFileFinder::default());
1940    picker.update(cx, |picker, _| {
1941        assert_eq!(picker.delegate.selected_index, 3);
1942        assert_eq!(picker.logical_scroll_top_index(), 3);
1943    });
1944}
1945
1946async fn open_close_queried_buffer(
1947    input: &str,
1948    expected_matches: usize,
1949    expected_editor_title: &str,
1950    workspace: &View<Workspace>,
1951    cx: &mut gpui::VisualTestContext,
1952) -> Vec<FoundPath> {
1953    let history_items = open_queried_buffer(
1954        input,
1955        expected_matches,
1956        expected_editor_title,
1957        workspace,
1958        cx,
1959    )
1960    .await;
1961
1962    cx.dispatch_action(workspace::CloseActiveItem { save_intent: None });
1963
1964    history_items
1965}
1966
1967async fn open_queried_buffer(
1968    input: &str,
1969    expected_matches: usize,
1970    expected_editor_title: &str,
1971    workspace: &View<Workspace>,
1972    cx: &mut gpui::VisualTestContext,
1973) -> Vec<FoundPath> {
1974    let picker = open_file_picker(&workspace, cx);
1975    cx.simulate_input(input);
1976
1977    let history_items = picker.update(cx, |finder, _| {
1978        assert_eq!(
1979            finder.delegate.matches.len(),
1980            expected_matches,
1981            "Unexpected number of matches found for query `{input}`, matches: {:?}",
1982            finder.delegate.matches
1983        );
1984        finder.delegate.history_items.clone()
1985    });
1986
1987    cx.dispatch_action(Confirm);
1988
1989    cx.read(|cx| {
1990        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1991        let active_editor_title = active_editor.read(cx).title(cx);
1992        assert_eq!(
1993            expected_editor_title, active_editor_title,
1994            "Unexpected editor title for query `{input}`"
1995        );
1996    });
1997
1998    history_items
1999}
2000
2001fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
2002    cx.update(|cx| {
2003        let state = AppState::test(cx);
2004        theme::init(theme::LoadThemes::JustBase, cx);
2005        language::init(cx);
2006        super::init(cx);
2007        editor::init(cx);
2008        workspace::init_settings(cx);
2009        Project::init_settings(cx);
2010        state
2011    })
2012}
2013
2014fn test_path_position(test_str: &str) -> FileSearchQuery {
2015    let path_position = PathWithPosition::parse_str(test_str);
2016
2017    FileSearchQuery {
2018        raw_query: test_str.to_owned(),
2019        file_query_end: if path_position.path.to_str().unwrap() == test_str {
2020            None
2021        } else {
2022            Some(path_position.path.to_str().unwrap().len())
2023        },
2024        path_position,
2025    }
2026}
2027
2028fn build_find_picker(
2029    project: Model<Project>,
2030    cx: &mut TestAppContext,
2031) -> (
2032    View<Picker<FileFinderDelegate>>,
2033    View<Workspace>,
2034    &mut VisualTestContext,
2035) {
2036    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
2037    let picker = open_file_picker(&workspace, cx);
2038    (picker, workspace, cx)
2039}
2040
2041#[track_caller]
2042fn open_file_picker(
2043    workspace: &View<Workspace>,
2044    cx: &mut VisualTestContext,
2045) -> View<Picker<FileFinderDelegate>> {
2046    cx.dispatch_action(ToggleFileFinder {
2047        separate_history: true,
2048    });
2049    active_file_picker(workspace, cx)
2050}
2051
2052#[track_caller]
2053fn active_file_picker(
2054    workspace: &View<Workspace>,
2055    cx: &mut VisualTestContext,
2056) -> View<Picker<FileFinderDelegate>> {
2057    workspace.update(cx, |workspace, cx| {
2058        workspace
2059            .active_modal::<FileFinder>(cx)
2060            .expect("file finder is not open")
2061            .read(cx)
2062            .picker
2063            .clone()
2064    })
2065}
2066
2067#[derive(Debug, Default)]
2068struct SearchEntries {
2069    history: Vec<PathBuf>,
2070    history_found_paths: Vec<FoundPath>,
2071    search: Vec<PathBuf>,
2072    search_matches: Vec<PathMatch>,
2073}
2074
2075impl SearchEntries {
2076    #[track_caller]
2077    fn search_paths_only(self) -> Vec<PathBuf> {
2078        assert!(
2079            self.history.is_empty(),
2080            "Should have no history matches, but got: {:?}",
2081            self.history
2082        );
2083        self.search
2084    }
2085
2086    #[track_caller]
2087    fn search_matches_only(self) -> Vec<PathMatch> {
2088        assert!(
2089            self.history.is_empty(),
2090            "Should have no history matches, but got: {:?}",
2091            self.history
2092        );
2093        self.search_matches
2094    }
2095}
2096
2097fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries {
2098    let mut search_entries = SearchEntries::default();
2099    for m in &picker.delegate.matches.matches {
2100        match &m {
2101            Match::History {
2102                path: history_path,
2103                panel_match: path_match,
2104            } => {
2105                search_entries.history.push(
2106                    path_match
2107                        .as_ref()
2108                        .map(|path_match| {
2109                            Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path)
2110                        })
2111                        .unwrap_or_else(|| {
2112                            history_path
2113                                .absolute
2114                                .as_deref()
2115                                .unwrap_or_else(|| &history_path.project.path)
2116                                .to_path_buf()
2117                        }),
2118                );
2119                search_entries
2120                    .history_found_paths
2121                    .push(history_path.clone());
2122            }
2123            Match::Search(path_match) => {
2124                search_entries
2125                    .search
2126                    .push(Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path));
2127                search_entries.search_matches.push(path_match.0.clone());
2128            }
2129        }
2130    }
2131    search_entries
2132}
2133
2134#[track_caller]
2135fn assert_match_selection(
2136    finder: &Picker<FileFinderDelegate>,
2137    expected_selection_index: usize,
2138    expected_file_name: &str,
2139) {
2140    assert_eq!(
2141        finder.delegate.selected_index(),
2142        expected_selection_index,
2143        "Match is not selected"
2144    );
2145    assert_match_at_position(finder, expected_selection_index, expected_file_name);
2146}
2147
2148#[track_caller]
2149fn assert_match_at_position(
2150    finder: &Picker<FileFinderDelegate>,
2151    match_index: usize,
2152    expected_file_name: &str,
2153) {
2154    let match_item = finder
2155        .delegate
2156        .matches
2157        .get(match_index)
2158        .unwrap_or_else(|| panic!("Finder has no match for index {match_index}"));
2159    let match_file_name = match &match_item {
2160        Match::History { path, .. } => path.absolute.as_deref().unwrap().file_name(),
2161        Match::Search(path_match) => path_match.0.path.file_name(),
2162    }
2163    .unwrap()
2164    .to_string_lossy();
2165    assert_eq!(match_file_name, expected_file_name);
2166}