file_finder_tests.rs

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