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