file_finder_tests.rs

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