file_finder_tests.rs

   1use std::{future::IntoFuture, path::Path, time::Duration};
   2
   3use super::*;
   4use editor::Editor;
   5use gpui::{Entity, TestAppContext, VisualTestContext};
   6use menu::{Confirm, SelectNext, SelectPrevious};
   7use pretty_assertions::{assert_eq, assert_matches};
   8use project::{FS_WATCH_LATENCY, RemoveOptions};
   9use serde_json::json;
  10use settings::SettingsStore;
  11use util::{path, rel_path::rel_path};
  12use workspace::{AppState, CloseActiveItem, OpenOptions, ToggleFileFinder, Workspace, open_paths};
  13
  14#[ctor::ctor]
  15fn init_logger() {
  16    zlog::init_test();
  17}
  18
  19#[test]
  20fn test_path_elision() {
  21    #[track_caller]
  22    fn check(path: &str, budget: usize, matches: impl IntoIterator<Item = usize>, expected: &str) {
  23        let mut path = path.to_owned();
  24        let slice = PathComponentSlice::new(&path);
  25        let matches = Vec::from_iter(matches);
  26        if let Some(range) = slice.elision_range(budget - 1, &matches) {
  27            path.replace_range(range, "");
  28        }
  29        assert_eq!(path, expected);
  30    }
  31
  32    // Simple cases, mostly to check that different path shapes are handled gracefully.
  33    check("p/a/b/c/d/", 6, [], "p/…/d/");
  34    check("p/a/b/c/d/", 1, [2, 4, 6], "p/a/b/c/d/");
  35    check("p/a/b/c/d/", 10, [2, 6], "p/a/…/c/d/");
  36    check("p/a/b/c/d/", 8, [6], "p/…/c/d/");
  37
  38    check("p/a/b/c/d", 5, [], "p/…/d");
  39    check("p/a/b/c/d", 9, [2, 4, 6], "p/a/b/c/d");
  40    check("p/a/b/c/d", 9, [2, 6], "p/a/…/c/d");
  41    check("p/a/b/c/d", 7, [6], "p/…/c/d");
  42
  43    check("/p/a/b/c/d/", 7, [], "/p/…/d/");
  44    check("/p/a/b/c/d/", 11, [3, 5, 7], "/p/a/b/c/d/");
  45    check("/p/a/b/c/d/", 11, [3, 7], "/p/a/…/c/d/");
  46    check("/p/a/b/c/d/", 9, [7], "/p/…/c/d/");
  47
  48    // If the budget can't be met, no elision is done.
  49    check(
  50        "project/dir/child/grandchild",
  51        5,
  52        [],
  53        "project/dir/child/grandchild",
  54    );
  55
  56    // The longest unmatched segment is picked for elision.
  57    check(
  58        "project/one/two/X/three/sub",
  59        21,
  60        [16],
  61        "project/…/X/three/sub",
  62    );
  63
  64    // Elision stops when the budget is met, even though there are more components in the chosen segment.
  65    // It proceeds from the end of the unmatched segment that is closer to the midpoint of the path.
  66    check(
  67        "project/one/two/three/X/sub",
  68        21,
  69        [22],
  70        "project/…/three/X/sub",
  71    )
  72}
  73
  74#[test]
  75fn test_custom_project_search_ordering_in_file_finder() {
  76    let mut file_finder_sorted_output = vec![
  77        ProjectPanelOrdMatch(PathMatch {
  78            score: 0.5,
  79            positions: Vec::new(),
  80            worktree_id: 0,
  81            path: rel_path("b0.5").into(),
  82            path_prefix: rel_path("").into(),
  83            distance_to_relative_ancestor: 0,
  84            is_dir: false,
  85        }),
  86        ProjectPanelOrdMatch(PathMatch {
  87            score: 1.0,
  88            positions: Vec::new(),
  89            worktree_id: 0,
  90            path: rel_path("c1.0").into(),
  91            path_prefix: rel_path("").into(),
  92            distance_to_relative_ancestor: 0,
  93            is_dir: false,
  94        }),
  95        ProjectPanelOrdMatch(PathMatch {
  96            score: 1.0,
  97            positions: Vec::new(),
  98            worktree_id: 0,
  99            path: rel_path("a1.0").into(),
 100            path_prefix: rel_path("").into(),
 101            distance_to_relative_ancestor: 0,
 102            is_dir: false,
 103        }),
 104        ProjectPanelOrdMatch(PathMatch {
 105            score: 0.5,
 106            positions: Vec::new(),
 107            worktree_id: 0,
 108            path: rel_path("a0.5").into(),
 109            path_prefix: rel_path("").into(),
 110            distance_to_relative_ancestor: 0,
 111            is_dir: false,
 112        }),
 113        ProjectPanelOrdMatch(PathMatch {
 114            score: 1.0,
 115            positions: Vec::new(),
 116            worktree_id: 0,
 117            path: rel_path("b1.0").into(),
 118            path_prefix: rel_path("").into(),
 119            distance_to_relative_ancestor: 0,
 120            is_dir: false,
 121        }),
 122    ];
 123    file_finder_sorted_output.sort_by(|a, b| b.cmp(a));
 124
 125    assert_eq!(
 126        file_finder_sorted_output,
 127        vec![
 128            ProjectPanelOrdMatch(PathMatch {
 129                score: 1.0,
 130                positions: Vec::new(),
 131                worktree_id: 0,
 132                path: rel_path("a1.0").into(),
 133                path_prefix: rel_path("").into(),
 134                distance_to_relative_ancestor: 0,
 135                is_dir: false,
 136            }),
 137            ProjectPanelOrdMatch(PathMatch {
 138                score: 1.0,
 139                positions: Vec::new(),
 140                worktree_id: 0,
 141                path: rel_path("b1.0").into(),
 142                path_prefix: rel_path("").into(),
 143                distance_to_relative_ancestor: 0,
 144                is_dir: false,
 145            }),
 146            ProjectPanelOrdMatch(PathMatch {
 147                score: 1.0,
 148                positions: Vec::new(),
 149                worktree_id: 0,
 150                path: rel_path("c1.0").into(),
 151                path_prefix: rel_path("").into(),
 152                distance_to_relative_ancestor: 0,
 153                is_dir: false,
 154            }),
 155            ProjectPanelOrdMatch(PathMatch {
 156                score: 0.5,
 157                positions: Vec::new(),
 158                worktree_id: 0,
 159                path: rel_path("a0.5").into(),
 160                path_prefix: rel_path("").into(),
 161                distance_to_relative_ancestor: 0,
 162                is_dir: false,
 163            }),
 164            ProjectPanelOrdMatch(PathMatch {
 165                score: 0.5,
 166                positions: Vec::new(),
 167                worktree_id: 0,
 168                path: rel_path("b0.5").into(),
 169                path_prefix: rel_path("").into(),
 170                distance_to_relative_ancestor: 0,
 171                is_dir: false,
 172            }),
 173        ]
 174    );
 175}
 176
 177#[gpui::test]
 178async fn test_matching_paths(cx: &mut TestAppContext) {
 179    let app_state = init_test(cx);
 180    app_state
 181        .fs
 182        .as_fake()
 183        .insert_tree(
 184            path!("/root"),
 185            json!({
 186                "a": {
 187                    "banana": "",
 188                    "bandana": "",
 189                }
 190            }),
 191        )
 192        .await;
 193
 194    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 195
 196    let (picker, workspace, cx) = build_find_picker(project, cx);
 197
 198    cx.simulate_input("bna");
 199    picker.update(cx, |picker, _| {
 200        assert_eq!(picker.delegate.matches.len(), 3);
 201    });
 202    cx.dispatch_action(SelectNext);
 203    cx.dispatch_action(Confirm);
 204    cx.read(|cx| {
 205        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
 206        assert_eq!(active_editor.read(cx).title(cx), "bandana");
 207    });
 208
 209    for bandana_query in [
 210        "bandana",
 211        "./bandana",
 212        ".\\bandana",
 213        util::path!("a/bandana"),
 214        "b/bandana",
 215        "b\\bandana",
 216        " bandana",
 217        "bandana ",
 218        " bandana ",
 219        " ndan ",
 220        " band ",
 221        "a bandana",
 222        "bandana:",
 223    ] {
 224        picker
 225            .update_in(cx, |picker, window, cx| {
 226                picker
 227                    .delegate
 228                    .update_matches(bandana_query.to_string(), window, cx)
 229            })
 230            .await;
 231        picker.update(cx, |picker, _| {
 232            assert_eq!(
 233                picker.delegate.matches.len(),
 234                // existence of CreateNew option depends on whether path already exists
 235                if bandana_query == util::path!("a/bandana") {
 236                    1
 237                } else {
 238                    2
 239                },
 240                "Wrong number of matches for bandana query '{bandana_query}'. Matches: {:?}",
 241                picker.delegate.matches
 242            );
 243        });
 244        cx.dispatch_action(SelectNext);
 245        cx.dispatch_action(Confirm);
 246        cx.read(|cx| {
 247            let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
 248            assert_eq!(
 249                active_editor.read(cx).title(cx),
 250                "bandana",
 251                "Wrong match for bandana query '{bandana_query}'"
 252            );
 253        });
 254    }
 255}
 256
 257#[gpui::test]
 258async fn test_matching_paths_with_colon(cx: &mut TestAppContext) {
 259    let app_state = init_test(cx);
 260    app_state
 261        .fs
 262        .as_fake()
 263        .insert_tree(
 264            path!("/root"),
 265            json!({
 266                "a": {
 267                    "foo:bar.rs": "",
 268                    "foo.rs": "",
 269                }
 270            }),
 271        )
 272        .await;
 273
 274    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 275
 276    let (picker, _, cx) = build_find_picker(project, cx);
 277
 278    // 'foo:' matches both files
 279    cx.simulate_input("foo:");
 280    picker.update(cx, |picker, _| {
 281        assert_eq!(picker.delegate.matches.len(), 3);
 282        assert_match_at_position(picker, 0, "foo.rs");
 283        assert_match_at_position(picker, 1, "foo:bar.rs");
 284    });
 285
 286    // 'foo:b' matches one of the files
 287    cx.simulate_input("b");
 288    picker.update(cx, |picker, _| {
 289        assert_eq!(picker.delegate.matches.len(), 2);
 290        assert_match_at_position(picker, 0, "foo:bar.rs");
 291    });
 292
 293    cx.dispatch_action(editor::actions::Backspace);
 294
 295    // 'foo:1' matches both files, specifying which row to jump to
 296    cx.simulate_input("1");
 297    picker.update(cx, |picker, _| {
 298        assert_eq!(picker.delegate.matches.len(), 3);
 299        assert_match_at_position(picker, 0, "foo.rs");
 300        assert_match_at_position(picker, 1, "foo:bar.rs");
 301    });
 302}
 303
 304#[gpui::test]
 305async fn test_unicode_paths(cx: &mut TestAppContext) {
 306    let app_state = init_test(cx);
 307    app_state
 308        .fs
 309        .as_fake()
 310        .insert_tree(
 311            path!("/root"),
 312            json!({
 313                "a": {
 314                    "İg": " ",
 315                }
 316            }),
 317        )
 318        .await;
 319
 320    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 321
 322    let (picker, workspace, cx) = build_find_picker(project, cx);
 323
 324    cx.simulate_input("g");
 325    picker.update(cx, |picker, _| {
 326        assert_eq!(picker.delegate.matches.len(), 2);
 327        assert_match_at_position(picker, 1, "g");
 328    });
 329    cx.dispatch_action(Confirm);
 330    cx.read(|cx| {
 331        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
 332        assert_eq!(active_editor.read(cx).title(cx), "İg");
 333    });
 334}
 335
 336#[gpui::test]
 337async fn test_absolute_paths(cx: &mut TestAppContext) {
 338    let app_state = init_test(cx);
 339    app_state
 340        .fs
 341        .as_fake()
 342        .insert_tree(
 343            path!("/root"),
 344            json!({
 345                "a": {
 346                    "file1.txt": "",
 347                    "b": {
 348                        "file2.txt": "",
 349                    },
 350                }
 351            }),
 352        )
 353        .await;
 354
 355    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 356
 357    let (picker, workspace, cx) = build_find_picker(project, cx);
 358
 359    let matching_abs_path = path!("/root/a/b/file2.txt").to_string();
 360    picker
 361        .update_in(cx, |picker, window, cx| {
 362            picker
 363                .delegate
 364                .update_matches(matching_abs_path, window, cx)
 365        })
 366        .await;
 367    picker.update(cx, |picker, _| {
 368        assert_eq!(
 369            collect_search_matches(picker).search_paths_only(),
 370            vec![rel_path("a/b/file2.txt").into()],
 371            "Matching abs path should be the only match"
 372        )
 373    });
 374    cx.dispatch_action(SelectNext);
 375    cx.dispatch_action(Confirm);
 376    cx.read(|cx| {
 377        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
 378        assert_eq!(active_editor.read(cx).title(cx), "file2.txt");
 379    });
 380
 381    let mismatching_abs_path = path!("/root/a/b/file1.txt").to_string();
 382    picker
 383        .update_in(cx, |picker, window, cx| {
 384            picker
 385                .delegate
 386                .update_matches(mismatching_abs_path, window, cx)
 387        })
 388        .await;
 389    picker.update(cx, |picker, _| {
 390        assert_eq!(
 391            collect_search_matches(picker).search_paths_only(),
 392            Vec::new(),
 393            "Mismatching abs path should produce no matches"
 394        )
 395    });
 396}
 397
 398#[gpui::test]
 399async fn test_complex_path(cx: &mut TestAppContext) {
 400    let app_state = init_test(cx);
 401    app_state
 402        .fs
 403        .as_fake()
 404        .insert_tree(
 405            path!("/root"),
 406            json!({
 407                "其他": {
 408                    "S数据表格": {
 409                        "task.xlsx": "some content",
 410                    },
 411                }
 412            }),
 413        )
 414        .await;
 415
 416    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 417
 418    let (picker, workspace, cx) = build_find_picker(project, cx);
 419
 420    cx.simulate_input("t");
 421    picker.update(cx, |picker, _| {
 422        assert_eq!(picker.delegate.matches.len(), 2);
 423        assert_eq!(
 424            collect_search_matches(picker).search_paths_only(),
 425            vec![rel_path("其他/S数据表格/task.xlsx").into()],
 426        )
 427    });
 428    cx.dispatch_action(Confirm);
 429    cx.read(|cx| {
 430        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
 431        assert_eq!(active_editor.read(cx).title(cx), "task.xlsx");
 432    });
 433}
 434
 435#[gpui::test]
 436async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
 437    let app_state = init_test(cx);
 438
 439    let first_file_name = "first.rs";
 440    let first_file_contents = "// First Rust file";
 441    app_state
 442        .fs
 443        .as_fake()
 444        .insert_tree(
 445            path!("/src"),
 446            json!({
 447                "test": {
 448                    first_file_name: first_file_contents,
 449                    "second.rs": "// Second Rust file",
 450                }
 451            }),
 452        )
 453        .await;
 454
 455    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
 456
 457    let (picker, workspace, cx) = build_find_picker(project, cx);
 458
 459    let file_query = &first_file_name[..3];
 460    let file_row = 1;
 461    let file_column = 3;
 462    assert!(file_column <= first_file_contents.len());
 463    let query_inside_file = format!("{file_query}:{file_row}:{file_column}");
 464    picker
 465        .update_in(cx, |finder, window, cx| {
 466            finder
 467                .delegate
 468                .update_matches(query_inside_file.to_string(), window, cx)
 469        })
 470        .await;
 471    picker.update(cx, |finder, _| {
 472        assert_match_at_position(finder, 1, &query_inside_file.to_string());
 473        let finder = &finder.delegate;
 474        assert_eq!(finder.matches.len(), 2);
 475        let latest_search_query = finder
 476            .latest_search_query
 477            .as_ref()
 478            .expect("Finder should have a query after the update_matches call");
 479        assert_eq!(latest_search_query.raw_query, query_inside_file);
 480        assert_eq!(latest_search_query.file_query_end, Some(file_query.len()));
 481        assert_eq!(latest_search_query.path_position.row, Some(file_row));
 482        assert_eq!(
 483            latest_search_query.path_position.column,
 484            Some(file_column as u32)
 485        );
 486    });
 487
 488    cx.dispatch_action(Confirm);
 489
 490    let editor = cx.update(|_, cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
 491    cx.executor().advance_clock(Duration::from_secs(2));
 492
 493    editor.update(cx, |editor, cx| {
 494            let all_selections = editor.selections.all_adjusted(&editor.display_snapshot(cx));
 495            assert_eq!(
 496                all_selections.len(),
 497                1,
 498                "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
 499            );
 500            let caret_selection = all_selections.into_iter().next().unwrap();
 501            assert_eq!(caret_selection.start, caret_selection.end,
 502                "Caret selection should have its start and end at the same position");
 503            assert_eq!(file_row, caret_selection.start.row + 1,
 504                "Query inside file should get caret with the same focus row");
 505            assert_eq!(file_column, caret_selection.start.column as usize + 1,
 506                "Query inside file should get caret with the same focus column");
 507        });
 508}
 509
 510#[gpui::test]
 511async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) {
 512    let app_state = init_test(cx);
 513
 514    let first_file_name = "first.rs";
 515    let first_file_contents = "// First Rust file";
 516    app_state
 517        .fs
 518        .as_fake()
 519        .insert_tree(
 520            path!("/src"),
 521            json!({
 522                "test": {
 523                    first_file_name: first_file_contents,
 524                    "second.rs": "// Second Rust file",
 525                }
 526            }),
 527        )
 528        .await;
 529
 530    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
 531
 532    let (picker, workspace, cx) = build_find_picker(project, cx);
 533
 534    let file_query = &first_file_name[..3];
 535    let file_row = 200;
 536    let file_column = 300;
 537    assert!(file_column > first_file_contents.len());
 538    let query_outside_file = format!("{file_query}:{file_row}:{file_column}");
 539    picker
 540        .update_in(cx, |picker, window, cx| {
 541            picker
 542                .delegate
 543                .update_matches(query_outside_file.to_string(), window, cx)
 544        })
 545        .await;
 546    picker.update(cx, |finder, _| {
 547        assert_match_at_position(finder, 1, &query_outside_file.to_string());
 548        let delegate = &finder.delegate;
 549        assert_eq!(delegate.matches.len(), 2);
 550        let latest_search_query = delegate
 551            .latest_search_query
 552            .as_ref()
 553            .expect("Finder should have a query after the update_matches call");
 554        assert_eq!(latest_search_query.raw_query, query_outside_file);
 555        assert_eq!(latest_search_query.file_query_end, Some(file_query.len()));
 556        assert_eq!(latest_search_query.path_position.row, Some(file_row));
 557        assert_eq!(
 558            latest_search_query.path_position.column,
 559            Some(file_column as u32)
 560        );
 561    });
 562
 563    cx.dispatch_action(Confirm);
 564
 565    let editor = cx.update(|_, cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
 566    cx.executor().advance_clock(Duration::from_secs(2));
 567
 568    editor.update(cx, |editor, cx| {
 569            let all_selections = editor.selections.all_adjusted(&editor.display_snapshot(cx));
 570            assert_eq!(
 571                all_selections.len(),
 572                1,
 573                "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
 574            );
 575            let caret_selection = all_selections.into_iter().next().unwrap();
 576            assert_eq!(caret_selection.start, caret_selection.end,
 577                "Caret selection should have its start and end at the same position");
 578            assert_eq!(0, caret_selection.start.row,
 579                "Excessive rows (as in query outside file borders) should get trimmed to last file row");
 580            assert_eq!(first_file_contents.len(), caret_selection.start.column as usize,
 581                "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column");
 582        });
 583}
 584
 585#[gpui::test]
 586async fn test_matching_cancellation(cx: &mut TestAppContext) {
 587    let app_state = init_test(cx);
 588    app_state
 589        .fs
 590        .as_fake()
 591        .insert_tree(
 592            "/dir",
 593            json!({
 594                "hello": "",
 595                "goodbye": "",
 596                "halogen-light": "",
 597                "happiness": "",
 598                "height": "",
 599                "hi": "",
 600                "hiccup": "",
 601            }),
 602        )
 603        .await;
 604
 605    let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
 606
 607    let (picker, _, cx) = build_find_picker(project, cx);
 608
 609    let query = test_path_position("hi");
 610    picker
 611        .update_in(cx, |picker, window, cx| {
 612            picker.delegate.spawn_search(query.clone(), window, cx)
 613        })
 614        .await;
 615
 616    picker.update(cx, |picker, _cx| {
 617        // CreateNew option not shown in this case since file already exists
 618        assert_eq!(picker.delegate.matches.len(), 5);
 619    });
 620
 621    picker.update_in(cx, |picker, window, cx| {
 622        let matches = collect_search_matches(picker).search_matches_only();
 623        let delegate = &mut picker.delegate;
 624
 625        // Simulate a search being cancelled after the time limit,
 626        // returning only a subset of the matches that would have been found.
 627        drop(delegate.spawn_search(query.clone(), window, cx));
 628        delegate.set_search_matches(
 629            delegate.latest_search_id,
 630            true, // did-cancel
 631            query.clone(),
 632            vec![
 633                ProjectPanelOrdMatch(matches[1].clone()),
 634                ProjectPanelOrdMatch(matches[3].clone()),
 635            ],
 636            cx,
 637        );
 638
 639        // Simulate another cancellation.
 640        drop(delegate.spawn_search(query.clone(), window, cx));
 641        delegate.set_search_matches(
 642            delegate.latest_search_id,
 643            true, // did-cancel
 644            query.clone(),
 645            vec![
 646                ProjectPanelOrdMatch(matches[0].clone()),
 647                ProjectPanelOrdMatch(matches[2].clone()),
 648                ProjectPanelOrdMatch(matches[3].clone()),
 649            ],
 650            cx,
 651        );
 652
 653        assert_eq!(
 654            collect_search_matches(picker)
 655                .search_matches_only()
 656                .as_slice(),
 657            &matches[0..4]
 658        );
 659    });
 660}
 661
 662#[gpui::test]
 663async fn test_ignored_root_with_file_inclusions(cx: &mut TestAppContext) {
 664    let app_state = init_test(cx);
 665    cx.update(|cx| {
 666        cx.update_global::<SettingsStore, _>(|store, cx| {
 667            store.update_user_settings(cx, |settings| {
 668                settings.project.worktree.file_scan_inclusions = Some(vec![
 669                    "height_demo/**/hi_bonjour".to_string(),
 670                    "**/height_1".to_string(),
 671                ]);
 672            });
 673        })
 674    });
 675    app_state
 676        .fs
 677        .as_fake()
 678        .insert_tree(
 679            "/ancestor",
 680            json!({
 681                ".gitignore": "ignored-root",
 682                "ignored-root": {
 683                    "happiness": "",
 684                    "height": "",
 685                    "hi": "",
 686                    "hiccup": "",
 687                },
 688                "tracked-root": {
 689                    ".gitignore": "height*",
 690                    "happiness": "",
 691                    "height": "",
 692                    "heights": {
 693                        "height_1": "",
 694                        "height_2": "",
 695                    },
 696                    "height_demo": {
 697                        "test_1": {
 698                            "hi_bonjour": "hi_bonjour",
 699                            "hi": "hello",
 700                        },
 701                        "hihi": "bye",
 702                        "test_2": {
 703                            "hoi": "nl"
 704                        }
 705                    },
 706                    "height_include": {
 707                        "height_1_include": "",
 708                        "height_2_include": "",
 709                    },
 710                    "hi": "",
 711                    "hiccup": "",
 712                },
 713            }),
 714        )
 715        .await;
 716
 717    let project = Project::test(
 718        app_state.fs.clone(),
 719        [
 720            Path::new(path!("/ancestor/tracked-root")),
 721            Path::new(path!("/ancestor/ignored-root")),
 722        ],
 723        cx,
 724    )
 725    .await;
 726    let (picker, _workspace, cx) = build_find_picker(project, cx);
 727
 728    picker
 729        .update_in(cx, |picker, window, cx| {
 730            picker
 731                .delegate
 732                .spawn_search(test_path_position("hi"), window, cx)
 733        })
 734        .await;
 735    picker.update(cx, |picker, _| {
 736        let matches = collect_search_matches(picker);
 737        assert_eq!(matches.history.len(), 0);
 738        assert_eq!(
 739            matches.search,
 740            vec![
 741                rel_path("ignored-root/hi").into(),
 742                rel_path("tracked-root/hi").into(),
 743                rel_path("ignored-root/hiccup").into(),
 744                rel_path("tracked-root/hiccup").into(),
 745                rel_path("tracked-root/height_demo/test_1/hi_bonjour").into(),
 746                rel_path("ignored-root/height").into(),
 747                rel_path("tracked-root/heights/height_1").into(),
 748                rel_path("ignored-root/happiness").into(),
 749                rel_path("tracked-root/happiness").into(),
 750            ],
 751            "All ignored files that were indexed are found for default ignored mode"
 752        );
 753    });
 754}
 755
 756#[gpui::test]
 757async fn test_ignored_root_with_file_inclusions_repro(cx: &mut TestAppContext) {
 758    let app_state = init_test(cx);
 759    cx.update(|cx| {
 760        cx.update_global::<SettingsStore, _>(|store, cx| {
 761            store.update_user_settings(cx, |settings| {
 762                settings.project.worktree.file_scan_inclusions = Some(vec!["**/.env".to_string()]);
 763            });
 764        })
 765    });
 766    app_state
 767        .fs
 768        .as_fake()
 769        .insert_tree(
 770            "/src",
 771            json!({
 772                ".gitignore": "node_modules",
 773                "node_modules": {
 774                    "package.json": "// package.json",
 775                    ".env": "BAR=FOO"
 776                },
 777                ".env": "FOO=BAR"
 778            }),
 779        )
 780        .await;
 781
 782    let project = Project::test(app_state.fs.clone(), [Path::new(path!("/src"))], cx).await;
 783    let (picker, _workspace, cx) = build_find_picker(project, cx);
 784
 785    picker
 786        .update_in(cx, |picker, window, cx| {
 787            picker
 788                .delegate
 789                .spawn_search(test_path_position("json"), window, cx)
 790        })
 791        .await;
 792    picker.update(cx, |picker, _| {
 793        let matches = collect_search_matches(picker);
 794        assert_eq!(matches.history.len(), 0);
 795        assert_eq!(
 796            matches.search,
 797            vec![],
 798            "All ignored files that were indexed are found for default ignored mode"
 799        );
 800    });
 801}
 802
 803#[gpui::test]
 804async fn test_ignored_root(cx: &mut TestAppContext) {
 805    let app_state = init_test(cx);
 806    app_state
 807        .fs
 808        .as_fake()
 809        .insert_tree(
 810            "/ancestor",
 811            json!({
 812                ".gitignore": "ignored-root",
 813                "ignored-root": {
 814                    "happiness": "",
 815                    "height": "",
 816                    "hi": "",
 817                    "hiccup": "",
 818                },
 819                "tracked-root": {
 820                    ".gitignore": "height*",
 821                    "happiness": "",
 822                    "height": "",
 823                    "heights": {
 824                        "height_1": "",
 825                        "height_2": "",
 826                    },
 827                    "hi": "",
 828                    "hiccup": "",
 829                },
 830            }),
 831        )
 832        .await;
 833
 834    let project = Project::test(
 835        app_state.fs.clone(),
 836        [
 837            Path::new(path!("/ancestor/tracked-root")),
 838            Path::new(path!("/ancestor/ignored-root")),
 839        ],
 840        cx,
 841    )
 842    .await;
 843    let (picker, workspace, cx) = build_find_picker(project, cx);
 844
 845    picker
 846        .update_in(cx, |picker, window, cx| {
 847            picker
 848                .delegate
 849                .spawn_search(test_path_position("hi"), window, cx)
 850        })
 851        .await;
 852    picker.update(cx, |picker, _| {
 853        let matches = collect_search_matches(picker);
 854        assert_eq!(matches.history.len(), 0);
 855        assert_eq!(
 856            matches.search,
 857            vec![
 858                rel_path("ignored-root/hi").into(),
 859                rel_path("tracked-root/hi").into(),
 860                rel_path("ignored-root/hiccup").into(),
 861                rel_path("tracked-root/hiccup").into(),
 862                rel_path("ignored-root/height").into(),
 863                rel_path("ignored-root/happiness").into(),
 864                rel_path("tracked-root/happiness").into(),
 865            ],
 866            "All ignored files that were indexed are found for default ignored mode"
 867        );
 868    });
 869    cx.dispatch_action(ToggleIncludeIgnored);
 870    picker
 871        .update_in(cx, |picker, window, cx| {
 872            picker
 873                .delegate
 874                .spawn_search(test_path_position("hi"), window, cx)
 875        })
 876        .await;
 877    picker.update(cx, |picker, _| {
 878        let matches = collect_search_matches(picker);
 879        assert_eq!(matches.history.len(), 0);
 880        assert_eq!(
 881            matches.search,
 882            vec![
 883                rel_path("ignored-root/hi").into(),
 884                rel_path("tracked-root/hi").into(),
 885                rel_path("ignored-root/hiccup").into(),
 886                rel_path("tracked-root/hiccup").into(),
 887                rel_path("ignored-root/height").into(),
 888                rel_path("tracked-root/height").into(),
 889                rel_path("ignored-root/happiness").into(),
 890                rel_path("tracked-root/happiness").into(),
 891            ],
 892            "All ignored files should be found, for the toggled on ignored mode"
 893        );
 894    });
 895
 896    picker
 897        .update_in(cx, |picker, window, cx| {
 898            picker.delegate.include_ignored = Some(false);
 899            picker
 900                .delegate
 901                .spawn_search(test_path_position("hi"), window, cx)
 902        })
 903        .await;
 904    picker.update(cx, |picker, _| {
 905        let matches = collect_search_matches(picker);
 906        assert_eq!(matches.history.len(), 0);
 907        assert_eq!(
 908            matches.search,
 909            vec![
 910                rel_path("tracked-root/hi").into(),
 911                rel_path("tracked-root/hiccup").into(),
 912                rel_path("tracked-root/happiness").into(),
 913            ],
 914            "Only non-ignored files should be found for the turned off ignored mode"
 915        );
 916    });
 917
 918    workspace
 919        .update_in(cx, |workspace, window, cx| {
 920            workspace.open_abs_path(
 921                PathBuf::from(path!("/ancestor/tracked-root/heights/height_1")),
 922                OpenOptions {
 923                    visible: Some(OpenVisible::None),
 924                    ..OpenOptions::default()
 925                },
 926                window,
 927                cx,
 928            )
 929        })
 930        .await
 931        .unwrap();
 932    cx.run_until_parked();
 933    workspace
 934        .update_in(cx, |workspace, window, cx| {
 935            workspace.active_pane().update(cx, |pane, cx| {
 936                pane.close_active_item(&CloseActiveItem::default(), window, cx)
 937            })
 938        })
 939        .await
 940        .unwrap();
 941    cx.run_until_parked();
 942
 943    picker
 944        .update_in(cx, |picker, window, cx| {
 945            picker.delegate.include_ignored = None;
 946            picker
 947                .delegate
 948                .spawn_search(test_path_position("hi"), window, cx)
 949        })
 950        .await;
 951    picker.update(cx, |picker, _| {
 952        let matches = collect_search_matches(picker);
 953        assert_eq!(matches.history.len(), 0);
 954        assert_eq!(
 955            matches.search,
 956            vec![
 957                rel_path("ignored-root/hi").into(),
 958                rel_path("tracked-root/hi").into(),
 959                rel_path("ignored-root/hiccup").into(),
 960                rel_path("tracked-root/hiccup").into(),
 961                rel_path("ignored-root/height").into(),
 962                rel_path("ignored-root/happiness").into(),
 963                rel_path("tracked-root/happiness").into(),
 964            ],
 965            "Only for the worktree with the ignored root, all indexed ignored files are found in the auto ignored mode"
 966        );
 967    });
 968
 969    picker
 970        .update_in(cx, |picker, window, cx| {
 971            picker.delegate.include_ignored = Some(true);
 972            picker
 973                .delegate
 974                .spawn_search(test_path_position("hi"), window, cx)
 975        })
 976        .await;
 977    picker.update(cx, |picker, _| {
 978        let matches = collect_search_matches(picker);
 979        assert_eq!(matches.history.len(), 0);
 980        assert_eq!(
 981            matches.search,
 982            vec![
 983                rel_path("ignored-root/hi").into(),
 984                rel_path("tracked-root/hi").into(),
 985                rel_path("ignored-root/hiccup").into(),
 986                rel_path("tracked-root/hiccup").into(),
 987                rel_path("ignored-root/height").into(),
 988                rel_path("tracked-root/height").into(),
 989                rel_path("tracked-root/heights/height_1").into(),
 990                rel_path("tracked-root/heights/height_2").into(),
 991                rel_path("ignored-root/happiness").into(),
 992                rel_path("tracked-root/happiness").into(),
 993            ],
 994            "All ignored files that were indexed are found in the turned on ignored mode"
 995        );
 996    });
 997
 998    picker
 999        .update_in(cx, |picker, window, cx| {
1000            picker.delegate.include_ignored = Some(false);
1001            picker
1002                .delegate
1003                .spawn_search(test_path_position("hi"), window, cx)
1004        })
1005        .await;
1006    picker.update(cx, |picker, _| {
1007        let matches = collect_search_matches(picker);
1008        assert_eq!(matches.history.len(), 0);
1009        assert_eq!(
1010            matches.search,
1011            vec![
1012                rel_path("tracked-root/hi").into(),
1013                rel_path("tracked-root/hiccup").into(),
1014                rel_path("tracked-root/happiness").into(),
1015            ],
1016            "Only non-ignored files should be found for the turned off ignored mode"
1017        );
1018    });
1019}
1020
1021#[gpui::test]
1022async fn test_single_file_worktrees(cx: &mut TestAppContext) {
1023    let app_state = init_test(cx);
1024    app_state
1025        .fs
1026        .as_fake()
1027        .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } }))
1028        .await;
1029
1030    let project = Project::test(
1031        app_state.fs.clone(),
1032        ["/root/the-parent-dir/the-file".as_ref()],
1033        cx,
1034    )
1035    .await;
1036
1037    let (picker, _, cx) = build_find_picker(project, cx);
1038
1039    // Even though there is only one worktree, that worktree's filename
1040    // is included in the matching, because the worktree is a single file.
1041    picker
1042        .update_in(cx, |picker, window, cx| {
1043            picker
1044                .delegate
1045                .spawn_search(test_path_position("thf"), window, cx)
1046        })
1047        .await;
1048    cx.read(|cx| {
1049        let picker = picker.read(cx);
1050        let delegate = &picker.delegate;
1051        let matches = collect_search_matches(picker).search_matches_only();
1052        assert_eq!(matches.len(), 1);
1053
1054        let (file_name, file_name_positions, full_path, full_path_positions) =
1055            delegate.labels_for_path_match(&matches[0], PathStyle::local());
1056        assert_eq!(file_name, "the-file");
1057        assert_eq!(file_name_positions, &[0, 1, 4]);
1058        assert_eq!(full_path, "");
1059        assert_eq!(full_path_positions, &[0; 0]);
1060    });
1061
1062    // Since the worktree root is a file, searching for its name followed by a slash does
1063    // not match anything.
1064    picker
1065        .update_in(cx, |picker, window, cx| {
1066            picker
1067                .delegate
1068                .spawn_search(test_path_position("thf/"), window, cx)
1069        })
1070        .await;
1071    picker.update(cx, |f, _| assert_eq!(f.delegate.matches.len(), 0));
1072}
1073
1074#[gpui::test]
1075async fn test_history_items_uniqueness_for_multiple_worktree(cx: &mut TestAppContext) {
1076    let app_state = init_test(cx);
1077    app_state
1078        .fs
1079        .as_fake()
1080        .insert_tree(
1081            path!("/repo1"),
1082            json!({
1083                "package.json": r#"{"name": "repo1"}"#,
1084                "src": {
1085                    "index.js": "// Repo 1 index",
1086                }
1087            }),
1088        )
1089        .await;
1090
1091    app_state
1092        .fs
1093        .as_fake()
1094        .insert_tree(
1095            path!("/repo2"),
1096            json!({
1097                "package.json": r#"{"name": "repo2"}"#,
1098                "src": {
1099                    "index.js": "// Repo 2 index",
1100                }
1101            }),
1102        )
1103        .await;
1104
1105    let project = Project::test(
1106        app_state.fs.clone(),
1107        [path!("/repo1").as_ref(), path!("/repo2").as_ref()],
1108        cx,
1109    )
1110    .await;
1111
1112    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1113    let (worktree_id1, worktree_id2) = cx.read(|cx| {
1114        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1115        (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
1116    });
1117
1118    workspace
1119        .update_in(cx, |workspace, window, cx| {
1120            workspace.open_path(
1121                ProjectPath {
1122                    worktree_id: worktree_id1,
1123                    path: rel_path("package.json").into(),
1124                },
1125                None,
1126                true,
1127                window,
1128                cx,
1129            )
1130        })
1131        .await
1132        .unwrap();
1133
1134    cx.dispatch_action(workspace::CloseActiveItem {
1135        save_intent: None,
1136        close_pinned: false,
1137    });
1138
1139    let picker = open_file_picker(&workspace, cx);
1140    cx.simulate_input("package.json");
1141
1142    picker.update(cx, |finder, _| {
1143        let matches = &finder.delegate.matches.matches;
1144
1145        assert_eq!(
1146            matches.len(),
1147            2,
1148            "Expected 1 history match + 1 search matches, but got {} matches: {:?}",
1149            matches.len(),
1150            matches
1151        );
1152
1153        assert_matches!(matches[0], Match::History { .. });
1154
1155        let search_matches = collect_search_matches(finder);
1156        assert_eq!(
1157            search_matches.history.len(),
1158            1,
1159            "Should have exactly 1 history match"
1160        );
1161        assert_eq!(
1162            search_matches.search.len(),
1163            1,
1164            "Should have exactly 1 search match (the other package.json)"
1165        );
1166
1167        if let Match::History { path, .. } = &matches[0] {
1168            assert_eq!(path.project.worktree_id, worktree_id1);
1169            assert_eq!(path.project.path.as_ref(), rel_path("package.json"));
1170        }
1171
1172        if let Match::Search(path_match) = &matches[1] {
1173            assert_eq!(
1174                WorktreeId::from_usize(path_match.0.worktree_id),
1175                worktree_id2
1176            );
1177            assert_eq!(path_match.0.path.as_ref(), rel_path("package.json"));
1178        }
1179    });
1180}
1181
1182#[gpui::test]
1183async fn test_create_file_for_multiple_worktrees(cx: &mut TestAppContext) {
1184    let app_state = init_test(cx);
1185    app_state
1186        .fs
1187        .as_fake()
1188        .insert_tree(
1189            path!("/roota"),
1190            json!({ "the-parent-dira": { "filea": "" } }),
1191        )
1192        .await;
1193
1194    app_state
1195        .fs
1196        .as_fake()
1197        .insert_tree(
1198            path!("/rootb"),
1199            json!({ "the-parent-dirb": { "fileb": "" } }),
1200        )
1201        .await;
1202
1203    let project = Project::test(
1204        app_state.fs.clone(),
1205        [path!("/roota").as_ref(), path!("/rootb").as_ref()],
1206        cx,
1207    )
1208    .await;
1209
1210    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1211    let (_worktree_id1, worktree_id2) = cx.read(|cx| {
1212        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1213        (
1214            WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize),
1215            WorktreeId::from_usize(worktrees[1].entity_id().as_u64() as usize),
1216        )
1217    });
1218
1219    let b_path = ProjectPath {
1220        worktree_id: worktree_id2,
1221        path: rel_path("the-parent-dirb/fileb").into(),
1222    };
1223    workspace
1224        .update_in(cx, |workspace, window, cx| {
1225            workspace.open_path(b_path, None, true, window, cx)
1226        })
1227        .await
1228        .unwrap();
1229
1230    let finder = open_file_picker(&workspace, cx);
1231
1232    finder
1233        .update_in(cx, |f, window, cx| {
1234            f.delegate.spawn_search(
1235                test_path_position(path!("the-parent-dirb/filec")),
1236                window,
1237                cx,
1238            )
1239        })
1240        .await;
1241    cx.run_until_parked();
1242    finder.update_in(cx, |picker, window, cx| {
1243        assert_eq!(picker.delegate.matches.len(), 1);
1244        picker.delegate.confirm(false, window, cx)
1245    });
1246    cx.run_until_parked();
1247    cx.read(|cx| {
1248        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1249        let project_path = active_editor.read(cx).project_path(cx);
1250        assert_eq!(
1251            project_path,
1252            Some(ProjectPath {
1253                worktree_id: worktree_id2,
1254                path: rel_path("the-parent-dirb/filec").into()
1255            })
1256        );
1257    });
1258}
1259
1260#[gpui::test]
1261async fn test_create_file_no_focused_with_multiple_worktrees(cx: &mut TestAppContext) {
1262    let app_state = init_test(cx);
1263    app_state
1264        .fs
1265        .as_fake()
1266        .insert_tree(
1267            path!("/roota"),
1268            json!({ "the-parent-dira": { "filea": "" } }),
1269        )
1270        .await;
1271
1272    app_state
1273        .fs
1274        .as_fake()
1275        .insert_tree(
1276            path!("/rootb"),
1277            json!({ "the-parent-dirb": { "fileb": "" } }),
1278        )
1279        .await;
1280
1281    let project = Project::test(
1282        app_state.fs.clone(),
1283        [path!("/roota").as_ref(), path!("/rootb").as_ref()],
1284        cx,
1285    )
1286    .await;
1287
1288    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1289    let (_worktree_id1, worktree_id2) = cx.read(|cx| {
1290        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1291        (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
1292    });
1293
1294    let finder = open_file_picker(&workspace, cx);
1295
1296    finder
1297        .update_in(cx, |f, window, cx| {
1298            f.delegate
1299                .spawn_search(test_path_position(path!("rootb/filec")), window, cx)
1300        })
1301        .await;
1302    cx.run_until_parked();
1303    finder.update_in(cx, |picker, window, cx| {
1304        assert_eq!(picker.delegate.matches.len(), 1);
1305        picker.delegate.confirm(false, window, cx)
1306    });
1307    cx.run_until_parked();
1308    cx.read(|cx| {
1309        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1310        let project_path = active_editor.read(cx).project_path(cx);
1311        assert_eq!(
1312            project_path,
1313            Some(ProjectPath {
1314                worktree_id: worktree_id2,
1315                path: rel_path("filec").into()
1316            })
1317        );
1318    });
1319}
1320
1321#[gpui::test]
1322async fn test_path_distance_ordering(cx: &mut TestAppContext) {
1323    let app_state = init_test(cx);
1324    app_state
1325        .fs
1326        .as_fake()
1327        .insert_tree(
1328            path!("/root"),
1329            json!({
1330                "dir1": { "a.txt": "" },
1331                "dir2": {
1332                    "a.txt": "",
1333                    "b.txt": ""
1334                }
1335            }),
1336        )
1337        .await;
1338
1339    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
1340    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1341
1342    let worktree_id = cx.read(|cx| {
1343        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1344        assert_eq!(worktrees.len(), 1);
1345        WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
1346    });
1347
1348    // When workspace has an active item, sort items which are closer to that item
1349    // first when they have the same name. In this case, b.txt is closer to dir2's a.txt
1350    // so that one should be sorted earlier
1351    let b_path = ProjectPath {
1352        worktree_id,
1353        path: rel_path("dir2/b.txt").into(),
1354    };
1355    workspace
1356        .update_in(cx, |workspace, window, cx| {
1357            workspace.open_path(b_path, None, true, window, cx)
1358        })
1359        .await
1360        .unwrap();
1361    let finder = open_file_picker(&workspace, cx);
1362    finder
1363        .update_in(cx, |f, window, cx| {
1364            f.delegate
1365                .spawn_search(test_path_position("a.txt"), window, cx)
1366        })
1367        .await;
1368
1369    finder.update(cx, |picker, _| {
1370        let matches = collect_search_matches(picker).search_paths_only();
1371        assert_eq!(matches[0].as_ref(), rel_path("dir2/a.txt"));
1372        assert_eq!(matches[1].as_ref(), rel_path("dir1/a.txt"));
1373    });
1374}
1375
1376#[gpui::test]
1377async fn test_search_worktree_without_files(cx: &mut TestAppContext) {
1378    let app_state = init_test(cx);
1379    app_state
1380        .fs
1381        .as_fake()
1382        .insert_tree(
1383            "/root",
1384            json!({
1385                "dir1": {},
1386                "dir2": {
1387                    "dir3": {}
1388                }
1389            }),
1390        )
1391        .await;
1392
1393    let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1394    let (picker, _workspace, cx) = build_find_picker(project, cx);
1395
1396    picker
1397        .update_in(cx, |f, window, cx| {
1398            f.delegate
1399                .spawn_search(test_path_position("dir"), window, cx)
1400        })
1401        .await;
1402    cx.read(|cx| {
1403        let finder = picker.read(cx);
1404        assert_eq!(finder.delegate.matches.len(), 1);
1405        assert_match_at_position(finder, 0, "dir");
1406    });
1407}
1408
1409#[gpui::test]
1410async fn test_query_history(cx: &mut gpui::TestAppContext) {
1411    let app_state = init_test(cx);
1412
1413    app_state
1414        .fs
1415        .as_fake()
1416        .insert_tree(
1417            path!("/src"),
1418            json!({
1419                "test": {
1420                    "first.rs": "// First Rust file",
1421                    "second.rs": "// Second Rust file",
1422                    "third.rs": "// Third Rust file",
1423                }
1424            }),
1425        )
1426        .await;
1427
1428    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1429    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1430    let worktree_id = cx.read(|cx| {
1431        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1432        assert_eq!(worktrees.len(), 1);
1433        WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
1434    });
1435
1436    // Open and close panels, getting their history items afterwards.
1437    // Ensure history items get populated with opened items, and items are kept in a certain order.
1438    // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen.
1439    //
1440    // TODO: without closing, the opened items do not propagate their history changes for some reason
1441    // it does work in real app though, only tests do not propagate.
1442    workspace.update_in(cx, |_workspace, window, cx| window.focused(cx));
1443
1444    let initial_history = open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1445    assert!(
1446        initial_history.is_empty(),
1447        "Should have no history before opening any files"
1448    );
1449
1450    let history_after_first =
1451        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1452    assert_eq!(
1453        history_after_first,
1454        vec![FoundPath::new(
1455            ProjectPath {
1456                worktree_id,
1457                path: rel_path("test/first.rs").into(),
1458            },
1459            PathBuf::from(path!("/src/test/first.rs"))
1460        )],
1461        "Should show 1st opened item in the history when opening the 2nd item"
1462    );
1463
1464    let history_after_second =
1465        open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1466    assert_eq!(
1467        history_after_second,
1468        vec![
1469            FoundPath::new(
1470                ProjectPath {
1471                    worktree_id,
1472                    path: rel_path("test/second.rs").into(),
1473                },
1474                PathBuf::from(path!("/src/test/second.rs"))
1475            ),
1476            FoundPath::new(
1477                ProjectPath {
1478                    worktree_id,
1479                    path: rel_path("test/first.rs").into(),
1480                },
1481                PathBuf::from(path!("/src/test/first.rs"))
1482            ),
1483        ],
1484        "Should show 1st and 2nd opened items in the history when opening the 3rd item. \
1485    2nd item should be the first in the history, as the last opened."
1486    );
1487
1488    let history_after_third =
1489        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1490    assert_eq!(
1491        history_after_third,
1492        vec![
1493            FoundPath::new(
1494                ProjectPath {
1495                    worktree_id,
1496                    path: rel_path("test/third.rs").into(),
1497                },
1498                PathBuf::from(path!("/src/test/third.rs"))
1499            ),
1500            FoundPath::new(
1501                ProjectPath {
1502                    worktree_id,
1503                    path: rel_path("test/second.rs").into(),
1504                },
1505                PathBuf::from(path!("/src/test/second.rs"))
1506            ),
1507            FoundPath::new(
1508                ProjectPath {
1509                    worktree_id,
1510                    path: rel_path("test/first.rs").into(),
1511                },
1512                PathBuf::from(path!("/src/test/first.rs"))
1513            ),
1514        ],
1515        "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
1516    3rd item should be the first in the history, as the last opened."
1517    );
1518
1519    let history_after_second_again =
1520        open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1521    assert_eq!(
1522        history_after_second_again,
1523        vec![
1524            FoundPath::new(
1525                ProjectPath {
1526                    worktree_id,
1527                    path: rel_path("test/second.rs").into(),
1528                },
1529                PathBuf::from(path!("/src/test/second.rs"))
1530            ),
1531            FoundPath::new(
1532                ProjectPath {
1533                    worktree_id,
1534                    path: rel_path("test/third.rs").into(),
1535                },
1536                PathBuf::from(path!("/src/test/third.rs"))
1537            ),
1538            FoundPath::new(
1539                ProjectPath {
1540                    worktree_id,
1541                    path: rel_path("test/first.rs").into(),
1542                },
1543                PathBuf::from(path!("/src/test/first.rs"))
1544            ),
1545        ],
1546        "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
1547    2nd item, as the last opened, 3rd item should go next as it was opened right before."
1548    );
1549}
1550
1551#[gpui::test]
1552async fn test_history_match_positions(cx: &mut gpui::TestAppContext) {
1553    let app_state = init_test(cx);
1554
1555    app_state
1556        .fs
1557        .as_fake()
1558        .insert_tree(
1559            path!("/src"),
1560            json!({
1561                "test": {
1562                    "first.rs": "// First Rust file",
1563                    "second.rs": "// Second Rust file",
1564                    "third.rs": "// Third Rust file",
1565                }
1566            }),
1567        )
1568        .await;
1569
1570    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1571    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1572
1573    workspace.update_in(cx, |_workspace, window, cx| window.focused(cx));
1574
1575    open_close_queried_buffer("efir", 1, "first.rs", &workspace, cx).await;
1576    let history = open_close_queried_buffer("second", 1, "second.rs", &workspace, cx).await;
1577    assert_eq!(history.len(), 1);
1578
1579    let picker = open_file_picker(&workspace, cx);
1580    cx.simulate_input("fir");
1581    picker.update_in(cx, |finder, window, cx| {
1582        let matches = &finder.delegate.matches.matches;
1583        assert_matches!(
1584            matches.as_slice(),
1585            [Match::History { .. }, Match::CreateNew { .. }]
1586        );
1587        assert_eq!(
1588            matches[0].panel_match().unwrap().0.path.as_ref(),
1589            rel_path("test/first.rs")
1590        );
1591        assert_eq!(matches[0].panel_match().unwrap().0.positions, &[5, 6, 7]);
1592
1593        let (file_label, path_label) =
1594            finder
1595                .delegate
1596                .labels_for_match(&finder.delegate.matches.matches[0], window, cx);
1597        assert_eq!(file_label.text(), "first.rs");
1598        assert_eq!(file_label.highlight_indices(), &[0, 1, 2]);
1599        assert_eq!(
1600            path_label.text(),
1601            format!("test{}", PathStyle::local().separator())
1602        );
1603        assert_eq!(path_label.highlight_indices(), &[] as &[usize]);
1604    });
1605}
1606
1607#[gpui::test]
1608async fn test_external_files_history(cx: &mut gpui::TestAppContext) {
1609    let app_state = init_test(cx);
1610
1611    app_state
1612        .fs
1613        .as_fake()
1614        .insert_tree(
1615            path!("/src"),
1616            json!({
1617                "test": {
1618                    "first.rs": "// First Rust file",
1619                    "second.rs": "// Second Rust file",
1620                }
1621            }),
1622        )
1623        .await;
1624
1625    app_state
1626        .fs
1627        .as_fake()
1628        .insert_tree(
1629            path!("/external-src"),
1630            json!({
1631                "test": {
1632                    "third.rs": "// Third Rust file",
1633                    "fourth.rs": "// Fourth Rust file",
1634                }
1635            }),
1636        )
1637        .await;
1638
1639    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1640    cx.update(|cx| {
1641        project.update(cx, |project, cx| {
1642            project.find_or_create_worktree(path!("/external-src"), false, cx)
1643        })
1644    })
1645    .detach();
1646    cx.background_executor.run_until_parked();
1647
1648    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1649    let worktree_id = cx.read(|cx| {
1650        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1651        assert_eq!(worktrees.len(), 1,);
1652
1653        WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
1654    });
1655    workspace
1656        .update_in(cx, |workspace, window, cx| {
1657            workspace.open_abs_path(
1658                PathBuf::from(path!("/external-src/test/third.rs")),
1659                OpenOptions {
1660                    visible: Some(OpenVisible::None),
1661                    ..Default::default()
1662                },
1663                window,
1664                cx,
1665            )
1666        })
1667        .detach();
1668    cx.background_executor.run_until_parked();
1669    let external_worktree_id = cx.read(|cx| {
1670        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1671        assert_eq!(
1672            worktrees.len(),
1673            2,
1674            "External file should get opened in a new worktree"
1675        );
1676
1677        WorktreeId::from_usize(
1678            worktrees
1679                .into_iter()
1680                .find(|worktree| worktree.entity_id().as_u64() as usize != worktree_id.to_usize())
1681                .expect("New worktree should have a different id")
1682                .entity_id()
1683                .as_u64() as usize,
1684        )
1685    });
1686    cx.dispatch_action(workspace::CloseActiveItem {
1687        save_intent: None,
1688        close_pinned: false,
1689    });
1690
1691    let initial_history_items =
1692        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1693    assert_eq!(
1694        initial_history_items,
1695        vec![FoundPath::new(
1696            ProjectPath {
1697                worktree_id: external_worktree_id,
1698                path: rel_path("").into(),
1699            },
1700            PathBuf::from(path!("/external-src/test/third.rs"))
1701        )],
1702        "Should show external file with its full path in the history after it was open"
1703    );
1704
1705    let updated_history_items =
1706        open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1707    assert_eq!(
1708        updated_history_items,
1709        vec![
1710            FoundPath::new(
1711                ProjectPath {
1712                    worktree_id,
1713                    path: rel_path("test/second.rs").into(),
1714                },
1715                PathBuf::from(path!("/src/test/second.rs"))
1716            ),
1717            FoundPath::new(
1718                ProjectPath {
1719                    worktree_id: external_worktree_id,
1720                    path: rel_path("").into(),
1721                },
1722                PathBuf::from(path!("/external-src/test/third.rs"))
1723            ),
1724        ],
1725        "Should keep external file with history updates",
1726    );
1727}
1728
1729#[gpui::test]
1730async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) {
1731    let app_state = init_test(cx);
1732
1733    app_state
1734        .fs
1735        .as_fake()
1736        .insert_tree(
1737            path!("/src"),
1738            json!({
1739                "test": {
1740                    "first.rs": "// First Rust file",
1741                    "second.rs": "// Second Rust file",
1742                    "third.rs": "// Third Rust file",
1743                }
1744            }),
1745        )
1746        .await;
1747
1748    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1749    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1750
1751    // generate some history to select from
1752    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1753    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1754    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1755    let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1756
1757    for expected_selected_index in 0..current_history.len() {
1758        cx.dispatch_action(ToggleFileFinder::default());
1759        let picker = active_file_picker(&workspace, cx);
1760        let selected_index = picker.update(cx, |picker, _| picker.delegate.selected_index());
1761        assert_eq!(
1762            selected_index, expected_selected_index,
1763            "Should select the next item in the history"
1764        );
1765    }
1766
1767    cx.dispatch_action(ToggleFileFinder::default());
1768    let selected_index = workspace.update(cx, |workspace, cx| {
1769        workspace
1770            .active_modal::<FileFinder>(cx)
1771            .unwrap()
1772            .read(cx)
1773            .picker
1774            .read(cx)
1775            .delegate
1776            .selected_index()
1777    });
1778    assert_eq!(
1779        selected_index, 0,
1780        "Should wrap around the history and start all over"
1781    );
1782}
1783
1784#[gpui::test]
1785async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
1786    let app_state = init_test(cx);
1787
1788    app_state
1789        .fs
1790        .as_fake()
1791        .insert_tree(
1792            path!("/src"),
1793            json!({
1794                "test": {
1795                    "first.rs": "// First Rust file",
1796                    "second.rs": "// Second Rust file",
1797                    "third.rs": "// Third Rust file",
1798                    "fourth.rs": "// Fourth Rust file",
1799                }
1800            }),
1801        )
1802        .await;
1803
1804    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1805    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1806    let worktree_id = cx.read(|cx| {
1807        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1808        assert_eq!(worktrees.len(), 1,);
1809
1810        WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
1811    });
1812
1813    // generate some history to select from
1814    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1815    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1816    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1817    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1818
1819    let finder = open_file_picker(&workspace, cx);
1820    let first_query = "f";
1821    finder
1822        .update_in(cx, |finder, window, cx| {
1823            finder
1824                .delegate
1825                .update_matches(first_query.to_string(), window, cx)
1826        })
1827        .await;
1828    finder.update(cx, |picker, _| {
1829            let matches = collect_search_matches(picker);
1830            assert_eq!(matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out");
1831            let history_match = matches.history_found_paths.first().expect("Should have path matches for history items after querying");
1832            assert_eq!(history_match, &FoundPath::new(
1833                ProjectPath {
1834                    worktree_id,
1835                    path: rel_path("test/first.rs").into(),
1836                },
1837                PathBuf::from(path!("/src/test/first.rs")),
1838            ));
1839            assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
1840            assert_eq!(matches.search.first().unwrap().as_ref(), rel_path("test/fourth.rs"));
1841        });
1842
1843    let second_query = "fsdasdsa";
1844    let finder = active_file_picker(&workspace, cx);
1845    finder
1846        .update_in(cx, |finder, window, cx| {
1847            finder
1848                .delegate
1849                .update_matches(second_query.to_string(), window, cx)
1850        })
1851        .await;
1852    finder.update(cx, |picker, _| {
1853        assert!(
1854            collect_search_matches(picker)
1855                .search_paths_only()
1856                .is_empty(),
1857            "No search entries should match {second_query}"
1858        );
1859    });
1860
1861    let first_query_again = first_query;
1862
1863    let finder = active_file_picker(&workspace, cx);
1864    finder
1865        .update_in(cx, |finder, window, cx| {
1866            finder
1867                .delegate
1868                .update_matches(first_query_again.to_string(), window, cx)
1869        })
1870        .await;
1871    finder.update(cx, |picker, _| {
1872            let matches = collect_search_matches(picker);
1873            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");
1874            let history_match = matches.history_found_paths.first().expect("Should have path matches for history items after querying");
1875            assert_eq!(history_match, &FoundPath::new(
1876                ProjectPath {
1877                    worktree_id,
1878                    path: rel_path("test/first.rs").into(),
1879                },
1880                PathBuf::from(path!("/src/test/first.rs"))
1881            ));
1882            assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
1883            assert_eq!(matches.search.first().unwrap().as_ref(), rel_path("test/fourth.rs"));
1884        });
1885}
1886
1887#[gpui::test]
1888async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) {
1889    let app_state = init_test(cx);
1890
1891    app_state
1892        .fs
1893        .as_fake()
1894        .insert_tree(
1895            path!("/root"),
1896            json!({
1897                "test": {
1898                    "1_qw": "// First file that matches the query",
1899                    "2_second": "// Second file",
1900                    "3_third": "// Third file",
1901                    "4_fourth": "// Fourth file",
1902                    "5_qwqwqw": "// A file with 3 more matches than the first one",
1903                    "6_qwqwqw": "// Same query matches as above, but closer to the end of the list due to the name",
1904                    "7_qwqwqw": "// One more, same amount of query matches as above",
1905                }
1906            }),
1907        )
1908        .await;
1909
1910    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
1911    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1912    // generate some history to select from
1913    open_close_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
1914    open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
1915    open_close_queried_buffer("3", 1, "3_third", &workspace, cx).await;
1916    open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
1917    open_close_queried_buffer("6", 1, "6_qwqwqw", &workspace, cx).await;
1918
1919    let finder = open_file_picker(&workspace, cx);
1920    let query = "qw";
1921    finder
1922        .update_in(cx, |finder, window, cx| {
1923            finder
1924                .delegate
1925                .update_matches(query.to_string(), window, cx)
1926        })
1927        .await;
1928    finder.update(cx, |finder, _| {
1929        let search_matches = collect_search_matches(finder);
1930        assert_eq!(
1931            search_matches.history,
1932            vec![
1933                rel_path("test/1_qw").into(),
1934                rel_path("test/6_qwqwqw").into()
1935            ],
1936        );
1937        assert_eq!(
1938            search_matches.search,
1939            vec![
1940                rel_path("test/5_qwqwqw").into(),
1941                rel_path("test/7_qwqwqw").into()
1942            ],
1943        );
1944    });
1945}
1946
1947#[gpui::test]
1948async fn test_select_current_open_file_when_no_history(cx: &mut gpui::TestAppContext) {
1949    let app_state = init_test(cx);
1950
1951    app_state
1952        .fs
1953        .as_fake()
1954        .insert_tree(
1955            path!("/root"),
1956            json!({
1957                "test": {
1958                    "1_qw": "",
1959                }
1960            }),
1961        )
1962        .await;
1963
1964    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
1965    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1966    // Open new buffer
1967    open_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
1968
1969    let picker = open_file_picker(&workspace, cx);
1970    picker.update(cx, |finder, _| {
1971        assert_match_selection(finder, 0, "1_qw");
1972    });
1973}
1974
1975#[gpui::test]
1976async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
1977    cx: &mut TestAppContext,
1978) {
1979    let app_state = init_test(cx);
1980
1981    app_state
1982        .fs
1983        .as_fake()
1984        .insert_tree(
1985            path!("/src"),
1986            json!({
1987                "test": {
1988                    "bar.rs": "// Bar file",
1989                    "lib.rs": "// Lib file",
1990                    "maaa.rs": "// Maaaaaaa",
1991                    "main.rs": "// Main file",
1992                    "moo.rs": "// Moooooo",
1993                }
1994            }),
1995        )
1996        .await;
1997
1998    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1999    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2000
2001    open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
2002    open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
2003    open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
2004
2005    // main.rs is on top, previously used is selected
2006    let picker = open_file_picker(&workspace, cx);
2007    picker.update(cx, |finder, _| {
2008        assert_eq!(finder.delegate.matches.len(), 3);
2009        assert_match_selection(finder, 0, "main.rs");
2010        assert_match_at_position(finder, 1, "lib.rs");
2011        assert_match_at_position(finder, 2, "bar.rs");
2012    });
2013
2014    // all files match, main.rs is still on top, but the second item is selected
2015    picker
2016        .update_in(cx, |finder, window, cx| {
2017            finder
2018                .delegate
2019                .update_matches(".rs".to_string(), window, cx)
2020        })
2021        .await;
2022    picker.update(cx, |finder, _| {
2023        assert_eq!(finder.delegate.matches.len(), 6);
2024        assert_match_at_position(finder, 0, "main.rs");
2025        assert_match_selection(finder, 1, "bar.rs");
2026        assert_match_at_position(finder, 2, "lib.rs");
2027        assert_match_at_position(finder, 3, "moo.rs");
2028        assert_match_at_position(finder, 4, "maaa.rs");
2029        assert_match_at_position(finder, 5, ".rs");
2030    });
2031
2032    // main.rs is not among matches, select top item
2033    picker
2034        .update_in(cx, |finder, window, cx| {
2035            finder.delegate.update_matches("b".to_string(), window, cx)
2036        })
2037        .await;
2038    picker.update(cx, |finder, _| {
2039        assert_eq!(finder.delegate.matches.len(), 3);
2040        assert_match_at_position(finder, 0, "bar.rs");
2041        assert_match_at_position(finder, 1, "lib.rs");
2042        assert_match_at_position(finder, 2, "b");
2043    });
2044
2045    // main.rs is back, put it on top and select next item
2046    picker
2047        .update_in(cx, |finder, window, cx| {
2048            finder.delegate.update_matches("m".to_string(), window, cx)
2049        })
2050        .await;
2051    picker.update(cx, |finder, _| {
2052        assert_eq!(finder.delegate.matches.len(), 4);
2053        assert_match_at_position(finder, 0, "main.rs");
2054        assert_match_selection(finder, 1, "moo.rs");
2055        assert_match_at_position(finder, 2, "maaa.rs");
2056        assert_match_at_position(finder, 3, "m");
2057    });
2058
2059    // get back to the initial state
2060    picker
2061        .update_in(cx, |finder, window, cx| {
2062            finder.delegate.update_matches("".to_string(), window, cx)
2063        })
2064        .await;
2065    picker.update(cx, |finder, _| {
2066        assert_eq!(finder.delegate.matches.len(), 3);
2067        assert_match_selection(finder, 0, "main.rs");
2068        assert_match_at_position(finder, 1, "lib.rs");
2069        assert_match_at_position(finder, 2, "bar.rs");
2070    });
2071}
2072
2073#[gpui::test]
2074async fn test_setting_auto_select_first_and_select_active_file(cx: &mut TestAppContext) {
2075    let app_state = init_test(cx);
2076
2077    cx.update(|cx| {
2078        let settings = *FileFinderSettings::get_global(cx);
2079
2080        FileFinderSettings::override_global(
2081            FileFinderSettings {
2082                skip_focus_for_active_in_search: false,
2083                ..settings
2084            },
2085            cx,
2086        );
2087    });
2088
2089    app_state
2090        .fs
2091        .as_fake()
2092        .insert_tree(
2093            path!("/src"),
2094            json!({
2095                "test": {
2096                    "bar.rs": "// Bar file",
2097                    "lib.rs": "// Lib file",
2098                    "maaa.rs": "// Maaaaaaa",
2099                    "main.rs": "// Main file",
2100                    "moo.rs": "// Moooooo",
2101                }
2102            }),
2103        )
2104        .await;
2105
2106    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2107    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2108
2109    open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
2110    open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
2111    open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
2112
2113    // main.rs is on top, previously used is selected
2114    let picker = open_file_picker(&workspace, cx);
2115    picker.update(cx, |finder, _| {
2116        assert_eq!(finder.delegate.matches.len(), 3);
2117        assert_match_selection(finder, 0, "main.rs");
2118        assert_match_at_position(finder, 1, "lib.rs");
2119        assert_match_at_position(finder, 2, "bar.rs");
2120    });
2121
2122    // all files match, main.rs is on top, and is selected
2123    picker
2124        .update_in(cx, |finder, window, cx| {
2125            finder
2126                .delegate
2127                .update_matches(".rs".to_string(), window, cx)
2128        })
2129        .await;
2130    picker.update(cx, |finder, _| {
2131        assert_eq!(finder.delegate.matches.len(), 6);
2132        assert_match_selection(finder, 0, "main.rs");
2133        assert_match_at_position(finder, 1, "bar.rs");
2134        assert_match_at_position(finder, 2, "lib.rs");
2135        assert_match_at_position(finder, 3, "moo.rs");
2136        assert_match_at_position(finder, 4, "maaa.rs");
2137        assert_match_at_position(finder, 5, ".rs");
2138    });
2139}
2140
2141#[gpui::test]
2142async fn test_non_separate_history_items(cx: &mut TestAppContext) {
2143    let app_state = init_test(cx);
2144
2145    app_state
2146        .fs
2147        .as_fake()
2148        .insert_tree(
2149            path!("/src"),
2150            json!({
2151                "test": {
2152                    "bar.rs": "// Bar file",
2153                    "lib.rs": "// Lib file",
2154                    "maaa.rs": "// Maaaaaaa",
2155                    "main.rs": "// Main file",
2156                    "moo.rs": "// Moooooo",
2157                }
2158            }),
2159        )
2160        .await;
2161
2162    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2163    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2164
2165    open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
2166    open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
2167    open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
2168
2169    cx.dispatch_action(ToggleFileFinder::default());
2170    let picker = active_file_picker(&workspace, cx);
2171    // main.rs is on top, previously used is selected
2172    picker.update(cx, |finder, _| {
2173        assert_eq!(finder.delegate.matches.len(), 3);
2174        assert_match_selection(finder, 0, "main.rs");
2175        assert_match_at_position(finder, 1, "lib.rs");
2176        assert_match_at_position(finder, 2, "bar.rs");
2177    });
2178
2179    // all files match, main.rs is still on top, but the second item is selected
2180    picker
2181        .update_in(cx, |finder, window, cx| {
2182            finder
2183                .delegate
2184                .update_matches(".rs".to_string(), window, cx)
2185        })
2186        .await;
2187    picker.update(cx, |finder, _| {
2188        assert_eq!(finder.delegate.matches.len(), 6);
2189        assert_match_at_position(finder, 0, "main.rs");
2190        assert_match_selection(finder, 1, "moo.rs");
2191        assert_match_at_position(finder, 2, "bar.rs");
2192        assert_match_at_position(finder, 3, "lib.rs");
2193        assert_match_at_position(finder, 4, "maaa.rs");
2194        assert_match_at_position(finder, 5, ".rs");
2195    });
2196
2197    // main.rs is not among matches, select top item
2198    picker
2199        .update_in(cx, |finder, window, cx| {
2200            finder.delegate.update_matches("b".to_string(), window, cx)
2201        })
2202        .await;
2203    picker.update(cx, |finder, _| {
2204        assert_eq!(finder.delegate.matches.len(), 3);
2205        assert_match_at_position(finder, 0, "bar.rs");
2206        assert_match_at_position(finder, 1, "lib.rs");
2207        assert_match_at_position(finder, 2, "b");
2208    });
2209
2210    // main.rs is back, put it on top and select next item
2211    picker
2212        .update_in(cx, |finder, window, cx| {
2213            finder.delegate.update_matches("m".to_string(), window, cx)
2214        })
2215        .await;
2216    picker.update(cx, |finder, _| {
2217        assert_eq!(finder.delegate.matches.len(), 4);
2218        assert_match_at_position(finder, 0, "main.rs");
2219        assert_match_selection(finder, 1, "moo.rs");
2220        assert_match_at_position(finder, 2, "maaa.rs");
2221        assert_match_at_position(finder, 3, "m");
2222    });
2223
2224    // get back to the initial state
2225    picker
2226        .update_in(cx, |finder, window, cx| {
2227            finder.delegate.update_matches("".to_string(), window, cx)
2228        })
2229        .await;
2230    picker.update(cx, |finder, _| {
2231        assert_eq!(finder.delegate.matches.len(), 3);
2232        assert_match_selection(finder, 0, "main.rs");
2233        assert_match_at_position(finder, 1, "lib.rs");
2234        assert_match_at_position(finder, 2, "bar.rs");
2235    });
2236}
2237
2238#[gpui::test]
2239async fn test_history_items_shown_in_order_of_open(cx: &mut TestAppContext) {
2240    let app_state = init_test(cx);
2241
2242    app_state
2243        .fs
2244        .as_fake()
2245        .insert_tree(
2246            path!("/test"),
2247            json!({
2248                "test": {
2249                    "1.txt": "// One",
2250                    "2.txt": "// Two",
2251                    "3.txt": "// Three",
2252                }
2253            }),
2254        )
2255        .await;
2256
2257    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2258    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2259
2260    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2261    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2262    open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
2263
2264    let picker = open_file_picker(&workspace, cx);
2265    picker.update(cx, |finder, _| {
2266        assert_eq!(finder.delegate.matches.len(), 3);
2267        assert_match_selection(finder, 0, "3.txt");
2268        assert_match_at_position(finder, 1, "2.txt");
2269        assert_match_at_position(finder, 2, "1.txt");
2270    });
2271
2272    cx.dispatch_action(SelectNext);
2273    cx.dispatch_action(Confirm); // Open 2.txt
2274
2275    let picker = open_file_picker(&workspace, cx);
2276    picker.update(cx, |finder, _| {
2277        assert_eq!(finder.delegate.matches.len(), 3);
2278        assert_match_selection(finder, 0, "2.txt");
2279        assert_match_at_position(finder, 1, "3.txt");
2280        assert_match_at_position(finder, 2, "1.txt");
2281    });
2282
2283    cx.dispatch_action(SelectNext);
2284    cx.dispatch_action(SelectNext);
2285    cx.dispatch_action(Confirm); // Open 1.txt
2286
2287    let picker = open_file_picker(&workspace, cx);
2288    picker.update(cx, |finder, _| {
2289        assert_eq!(finder.delegate.matches.len(), 3);
2290        assert_match_selection(finder, 0, "1.txt");
2291        assert_match_at_position(finder, 1, "2.txt");
2292        assert_match_at_position(finder, 2, "3.txt");
2293    });
2294}
2295
2296#[gpui::test]
2297async fn test_selected_history_item_stays_selected_on_worktree_updated(cx: &mut TestAppContext) {
2298    let app_state = init_test(cx);
2299
2300    app_state
2301        .fs
2302        .as_fake()
2303        .insert_tree(
2304            path!("/test"),
2305            json!({
2306                "test": {
2307                    "1.txt": "// One",
2308                    "2.txt": "// Two",
2309                    "3.txt": "// Three",
2310                }
2311            }),
2312        )
2313        .await;
2314
2315    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2316    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2317
2318    open_close_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2319    open_close_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2320    open_close_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
2321
2322    let picker = open_file_picker(&workspace, cx);
2323    picker.update(cx, |finder, _| {
2324        assert_eq!(finder.delegate.matches.len(), 3);
2325        assert_match_selection(finder, 0, "3.txt");
2326        assert_match_at_position(finder, 1, "2.txt");
2327        assert_match_at_position(finder, 2, "1.txt");
2328    });
2329
2330    cx.dispatch_action(SelectNext);
2331
2332    // Add more files to the worktree to trigger update matches
2333    for i in 0..5 {
2334        let filename = if cfg!(windows) {
2335            format!("C:/test/{}.txt", 4 + i)
2336        } else {
2337            format!("/test/{}.txt", 4 + i)
2338        };
2339        app_state
2340            .fs
2341            .create_file(Path::new(&filename), Default::default())
2342            .await
2343            .expect("unable to create file");
2344    }
2345
2346    cx.executor().advance_clock(FS_WATCH_LATENCY);
2347
2348    picker.update(cx, |finder, _| {
2349        assert_eq!(finder.delegate.matches.len(), 3);
2350        assert_match_at_position(finder, 0, "3.txt");
2351        assert_match_selection(finder, 1, "2.txt");
2352        assert_match_at_position(finder, 2, "1.txt");
2353    });
2354}
2355
2356#[gpui::test]
2357async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
2358    let app_state = init_test(cx);
2359
2360    app_state
2361        .fs
2362        .as_fake()
2363        .insert_tree(
2364            path!("/src"),
2365            json!({
2366                "collab_ui": {
2367                    "first.rs": "// First Rust file",
2368                    "second.rs": "// Second Rust file",
2369                    "third.rs": "// Third Rust file",
2370                    "collab_ui.rs": "// Fourth Rust file",
2371                }
2372            }),
2373        )
2374        .await;
2375
2376    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2377    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2378    // generate some history to select from
2379    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2380    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
2381    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
2382    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
2383
2384    let finder = open_file_picker(&workspace, cx);
2385    let query = "collab_ui";
2386    cx.simulate_input(query);
2387    finder.update(cx, |picker, _| {
2388            let search_entries = collect_search_matches(picker).search_paths_only();
2389            assert_eq!(
2390                search_entries,
2391                vec![
2392                    rel_path("collab_ui/collab_ui.rs").into(),
2393                    rel_path("collab_ui/first.rs").into(),
2394                    rel_path("collab_ui/third.rs").into(),
2395                    rel_path("collab_ui/second.rs").into(),
2396                ],
2397                "Despite all search results having the same directory name, the most matching one should be on top"
2398            );
2399        });
2400}
2401
2402#[gpui::test]
2403async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) {
2404    let app_state = init_test(cx);
2405
2406    app_state
2407        .fs
2408        .as_fake()
2409        .insert_tree(
2410            path!("/src"),
2411            json!({
2412                "test": {
2413                    "first.rs": "// First Rust file",
2414                    "nonexistent.rs": "// Second Rust file",
2415                    "third.rs": "// Third Rust file",
2416                }
2417            }),
2418        )
2419        .await;
2420
2421    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2422    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); // generate some history to select from
2423    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2424    open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await;
2425    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
2426    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2427    app_state
2428        .fs
2429        .remove_file(
2430            Path::new(path!("/src/test/nonexistent.rs")),
2431            RemoveOptions::default(),
2432        )
2433        .await
2434        .unwrap();
2435    cx.run_until_parked();
2436
2437    let picker = open_file_picker(&workspace, cx);
2438    cx.simulate_input("rs");
2439
2440    picker.update(cx, |picker, _| {
2441        assert_eq!(
2442            collect_search_matches(picker).history,
2443            vec![
2444                rel_path("test/first.rs").into(),
2445                rel_path("test/third.rs").into()
2446            ],
2447            "Should have all opened files in the history, except the ones that do not exist on disk"
2448        );
2449    });
2450}
2451
2452#[gpui::test]
2453async fn test_search_results_refreshed_on_worktree_updates(cx: &mut gpui::TestAppContext) {
2454    let app_state = init_test(cx);
2455
2456    app_state
2457        .fs
2458        .as_fake()
2459        .insert_tree(
2460            "/src",
2461            json!({
2462                "lib.rs": "// Lib file",
2463                "main.rs": "// Bar file",
2464                "read.me": "// Readme file",
2465            }),
2466        )
2467        .await;
2468
2469    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
2470    let (workspace, cx) =
2471        cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2472
2473    // Initial state
2474    let picker = open_file_picker(&workspace, cx);
2475    cx.simulate_input("rs");
2476    picker.update(cx, |finder, _| {
2477        assert_eq!(finder.delegate.matches.len(), 3);
2478        assert_match_at_position(finder, 0, "lib.rs");
2479        assert_match_at_position(finder, 1, "main.rs");
2480        assert_match_at_position(finder, 2, "rs");
2481    });
2482    // Delete main.rs
2483    app_state
2484        .fs
2485        .remove_file("/src/main.rs".as_ref(), Default::default())
2486        .await
2487        .expect("unable to remove file");
2488    cx.executor().advance_clock(FS_WATCH_LATENCY);
2489
2490    // main.rs is in not among search results anymore
2491    picker.update(cx, |finder, _| {
2492        assert_eq!(finder.delegate.matches.len(), 2);
2493        assert_match_at_position(finder, 0, "lib.rs");
2494        assert_match_at_position(finder, 1, "rs");
2495    });
2496
2497    // Create util.rs
2498    app_state
2499        .fs
2500        .create_file("/src/util.rs".as_ref(), Default::default())
2501        .await
2502        .expect("unable to create file");
2503    cx.executor().advance_clock(FS_WATCH_LATENCY);
2504
2505    // util.rs is among search results
2506    picker.update(cx, |finder, _| {
2507        assert_eq!(finder.delegate.matches.len(), 3);
2508        assert_match_at_position(finder, 0, "lib.rs");
2509        assert_match_at_position(finder, 1, "util.rs");
2510        assert_match_at_position(finder, 2, "rs");
2511    });
2512}
2513
2514#[gpui::test]
2515async fn test_search_results_refreshed_on_standalone_file_creation(cx: &mut gpui::TestAppContext) {
2516    let app_state = init_test(cx);
2517
2518    app_state
2519        .fs
2520        .as_fake()
2521        .insert_tree(
2522            "/src",
2523            json!({
2524                "lib.rs": "// Lib file",
2525                "main.rs": "// Bar file",
2526                "read.me": "// Readme file",
2527            }),
2528        )
2529        .await;
2530    app_state
2531        .fs
2532        .as_fake()
2533        .insert_tree(
2534            "/test",
2535            json!({
2536                "new.rs": "// New file",
2537            }),
2538        )
2539        .await;
2540
2541    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
2542    let (workspace, cx) =
2543        cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2544
2545    cx.update(|_, cx| {
2546        open_paths(
2547            &[PathBuf::from(path!("/test/new.rs"))],
2548            app_state,
2549            workspace::OpenOptions::default(),
2550            cx,
2551        )
2552    })
2553    .await
2554    .unwrap();
2555    assert_eq!(cx.update(|_, cx| cx.windows().len()), 1);
2556
2557    let initial_history = open_close_queried_buffer("new", 1, "new.rs", &workspace, cx).await;
2558    assert_eq!(
2559        initial_history.first().unwrap().absolute,
2560        PathBuf::from(path!("/test/new.rs")),
2561        "Should show 1st opened item in the history when opening the 2nd item"
2562    );
2563
2564    let history_after_first = open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
2565    assert_eq!(
2566        history_after_first.first().unwrap().absolute,
2567        PathBuf::from(path!("/test/new.rs")),
2568        "Should show 1st opened item in the history when opening the 2nd item"
2569    );
2570}
2571
2572#[gpui::test]
2573async fn test_search_results_refreshed_on_adding_and_removing_worktrees(
2574    cx: &mut gpui::TestAppContext,
2575) {
2576    let app_state = init_test(cx);
2577
2578    app_state
2579        .fs
2580        .as_fake()
2581        .insert_tree(
2582            "/test",
2583            json!({
2584                "project_1": {
2585                    "bar.rs": "// Bar file",
2586                    "lib.rs": "// Lib file",
2587                },
2588                "project_2": {
2589                    "Cargo.toml": "// Cargo file",
2590                    "main.rs": "// Main file",
2591                }
2592            }),
2593        )
2594        .await;
2595
2596    let project = Project::test(app_state.fs.clone(), ["/test/project_1".as_ref()], cx).await;
2597    let (workspace, cx) =
2598        cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2599    let worktree_1_id = project.update(cx, |project, cx| {
2600        let worktree = project.worktrees(cx).last().expect("worktree not found");
2601        worktree.read(cx).id()
2602    });
2603
2604    // Initial state
2605    let picker = open_file_picker(&workspace, cx);
2606    cx.simulate_input("rs");
2607    picker.update(cx, |finder, _| {
2608        assert_eq!(finder.delegate.matches.len(), 3);
2609        assert_match_at_position(finder, 0, "bar.rs");
2610        assert_match_at_position(finder, 1, "lib.rs");
2611        assert_match_at_position(finder, 2, "rs");
2612    });
2613
2614    // Add new worktree
2615    project
2616        .update(cx, |project, cx| {
2617            project
2618                .find_or_create_worktree("/test/project_2", true, cx)
2619                .into_future()
2620        })
2621        .await
2622        .expect("unable to create workdir");
2623    cx.executor().advance_clock(FS_WATCH_LATENCY);
2624
2625    // main.rs is among search results
2626    picker.update(cx, |finder, _| {
2627        assert_eq!(finder.delegate.matches.len(), 4);
2628        assert_match_at_position(finder, 0, "bar.rs");
2629        assert_match_at_position(finder, 1, "lib.rs");
2630        assert_match_at_position(finder, 2, "main.rs");
2631        assert_match_at_position(finder, 3, "rs");
2632    });
2633
2634    // Remove the first worktree
2635    project.update(cx, |project, cx| {
2636        project.remove_worktree(worktree_1_id, cx);
2637    });
2638    cx.executor().advance_clock(FS_WATCH_LATENCY);
2639
2640    // Files from the first worktree are not in the search results anymore
2641    picker.update(cx, |finder, _| {
2642        assert_eq!(finder.delegate.matches.len(), 2);
2643        assert_match_at_position(finder, 0, "main.rs");
2644        assert_match_at_position(finder, 1, "rs");
2645    });
2646}
2647
2648#[gpui::test]
2649async fn test_history_items_uniqueness_for_multiple_worktree_open_all_files(
2650    cx: &mut TestAppContext,
2651) {
2652    let app_state = init_test(cx);
2653    app_state
2654        .fs
2655        .as_fake()
2656        .insert_tree(
2657            path!("/repo1"),
2658            json!({
2659                "package.json": r#"{"name": "repo1"}"#,
2660                "src": {
2661                    "index.js": "// Repo 1 index",
2662                }
2663            }),
2664        )
2665        .await;
2666
2667    app_state
2668        .fs
2669        .as_fake()
2670        .insert_tree(
2671            path!("/repo2"),
2672            json!({
2673                "package.json": r#"{"name": "repo2"}"#,
2674                "src": {
2675                    "index.js": "// Repo 2 index",
2676                }
2677            }),
2678        )
2679        .await;
2680
2681    let project = Project::test(
2682        app_state.fs.clone(),
2683        [path!("/repo1").as_ref(), path!("/repo2").as_ref()],
2684        cx,
2685    )
2686    .await;
2687
2688    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2689    let (worktree_id1, worktree_id2) = cx.read(|cx| {
2690        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
2691        (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
2692    });
2693
2694    workspace
2695        .update_in(cx, |workspace, window, cx| {
2696            workspace.open_path(
2697                ProjectPath {
2698                    worktree_id: worktree_id1,
2699                    path: rel_path("package.json").into(),
2700                },
2701                None,
2702                true,
2703                window,
2704                cx,
2705            )
2706        })
2707        .await
2708        .unwrap();
2709
2710    cx.dispatch_action(workspace::CloseActiveItem {
2711        save_intent: None,
2712        close_pinned: false,
2713    });
2714    workspace
2715        .update_in(cx, |workspace, window, cx| {
2716            workspace.open_path(
2717                ProjectPath {
2718                    worktree_id: worktree_id2,
2719                    path: rel_path("package.json").into(),
2720                },
2721                None,
2722                true,
2723                window,
2724                cx,
2725            )
2726        })
2727        .await
2728        .unwrap();
2729
2730    cx.dispatch_action(workspace::CloseActiveItem {
2731        save_intent: None,
2732        close_pinned: false,
2733    });
2734
2735    let picker = open_file_picker(&workspace, cx);
2736    cx.simulate_input("package.json");
2737
2738    picker.update(cx, |finder, _| {
2739        let matches = &finder.delegate.matches.matches;
2740
2741        assert_eq!(
2742            matches.len(),
2743            2,
2744            "Expected 1 history match + 1 search matches, but got {} matches: {:?}",
2745            matches.len(),
2746            matches
2747        );
2748
2749        assert_matches!(matches[0], Match::History { .. });
2750
2751        let search_matches = collect_search_matches(finder);
2752        assert_eq!(
2753            search_matches.history.len(),
2754            2,
2755            "Should have exactly 2 history match"
2756        );
2757        assert_eq!(
2758            search_matches.search.len(),
2759            0,
2760            "Should have exactly 0 search match (because we already opened the 2 package.json)"
2761        );
2762
2763        if let Match::History { path, panel_match } = &matches[0] {
2764            assert_eq!(path.project.worktree_id, worktree_id2);
2765            assert_eq!(path.project.path.as_ref(), rel_path("package.json"));
2766            let panel_match = panel_match.as_ref().unwrap();
2767            assert_eq!(panel_match.0.path_prefix, rel_path("repo2").into());
2768            assert_eq!(panel_match.0.path, rel_path("package.json").into());
2769            assert_eq!(
2770                panel_match.0.positions,
2771                vec![6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
2772            );
2773        }
2774
2775        if let Match::History { path, panel_match } = &matches[1] {
2776            assert_eq!(path.project.worktree_id, worktree_id1);
2777            assert_eq!(path.project.path.as_ref(), rel_path("package.json"));
2778            let panel_match = panel_match.as_ref().unwrap();
2779            assert_eq!(panel_match.0.path_prefix, rel_path("repo1").into());
2780            assert_eq!(panel_match.0.path, rel_path("package.json").into());
2781            assert_eq!(
2782                panel_match.0.positions,
2783                vec![6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
2784            );
2785        }
2786    });
2787}
2788
2789#[gpui::test]
2790async fn test_selected_match_stays_selected_after_matches_refreshed(cx: &mut gpui::TestAppContext) {
2791    let app_state = init_test(cx);
2792
2793    app_state.fs.as_fake().insert_tree("/src", json!({})).await;
2794
2795    app_state
2796        .fs
2797        .create_dir("/src/even".as_ref())
2798        .await
2799        .expect("unable to create dir");
2800
2801    let initial_files_num = 5;
2802    for i in 0..initial_files_num {
2803        let filename = format!("/src/even/file_{}.txt", 10 + i);
2804        app_state
2805            .fs
2806            .create_file(Path::new(&filename), Default::default())
2807            .await
2808            .expect("unable to create file");
2809    }
2810
2811    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
2812    let (workspace, cx) =
2813        cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2814
2815    // Initial state
2816    let picker = open_file_picker(&workspace, cx);
2817    cx.simulate_input("file");
2818    let selected_index = 3;
2819    // Checking only the filename, not the whole path
2820    let selected_file = format!("file_{}.txt", 10 + selected_index);
2821    // Select even/file_13.txt
2822    for _ in 0..selected_index {
2823        cx.dispatch_action(SelectNext);
2824    }
2825
2826    picker.update(cx, |finder, _| {
2827        assert_match_selection(finder, selected_index, &selected_file)
2828    });
2829
2830    // Add more matches to the search results
2831    let files_to_add = 10;
2832    for i in 0..files_to_add {
2833        let filename = format!("/src/file_{}.txt", 20 + i);
2834        app_state
2835            .fs
2836            .create_file(Path::new(&filename), Default::default())
2837            .await
2838            .expect("unable to create file");
2839    }
2840    cx.executor().advance_clock(FS_WATCH_LATENCY);
2841
2842    // file_13.txt is still selected
2843    picker.update(cx, |finder, _| {
2844        let expected_selected_index = selected_index + files_to_add;
2845        assert_match_selection(finder, expected_selected_index, &selected_file);
2846    });
2847}
2848
2849#[gpui::test]
2850async fn test_first_match_selected_if_previous_one_is_not_in_the_match_list(
2851    cx: &mut gpui::TestAppContext,
2852) {
2853    let app_state = init_test(cx);
2854
2855    app_state
2856        .fs
2857        .as_fake()
2858        .insert_tree(
2859            "/src",
2860            json!({
2861                "file_1.txt": "// file_1",
2862                "file_2.txt": "// file_2",
2863                "file_3.txt": "// file_3",
2864            }),
2865        )
2866        .await;
2867
2868    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
2869    let (workspace, cx) =
2870        cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2871
2872    // Initial state
2873    let picker = open_file_picker(&workspace, cx);
2874    cx.simulate_input("file");
2875    // Select even/file_2.txt
2876    cx.dispatch_action(SelectNext);
2877
2878    // Remove the selected entry
2879    app_state
2880        .fs
2881        .remove_file("/src/file_2.txt".as_ref(), Default::default())
2882        .await
2883        .expect("unable to remove file");
2884    cx.executor().advance_clock(FS_WATCH_LATENCY);
2885
2886    // file_1.txt is now selected
2887    picker.update(cx, |finder, _| {
2888        assert_match_selection(finder, 0, "file_1.txt");
2889    });
2890}
2891
2892#[gpui::test]
2893async fn test_keeps_file_finder_open_after_modifier_keys_release(cx: &mut gpui::TestAppContext) {
2894    let app_state = init_test(cx);
2895
2896    app_state
2897        .fs
2898        .as_fake()
2899        .insert_tree(
2900            path!("/test"),
2901            json!({
2902                "1.txt": "// One",
2903            }),
2904        )
2905        .await;
2906
2907    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2908    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2909
2910    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2911
2912    cx.simulate_modifiers_change(Modifiers::secondary_key());
2913    open_file_picker(&workspace, cx);
2914
2915    cx.simulate_modifiers_change(Modifiers::none());
2916    active_file_picker(&workspace, cx);
2917}
2918
2919#[gpui::test]
2920async fn test_opens_file_on_modifier_keys_release(cx: &mut gpui::TestAppContext) {
2921    let app_state = init_test(cx);
2922
2923    app_state
2924        .fs
2925        .as_fake()
2926        .insert_tree(
2927            path!("/test"),
2928            json!({
2929                "1.txt": "// One",
2930                "2.txt": "// Two",
2931            }),
2932        )
2933        .await;
2934
2935    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2936    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2937
2938    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2939    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2940
2941    cx.simulate_modifiers_change(Modifiers::secondary_key());
2942    let picker = open_file_picker(&workspace, cx);
2943    picker.update(cx, |finder, _| {
2944        assert_eq!(finder.delegate.matches.len(), 2);
2945        assert_match_selection(finder, 0, "2.txt");
2946        assert_match_at_position(finder, 1, "1.txt");
2947    });
2948
2949    cx.dispatch_action(SelectNext);
2950    cx.simulate_modifiers_change(Modifiers::none());
2951    cx.read(|cx| {
2952        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
2953        assert_eq!(active_editor.read(cx).title(cx), "1.txt");
2954    });
2955}
2956
2957#[gpui::test]
2958async fn test_switches_between_release_norelease_modes_on_forward_nav(
2959    cx: &mut gpui::TestAppContext,
2960) {
2961    let app_state = init_test(cx);
2962
2963    app_state
2964        .fs
2965        .as_fake()
2966        .insert_tree(
2967            path!("/test"),
2968            json!({
2969                "1.txt": "// One",
2970                "2.txt": "// Two",
2971            }),
2972        )
2973        .await;
2974
2975    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2976    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2977
2978    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2979    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2980
2981    // Open with a shortcut
2982    cx.simulate_modifiers_change(Modifiers::secondary_key());
2983    let picker = open_file_picker(&workspace, cx);
2984    picker.update(cx, |finder, _| {
2985        assert_eq!(finder.delegate.matches.len(), 2);
2986        assert_match_selection(finder, 0, "2.txt");
2987        assert_match_at_position(finder, 1, "1.txt");
2988    });
2989
2990    // Switch to navigating with other shortcuts
2991    // Don't open file on modifiers release
2992    cx.simulate_modifiers_change(Modifiers::control());
2993    cx.dispatch_action(SelectNext);
2994    cx.simulate_modifiers_change(Modifiers::none());
2995    picker.update(cx, |finder, _| {
2996        assert_eq!(finder.delegate.matches.len(), 2);
2997        assert_match_at_position(finder, 0, "2.txt");
2998        assert_match_selection(finder, 1, "1.txt");
2999    });
3000
3001    // Back to navigation with initial shortcut
3002    // Open file on modifiers release
3003    cx.simulate_modifiers_change(Modifiers::secondary_key());
3004    cx.dispatch_action(ToggleFileFinder::default());
3005    cx.simulate_modifiers_change(Modifiers::none());
3006    cx.read(|cx| {
3007        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
3008        assert_eq!(active_editor.read(cx).title(cx), "2.txt");
3009    });
3010}
3011
3012#[gpui::test]
3013async fn test_switches_between_release_norelease_modes_on_backward_nav(
3014    cx: &mut gpui::TestAppContext,
3015) {
3016    let app_state = init_test(cx);
3017
3018    app_state
3019        .fs
3020        .as_fake()
3021        .insert_tree(
3022            path!("/test"),
3023            json!({
3024                "1.txt": "// One",
3025                "2.txt": "// Two",
3026                "3.txt": "// Three"
3027            }),
3028        )
3029        .await;
3030
3031    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
3032    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
3033
3034    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
3035    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
3036    open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
3037
3038    // Open with a shortcut
3039    cx.simulate_modifiers_change(Modifiers::secondary_key());
3040    let picker = open_file_picker(&workspace, cx);
3041    picker.update(cx, |finder, _| {
3042        assert_eq!(finder.delegate.matches.len(), 3);
3043        assert_match_selection(finder, 0, "3.txt");
3044        assert_match_at_position(finder, 1, "2.txt");
3045        assert_match_at_position(finder, 2, "1.txt");
3046    });
3047
3048    // Switch to navigating with other shortcuts
3049    // Don't open file on modifiers release
3050    cx.simulate_modifiers_change(Modifiers::control());
3051    cx.dispatch_action(menu::SelectPrevious);
3052    cx.simulate_modifiers_change(Modifiers::none());
3053    picker.update(cx, |finder, _| {
3054        assert_eq!(finder.delegate.matches.len(), 3);
3055        assert_match_at_position(finder, 0, "3.txt");
3056        assert_match_at_position(finder, 1, "2.txt");
3057        assert_match_selection(finder, 2, "1.txt");
3058    });
3059
3060    // Back to navigation with initial shortcut
3061    // Open file on modifiers release
3062    cx.simulate_modifiers_change(Modifiers::secondary_key());
3063    cx.dispatch_action(SelectPrevious); // <-- File Finder's SelectPrevious, not menu's
3064    cx.simulate_modifiers_change(Modifiers::none());
3065    cx.read(|cx| {
3066        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
3067        assert_eq!(active_editor.read(cx).title(cx), "3.txt");
3068    });
3069}
3070
3071#[gpui::test]
3072async fn test_extending_modifiers_does_not_confirm_selection(cx: &mut gpui::TestAppContext) {
3073    let app_state = init_test(cx);
3074
3075    app_state
3076        .fs
3077        .as_fake()
3078        .insert_tree(
3079            path!("/test"),
3080            json!({
3081                "1.txt": "// One",
3082            }),
3083        )
3084        .await;
3085
3086    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
3087    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
3088
3089    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
3090
3091    cx.simulate_modifiers_change(Modifiers::secondary_key());
3092    open_file_picker(&workspace, cx);
3093
3094    cx.simulate_modifiers_change(Modifiers::command_shift());
3095    active_file_picker(&workspace, cx);
3096}
3097
3098#[gpui::test]
3099async fn test_repeat_toggle_action(cx: &mut gpui::TestAppContext) {
3100    let app_state = init_test(cx);
3101    app_state
3102        .fs
3103        .as_fake()
3104        .insert_tree(
3105            "/test",
3106            json!({
3107                "00.txt": "",
3108                "01.txt": "",
3109                "02.txt": "",
3110                "03.txt": "",
3111                "04.txt": "",
3112                "05.txt": "",
3113            }),
3114        )
3115        .await;
3116
3117    let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
3118    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
3119
3120    cx.dispatch_action(ToggleFileFinder::default());
3121    let picker = active_file_picker(&workspace, cx);
3122
3123    picker.update_in(cx, |picker, window, cx| {
3124        picker.update_matches(".txt".to_string(), window, cx)
3125    });
3126
3127    cx.run_until_parked();
3128
3129    picker.update(cx, |picker, _| {
3130        assert_eq!(picker.delegate.matches.len(), 7);
3131        assert_eq!(picker.delegate.selected_index, 0);
3132    });
3133
3134    // When toggling repeatedly, the picker scrolls to reveal the selected item.
3135    cx.dispatch_action(ToggleFileFinder::default());
3136    cx.dispatch_action(ToggleFileFinder::default());
3137    cx.dispatch_action(ToggleFileFinder::default());
3138
3139    cx.run_until_parked();
3140
3141    picker.update(cx, |picker, _| {
3142        assert_eq!(picker.delegate.matches.len(), 7);
3143        assert_eq!(picker.delegate.selected_index, 3);
3144    });
3145}
3146
3147async fn open_close_queried_buffer(
3148    input: &str,
3149    expected_matches: usize,
3150    expected_editor_title: &str,
3151    workspace: &Entity<Workspace>,
3152    cx: &mut gpui::VisualTestContext,
3153) -> Vec<FoundPath> {
3154    let history_items = open_queried_buffer(
3155        input,
3156        expected_matches,
3157        expected_editor_title,
3158        workspace,
3159        cx,
3160    )
3161    .await;
3162
3163    cx.dispatch_action(workspace::CloseActiveItem {
3164        save_intent: None,
3165        close_pinned: false,
3166    });
3167
3168    history_items
3169}
3170
3171async fn open_queried_buffer(
3172    input: &str,
3173    expected_matches: usize,
3174    expected_editor_title: &str,
3175    workspace: &Entity<Workspace>,
3176    cx: &mut gpui::VisualTestContext,
3177) -> Vec<FoundPath> {
3178    let picker = open_file_picker(workspace, cx);
3179    cx.simulate_input(input);
3180
3181    let history_items = picker.update(cx, |finder, _| {
3182        assert_eq!(
3183            finder.delegate.matches.len(),
3184            expected_matches + 1, // +1 from CreateNew option
3185            "Unexpected number of matches found for query `{input}`, matches: {:?}",
3186            finder.delegate.matches
3187        );
3188        finder.delegate.history_items.clone()
3189    });
3190
3191    cx.dispatch_action(Confirm);
3192
3193    cx.read(|cx| {
3194        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
3195        let active_editor_title = active_editor.read(cx).title(cx);
3196        assert_eq!(
3197            expected_editor_title, active_editor_title,
3198            "Unexpected editor title for query `{input}`"
3199        );
3200    });
3201
3202    history_items
3203}
3204
3205fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
3206    cx.update(|cx| {
3207        let state = AppState::test(cx);
3208        theme::init(theme::LoadThemes::JustBase, cx);
3209        super::init(cx);
3210        editor::init(cx);
3211        state
3212    })
3213}
3214
3215fn test_path_position(test_str: &str) -> FileSearchQuery {
3216    let path_position = PathWithPosition::parse_str(test_str);
3217
3218    FileSearchQuery {
3219        raw_query: test_str.to_owned(),
3220        file_query_end: if path_position.path.to_str().unwrap() == test_str {
3221            None
3222        } else {
3223            Some(path_position.path.to_str().unwrap().len())
3224        },
3225        path_position,
3226    }
3227}
3228
3229fn build_find_picker(
3230    project: Entity<Project>,
3231    cx: &mut TestAppContext,
3232) -> (
3233    Entity<Picker<FileFinderDelegate>>,
3234    Entity<Workspace>,
3235    &mut VisualTestContext,
3236) {
3237    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
3238    let picker = open_file_picker(&workspace, cx);
3239    (picker, workspace, cx)
3240}
3241
3242#[track_caller]
3243fn open_file_picker(
3244    workspace: &Entity<Workspace>,
3245    cx: &mut VisualTestContext,
3246) -> Entity<Picker<FileFinderDelegate>> {
3247    cx.dispatch_action(ToggleFileFinder {
3248        separate_history: true,
3249    });
3250    active_file_picker(workspace, cx)
3251}
3252
3253#[track_caller]
3254fn active_file_picker(
3255    workspace: &Entity<Workspace>,
3256    cx: &mut VisualTestContext,
3257) -> Entity<Picker<FileFinderDelegate>> {
3258    workspace.update(cx, |workspace, cx| {
3259        workspace
3260            .active_modal::<FileFinder>(cx)
3261            .expect("file finder is not open")
3262            .read(cx)
3263            .picker
3264            .clone()
3265    })
3266}
3267
3268#[derive(Debug, Default)]
3269struct SearchEntries {
3270    history: Vec<Arc<RelPath>>,
3271    history_found_paths: Vec<FoundPath>,
3272    search: Vec<Arc<RelPath>>,
3273    search_matches: Vec<PathMatch>,
3274}
3275
3276impl SearchEntries {
3277    #[track_caller]
3278    fn search_paths_only(self) -> Vec<Arc<RelPath>> {
3279        assert!(
3280            self.history.is_empty(),
3281            "Should have no history matches, but got: {:?}",
3282            self.history
3283        );
3284        self.search
3285    }
3286
3287    #[track_caller]
3288    fn search_matches_only(self) -> Vec<PathMatch> {
3289        assert!(
3290            self.history.is_empty(),
3291            "Should have no history matches, but got: {:?}",
3292            self.history
3293        );
3294        self.search_matches
3295    }
3296}
3297
3298fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries {
3299    let mut search_entries = SearchEntries::default();
3300    for m in &picker.delegate.matches.matches {
3301        match &m {
3302            Match::History {
3303                path: history_path,
3304                panel_match: path_match,
3305            } => {
3306                if let Some(path_match) = path_match.as_ref() {
3307                    search_entries
3308                        .history
3309                        .push(path_match.0.path_prefix.join(&path_match.0.path));
3310                } else {
3311                    // This occurs when the query is empty and we show history matches
3312                    // that are outside the project.
3313                    panic!("currently not exercised in tests");
3314                }
3315                search_entries
3316                    .history_found_paths
3317                    .push(history_path.clone());
3318            }
3319            Match::Search(path_match) => {
3320                search_entries
3321                    .search
3322                    .push(path_match.0.path_prefix.join(&path_match.0.path));
3323                search_entries.search_matches.push(path_match.0.clone());
3324            }
3325            Match::CreateNew(_) => {}
3326        }
3327    }
3328    search_entries
3329}
3330
3331#[track_caller]
3332fn assert_match_selection(
3333    finder: &Picker<FileFinderDelegate>,
3334    expected_selection_index: usize,
3335    expected_file_name: &str,
3336) {
3337    assert_eq!(
3338        finder.delegate.selected_index(),
3339        expected_selection_index,
3340        "Match is not selected"
3341    );
3342    assert_match_at_position(finder, expected_selection_index, expected_file_name);
3343}
3344
3345#[track_caller]
3346fn assert_match_at_position(
3347    finder: &Picker<FileFinderDelegate>,
3348    match_index: usize,
3349    expected_file_name: &str,
3350) {
3351    let match_item = finder
3352        .delegate
3353        .matches
3354        .get(match_index)
3355        .unwrap_or_else(|| panic!("Finder has no match for index {match_index}"));
3356    let match_file_name = match &match_item {
3357        Match::History { path, .. } => path.absolute.file_name().and_then(|s| s.to_str()),
3358        Match::Search(path_match) => path_match.0.path.file_name(),
3359        Match::CreateNew(project_path) => project_path.path.file_name(),
3360    }
3361    .unwrap();
3362    assert_eq!(match_file_name, expected_file_name);
3363}
3364
3365#[gpui::test]
3366async fn test_filename_precedence(cx: &mut TestAppContext) {
3367    let app_state = init_test(cx);
3368
3369    app_state
3370        .fs
3371        .as_fake()
3372        .insert_tree(
3373            path!("/src"),
3374            json!({
3375                "layout": {
3376                    "app.css": "",
3377                    "app.d.ts": "",
3378                    "app.html": "",
3379                    "+page.svelte": "",
3380                },
3381                "routes": {
3382                    "+layout.svelte": "",
3383                }
3384            }),
3385        )
3386        .await;
3387
3388    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
3389    let (picker, _, cx) = build_find_picker(project, cx);
3390
3391    cx.simulate_input("layout");
3392
3393    picker.update(cx, |finder, _| {
3394        let search_matches = collect_search_matches(finder).search_paths_only();
3395
3396        assert_eq!(
3397            search_matches,
3398            vec![
3399                rel_path("routes/+layout.svelte").into(),
3400                rel_path("layout/app.css").into(),
3401                rel_path("layout/app.d.ts").into(),
3402                rel_path("layout/app.html").into(),
3403                rel_path("layout/+page.svelte").into(),
3404            ],
3405            "File with 'layout' in filename should be prioritized over files in 'layout' directory"
3406        );
3407    });
3408}
3409
3410#[gpui::test]
3411async fn test_paths_with_starting_slash(cx: &mut TestAppContext) {
3412    let app_state = init_test(cx);
3413    app_state
3414        .fs
3415        .as_fake()
3416        .insert_tree(
3417            path!("/root"),
3418            json!({
3419                "a": {
3420                    "file1.txt": "",
3421                    "b": {
3422                        "file2.txt": "",
3423                    },
3424                }
3425            }),
3426        )
3427        .await;
3428
3429    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3430
3431    let (picker, workspace, cx) = build_find_picker(project, cx);
3432
3433    let matching_abs_path = "/file1.txt".to_string();
3434    picker
3435        .update_in(cx, |picker, window, cx| {
3436            picker
3437                .delegate
3438                .update_matches(matching_abs_path, window, cx)
3439        })
3440        .await;
3441    picker.update(cx, |picker, _| {
3442        assert_eq!(
3443            collect_search_matches(picker).search_paths_only(),
3444            vec![rel_path("a/file1.txt").into()],
3445            "Relative path starting with slash should match"
3446        )
3447    });
3448    cx.dispatch_action(SelectNext);
3449    cx.dispatch_action(Confirm);
3450    cx.read(|cx| {
3451        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
3452        assert_eq!(active_editor.read(cx).title(cx), "file1.txt");
3453    });
3454}