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};
   7use project::worktree::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    cx.executor().run_until_parked();
 876    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
 877    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
 878    let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
 879
 880    for expected_selected_index in 0..current_history.len() {
 881        cx.dispatch_action(Toggle);
 882        let picker = active_file_picker(&workspace, cx);
 883        let selected_index = picker.update(cx, |picker, _| picker.delegate.selected_index());
 884        assert_eq!(
 885            selected_index, expected_selected_index,
 886            "Should select the next item in the history"
 887        );
 888    }
 889
 890    cx.dispatch_action(Toggle);
 891    let selected_index = workspace.update(cx, |workspace, cx| {
 892        workspace
 893            .active_modal::<FileFinder>(cx)
 894            .unwrap()
 895            .read(cx)
 896            .picker
 897            .read(cx)
 898            .delegate
 899            .selected_index()
 900    });
 901    assert_eq!(
 902        selected_index, 0,
 903        "Should wrap around the history and start all over"
 904    );
 905}
 906
 907#[gpui::test]
 908async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
 909    let app_state = init_test(cx);
 910
 911    app_state
 912        .fs
 913        .as_fake()
 914        .insert_tree(
 915            "/src",
 916            json!({
 917                "test": {
 918                    "first.rs": "// First Rust file",
 919                    "second.rs": "// Second Rust file",
 920                    "third.rs": "// Third Rust file",
 921                    "fourth.rs": "// Fourth Rust file",
 922                }
 923            }),
 924        )
 925        .await;
 926
 927    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
 928    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
 929    let worktree_id = cx.read(|cx| {
 930        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
 931        assert_eq!(worktrees.len(), 1,);
 932
 933        WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
 934    });
 935
 936    // generate some history to select from
 937    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
 938    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
 939    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
 940    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
 941
 942    let finder = open_file_picker(&workspace, cx);
 943    let first_query = "f";
 944    finder
 945        .update(cx, |finder, cx| {
 946            finder.delegate.update_matches(first_query.to_string(), cx)
 947        })
 948        .await;
 949    finder.update(cx, |finder, _| {
 950            let delegate = &finder.delegate;
 951            assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out");
 952            let history_match = delegate.matches.history.first().unwrap();
 953            assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
 954            assert_eq!(history_match.0, FoundPath::new(
 955                ProjectPath {
 956                    worktree_id,
 957                    path: Arc::from(Path::new("test/first.rs")),
 958                },
 959                Some(PathBuf::from("/src/test/first.rs"))
 960            ));
 961            assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
 962            assert_eq!(delegate.matches.search.first().unwrap().0.path.as_ref(), Path::new("test/fourth.rs"));
 963        });
 964
 965    let second_query = "fsdasdsa";
 966    let finder = active_file_picker(&workspace, cx);
 967    finder
 968        .update(cx, |finder, cx| {
 969            finder.delegate.update_matches(second_query.to_string(), cx)
 970        })
 971        .await;
 972    finder.update(cx, |finder, _| {
 973        let delegate = &finder.delegate;
 974        assert!(
 975            delegate.matches.history.is_empty(),
 976            "No history entries should match {second_query}"
 977        );
 978        assert!(
 979            delegate.matches.search.is_empty(),
 980            "No search entries should match {second_query}"
 981        );
 982    });
 983
 984    let first_query_again = first_query;
 985
 986    let finder = active_file_picker(&workspace, cx);
 987    finder
 988        .update(cx, |finder, cx| {
 989            finder
 990                .delegate
 991                .update_matches(first_query_again.to_string(), cx)
 992        })
 993        .await;
 994    finder.update(cx, |finder, _| {
 995            let delegate = &finder.delegate;
 996            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");
 997            let history_match = delegate.matches.history.first().unwrap();
 998            assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
 999            assert_eq!(history_match.0, FoundPath::new(
1000                ProjectPath {
1001                    worktree_id,
1002                    path: Arc::from(Path::new("test/first.rs")),
1003                },
1004                Some(PathBuf::from("/src/test/first.rs"))
1005            ));
1006            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");
1007            assert_eq!(delegate.matches.search.first().unwrap().0.path.as_ref(), Path::new("test/fourth.rs"));
1008        });
1009}
1010
1011#[gpui::test]
1012async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) {
1013    let app_state = init_test(cx);
1014
1015    app_state
1016        .fs
1017        .as_fake()
1018        .insert_tree(
1019            "/root",
1020            json!({
1021                "test": {
1022                    "1_qw": "// First file that matches the query",
1023                    "2_second": "// Second file",
1024                    "3_third": "// Third file",
1025                    "4_fourth": "// Fourth file",
1026                    "5_qwqwqw": "// A file with 3 more matches than the first one",
1027                    "6_qwqwqw": "// Same query matches as above, but closer to the end of the list due to the name",
1028                    "7_qwqwqw": "// One more, same amount of query matches as above",
1029                }
1030            }),
1031        )
1032        .await;
1033
1034    let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1035    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1036    // generate some history to select from
1037    open_close_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
1038    open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
1039    open_close_queried_buffer("3", 1, "3_third", &workspace, cx).await;
1040    open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
1041    open_close_queried_buffer("6", 1, "6_qwqwqw", &workspace, cx).await;
1042
1043    let finder = open_file_picker(&workspace, cx);
1044    let query = "qw";
1045    finder
1046        .update(cx, |finder, cx| {
1047            finder.delegate.update_matches(query.to_string(), cx)
1048        })
1049        .await;
1050    finder.update(cx, |finder, _| {
1051        let search_matches = collect_search_matches(finder);
1052        assert_eq!(
1053            search_matches.history,
1054            vec![PathBuf::from("test/1_qw"), PathBuf::from("test/6_qwqwqw"),],
1055        );
1056        assert_eq!(
1057            search_matches.search,
1058            vec![
1059                PathBuf::from("test/5_qwqwqw"),
1060                PathBuf::from("test/7_qwqwqw"),
1061            ],
1062        );
1063    });
1064}
1065
1066#[gpui::test]
1067async fn test_select_current_open_file_when_no_history(cx: &mut gpui::TestAppContext) {
1068    let app_state = init_test(cx);
1069
1070    app_state
1071        .fs
1072        .as_fake()
1073        .insert_tree(
1074            "/root",
1075            json!({
1076                "test": {
1077                    "1_qw": "",
1078                }
1079            }),
1080        )
1081        .await;
1082
1083    let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1084    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1085    // Open new buffer
1086    open_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
1087
1088    let picker = open_file_picker(&workspace, cx);
1089    picker.update(cx, |finder, _| {
1090        assert_match_selection(&finder, 0, "1_qw");
1091    });
1092}
1093
1094#[gpui::test]
1095async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
1096    cx: &mut TestAppContext,
1097) {
1098    let app_state = init_test(cx);
1099
1100    app_state
1101        .fs
1102        .as_fake()
1103        .insert_tree(
1104            "/src",
1105            json!({
1106                "test": {
1107                    "bar.rs": "// Bar file",
1108                    "lib.rs": "// Lib file",
1109                    "maaa.rs": "// Maaaaaaa",
1110                    "main.rs": "// Main file",
1111                    "moo.rs": "// Moooooo",
1112                }
1113            }),
1114        )
1115        .await;
1116
1117    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1118    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1119
1120    open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
1121    open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
1122    open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
1123
1124    // main.rs is on top, previously used is selected
1125    let picker = open_file_picker(&workspace, cx);
1126    picker.update(cx, |finder, _| {
1127        assert_eq!(finder.delegate.matches.len(), 3);
1128        assert_match_at_position(finder, 0, "main.rs");
1129        assert_match_selection(finder, 1, "lib.rs");
1130        assert_match_at_position(finder, 2, "bar.rs");
1131    });
1132
1133    // all files match, main.rs is still on top
1134    picker
1135        .update(cx, |finder, cx| {
1136            finder.delegate.update_matches(".rs".to_string(), cx)
1137        })
1138        .await;
1139    picker.update(cx, |finder, _| {
1140        assert_eq!(finder.delegate.matches.len(), 5);
1141        assert_match_at_position(finder, 0, "main.rs");
1142        assert_match_selection(finder, 1, "bar.rs");
1143    });
1144
1145    // main.rs is not among matches, select top item
1146    picker
1147        .update(cx, |finder, cx| {
1148            finder.delegate.update_matches("b".to_string(), cx)
1149        })
1150        .await;
1151    picker.update(cx, |finder, _| {
1152        assert_eq!(finder.delegate.matches.len(), 2);
1153        assert_match_at_position(finder, 0, "bar.rs");
1154    });
1155
1156    // main.rs is back, put it on top and select next item
1157    picker
1158        .update(cx, |finder, cx| {
1159            finder.delegate.update_matches("m".to_string(), cx)
1160        })
1161        .await;
1162    picker.update(cx, |finder, _| {
1163        assert_eq!(finder.delegate.matches.len(), 3);
1164        assert_match_at_position(finder, 0, "main.rs");
1165        assert_match_selection(finder, 1, "moo.rs");
1166    });
1167
1168    // get back to the initial state
1169    picker
1170        .update(cx, |finder, cx| {
1171            finder.delegate.update_matches("".to_string(), cx)
1172        })
1173        .await;
1174    picker.update(cx, |finder, _| {
1175        assert_eq!(finder.delegate.matches.len(), 3);
1176        assert_match_at_position(finder, 0, "main.rs");
1177        assert_match_selection(finder, 1, "lib.rs");
1178    });
1179}
1180
1181#[gpui::test]
1182async fn test_history_items_shown_in_order_of_open(cx: &mut TestAppContext) {
1183    let app_state = init_test(cx);
1184
1185    app_state
1186        .fs
1187        .as_fake()
1188        .insert_tree(
1189            "/test",
1190            json!({
1191                "test": {
1192                    "1.txt": "// One",
1193                    "2.txt": "// Two",
1194                    "3.txt": "// Three",
1195                }
1196            }),
1197        )
1198        .await;
1199
1200    let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
1201    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1202
1203    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1204    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
1205    open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
1206
1207    let picker = open_file_picker(&workspace, cx);
1208    picker.update(cx, |finder, _| {
1209        assert_eq!(finder.delegate.matches.len(), 3);
1210        assert_match_at_position(finder, 0, "3.txt");
1211        assert_match_selection(finder, 1, "2.txt");
1212        assert_match_at_position(finder, 2, "1.txt");
1213    });
1214
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_at_position(finder, 0, "2.txt");
1221        assert_match_selection(finder, 1, "3.txt");
1222        assert_match_at_position(finder, 2, "1.txt");
1223    });
1224
1225    cx.dispatch_action(SelectNext);
1226    cx.dispatch_action(Confirm); // Open 1.txt
1227
1228    let picker = open_file_picker(&workspace, cx);
1229    picker.update(cx, |finder, _| {
1230        assert_eq!(finder.delegate.matches.len(), 3);
1231        assert_match_at_position(finder, 0, "1.txt");
1232        assert_match_selection(finder, 1, "2.txt");
1233        assert_match_at_position(finder, 2, "3.txt");
1234    });
1235}
1236
1237#[gpui::test]
1238async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
1239    let app_state = init_test(cx);
1240
1241    app_state
1242        .fs
1243        .as_fake()
1244        .insert_tree(
1245            "/src",
1246            json!({
1247                "collab_ui": {
1248                    "first.rs": "// First Rust file",
1249                    "second.rs": "// Second Rust file",
1250                    "third.rs": "// Third Rust file",
1251                    "collab_ui.rs": "// Fourth Rust file",
1252                }
1253            }),
1254        )
1255        .await;
1256
1257    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1258    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1259    // generate some history to select from
1260    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1261    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1262    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1263    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1264
1265    let finder = open_file_picker(&workspace, cx);
1266    let query = "collab_ui";
1267    cx.simulate_input(query);
1268    finder.update(cx, |finder, _| {
1269            let delegate = &finder.delegate;
1270            assert!(
1271                delegate.matches.history.is_empty(),
1272                "History items should not math query {query}, they should be matched by name only"
1273            );
1274
1275            let search_entries = delegate
1276                .matches
1277                .search
1278                .iter()
1279                .map(|path_match| path_match.0.path.to_path_buf())
1280                .collect::<Vec<_>>();
1281            assert_eq!(
1282                search_entries,
1283                vec![
1284                    PathBuf::from("collab_ui/collab_ui.rs"),
1285                    PathBuf::from("collab_ui/first.rs"),
1286                    PathBuf::from("collab_ui/third.rs"),
1287                    PathBuf::from("collab_ui/second.rs"),
1288                ],
1289                "Despite all search results having the same directory name, the most matching one should be on top"
1290            );
1291        });
1292}
1293
1294#[gpui::test]
1295async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) {
1296    let app_state = init_test(cx);
1297
1298    app_state
1299        .fs
1300        .as_fake()
1301        .insert_tree(
1302            "/src",
1303            json!({
1304                "test": {
1305                    "first.rs": "// First Rust file",
1306                    "nonexistent.rs": "// Second Rust file",
1307                    "third.rs": "// Third Rust file",
1308                }
1309            }),
1310        )
1311        .await;
1312
1313    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1314    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); // generate some history to select from
1315    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1316    open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await;
1317    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1318    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1319
1320    let picker = open_file_picker(&workspace, cx);
1321    cx.simulate_input("rs");
1322
1323    picker.update(cx, |finder, _| {
1324            let history_entries = finder.delegate
1325                .matches
1326                .history
1327                .iter()
1328                .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").0.path.to_path_buf())
1329                .collect::<Vec<_>>();
1330            assert_eq!(
1331                history_entries,
1332                vec![
1333                    PathBuf::from("test/first.rs"),
1334                    PathBuf::from("test/third.rs"),
1335                ],
1336                "Should have all opened files in the history, except the ones that do not exist on disk"
1337            );
1338        });
1339}
1340
1341#[gpui::test]
1342async fn test_search_results_refreshed_on_worktree_updates(cx: &mut gpui::TestAppContext) {
1343    let app_state = init_test(cx);
1344
1345    app_state
1346        .fs
1347        .as_fake()
1348        .insert_tree(
1349            "/src",
1350            json!({
1351                "lib.rs": "// Lib file",
1352                "main.rs": "// Bar file",
1353                "read.me": "// Readme file",
1354            }),
1355        )
1356        .await;
1357
1358    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1359    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
1360
1361    // Initial state
1362    let picker = open_file_picker(&workspace, cx);
1363    cx.simulate_input("rs");
1364    picker.update(cx, |finder, _| {
1365        assert_eq!(finder.delegate.matches.len(), 2);
1366        assert_match_at_position(finder, 0, "lib.rs");
1367        assert_match_at_position(finder, 1, "main.rs");
1368    });
1369
1370    // Delete main.rs
1371    app_state
1372        .fs
1373        .remove_file("/src/main.rs".as_ref(), Default::default())
1374        .await
1375        .expect("unable to remove file");
1376    cx.executor().advance_clock(FS_WATCH_LATENCY);
1377
1378    // main.rs is in not among search results anymore
1379    picker.update(cx, |finder, _| {
1380        assert_eq!(finder.delegate.matches.len(), 1);
1381        assert_match_at_position(finder, 0, "lib.rs");
1382    });
1383
1384    // Create util.rs
1385    app_state
1386        .fs
1387        .create_file("/src/util.rs".as_ref(), Default::default())
1388        .await
1389        .expect("unable to create file");
1390    cx.executor().advance_clock(FS_WATCH_LATENCY);
1391
1392    // util.rs is among search results
1393    picker.update(cx, |finder, _| {
1394        assert_eq!(finder.delegate.matches.len(), 2);
1395        assert_match_at_position(finder, 0, "lib.rs");
1396        assert_match_at_position(finder, 1, "util.rs");
1397    });
1398}
1399
1400#[gpui::test]
1401async fn test_search_results_refreshed_on_adding_and_removing_worktrees(
1402    cx: &mut gpui::TestAppContext,
1403) {
1404    let app_state = init_test(cx);
1405
1406    app_state
1407        .fs
1408        .as_fake()
1409        .insert_tree(
1410            "/test",
1411            json!({
1412                "project_1": {
1413                    "bar.rs": "// Bar file",
1414                    "lib.rs": "// Lib file",
1415                },
1416                "project_2": {
1417                    "Cargo.toml": "// Cargo file",
1418                    "main.rs": "// Main file",
1419                }
1420            }),
1421        )
1422        .await;
1423
1424    let project = Project::test(app_state.fs.clone(), ["/test/project_1".as_ref()], cx).await;
1425    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
1426    let worktree_1_id = project.update(cx, |project, cx| {
1427        let worktree = project.worktrees().last().expect("worktree not found");
1428        worktree.read(cx).id()
1429    });
1430
1431    // Initial state
1432    let picker = open_file_picker(&workspace, cx);
1433    cx.simulate_input("rs");
1434    picker.update(cx, |finder, _| {
1435        assert_eq!(finder.delegate.matches.len(), 2);
1436        assert_match_at_position(finder, 0, "bar.rs");
1437        assert_match_at_position(finder, 1, "lib.rs");
1438    });
1439
1440    // Add new worktree
1441    project
1442        .update(cx, |project, cx| {
1443            project
1444                .find_or_create_local_worktree("/test/project_2", true, cx)
1445                .into_future()
1446        })
1447        .await
1448        .expect("unable to create workdir");
1449    cx.executor().advance_clock(FS_WATCH_LATENCY);
1450
1451    // main.rs is among search results
1452    picker.update(cx, |finder, _| {
1453        assert_eq!(finder.delegate.matches.len(), 3);
1454        assert_match_at_position(finder, 0, "bar.rs");
1455        assert_match_at_position(finder, 1, "lib.rs");
1456        assert_match_at_position(finder, 2, "main.rs");
1457    });
1458
1459    // Remove the first worktree
1460    project.update(cx, |project, cx| {
1461        project.remove_worktree(worktree_1_id, cx);
1462    });
1463    cx.executor().advance_clock(FS_WATCH_LATENCY);
1464
1465    // Files from the first worktree are not in the search results anymore
1466    picker.update(cx, |finder, _| {
1467        assert_eq!(finder.delegate.matches.len(), 1);
1468        assert_match_at_position(finder, 0, "main.rs");
1469    });
1470}
1471
1472async fn open_close_queried_buffer(
1473    input: &str,
1474    expected_matches: usize,
1475    expected_editor_title: &str,
1476    workspace: &View<Workspace>,
1477    cx: &mut gpui::VisualTestContext,
1478) -> Vec<FoundPath> {
1479    let history_items = open_queried_buffer(
1480        input,
1481        expected_matches,
1482        expected_editor_title,
1483        workspace,
1484        cx,
1485    )
1486    .await;
1487
1488    cx.dispatch_action(workspace::CloseActiveItem { save_intent: None });
1489
1490    history_items
1491}
1492
1493async fn open_queried_buffer(
1494    input: &str,
1495    expected_matches: usize,
1496    expected_editor_title: &str,
1497    workspace: &View<Workspace>,
1498    cx: &mut gpui::VisualTestContext,
1499) -> Vec<FoundPath> {
1500    let picker = open_file_picker(&workspace, cx);
1501    cx.simulate_input(input);
1502
1503    let history_items = picker.update(cx, |finder, _| {
1504        assert_eq!(
1505            finder.delegate.matches.len(),
1506            expected_matches,
1507            "Unexpected number of matches found for query `{input}`, matches: {:?}",
1508            finder.delegate.matches
1509        );
1510        finder.delegate.history_items.clone()
1511    });
1512
1513    cx.dispatch_action(Confirm);
1514
1515    cx.read(|cx| {
1516        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1517        let active_editor_title = active_editor.read(cx).title(cx);
1518        assert_eq!(
1519            expected_editor_title, active_editor_title,
1520            "Unexpected editor title for query `{input}`"
1521        );
1522    });
1523
1524    history_items
1525}
1526
1527fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
1528    cx.update(|cx| {
1529        let state = AppState::test(cx);
1530        theme::init(theme::LoadThemes::JustBase, cx);
1531        language::init(cx);
1532        super::init(cx);
1533        editor::init(cx);
1534        workspace::init_settings(cx);
1535        Project::init_settings(cx);
1536        state
1537    })
1538}
1539
1540fn test_path_like(test_str: &str) -> PathLikeWithPosition<FileSearchQuery> {
1541    PathLikeWithPosition::parse_str(test_str, |path_like_str| {
1542        Ok::<_, std::convert::Infallible>(FileSearchQuery {
1543            raw_query: test_str.to_owned(),
1544            file_query_end: if path_like_str == test_str {
1545                None
1546            } else {
1547                Some(path_like_str.len())
1548            },
1549        })
1550    })
1551    .unwrap()
1552}
1553
1554fn build_find_picker(
1555    project: Model<Project>,
1556    cx: &mut TestAppContext,
1557) -> (
1558    View<Picker<FileFinderDelegate>>,
1559    View<Workspace>,
1560    &mut VisualTestContext,
1561) {
1562    let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1563    let picker = open_file_picker(&workspace, cx);
1564    (picker, workspace, cx)
1565}
1566
1567#[track_caller]
1568fn open_file_picker(
1569    workspace: &View<Workspace>,
1570    cx: &mut VisualTestContext,
1571) -> View<Picker<FileFinderDelegate>> {
1572    cx.dispatch_action(Toggle);
1573    active_file_picker(workspace, cx)
1574}
1575
1576#[track_caller]
1577fn active_file_picker(
1578    workspace: &View<Workspace>,
1579    cx: &mut VisualTestContext,
1580) -> View<Picker<FileFinderDelegate>> {
1581    workspace.update(cx, |workspace, cx| {
1582        workspace
1583            .active_modal::<FileFinder>(cx)
1584            .unwrap()
1585            .read(cx)
1586            .picker
1587            .clone()
1588    })
1589}
1590
1591#[derive(Debug)]
1592struct SearchEntries {
1593    history: Vec<PathBuf>,
1594    search: Vec<PathBuf>,
1595}
1596
1597impl SearchEntries {
1598    #[track_caller]
1599    fn search_only(self) -> Vec<PathBuf> {
1600        assert!(
1601            self.history.is_empty(),
1602            "Should have no history matches, but got: {:?}",
1603            self.history
1604        );
1605        self.search
1606    }
1607}
1608
1609fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries {
1610    let matches = &picker.delegate.matches;
1611    SearchEntries {
1612        history: matches
1613            .history
1614            .iter()
1615            .map(|(history_path, path_match)| {
1616                path_match
1617                    .as_ref()
1618                    .map(|path_match| {
1619                        Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path)
1620                    })
1621                    .unwrap_or_else(|| {
1622                        history_path
1623                            .absolute
1624                            .as_deref()
1625                            .unwrap_or_else(|| &history_path.project.path)
1626                            .to_path_buf()
1627                    })
1628            })
1629            .collect(),
1630        search: matches
1631            .search
1632            .iter()
1633            .map(|path_match| Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path))
1634            .collect(),
1635    }
1636}
1637
1638#[track_caller]
1639fn assert_match_selection(
1640    finder: &Picker<FileFinderDelegate>,
1641    expected_selection_index: usize,
1642    expected_file_name: &str,
1643) {
1644    assert_eq!(
1645        finder.delegate.selected_index(),
1646        expected_selection_index,
1647        "Match is not selected"
1648    );
1649    assert_match_at_position(finder, expected_selection_index, expected_file_name);
1650}
1651
1652#[track_caller]
1653fn assert_match_at_position(
1654    finder: &Picker<FileFinderDelegate>,
1655    match_index: usize,
1656    expected_file_name: &str,
1657) {
1658    let match_item = finder
1659        .delegate
1660        .matches
1661        .get(match_index)
1662        .unwrap_or_else(|| panic!("Finder has no match for index {match_index}"));
1663    let match_file_name = match match_item {
1664        Match::History(found_path, _) => found_path.absolute.as_deref().unwrap().file_name(),
1665        Match::Search(path_match) => path_match.0.path.file_name(),
1666    }
1667    .unwrap()
1668    .to_string_lossy();
1669    assert_eq!(match_file_name, expected_file_name);
1670}