file_finder_tests.rs

   1use std::{assert_eq, future::IntoFuture, path::Path, time::Duration};
   2
   3use super::*;
   4use editor::Editor;
   5use gpui::{Entity, TestAppContext, VisualTestContext};
   6use menu::{Confirm, SelectNext, SelectPrev};
   7use project::FS_WATCH_LATENCY;
   8use serde_json::json;
   9use workspace::{AppState, Workspace};
  10
  11#[ctor::ctor]
  12fn init_logger() {
  13    if std::env::var("RUST_LOG").is_ok() {
  14        env_logger::init();
  15    }
  16}
  17
  18#[gpui::test]
  19async fn test_matching_paths(cx: &mut TestAppContext) {
  20    let app_state = init_test(cx);
  21    app_state
  22        .fs
  23        .as_fake()
  24        .insert_tree(
  25            "/root",
  26            json!({
  27                "a": {
  28                    "banana": "",
  29                    "bandana": "",
  30                }
  31            }),
  32        )
  33        .await;
  34
  35    let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
  36
  37    let (picker, workspace, cx) = build_find_picker(project, cx);
  38
  39    cx.simulate_input("bna");
  40    picker.update(cx, |picker, _| {
  41        assert_eq!(picker.delegate.matches.len(), 2);
  42    });
  43    cx.dispatch_action(SelectNext);
  44    cx.dispatch_action(Confirm);
  45    cx.read(|cx| {
  46        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
  47        assert_eq!(active_editor.read(cx).title(cx), "bandana");
  48    });
  49
  50    for bandana_query in [
  51        "bandana",
  52        " bandana",
  53        "bandana ",
  54        " bandana ",
  55        " ndan ",
  56        " band ",
  57        "a bandana",
  58    ] {
  59        picker
  60            .update(cx, |picker, cx| {
  61                picker
  62                    .delegate
  63                    .update_matches(bandana_query.to_string(), cx)
  64            })
  65            .await;
  66        picker.update(cx, |picker, _| {
  67            assert_eq!(
  68                picker.delegate.matches.len(),
  69                1,
  70                "Wrong number of matches for bandana query '{bandana_query}'"
  71            );
  72        });
  73        cx.dispatch_action(SelectNext);
  74        cx.dispatch_action(Confirm);
  75        cx.read(|cx| {
  76            let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
  77            assert_eq!(
  78                active_editor.read(cx).title(cx),
  79                "bandana",
  80                "Wrong match for bandana query '{bandana_query}'"
  81            );
  82        });
  83    }
  84}
  85
  86#[gpui::test]
  87async fn test_absolute_paths(cx: &mut TestAppContext) {
  88    let app_state = init_test(cx);
  89    app_state
  90        .fs
  91        .as_fake()
  92        .insert_tree(
  93            "/root",
  94            json!({
  95                "a": {
  96                    "file1.txt": "",
  97                    "b": {
  98                        "file2.txt": "",
  99                    },
 100                }
 101            }),
 102        )
 103        .await;
 104
 105    let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
 106
 107    let (picker, workspace, cx) = build_find_picker(project, cx);
 108
 109    let matching_abs_path = "/root/a/b/file2.txt";
 110    picker
 111        .update(cx, |picker, cx| {
 112            picker
 113                .delegate
 114                .update_matches(matching_abs_path.to_string(), cx)
 115        })
 116        .await;
 117    picker.update(cx, |picker, _| {
 118        assert_eq!(
 119            collect_search_matches(picker).search_only(),
 120            vec![PathBuf::from("a/b/file2.txt")],
 121            "Matching abs path should be the only match"
 122        )
 123    });
 124    cx.dispatch_action(SelectNext);
 125    cx.dispatch_action(Confirm);
 126    cx.read(|cx| {
 127        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
 128        assert_eq!(active_editor.read(cx).title(cx), "file2.txt");
 129    });
 130
 131    let mismatching_abs_path = "/root/a/b/file1.txt";
 132    picker
 133        .update(cx, |picker, cx| {
 134            picker
 135                .delegate
 136                .update_matches(mismatching_abs_path.to_string(), cx)
 137        })
 138        .await;
 139    picker.update(cx, |picker, _| {
 140        assert_eq!(
 141            collect_search_matches(picker).search_only(),
 142            Vec::<PathBuf>::new(),
 143            "Mismatching abs path should produce no matches"
 144        )
 145    });
 146}
 147
 148#[gpui::test]
 149async fn test_complex_path(cx: &mut TestAppContext) {
 150    let app_state = init_test(cx);
 151    app_state
 152        .fs
 153        .as_fake()
 154        .insert_tree(
 155            "/root",
 156            json!({
 157                "其他": {
 158                    "S数据表格": {
 159                        "task.xlsx": "some content",
 160                    },
 161                }
 162            }),
 163        )
 164        .await;
 165
 166    let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
 167
 168    let (picker, workspace, cx) = build_find_picker(project, cx);
 169
 170    cx.simulate_input("t");
 171    picker.update(cx, |picker, _| {
 172        assert_eq!(picker.delegate.matches.len(), 1);
 173        assert_eq!(
 174            collect_search_matches(picker).search_only(),
 175            vec![PathBuf::from("其他/S数据表格/task.xlsx")],
 176        )
 177    });
 178    cx.dispatch_action(SelectNext);
 179    cx.dispatch_action(Confirm);
 180    cx.read(|cx| {
 181        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
 182        assert_eq!(active_editor.read(cx).title(cx), "task.xlsx");
 183    });
 184}
 185
 186#[gpui::test]
 187async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
 188    let app_state = init_test(cx);
 189
 190    let first_file_name = "first.rs";
 191    let first_file_contents = "// First Rust file";
 192    app_state
 193        .fs
 194        .as_fake()
 195        .insert_tree(
 196            "/src",
 197            json!({
 198                "test": {
 199                    first_file_name: first_file_contents,
 200                    "second.rs": "// Second Rust file",
 201                }
 202            }),
 203        )
 204        .await;
 205
 206    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
 207
 208    let (picker, workspace, cx) = build_find_picker(project, cx);
 209
 210    let file_query = &first_file_name[..3];
 211    let file_row = 1;
 212    let file_column = 3;
 213    assert!(file_column <= first_file_contents.len());
 214    let query_inside_file = format!("{file_query}:{file_row}:{file_column}");
 215    picker
 216        .update(cx, |finder, cx| {
 217            finder
 218                .delegate
 219                .update_matches(query_inside_file.to_string(), cx)
 220        })
 221        .await;
 222    picker.update(cx, |finder, _| {
 223        let finder = &finder.delegate;
 224        assert_eq!(finder.matches.len(), 1);
 225        let latest_search_query = finder
 226            .latest_search_query
 227            .as_ref()
 228            .expect("Finder should have a query after the update_matches call");
 229        assert_eq!(latest_search_query.path_like.raw_query, query_inside_file);
 230        assert_eq!(
 231            latest_search_query.path_like.file_query_end,
 232            Some(file_query.len())
 233        );
 234        assert_eq!(latest_search_query.row, Some(file_row));
 235        assert_eq!(latest_search_query.column, Some(file_column as u32));
 236    });
 237
 238    cx.dispatch_action(SelectNext);
 239    cx.dispatch_action(Confirm);
 240
 241    let editor = cx.update(|cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
 242    cx.executor().advance_clock(Duration::from_secs(2));
 243
 244    editor.update(cx, |editor, cx| {
 245            let all_selections = editor.selections.all_adjusted(cx);
 246            assert_eq!(
 247                all_selections.len(),
 248                1,
 249                "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
 250            );
 251            let caret_selection = all_selections.into_iter().next().unwrap();
 252            assert_eq!(caret_selection.start, caret_selection.end,
 253                "Caret selection should have its start and end at the same position");
 254            assert_eq!(file_row, caret_selection.start.row + 1,
 255                "Query inside file should get caret with the same focus row");
 256            assert_eq!(file_column, caret_selection.start.column as usize + 1,
 257                "Query inside file should get caret with the same focus column");
 258        });
 259}
 260
 261#[gpui::test]
 262async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) {
 263    let app_state = init_test(cx);
 264
 265    let first_file_name = "first.rs";
 266    let first_file_contents = "// First Rust file";
 267    app_state
 268        .fs
 269        .as_fake()
 270        .insert_tree(
 271            "/src",
 272            json!({
 273                "test": {
 274                    first_file_name: first_file_contents,
 275                    "second.rs": "// Second Rust file",
 276                }
 277            }),
 278        )
 279        .await;
 280
 281    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
 282
 283    let (picker, workspace, cx) = build_find_picker(project, cx);
 284
 285    let file_query = &first_file_name[..3];
 286    let file_row = 200;
 287    let file_column = 300;
 288    assert!(file_column > first_file_contents.len());
 289    let query_outside_file = format!("{file_query}:{file_row}:{file_column}");
 290    picker
 291        .update(cx, |picker, cx| {
 292            picker
 293                .delegate
 294                .update_matches(query_outside_file.to_string(), cx)
 295        })
 296        .await;
 297    picker.update(cx, |finder, _| {
 298        let delegate = &finder.delegate;
 299        assert_eq!(delegate.matches.len(), 1);
 300        let latest_search_query = delegate
 301            .latest_search_query
 302            .as_ref()
 303            .expect("Finder should have a query after the update_matches call");
 304        assert_eq!(latest_search_query.path_like.raw_query, query_outside_file);
 305        assert_eq!(
 306            latest_search_query.path_like.file_query_end,
 307            Some(file_query.len())
 308        );
 309        assert_eq!(latest_search_query.row, Some(file_row));
 310        assert_eq!(latest_search_query.column, Some(file_column as u32));
 311    });
 312
 313    cx.dispatch_action(SelectNext);
 314    cx.dispatch_action(Confirm);
 315
 316    let editor = cx.update(|cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
 317    cx.executor().advance_clock(Duration::from_secs(2));
 318
 319    editor.update(cx, |editor, cx| {
 320            let all_selections = editor.selections.all_adjusted(cx);
 321            assert_eq!(
 322                all_selections.len(),
 323                1,
 324                "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
 325            );
 326            let caret_selection = all_selections.into_iter().next().unwrap();
 327            assert_eq!(caret_selection.start, caret_selection.end,
 328                "Caret selection should have its start and end at the same position");
 329            assert_eq!(0, caret_selection.start.row,
 330                "Excessive rows (as in query outside file borders) should get trimmed to last file row");
 331            assert_eq!(first_file_contents.len(), caret_selection.start.column as usize,
 332                "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column");
 333        });
 334}
 335
 336#[gpui::test]
 337async fn test_matching_cancellation(cx: &mut TestAppContext) {
 338    let app_state = init_test(cx);
 339    app_state
 340        .fs
 341        .as_fake()
 342        .insert_tree(
 343            "/dir",
 344            json!({
 345                "hello": "",
 346                "goodbye": "",
 347                "halogen-light": "",
 348                "happiness": "",
 349                "height": "",
 350                "hi": "",
 351                "hiccup": "",
 352            }),
 353        )
 354        .await;
 355
 356    let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
 357
 358    let (picker, _, cx) = build_find_picker(project, cx);
 359
 360    let query = test_path_like("hi");
 361    picker
 362        .update(cx, |picker, cx| {
 363            picker.delegate.spawn_search(query.clone(), cx)
 364        })
 365        .await;
 366
 367    picker.update(cx, |picker, _cx| {
 368        assert_eq!(picker.delegate.matches.len(), 5)
 369    });
 370
 371    picker.update(cx, |picker, cx| {
 372        let delegate = &mut picker.delegate;
 373        assert!(
 374            delegate.matches.history.is_empty(),
 375            "Search matches expected"
 376        );
 377        let matches = delegate.matches.search.clone();
 378
 379        // Simulate a search being cancelled after the time limit,
 380        // returning only a subset of the matches that would have been found.
 381        drop(delegate.spawn_search(query.clone(), cx));
 382        delegate.set_search_matches(
 383            delegate.latest_search_id,
 384            true, // did-cancel
 385            query.clone(),
 386            vec![matches[1].clone(), matches[3].clone()],
 387            cx,
 388        );
 389
 390        // Simulate another cancellation.
 391        drop(delegate.spawn_search(query.clone(), cx));
 392        delegate.set_search_matches(
 393            delegate.latest_search_id,
 394            true, // did-cancel
 395            query.clone(),
 396            vec![matches[0].clone(), matches[2].clone(), matches[3].clone()],
 397            cx,
 398        );
 399
 400        assert!(
 401            delegate.matches.history.is_empty(),
 402            "Search matches expected"
 403        );
 404        assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]);
 405    });
 406}
 407
 408#[gpui::test]
 409async fn test_ignored_root(cx: &mut TestAppContext) {
 410    let app_state = init_test(cx);
 411    app_state
 412        .fs
 413        .as_fake()
 414        .insert_tree(
 415            "/ancestor",
 416            json!({
 417                ".gitignore": "ignored-root",
 418                "ignored-root": {
 419                    "happiness": "",
 420                    "height": "",
 421                    "hi": "",
 422                    "hiccup": "",
 423                },
 424                "tracked-root": {
 425                    ".gitignore": "height",
 426                    "happiness": "",
 427                    "height": "",
 428                    "hi": "",
 429                    "hiccup": "",
 430                },
 431            }),
 432        )
 433        .await;
 434
 435    let project = Project::test(
 436        app_state.fs.clone(),
 437        [
 438            "/ancestor/tracked-root".as_ref(),
 439            "/ancestor/ignored-root".as_ref(),
 440        ],
 441        cx,
 442    )
 443    .await;
 444
 445    let (picker, _, cx) = build_find_picker(project, cx);
 446
 447    picker
 448        .update(cx, |picker, cx| {
 449            picker.delegate.spawn_search(test_path_like("hi"), cx)
 450        })
 451        .await;
 452    picker.update(cx, |picker, _| assert_eq!(picker.delegate.matches.len(), 7));
 453}
 454
 455#[gpui::test]
 456async fn test_single_file_worktrees(cx: &mut TestAppContext) {
 457    let app_state = init_test(cx);
 458    app_state
 459        .fs
 460        .as_fake()
 461        .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } }))
 462        .await;
 463
 464    let project = Project::test(
 465        app_state.fs.clone(),
 466        ["/root/the-parent-dir/the-file".as_ref()],
 467        cx,
 468    )
 469    .await;
 470
 471    let (picker, _, cx) = build_find_picker(project, cx);
 472
 473    // Even though there is only one worktree, that worktree's filename
 474    // is included in the matching, because the worktree is a single file.
 475    picker
 476        .update(cx, |picker, cx| {
 477            picker.delegate.spawn_search(test_path_like("thf"), cx)
 478        })
 479        .await;
 480    cx.read(|cx| {
 481        let picker = picker.read(cx);
 482        let delegate = &picker.delegate;
 483        assert!(
 484            delegate.matches.history.is_empty(),
 485            "Search matches expected"
 486        );
 487        let matches = delegate.matches.search.clone();
 488        assert_eq!(matches.len(), 1);
 489
 490        let (file_name, file_name_positions, full_path, full_path_positions) =
 491            delegate.labels_for_path_match(&matches[0].0);
 492        assert_eq!(file_name, "the-file");
 493        assert_eq!(file_name_positions, &[0, 1, 4]);
 494        assert_eq!(full_path, "");
 495        assert_eq!(full_path_positions, &[0; 0]);
 496    });
 497
 498    // Since the worktree root is a file, searching for its name followed by a slash does
 499    // not match anything.
 500    picker
 501        .update(cx, |f, cx| {
 502            f.delegate.spawn_search(test_path_like("thf/"), cx)
 503        })
 504        .await;
 505    picker.update(cx, |f, _| assert_eq!(f.delegate.matches.len(), 0));
 506}
 507
 508#[gpui::test]
 509async fn test_path_distance_ordering(cx: &mut TestAppContext) {
 510    let app_state = init_test(cx);
 511    app_state
 512        .fs
 513        .as_fake()
 514        .insert_tree(
 515            "/root",
 516            json!({
 517                "dir1": { "a.txt": "" },
 518                "dir2": {
 519                    "a.txt": "",
 520                    "b.txt": ""
 521                }
 522            }),
 523        )
 524        .await;
 525
 526    let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
 527    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
 528
 529    let worktree_id = cx.read(|cx| {
 530        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
 531        assert_eq!(worktrees.len(), 1);
 532        WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
 533    });
 534
 535    // When workspace has an active item, sort items which are closer to that item
 536    // first when they have the same name. In this case, b.txt is closer to dir2's a.txt
 537    // so that one should be sorted earlier
 538    let b_path = ProjectPath {
 539        worktree_id,
 540        path: Arc::from(Path::new("dir2/b.txt")),
 541    };
 542    workspace
 543        .update(cx, |workspace, cx| {
 544            workspace.open_path(b_path, None, true, cx)
 545        })
 546        .await
 547        .unwrap();
 548    let finder = open_file_picker(&workspace, cx);
 549    finder
 550        .update(cx, |f, cx| {
 551            f.delegate.spawn_search(test_path_like("a.txt"), cx)
 552        })
 553        .await;
 554
 555    finder.update(cx, |f, _| {
 556        let delegate = &f.delegate;
 557        assert!(
 558            delegate.matches.history.is_empty(),
 559            "Search matches expected"
 560        );
 561        let matches = &delegate.matches.search;
 562        assert_eq!(matches[0].0.path.as_ref(), Path::new("dir2/a.txt"));
 563        assert_eq!(matches[1].0.path.as_ref(), Path::new("dir1/a.txt"));
 564    });
 565}
 566
 567#[gpui::test]
 568async fn test_search_worktree_without_files(cx: &mut TestAppContext) {
 569    let app_state = init_test(cx);
 570    app_state
 571        .fs
 572        .as_fake()
 573        .insert_tree(
 574            "/root",
 575            json!({
 576                "dir1": {},
 577                "dir2": {
 578                    "dir3": {}
 579                }
 580            }),
 581        )
 582        .await;
 583
 584    let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
 585    let (picker, _workspace, cx) = build_find_picker(project, cx);
 586
 587    picker
 588        .update(cx, |f, cx| {
 589            f.delegate.spawn_search(test_path_like("dir"), cx)
 590        })
 591        .await;
 592    cx.read(|cx| {
 593        let finder = picker.read(cx);
 594        assert_eq!(finder.delegate.matches.len(), 0);
 595    });
 596}
 597
 598#[gpui::test]
 599async fn test_query_history(cx: &mut gpui::TestAppContext) {
 600    let app_state = init_test(cx);
 601
 602    app_state
 603        .fs
 604        .as_fake()
 605        .insert_tree(
 606            "/src",
 607            json!({
 608                "test": {
 609                    "first.rs": "// First Rust file",
 610                    "second.rs": "// Second Rust file",
 611                    "third.rs": "// Third Rust file",
 612                }
 613            }),
 614        )
 615        .await;
 616
 617    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
 618    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
 619    let worktree_id = cx.read(|cx| {
 620        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
 621        assert_eq!(worktrees.len(), 1);
 622        WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
 623    });
 624
 625    // Open and close panels, getting their history items afterwards.
 626    // Ensure history items get populated with opened items, and items are kept in a certain order.
 627    // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen.
 628    //
 629    // TODO: without closing, the opened items do not propagate their history changes for some reason
 630    // it does work in real app though, only tests do not propagate.
 631    workspace.update(cx, |_, cx| cx.focused());
 632
 633    let initial_history = open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
 634    assert!(
 635        initial_history.is_empty(),
 636        "Should have no history before opening any files"
 637    );
 638
 639    let history_after_first =
 640        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
 641    assert_eq!(
 642        history_after_first,
 643        vec![FoundPath::new(
 644            ProjectPath {
 645                worktree_id,
 646                path: Arc::from(Path::new("test/first.rs")),
 647            },
 648            Some(PathBuf::from("/src/test/first.rs"))
 649        )],
 650        "Should show 1st opened item in the history when opening the 2nd item"
 651    );
 652
 653    let history_after_second =
 654        open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
 655    assert_eq!(
 656        history_after_second,
 657        vec![
 658            FoundPath::new(
 659                ProjectPath {
 660                    worktree_id,
 661                    path: Arc::from(Path::new("test/second.rs")),
 662                },
 663                Some(PathBuf::from("/src/test/second.rs"))
 664            ),
 665            FoundPath::new(
 666                ProjectPath {
 667                    worktree_id,
 668                    path: Arc::from(Path::new("test/first.rs")),
 669                },
 670                Some(PathBuf::from("/src/test/first.rs"))
 671            ),
 672        ],
 673        "Should show 1st and 2nd opened items in the history when opening the 3rd item. \
 674    2nd item should be the first in the history, as the last opened."
 675    );
 676
 677    let history_after_third =
 678        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
 679    assert_eq!(
 680                history_after_third,
 681                vec![
 682                    FoundPath::new(
 683                        ProjectPath {
 684                            worktree_id,
 685                            path: Arc::from(Path::new("test/third.rs")),
 686                        },
 687                        Some(PathBuf::from("/src/test/third.rs"))
 688                    ),
 689                    FoundPath::new(
 690                        ProjectPath {
 691                            worktree_id,
 692                            path: Arc::from(Path::new("test/second.rs")),
 693                        },
 694                        Some(PathBuf::from("/src/test/second.rs"))
 695                    ),
 696                    FoundPath::new(
 697                        ProjectPath {
 698                            worktree_id,
 699                            path: Arc::from(Path::new("test/first.rs")),
 700                        },
 701                        Some(PathBuf::from("/src/test/first.rs"))
 702                    ),
 703                ],
 704                "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
 705    3rd item should be the first in the history, as the last opened."
 706            );
 707
 708    let history_after_second_again =
 709        open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
 710    assert_eq!(
 711                history_after_second_again,
 712                vec![
 713                    FoundPath::new(
 714                        ProjectPath {
 715                            worktree_id,
 716                            path: Arc::from(Path::new("test/second.rs")),
 717                        },
 718                        Some(PathBuf::from("/src/test/second.rs"))
 719                    ),
 720                    FoundPath::new(
 721                        ProjectPath {
 722                            worktree_id,
 723                            path: Arc::from(Path::new("test/third.rs")),
 724                        },
 725                        Some(PathBuf::from("/src/test/third.rs"))
 726                    ),
 727                    FoundPath::new(
 728                        ProjectPath {
 729                            worktree_id,
 730                            path: Arc::from(Path::new("test/first.rs")),
 731                        },
 732                        Some(PathBuf::from("/src/test/first.rs"))
 733                    ),
 734                ],
 735                "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
 736    2nd item, as the last opened, 3rd item should go next as it was opened right before."
 737            );
 738}
 739
 740#[gpui::test]
 741async fn test_external_files_history(cx: &mut gpui::TestAppContext) {
 742    let app_state = init_test(cx);
 743
 744    app_state
 745        .fs
 746        .as_fake()
 747        .insert_tree(
 748            "/src",
 749            json!({
 750                "test": {
 751                    "first.rs": "// First Rust file",
 752                    "second.rs": "// Second Rust file",
 753                }
 754            }),
 755        )
 756        .await;
 757
 758    app_state
 759        .fs
 760        .as_fake()
 761        .insert_tree(
 762            "/external-src",
 763            json!({
 764                "test": {
 765                    "third.rs": "// Third Rust file",
 766                    "fourth.rs": "// Fourth Rust file",
 767                }
 768            }),
 769        )
 770        .await;
 771
 772    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
 773    cx.update(|cx| {
 774        project.update(cx, |project, cx| {
 775            project.find_or_create_local_worktree("/external-src", false, cx)
 776        })
 777    })
 778    .detach();
 779    cx.background_executor.run_until_parked();
 780
 781    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
 782    let worktree_id = cx.read(|cx| {
 783        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
 784        assert_eq!(worktrees.len(), 1,);
 785
 786        WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
 787    });
 788    workspace
 789        .update(cx, |workspace, cx| {
 790            workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx)
 791        })
 792        .detach();
 793    cx.background_executor.run_until_parked();
 794    let external_worktree_id = cx.read(|cx| {
 795        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
 796        assert_eq!(
 797            worktrees.len(),
 798            2,
 799            "External file should get opened in a new worktree"
 800        );
 801
 802        WorktreeId::from_usize(
 803            worktrees
 804                .into_iter()
 805                .find(|worktree| worktree.entity_id().as_u64() as usize != worktree_id.to_usize())
 806                .expect("New worktree should have a different id")
 807                .entity_id()
 808                .as_u64() as usize,
 809        )
 810    });
 811    cx.dispatch_action(workspace::CloseActiveItem { save_intent: None });
 812
 813    let initial_history_items =
 814        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
 815    assert_eq!(
 816        initial_history_items,
 817        vec![FoundPath::new(
 818            ProjectPath {
 819                worktree_id: external_worktree_id,
 820                path: Arc::from(Path::new("")),
 821            },
 822            Some(PathBuf::from("/external-src/test/third.rs"))
 823        )],
 824        "Should show external file with its full path in the history after it was open"
 825    );
 826
 827    let updated_history_items =
 828        open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
 829    assert_eq!(
 830        updated_history_items,
 831        vec![
 832            FoundPath::new(
 833                ProjectPath {
 834                    worktree_id,
 835                    path: Arc::from(Path::new("test/second.rs")),
 836                },
 837                Some(PathBuf::from("/src/test/second.rs"))
 838            ),
 839            FoundPath::new(
 840                ProjectPath {
 841                    worktree_id: external_worktree_id,
 842                    path: Arc::from(Path::new("")),
 843                },
 844                Some(PathBuf::from("/external-src/test/third.rs"))
 845            ),
 846        ],
 847        "Should keep external file with history updates",
 848    );
 849}
 850
 851#[gpui::test]
 852async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) {
 853    let app_state = init_test(cx);
 854
 855    app_state
 856        .fs
 857        .as_fake()
 858        .insert_tree(
 859            "/src",
 860            json!({
 861                "test": {
 862                    "first.rs": "// First Rust file",
 863                    "second.rs": "// Second Rust file",
 864                    "third.rs": "// Third Rust file",
 865                }
 866            }),
 867        )
 868        .await;
 869
 870    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
 871    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
 872
 873    // generate some history to select from
 874    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
 875    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
 876    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
 877    let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
 878
 879    for expected_selected_index in 0..current_history.len() {
 880        cx.dispatch_action(Toggle);
 881        let picker = active_file_picker(&workspace, cx);
 882        let selected_index = picker.update(cx, |picker, _| picker.delegate.selected_index());
 883        assert_eq!(
 884            selected_index, expected_selected_index,
 885            "Should select the next item in the history"
 886        );
 887    }
 888
 889    cx.dispatch_action(Toggle);
 890    let selected_index = workspace.update(cx, |workspace, cx| {
 891        workspace
 892            .active_modal::<FileFinder>(cx)
 893            .unwrap()
 894            .read(cx)
 895            .picker
 896            .read(cx)
 897            .delegate
 898            .selected_index()
 899    });
 900    assert_eq!(
 901        selected_index, 0,
 902        "Should wrap around the history and start all over"
 903    );
 904}
 905
 906#[gpui::test]
 907async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
 908    let app_state = init_test(cx);
 909
 910    app_state
 911        .fs
 912        .as_fake()
 913        .insert_tree(
 914            "/src",
 915            json!({
 916                "test": {
 917                    "first.rs": "// First Rust file",
 918                    "second.rs": "// Second Rust file",
 919                    "third.rs": "// Third Rust file",
 920                    "fourth.rs": "// Fourth Rust file",
 921                }
 922            }),
 923        )
 924        .await;
 925
 926    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
 927    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
 928    let worktree_id = cx.read(|cx| {
 929        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
 930        assert_eq!(worktrees.len(), 1,);
 931
 932        WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
 933    });
 934
 935    // generate some history to select from
 936    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
 937    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
 938    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
 939    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
 940
 941    let finder = open_file_picker(&workspace, cx);
 942    let first_query = "f";
 943    finder
 944        .update(cx, |finder, cx| {
 945            finder.delegate.update_matches(first_query.to_string(), cx)
 946        })
 947        .await;
 948    finder.update(cx, |finder, _| {
 949            let delegate = &finder.delegate;
 950            assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out");
 951            let history_match = delegate.matches.history.first().unwrap();
 952            assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
 953            assert_eq!(history_match.0, FoundPath::new(
 954                ProjectPath {
 955                    worktree_id,
 956                    path: Arc::from(Path::new("test/first.rs")),
 957                },
 958                Some(PathBuf::from("/src/test/first.rs"))
 959            ));
 960            assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
 961            assert_eq!(delegate.matches.search.first().unwrap().0.path.as_ref(), Path::new("test/fourth.rs"));
 962        });
 963
 964    let second_query = "fsdasdsa";
 965    let finder = active_file_picker(&workspace, cx);
 966    finder
 967        .update(cx, |finder, cx| {
 968            finder.delegate.update_matches(second_query.to_string(), cx)
 969        })
 970        .await;
 971    finder.update(cx, |finder, _| {
 972        let delegate = &finder.delegate;
 973        assert!(
 974            delegate.matches.history.is_empty(),
 975            "No history entries should match {second_query}"
 976        );
 977        assert!(
 978            delegate.matches.search.is_empty(),
 979            "No search entries should match {second_query}"
 980        );
 981    });
 982
 983    let first_query_again = first_query;
 984
 985    let finder = active_file_picker(&workspace, cx);
 986    finder
 987        .update(cx, |finder, cx| {
 988            finder
 989                .delegate
 990                .update_matches(first_query_again.to_string(), cx)
 991        })
 992        .await;
 993    finder.update(cx, |finder, _| {
 994            let delegate = &finder.delegate;
 995            assert_eq!(delegate.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");
 996            let history_match = delegate.matches.history.first().unwrap();
 997            assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
 998            assert_eq!(history_match.0, FoundPath::new(
 999                ProjectPath {
1000                    worktree_id,
1001                    path: Arc::from(Path::new("test/first.rs")),
1002                },
1003                Some(PathBuf::from("/src/test/first.rs"))
1004            ));
1005            assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
1006            assert_eq!(delegate.matches.search.first().unwrap().0.path.as_ref(), Path::new("test/fourth.rs"));
1007        });
1008}
1009
1010#[gpui::test]
1011async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) {
1012    let app_state = init_test(cx);
1013
1014    app_state
1015        .fs
1016        .as_fake()
1017        .insert_tree(
1018            "/root",
1019            json!({
1020                "test": {
1021                    "1_qw": "// First file that matches the query",
1022                    "2_second": "// Second file",
1023                    "3_third": "// Third file",
1024                    "4_fourth": "// Fourth file",
1025                    "5_qwqwqw": "// A file with 3 more matches than the first one",
1026                    "6_qwqwqw": "// Same query matches as above, but closer to the end of the list due to the name",
1027                    "7_qwqwqw": "// One more, same amount of query matches as above",
1028                }
1029            }),
1030        )
1031        .await;
1032
1033    let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1034    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1035    // generate some history to select from
1036    open_close_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
1037    open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
1038    open_close_queried_buffer("3", 1, "3_third", &workspace, cx).await;
1039    open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
1040    open_close_queried_buffer("6", 1, "6_qwqwqw", &workspace, cx).await;
1041
1042    let finder = open_file_picker(&workspace, cx);
1043    let query = "qw";
1044    finder
1045        .update(cx, |finder, cx| {
1046            finder.delegate.update_matches(query.to_string(), cx)
1047        })
1048        .await;
1049    finder.update(cx, |finder, _| {
1050        let search_matches = collect_search_matches(finder);
1051        assert_eq!(
1052            search_matches.history,
1053            vec![PathBuf::from("test/1_qw"), PathBuf::from("test/6_qwqwqw"),],
1054        );
1055        assert_eq!(
1056            search_matches.search,
1057            vec![
1058                PathBuf::from("test/5_qwqwqw"),
1059                PathBuf::from("test/7_qwqwqw"),
1060            ],
1061        );
1062    });
1063}
1064
1065#[gpui::test]
1066async fn test_select_current_open_file_when_no_history(cx: &mut gpui::TestAppContext) {
1067    let app_state = init_test(cx);
1068
1069    app_state
1070        .fs
1071        .as_fake()
1072        .insert_tree(
1073            "/root",
1074            json!({
1075                "test": {
1076                    "1_qw": "",
1077                }
1078            }),
1079        )
1080        .await;
1081
1082    let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1083    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1084    // Open new buffer
1085    open_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
1086
1087    let picker = open_file_picker(&workspace, cx);
1088    picker.update(cx, |finder, _| {
1089        assert_match_selection(&finder, 0, "1_qw");
1090    });
1091}
1092
1093#[gpui::test]
1094async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
1095    cx: &mut TestAppContext,
1096) {
1097    let app_state = init_test(cx);
1098
1099    app_state
1100        .fs
1101        .as_fake()
1102        .insert_tree(
1103            "/src",
1104            json!({
1105                "test": {
1106                    "bar.rs": "// Bar file",
1107                    "lib.rs": "// Lib file",
1108                    "maaa.rs": "// Maaaaaaa",
1109                    "main.rs": "// Main file",
1110                    "moo.rs": "// Moooooo",
1111                }
1112            }),
1113        )
1114        .await;
1115
1116    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1117    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1118
1119    open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
1120    open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
1121    open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
1122
1123    // main.rs is on top, previously used is selected
1124    let picker = open_file_picker(&workspace, cx);
1125    picker.update(cx, |finder, _| {
1126        assert_eq!(finder.delegate.matches.len(), 3);
1127        assert_match_selection(finder, 0, "main.rs");
1128        assert_match_at_position(finder, 1, "lib.rs");
1129        assert_match_at_position(finder, 2, "bar.rs");
1130    });
1131
1132    // all files match, main.rs is still on top, but the second item is selected
1133    picker
1134        .update(cx, |finder, cx| {
1135            finder.delegate.update_matches(".rs".to_string(), cx)
1136        })
1137        .await;
1138    picker.update(cx, |finder, _| {
1139        assert_eq!(finder.delegate.matches.len(), 5);
1140        assert_match_at_position(finder, 0, "main.rs");
1141        assert_match_selection(finder, 1, "bar.rs");
1142    });
1143
1144    // main.rs is not among matches, select top item
1145    picker
1146        .update(cx, |finder, cx| {
1147            finder.delegate.update_matches("b".to_string(), cx)
1148        })
1149        .await;
1150    picker.update(cx, |finder, _| {
1151        assert_eq!(finder.delegate.matches.len(), 2);
1152        assert_match_at_position(finder, 0, "bar.rs");
1153    });
1154
1155    // main.rs is back, put it on top and select next item
1156    picker
1157        .update(cx, |finder, cx| {
1158            finder.delegate.update_matches("m".to_string(), cx)
1159        })
1160        .await;
1161    picker.update(cx, |finder, _| {
1162        assert_eq!(finder.delegate.matches.len(), 3);
1163        assert_match_at_position(finder, 0, "main.rs");
1164        assert_match_selection(finder, 1, "moo.rs");
1165    });
1166
1167    // get back to the initial state
1168    picker
1169        .update(cx, |finder, cx| {
1170            finder.delegate.update_matches("".to_string(), cx)
1171        })
1172        .await;
1173    picker.update(cx, |finder, _| {
1174        assert_eq!(finder.delegate.matches.len(), 3);
1175        assert_match_selection(finder, 0, "main.rs");
1176        assert_match_at_position(finder, 1, "lib.rs");
1177    });
1178}
1179
1180#[gpui::test]
1181async fn test_history_items_shown_in_order_of_open(cx: &mut TestAppContext) {
1182    let app_state = init_test(cx);
1183
1184    app_state
1185        .fs
1186        .as_fake()
1187        .insert_tree(
1188            "/test",
1189            json!({
1190                "test": {
1191                    "1.txt": "// One",
1192                    "2.txt": "// Two",
1193                    "3.txt": "// Three",
1194                }
1195            }),
1196        )
1197        .await;
1198
1199    let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
1200    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1201
1202    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1203    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
1204    open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
1205
1206    let picker = open_file_picker(&workspace, cx);
1207    picker.update(cx, |finder, _| {
1208        assert_eq!(finder.delegate.matches.len(), 3);
1209        assert_match_selection(finder, 0, "3.txt");
1210        assert_match_at_position(finder, 1, "2.txt");
1211        assert_match_at_position(finder, 2, "1.txt");
1212    });
1213
1214    cx.dispatch_action(SelectNext);
1215    cx.dispatch_action(Confirm); // Open 2.txt
1216
1217    let picker = open_file_picker(&workspace, cx);
1218    picker.update(cx, |finder, _| {
1219        assert_eq!(finder.delegate.matches.len(), 3);
1220        assert_match_selection(finder, 0, "2.txt");
1221        assert_match_at_position(finder, 1, "3.txt");
1222        assert_match_at_position(finder, 2, "1.txt");
1223    });
1224
1225    cx.dispatch_action(SelectNext);
1226    cx.dispatch_action(SelectNext);
1227    cx.dispatch_action(Confirm); // Open 1.txt
1228
1229    let picker = open_file_picker(&workspace, cx);
1230    picker.update(cx, |finder, _| {
1231        assert_eq!(finder.delegate.matches.len(), 3);
1232        assert_match_selection(finder, 0, "1.txt");
1233        assert_match_at_position(finder, 1, "2.txt");
1234        assert_match_at_position(finder, 2, "3.txt");
1235    });
1236}
1237
1238#[gpui::test]
1239async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
1240    let app_state = init_test(cx);
1241
1242    app_state
1243        .fs
1244        .as_fake()
1245        .insert_tree(
1246            "/src",
1247            json!({
1248                "collab_ui": {
1249                    "first.rs": "// First Rust file",
1250                    "second.rs": "// Second Rust file",
1251                    "third.rs": "// Third Rust file",
1252                    "collab_ui.rs": "// Fourth Rust file",
1253                }
1254            }),
1255        )
1256        .await;
1257
1258    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1259    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1260    // generate some history to select from
1261    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1262    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1263    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1264    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1265
1266    let finder = open_file_picker(&workspace, cx);
1267    let query = "collab_ui";
1268    cx.simulate_input(query);
1269    finder.update(cx, |finder, _| {
1270            let delegate = &finder.delegate;
1271            assert!(
1272                delegate.matches.history.is_empty(),
1273                "History items should not math query {query}, they should be matched by name only"
1274            );
1275
1276            let search_entries = delegate
1277                .matches
1278                .search
1279                .iter()
1280                .map(|path_match| path_match.0.path.to_path_buf())
1281                .collect::<Vec<_>>();
1282            assert_eq!(
1283                search_entries,
1284                vec![
1285                    PathBuf::from("collab_ui/collab_ui.rs"),
1286                    PathBuf::from("collab_ui/first.rs"),
1287                    PathBuf::from("collab_ui/third.rs"),
1288                    PathBuf::from("collab_ui/second.rs"),
1289                ],
1290                "Despite all search results having the same directory name, the most matching one should be on top"
1291            );
1292        });
1293}
1294
1295#[gpui::test]
1296async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) {
1297    let app_state = init_test(cx);
1298
1299    app_state
1300        .fs
1301        .as_fake()
1302        .insert_tree(
1303            "/src",
1304            json!({
1305                "test": {
1306                    "first.rs": "// First Rust file",
1307                    "nonexistent.rs": "// Second Rust file",
1308                    "third.rs": "// Third Rust file",
1309                }
1310            }),
1311        )
1312        .await;
1313
1314    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1315    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); // generate some history to select from
1316    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1317    open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await;
1318    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1319    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1320
1321    let picker = open_file_picker(&workspace, cx);
1322    cx.simulate_input("rs");
1323
1324    picker.update(cx, |finder, _| {
1325            let history_entries = finder.delegate
1326                .matches
1327                .history
1328                .iter()
1329                .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").0.path.to_path_buf())
1330                .collect::<Vec<_>>();
1331            assert_eq!(
1332                history_entries,
1333                vec![
1334                    PathBuf::from("test/first.rs"),
1335                    PathBuf::from("test/third.rs"),
1336                ],
1337                "Should have all opened files in the history, except the ones that do not exist on disk"
1338            );
1339        });
1340}
1341
1342#[gpui::test]
1343async fn test_search_results_refreshed_on_worktree_updates(cx: &mut gpui::TestAppContext) {
1344    let app_state = init_test(cx);
1345
1346    app_state
1347        .fs
1348        .as_fake()
1349        .insert_tree(
1350            "/src",
1351            json!({
1352                "lib.rs": "// Lib file",
1353                "main.rs": "// Bar file",
1354                "read.me": "// Readme file",
1355            }),
1356        )
1357        .await;
1358
1359    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1360    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
1361
1362    // Initial state
1363    let picker = open_file_picker(&workspace, cx);
1364    cx.simulate_input("rs");
1365    picker.update(cx, |finder, _| {
1366        assert_eq!(finder.delegate.matches.len(), 2);
1367        assert_match_at_position(finder, 0, "lib.rs");
1368        assert_match_at_position(finder, 1, "main.rs");
1369    });
1370
1371    // Delete main.rs
1372    app_state
1373        .fs
1374        .remove_file("/src/main.rs".as_ref(), Default::default())
1375        .await
1376        .expect("unable to remove file");
1377    cx.executor().advance_clock(FS_WATCH_LATENCY);
1378
1379    // main.rs is in not among search results anymore
1380    picker.update(cx, |finder, _| {
1381        assert_eq!(finder.delegate.matches.len(), 1);
1382        assert_match_at_position(finder, 0, "lib.rs");
1383    });
1384
1385    // Create util.rs
1386    app_state
1387        .fs
1388        .create_file("/src/util.rs".as_ref(), Default::default())
1389        .await
1390        .expect("unable to create file");
1391    cx.executor().advance_clock(FS_WATCH_LATENCY);
1392
1393    // util.rs is among search results
1394    picker.update(cx, |finder, _| {
1395        assert_eq!(finder.delegate.matches.len(), 2);
1396        assert_match_at_position(finder, 0, "lib.rs");
1397        assert_match_at_position(finder, 1, "util.rs");
1398    });
1399}
1400
1401#[gpui::test]
1402async fn test_search_results_refreshed_on_adding_and_removing_worktrees(
1403    cx: &mut gpui::TestAppContext,
1404) {
1405    let app_state = init_test(cx);
1406
1407    app_state
1408        .fs
1409        .as_fake()
1410        .insert_tree(
1411            "/test",
1412            json!({
1413                "project_1": {
1414                    "bar.rs": "// Bar file",
1415                    "lib.rs": "// Lib file",
1416                },
1417                "project_2": {
1418                    "Cargo.toml": "// Cargo file",
1419                    "main.rs": "// Main file",
1420                }
1421            }),
1422        )
1423        .await;
1424
1425    let project = Project::test(app_state.fs.clone(), ["/test/project_1".as_ref()], cx).await;
1426    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
1427    let worktree_1_id = project.update(cx, |project, cx| {
1428        let worktree = project.worktrees().last().expect("worktree not found");
1429        worktree.read(cx).id()
1430    });
1431
1432    // Initial state
1433    let picker = open_file_picker(&workspace, cx);
1434    cx.simulate_input("rs");
1435    picker.update(cx, |finder, _| {
1436        assert_eq!(finder.delegate.matches.len(), 2);
1437        assert_match_at_position(finder, 0, "bar.rs");
1438        assert_match_at_position(finder, 1, "lib.rs");
1439    });
1440
1441    // Add new worktree
1442    project
1443        .update(cx, |project, cx| {
1444            project
1445                .find_or_create_local_worktree("/test/project_2", true, cx)
1446                .into_future()
1447        })
1448        .await
1449        .expect("unable to create workdir");
1450    cx.executor().advance_clock(FS_WATCH_LATENCY);
1451
1452    // main.rs is among search results
1453    picker.update(cx, |finder, _| {
1454        assert_eq!(finder.delegate.matches.len(), 3);
1455        assert_match_at_position(finder, 0, "bar.rs");
1456        assert_match_at_position(finder, 1, "lib.rs");
1457        assert_match_at_position(finder, 2, "main.rs");
1458    });
1459
1460    // Remove the first worktree
1461    project.update(cx, |project, cx| {
1462        project.remove_worktree(worktree_1_id, cx);
1463    });
1464    cx.executor().advance_clock(FS_WATCH_LATENCY);
1465
1466    // Files from the first worktree are not in the search results anymore
1467    picker.update(cx, |finder, _| {
1468        assert_eq!(finder.delegate.matches.len(), 1);
1469        assert_match_at_position(finder, 0, "main.rs");
1470    });
1471}
1472
1473#[gpui::test]
1474async fn test_keeps_file_finder_open_after_modifier_keys_release(cx: &mut gpui::TestAppContext) {
1475    let app_state = init_test(cx);
1476
1477    app_state
1478        .fs
1479        .as_fake()
1480        .insert_tree(
1481            "/test",
1482            json!({
1483                "1.txt": "// One",
1484            }),
1485        )
1486        .await;
1487
1488    let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
1489    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1490
1491    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1492
1493    cx.simulate_modifiers_change(Modifiers::secondary_key());
1494    open_file_picker(&workspace, cx);
1495
1496    cx.simulate_modifiers_change(Modifiers::none());
1497    active_file_picker(&workspace, cx);
1498}
1499
1500#[gpui::test]
1501async fn test_opens_file_on_modifier_keys_release(cx: &mut gpui::TestAppContext) {
1502    let app_state = init_test(cx);
1503
1504    app_state
1505        .fs
1506        .as_fake()
1507        .insert_tree(
1508            "/test",
1509            json!({
1510                "1.txt": "// One",
1511                "2.txt": "// Two",
1512            }),
1513        )
1514        .await;
1515
1516    let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
1517    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1518
1519    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1520    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
1521
1522    cx.simulate_modifiers_change(Modifiers::secondary_key());
1523    let picker = open_file_picker(&workspace, cx);
1524    picker.update(cx, |finder, _| {
1525        assert_eq!(finder.delegate.matches.len(), 2);
1526        assert_match_selection(finder, 0, "2.txt");
1527        assert_match_at_position(finder, 1, "1.txt");
1528    });
1529
1530    cx.dispatch_action(SelectNext);
1531    cx.simulate_modifiers_change(Modifiers::none());
1532    cx.read(|cx| {
1533        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1534        assert_eq!(active_editor.read(cx).title(cx), "1.txt");
1535    });
1536}
1537
1538#[gpui::test]
1539async fn test_switches_between_release_norelease_modes_on_forward_nav(
1540    cx: &mut gpui::TestAppContext,
1541) {
1542    let app_state = init_test(cx);
1543
1544    app_state
1545        .fs
1546        .as_fake()
1547        .insert_tree(
1548            "/test",
1549            json!({
1550                "1.txt": "// One",
1551                "2.txt": "// Two",
1552            }),
1553        )
1554        .await;
1555
1556    let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
1557    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1558
1559    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1560    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
1561
1562    // Open with a shortcut
1563    cx.simulate_modifiers_change(Modifiers::secondary_key());
1564    let picker = open_file_picker(&workspace, cx);
1565    picker.update(cx, |finder, _| {
1566        assert_eq!(finder.delegate.matches.len(), 2);
1567        assert_match_selection(finder, 0, "2.txt");
1568        assert_match_at_position(finder, 1, "1.txt");
1569    });
1570
1571    // Switch to navigating with other shortcuts
1572    // Don't open file on modifiers release
1573    cx.simulate_modifiers_change(Modifiers::control());
1574    cx.dispatch_action(SelectNext);
1575    cx.simulate_modifiers_change(Modifiers::none());
1576    picker.update(cx, |finder, _| {
1577        assert_eq!(finder.delegate.matches.len(), 2);
1578        assert_match_at_position(finder, 0, "2.txt");
1579        assert_match_selection(finder, 1, "1.txt");
1580    });
1581
1582    // Back to navigation with initial shortcut
1583    // Open file on modifiers release
1584    cx.simulate_modifiers_change(Modifiers::secondary_key());
1585    cx.dispatch_action(Toggle);
1586    cx.simulate_modifiers_change(Modifiers::none());
1587    cx.read(|cx| {
1588        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1589        assert_eq!(active_editor.read(cx).title(cx), "2.txt");
1590    });
1591}
1592
1593#[gpui::test]
1594async fn test_switches_between_release_norelease_modes_on_backward_nav(
1595    cx: &mut gpui::TestAppContext,
1596) {
1597    let app_state = init_test(cx);
1598
1599    app_state
1600        .fs
1601        .as_fake()
1602        .insert_tree(
1603            "/test",
1604            json!({
1605                "1.txt": "// One",
1606                "2.txt": "// Two",
1607                "3.txt": "// Three"
1608            }),
1609        )
1610        .await;
1611
1612    let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
1613    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1614
1615    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1616    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
1617    open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
1618
1619    // Open with a shortcut
1620    cx.simulate_modifiers_change(Modifiers::secondary_key());
1621    let picker = open_file_picker(&workspace, cx);
1622    picker.update(cx, |finder, _| {
1623        assert_eq!(finder.delegate.matches.len(), 3);
1624        assert_match_selection(finder, 0, "3.txt");
1625        assert_match_at_position(finder, 1, "2.txt");
1626        assert_match_at_position(finder, 2, "1.txt");
1627    });
1628
1629    // Switch to navigating with other shortcuts
1630    // Don't open file on modifiers release
1631    cx.simulate_modifiers_change(Modifiers::control());
1632    cx.dispatch_action(menu::SelectPrev);
1633    cx.simulate_modifiers_change(Modifiers::none());
1634    picker.update(cx, |finder, _| {
1635        assert_eq!(finder.delegate.matches.len(), 3);
1636        assert_match_at_position(finder, 0, "3.txt");
1637        assert_match_at_position(finder, 1, "2.txt");
1638        assert_match_selection(finder, 2, "1.txt");
1639    });
1640
1641    // Back to navigation with initial shortcut
1642    // Open file on modifiers release
1643    cx.simulate_modifiers_change(Modifiers::secondary_key());
1644    cx.dispatch_action(SelectPrev); // <-- File Finder's SelectPrev, not menu's
1645    cx.simulate_modifiers_change(Modifiers::none());
1646    cx.read(|cx| {
1647        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1648        assert_eq!(active_editor.read(cx).title(cx), "3.txt");
1649    });
1650}
1651
1652#[gpui::test]
1653async fn test_extending_modifiers_does_not_confirm_selection(cx: &mut gpui::TestAppContext) {
1654    let app_state = init_test(cx);
1655
1656    app_state
1657        .fs
1658        .as_fake()
1659        .insert_tree(
1660            "/test",
1661            json!({
1662                "1.txt": "// One",
1663            }),
1664        )
1665        .await;
1666
1667    let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
1668    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1669
1670    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1671
1672    cx.simulate_modifiers_change(Modifiers::secondary_key());
1673    open_file_picker(&workspace, cx);
1674
1675    cx.simulate_modifiers_change(Modifiers::command_shift());
1676    active_file_picker(&workspace, cx);
1677}
1678
1679async fn open_close_queried_buffer(
1680    input: &str,
1681    expected_matches: usize,
1682    expected_editor_title: &str,
1683    workspace: &View<Workspace>,
1684    cx: &mut gpui::VisualTestContext,
1685) -> Vec<FoundPath> {
1686    let history_items = open_queried_buffer(
1687        input,
1688        expected_matches,
1689        expected_editor_title,
1690        workspace,
1691        cx,
1692    )
1693    .await;
1694
1695    cx.dispatch_action(workspace::CloseActiveItem { save_intent: None });
1696
1697    history_items
1698}
1699
1700async fn open_queried_buffer(
1701    input: &str,
1702    expected_matches: usize,
1703    expected_editor_title: &str,
1704    workspace: &View<Workspace>,
1705    cx: &mut gpui::VisualTestContext,
1706) -> Vec<FoundPath> {
1707    let picker = open_file_picker(&workspace, cx);
1708    cx.simulate_input(input);
1709
1710    let history_items = picker.update(cx, |finder, _| {
1711        assert_eq!(
1712            finder.delegate.matches.len(),
1713            expected_matches,
1714            "Unexpected number of matches found for query `{input}`, matches: {:?}",
1715            finder.delegate.matches
1716        );
1717        finder.delegate.history_items.clone()
1718    });
1719
1720    cx.dispatch_action(Confirm);
1721
1722    cx.read(|cx| {
1723        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1724        let active_editor_title = active_editor.read(cx).title(cx);
1725        assert_eq!(
1726            expected_editor_title, active_editor_title,
1727            "Unexpected editor title for query `{input}`"
1728        );
1729    });
1730
1731    history_items
1732}
1733
1734fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
1735    cx.update(|cx| {
1736        let state = AppState::test(cx);
1737        theme::init(theme::LoadThemes::JustBase, cx);
1738        language::init(cx);
1739        super::init(cx);
1740        editor::init(cx);
1741        workspace::init_settings(cx);
1742        Project::init_settings(cx);
1743        state
1744    })
1745}
1746
1747fn test_path_like(test_str: &str) -> PathLikeWithPosition<FileSearchQuery> {
1748    PathLikeWithPosition::parse_str(test_str, |path_like_str| {
1749        Ok::<_, std::convert::Infallible>(FileSearchQuery {
1750            raw_query: test_str.to_owned(),
1751            file_query_end: if path_like_str == test_str {
1752                None
1753            } else {
1754                Some(path_like_str.len())
1755            },
1756        })
1757    })
1758    .unwrap()
1759}
1760
1761fn build_find_picker(
1762    project: Model<Project>,
1763    cx: &mut TestAppContext,
1764) -> (
1765    View<Picker<FileFinderDelegate>>,
1766    View<Workspace>,
1767    &mut VisualTestContext,
1768) {
1769    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1770    let picker = open_file_picker(&workspace, cx);
1771    (picker, workspace, cx)
1772}
1773
1774#[track_caller]
1775fn open_file_picker(
1776    workspace: &View<Workspace>,
1777    cx: &mut VisualTestContext,
1778) -> View<Picker<FileFinderDelegate>> {
1779    cx.dispatch_action(Toggle);
1780    active_file_picker(workspace, cx)
1781}
1782
1783#[track_caller]
1784fn active_file_picker(
1785    workspace: &View<Workspace>,
1786    cx: &mut VisualTestContext,
1787) -> View<Picker<FileFinderDelegate>> {
1788    workspace.update(cx, |workspace, cx| {
1789        workspace
1790            .active_modal::<FileFinder>(cx)
1791            .expect("file finder is not open")
1792            .read(cx)
1793            .picker
1794            .clone()
1795    })
1796}
1797
1798#[derive(Debug)]
1799struct SearchEntries {
1800    history: Vec<PathBuf>,
1801    search: Vec<PathBuf>,
1802}
1803
1804impl SearchEntries {
1805    #[track_caller]
1806    fn search_only(self) -> Vec<PathBuf> {
1807        assert!(
1808            self.history.is_empty(),
1809            "Should have no history matches, but got: {:?}",
1810            self.history
1811        );
1812        self.search
1813    }
1814}
1815
1816fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries {
1817    let matches = &picker.delegate.matches;
1818    SearchEntries {
1819        history: matches
1820            .history
1821            .iter()
1822            .map(|(history_path, path_match)| {
1823                path_match
1824                    .as_ref()
1825                    .map(|path_match| {
1826                        Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path)
1827                    })
1828                    .unwrap_or_else(|| {
1829                        history_path
1830                            .absolute
1831                            .as_deref()
1832                            .unwrap_or_else(|| &history_path.project.path)
1833                            .to_path_buf()
1834                    })
1835            })
1836            .collect(),
1837        search: matches
1838            .search
1839            .iter()
1840            .map(|path_match| Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path))
1841            .collect(),
1842    }
1843}
1844
1845#[track_caller]
1846fn assert_match_selection(
1847    finder: &Picker<FileFinderDelegate>,
1848    expected_selection_index: usize,
1849    expected_file_name: &str,
1850) {
1851    assert_eq!(
1852        finder.delegate.selected_index(),
1853        expected_selection_index,
1854        "Match is not selected"
1855    );
1856    assert_match_at_position(finder, expected_selection_index, expected_file_name);
1857}
1858
1859#[track_caller]
1860fn assert_match_at_position(
1861    finder: &Picker<FileFinderDelegate>,
1862    match_index: usize,
1863    expected_file_name: &str,
1864) {
1865    let match_item = finder
1866        .delegate
1867        .matches
1868        .get(match_index)
1869        .unwrap_or_else(|| panic!("Finder has no match for index {match_index}"));
1870    let match_file_name = match match_item {
1871        Match::History(found_path, _) => found_path.absolute.as_deref().unwrap().file_name(),
1872        Match::Search(path_match) => path_match.0.path.file_name(),
1873    }
1874    .unwrap()
1875    .to_string_lossy();
1876    assert_eq!(match_file_name, expected_file_name);
1877}