project_tests.rs

   1#![allow(clippy::format_collect)]
   2
   3use crate::{
   4    Event, git_store::StatusEntry, task_inventory::TaskContexts, task_store::TaskSettingsLocation,
   5    *,
   6};
   7use buffer_diff::{
   8    BufferDiffEvent, CALCULATE_DIFF_TASK, DiffHunkSecondaryStatus, DiffHunkStatus,
   9    DiffHunkStatusKind, assert_hunks,
  10};
  11use fs::FakeFs;
  12use futures::{StreamExt, future};
  13use git::{
  14    GitHostingProviderRegistry,
  15    repository::RepoPath,
  16    status::{StatusCode, TrackedStatus},
  17};
  18use git2::RepositoryInitOptions;
  19use gpui::{App, BackgroundExecutor, SemanticVersion, UpdateGlobal};
  20use http_client::Url;
  21use language::{
  22    Diagnostic, DiagnosticEntry, DiagnosticSet, DiskState, FakeLspAdapter, LanguageConfig,
  23    LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint,
  24    language_settings::{AllLanguageSettings, LanguageSettingsContent, language_settings},
  25    tree_sitter_rust, tree_sitter_typescript,
  26};
  27use lsp::{
  28    DiagnosticSeverity, DocumentChanges, FileOperationFilter, NumberOrString, TextDocumentEdit,
  29    WillRenameFiles, notification::DidRenameFiles,
  30};
  31use parking_lot::Mutex;
  32use paths::{config_dir, tasks_file};
  33use postage::stream::Stream as _;
  34use pretty_assertions::{assert_eq, assert_matches};
  35use rand::{Rng as _, rngs::StdRng};
  36use serde_json::json;
  37#[cfg(not(windows))]
  38use std::os;
  39use std::{env, mem, num::NonZeroU32, ops::Range, str::FromStr, sync::OnceLock, task::Poll};
  40use task::{ResolvedTask, TaskContext};
  41use unindent::Unindent as _;
  42use util::{
  43    TryFutureExt as _, assert_set_eq, maybe, path,
  44    paths::PathMatcher,
  45    separator,
  46    test::{TempTree, marked_text_offsets},
  47    uri,
  48};
  49use worktree::WorktreeModelHandle as _;
  50
  51#[gpui::test]
  52async fn test_block_via_channel(cx: &mut gpui::TestAppContext) {
  53    cx.executor().allow_parking();
  54
  55    let (tx, mut rx) = futures::channel::mpsc::unbounded();
  56    let _thread = std::thread::spawn(move || {
  57        #[cfg(not(target_os = "windows"))]
  58        std::fs::metadata("/tmp").unwrap();
  59        #[cfg(target_os = "windows")]
  60        std::fs::metadata("C:/Windows").unwrap();
  61        std::thread::sleep(Duration::from_millis(1000));
  62        tx.unbounded_send(1).unwrap();
  63    });
  64    rx.next().await.unwrap();
  65}
  66
  67#[gpui::test]
  68async fn test_block_via_smol(cx: &mut gpui::TestAppContext) {
  69    cx.executor().allow_parking();
  70
  71    let io_task = smol::unblock(move || {
  72        println!("sleeping on thread {:?}", std::thread::current().id());
  73        std::thread::sleep(Duration::from_millis(10));
  74        1
  75    });
  76
  77    let task = cx.foreground_executor().spawn(async move {
  78        io_task.await;
  79    });
  80
  81    task.await;
  82}
  83
  84#[cfg(not(windows))]
  85#[gpui::test]
  86async fn test_symlinks(cx: &mut gpui::TestAppContext) {
  87    init_test(cx);
  88    cx.executor().allow_parking();
  89
  90    let dir = TempTree::new(json!({
  91        "root": {
  92            "apple": "",
  93            "banana": {
  94                "carrot": {
  95                    "date": "",
  96                    "endive": "",
  97                }
  98            },
  99            "fennel": {
 100                "grape": "",
 101            }
 102        }
 103    }));
 104
 105    let root_link_path = dir.path().join("root_link");
 106    os::unix::fs::symlink(dir.path().join("root"), &root_link_path).unwrap();
 107    os::unix::fs::symlink(
 108        dir.path().join("root/fennel"),
 109        dir.path().join("root/finnochio"),
 110    )
 111    .unwrap();
 112
 113    let project = Project::test(
 114        Arc::new(RealFs::new(None, cx.executor())),
 115        [root_link_path.as_ref()],
 116        cx,
 117    )
 118    .await;
 119
 120    project.update(cx, |project, cx| {
 121        let tree = project.worktrees(cx).next().unwrap().read(cx);
 122        assert_eq!(tree.file_count(), 5);
 123        assert_eq!(
 124            tree.inode_for_path("fennel/grape"),
 125            tree.inode_for_path("finnochio/grape")
 126        );
 127    });
 128}
 129
 130#[gpui::test]
 131async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) {
 132    init_test(cx);
 133
 134    let dir = TempTree::new(json!({
 135        ".editorconfig": r#"
 136        root = true
 137        [*.rs]
 138            indent_style = tab
 139            indent_size = 3
 140            end_of_line = lf
 141            insert_final_newline = true
 142            trim_trailing_whitespace = true
 143        [*.js]
 144            tab_width = 10
 145        "#,
 146        ".zed": {
 147            "settings.json": r#"{
 148                "tab_size": 8,
 149                "hard_tabs": false,
 150                "ensure_final_newline_on_save": false,
 151                "remove_trailing_whitespace_on_save": false,
 152                "soft_wrap": "editor_width"
 153            }"#,
 154        },
 155        "a.rs": "fn a() {\n    A\n}",
 156        "b": {
 157            ".editorconfig": r#"
 158            [*.rs]
 159                indent_size = 2
 160            "#,
 161            "b.rs": "fn b() {\n    B\n}",
 162        },
 163        "c.js": "def c\n  C\nend",
 164        "README.json": "tabs are better\n",
 165    }));
 166
 167    let path = dir.path();
 168    let fs = FakeFs::new(cx.executor());
 169    fs.insert_tree_from_real_fs(path, path).await;
 170    let project = Project::test(fs, [path], cx).await;
 171
 172    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 173    language_registry.add(js_lang());
 174    language_registry.add(json_lang());
 175    language_registry.add(rust_lang());
 176
 177    let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
 178
 179    cx.executor().run_until_parked();
 180
 181    cx.update(|cx| {
 182        let tree = worktree.read(cx);
 183        let settings_for = |path: &str| {
 184            let file_entry = tree.entry_for_path(path).unwrap().clone();
 185            let file = File::for_entry(file_entry, worktree.clone());
 186            let file_language = project
 187                .read(cx)
 188                .languages()
 189                .language_for_file_path(file.path.as_ref());
 190            let file_language = cx
 191                .background_executor()
 192                .block(file_language)
 193                .expect("Failed to get file language");
 194            let file = file as _;
 195            language_settings(Some(file_language.name()), Some(&file), cx).into_owned()
 196        };
 197
 198        let settings_a = settings_for("a.rs");
 199        let settings_b = settings_for("b/b.rs");
 200        let settings_c = settings_for("c.js");
 201        let settings_readme = settings_for("README.json");
 202
 203        // .editorconfig overrides .zed/settings
 204        assert_eq!(Some(settings_a.tab_size), NonZeroU32::new(3));
 205        assert_eq!(settings_a.hard_tabs, true);
 206        assert_eq!(settings_a.ensure_final_newline_on_save, true);
 207        assert_eq!(settings_a.remove_trailing_whitespace_on_save, true);
 208
 209        // .editorconfig in b/ overrides .editorconfig in root
 210        assert_eq!(Some(settings_b.tab_size), NonZeroU32::new(2));
 211
 212        // "indent_size" is not set, so "tab_width" is used
 213        assert_eq!(Some(settings_c.tab_size), NonZeroU32::new(10));
 214
 215        // README.md should not be affected by .editorconfig's globe "*.rs"
 216        assert_eq!(Some(settings_readme.tab_size), NonZeroU32::new(8));
 217    });
 218}
 219
 220#[gpui::test]
 221async fn test_git_provider_project_setting(cx: &mut gpui::TestAppContext) {
 222    init_test(cx);
 223    cx.update(|cx| {
 224        GitHostingProviderRegistry::default_global(cx);
 225        git_hosting_providers::init(cx);
 226    });
 227
 228    let fs = FakeFs::new(cx.executor());
 229    let str_path = path!("/dir");
 230    let path = Path::new(str_path);
 231
 232    fs.insert_tree(
 233        path!("/dir"),
 234        json!({
 235            ".zed": {
 236                "settings.json": r#"{
 237                    "git_hosting_providers": [
 238                        {
 239                            "provider": "gitlab",
 240                            "base_url": "https://google.com",
 241                            "name": "foo"
 242                        }
 243                    ]
 244                }"#
 245            },
 246        }),
 247    )
 248    .await;
 249
 250    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 251    let (_worktree, _) =
 252        project.read_with(cx, |project, cx| project.find_worktree(path, cx).unwrap());
 253    cx.executor().run_until_parked();
 254
 255    cx.update(|cx| {
 256        let provider = GitHostingProviderRegistry::global(cx);
 257        assert!(
 258            provider
 259                .list_hosting_providers()
 260                .into_iter()
 261                .any(|provider| provider.name() == "foo")
 262        );
 263    });
 264
 265    fs.atomic_write(
 266        Path::new(path!("/dir/.zed/settings.json")).to_owned(),
 267        "{}".into(),
 268    )
 269    .await
 270    .unwrap();
 271
 272    cx.run_until_parked();
 273
 274    cx.update(|cx| {
 275        let provider = GitHostingProviderRegistry::global(cx);
 276        assert!(
 277            !provider
 278                .list_hosting_providers()
 279                .into_iter()
 280                .any(|provider| provider.name() == "foo")
 281        );
 282    });
 283}
 284
 285#[gpui::test]
 286async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) {
 287    init_test(cx);
 288    TaskStore::init(None);
 289
 290    let fs = FakeFs::new(cx.executor());
 291    fs.insert_tree(
 292        path!("/dir"),
 293        json!({
 294            ".zed": {
 295                "settings.json": r#"{ "tab_size": 8 }"#,
 296                "tasks.json": r#"[{
 297                    "label": "cargo check all",
 298                    "command": "cargo",
 299                    "args": ["check", "--all"]
 300                },]"#,
 301            },
 302            "a": {
 303                "a.rs": "fn a() {\n    A\n}"
 304            },
 305            "b": {
 306                ".zed": {
 307                    "settings.json": r#"{ "tab_size": 2 }"#,
 308                    "tasks.json": r#"[{
 309                        "label": "cargo check",
 310                        "command": "cargo",
 311                        "args": ["check"]
 312                    },]"#,
 313                },
 314                "b.rs": "fn b() {\n  B\n}"
 315            }
 316        }),
 317    )
 318    .await;
 319
 320    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 321    let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
 322
 323    cx.executor().run_until_parked();
 324    let worktree_id = cx.update(|cx| {
 325        project.update(cx, |project, cx| {
 326            project.worktrees(cx).next().unwrap().read(cx).id()
 327        })
 328    });
 329
 330    let mut task_contexts = TaskContexts::default();
 331    task_contexts.active_worktree_context = Some((worktree_id, TaskContext::default()));
 332
 333    let topmost_local_task_source_kind = TaskSourceKind::Worktree {
 334        id: worktree_id,
 335        directory_in_worktree: PathBuf::from(".zed"),
 336        id_base: "local worktree tasks from directory \".zed\"".into(),
 337    };
 338
 339    let all_tasks = cx
 340        .update(|cx| {
 341            let tree = worktree.read(cx);
 342
 343            let file_a = File::for_entry(
 344                tree.entry_for_path("a/a.rs").unwrap().clone(),
 345                worktree.clone(),
 346            ) as _;
 347            let settings_a = language_settings(None, Some(&file_a), cx);
 348            let file_b = File::for_entry(
 349                tree.entry_for_path("b/b.rs").unwrap().clone(),
 350                worktree.clone(),
 351            ) as _;
 352            let settings_b = language_settings(None, Some(&file_b), cx);
 353
 354            assert_eq!(settings_a.tab_size.get(), 8);
 355            assert_eq!(settings_b.tab_size.get(), 2);
 356
 357            get_all_tasks(&project, &task_contexts, cx)
 358        })
 359        .into_iter()
 360        .map(|(source_kind, task)| {
 361            let resolved = task.resolved;
 362            (
 363                source_kind,
 364                task.resolved_label,
 365                resolved.args,
 366                resolved.env,
 367            )
 368        })
 369        .collect::<Vec<_>>();
 370    assert_eq!(
 371        all_tasks,
 372        vec![
 373            (
 374                TaskSourceKind::Worktree {
 375                    id: worktree_id,
 376                    directory_in_worktree: PathBuf::from(separator!("b/.zed")),
 377                    id_base: if cfg!(windows) {
 378                        "local worktree tasks from directory \"b\\\\.zed\"".into()
 379                    } else {
 380                        "local worktree tasks from directory \"b/.zed\"".into()
 381                    },
 382                },
 383                "cargo check".to_string(),
 384                vec!["check".to_string()],
 385                HashMap::default(),
 386            ),
 387            (
 388                topmost_local_task_source_kind.clone(),
 389                "cargo check all".to_string(),
 390                vec!["check".to_string(), "--all".to_string()],
 391                HashMap::default(),
 392            ),
 393        ]
 394    );
 395
 396    let (_, resolved_task) = cx
 397        .update(|cx| get_all_tasks(&project, &task_contexts, cx))
 398        .into_iter()
 399        .find(|(source_kind, _)| source_kind == &topmost_local_task_source_kind)
 400        .expect("should have one global task");
 401    project.update(cx, |project, cx| {
 402        let task_inventory = project
 403            .task_store
 404            .read(cx)
 405            .task_inventory()
 406            .cloned()
 407            .unwrap();
 408        task_inventory.update(cx, |inventory, _| {
 409            inventory.task_scheduled(topmost_local_task_source_kind.clone(), resolved_task);
 410            inventory
 411                .update_file_based_tasks(
 412                    TaskSettingsLocation::Global(tasks_file()),
 413                    Some(
 414                        &json!([{
 415                            "label": "cargo check unstable",
 416                            "command": "cargo",
 417                            "args": [
 418                                "check",
 419                                "--all",
 420                                "--all-targets"
 421                            ],
 422                            "env": {
 423                                "RUSTFLAGS": "-Zunstable-options"
 424                            }
 425                        }])
 426                        .to_string(),
 427                    ),
 428                )
 429                .unwrap();
 430        });
 431    });
 432    cx.run_until_parked();
 433
 434    let all_tasks = cx
 435        .update(|cx| get_all_tasks(&project, &task_contexts, cx))
 436        .into_iter()
 437        .map(|(source_kind, task)| {
 438            let resolved = task.resolved;
 439            (
 440                source_kind,
 441                task.resolved_label,
 442                resolved.args,
 443                resolved.env,
 444            )
 445        })
 446        .collect::<Vec<_>>();
 447    assert_eq!(
 448        all_tasks,
 449        vec![
 450            (
 451                topmost_local_task_source_kind.clone(),
 452                "cargo check all".to_string(),
 453                vec!["check".to_string(), "--all".to_string()],
 454                HashMap::default(),
 455            ),
 456            (
 457                TaskSourceKind::Worktree {
 458                    id: worktree_id,
 459                    directory_in_worktree: PathBuf::from(separator!("b/.zed")),
 460                    id_base: if cfg!(windows) {
 461                        "local worktree tasks from directory \"b\\\\.zed\"".into()
 462                    } else {
 463                        "local worktree tasks from directory \"b/.zed\"".into()
 464                    },
 465                },
 466                "cargo check".to_string(),
 467                vec!["check".to_string()],
 468                HashMap::default(),
 469            ),
 470            (
 471                TaskSourceKind::AbsPath {
 472                    abs_path: paths::tasks_file().clone(),
 473                    id_base: "global tasks.json".into(),
 474                },
 475                "cargo check unstable".to_string(),
 476                vec![
 477                    "check".to_string(),
 478                    "--all".to_string(),
 479                    "--all-targets".to_string(),
 480                ],
 481                HashMap::from_iter(Some((
 482                    "RUSTFLAGS".to_string(),
 483                    "-Zunstable-options".to_string()
 484                ))),
 485            ),
 486        ]
 487    );
 488}
 489
 490#[gpui::test]
 491async fn test_fallback_to_single_worktree_tasks(cx: &mut gpui::TestAppContext) {
 492    init_test(cx);
 493    TaskStore::init(None);
 494
 495    let fs = FakeFs::new(cx.executor());
 496    fs.insert_tree(
 497        path!("/dir"),
 498        json!({
 499            ".zed": {
 500                "tasks.json": r#"[{
 501                    "label": "test worktree root",
 502                    "command": "echo $ZED_WORKTREE_ROOT"
 503                }]"#,
 504            },
 505            "a": {
 506                "a.rs": "fn a() {\n    A\n}"
 507            },
 508        }),
 509    )
 510    .await;
 511
 512    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 513    let _worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
 514
 515    cx.executor().run_until_parked();
 516    let worktree_id = cx.update(|cx| {
 517        project.update(cx, |project, cx| {
 518            project.worktrees(cx).next().unwrap().read(cx).id()
 519        })
 520    });
 521
 522    let active_non_worktree_item_tasks = cx.update(|cx| {
 523        get_all_tasks(
 524            &project,
 525            &TaskContexts {
 526                active_item_context: Some((Some(worktree_id), None, TaskContext::default())),
 527                active_worktree_context: None,
 528                other_worktree_contexts: Vec::new(),
 529                lsp_task_sources: HashMap::default(),
 530                latest_selection: None,
 531            },
 532            cx,
 533        )
 534    });
 535    assert!(
 536        active_non_worktree_item_tasks.is_empty(),
 537        "A task can not be resolved with context with no ZED_WORKTREE_ROOT data"
 538    );
 539
 540    let active_worktree_tasks = cx.update(|cx| {
 541        get_all_tasks(
 542            &project,
 543            &TaskContexts {
 544                active_item_context: Some((Some(worktree_id), None, TaskContext::default())),
 545                active_worktree_context: Some((worktree_id, {
 546                    let mut worktree_context = TaskContext::default();
 547                    worktree_context
 548                        .task_variables
 549                        .insert(task::VariableName::WorktreeRoot, "/dir".to_string());
 550                    worktree_context
 551                })),
 552                other_worktree_contexts: Vec::new(),
 553                lsp_task_sources: HashMap::default(),
 554                latest_selection: None,
 555            },
 556            cx,
 557        )
 558    });
 559    assert_eq!(
 560        active_worktree_tasks
 561            .into_iter()
 562            .map(|(source_kind, task)| {
 563                let resolved = task.resolved;
 564                (source_kind, resolved.command)
 565            })
 566            .collect::<Vec<_>>(),
 567        vec![(
 568            TaskSourceKind::Worktree {
 569                id: worktree_id,
 570                directory_in_worktree: PathBuf::from(separator!(".zed")),
 571                id_base: if cfg!(windows) {
 572                    "local worktree tasks from directory \".zed\"".into()
 573                } else {
 574                    "local worktree tasks from directory \".zed\"".into()
 575                },
 576            },
 577            "echo /dir".to_string(),
 578        )]
 579    );
 580}
 581
 582#[gpui::test]
 583async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
 584    init_test(cx);
 585
 586    let fs = FakeFs::new(cx.executor());
 587    fs.insert_tree(
 588        path!("/dir"),
 589        json!({
 590            "test.rs": "const A: i32 = 1;",
 591            "test2.rs": "",
 592            "Cargo.toml": "a = 1",
 593            "package.json": "{\"a\": 1}",
 594        }),
 595    )
 596    .await;
 597
 598    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 599    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 600
 601    let mut fake_rust_servers = language_registry.register_fake_lsp(
 602        "Rust",
 603        FakeLspAdapter {
 604            name: "the-rust-language-server",
 605            capabilities: lsp::ServerCapabilities {
 606                completion_provider: Some(lsp::CompletionOptions {
 607                    trigger_characters: Some(vec![".".to_string(), "::".to_string()]),
 608                    ..Default::default()
 609                }),
 610                text_document_sync: Some(lsp::TextDocumentSyncCapability::Options(
 611                    lsp::TextDocumentSyncOptions {
 612                        save: Some(lsp::TextDocumentSyncSaveOptions::Supported(true)),
 613                        ..Default::default()
 614                    },
 615                )),
 616                ..Default::default()
 617            },
 618            ..Default::default()
 619        },
 620    );
 621    let mut fake_json_servers = language_registry.register_fake_lsp(
 622        "JSON",
 623        FakeLspAdapter {
 624            name: "the-json-language-server",
 625            capabilities: lsp::ServerCapabilities {
 626                completion_provider: Some(lsp::CompletionOptions {
 627                    trigger_characters: Some(vec![":".to_string()]),
 628                    ..Default::default()
 629                }),
 630                text_document_sync: Some(lsp::TextDocumentSyncCapability::Options(
 631                    lsp::TextDocumentSyncOptions {
 632                        save: Some(lsp::TextDocumentSyncSaveOptions::Supported(true)),
 633                        ..Default::default()
 634                    },
 635                )),
 636                ..Default::default()
 637            },
 638            ..Default::default()
 639        },
 640    );
 641
 642    // Open a buffer without an associated language server.
 643    let (toml_buffer, _handle) = project
 644        .update(cx, |project, cx| {
 645            project.open_local_buffer_with_lsp(path!("/dir/Cargo.toml"), cx)
 646        })
 647        .await
 648        .unwrap();
 649
 650    // Open a buffer with an associated language server before the language for it has been loaded.
 651    let (rust_buffer, _handle2) = project
 652        .update(cx, |project, cx| {
 653            project.open_local_buffer_with_lsp(path!("/dir/test.rs"), cx)
 654        })
 655        .await
 656        .unwrap();
 657    rust_buffer.update(cx, |buffer, _| {
 658        assert_eq!(buffer.language().map(|l| l.name()), None);
 659    });
 660
 661    // Now we add the languages to the project, and ensure they get assigned to all
 662    // the relevant open buffers.
 663    language_registry.add(json_lang());
 664    language_registry.add(rust_lang());
 665    cx.executor().run_until_parked();
 666    rust_buffer.update(cx, |buffer, _| {
 667        assert_eq!(buffer.language().map(|l| l.name()), Some("Rust".into()));
 668    });
 669
 670    // A server is started up, and it is notified about Rust files.
 671    let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
 672    assert_eq!(
 673        fake_rust_server
 674            .receive_notification::<lsp::notification::DidOpenTextDocument>()
 675            .await
 676            .text_document,
 677        lsp::TextDocumentItem {
 678            uri: lsp::Url::from_file_path(path!("/dir/test.rs")).unwrap(),
 679            version: 0,
 680            text: "const A: i32 = 1;".to_string(),
 681            language_id: "rust".to_string(),
 682        }
 683    );
 684
 685    // The buffer is configured based on the language server's capabilities.
 686    rust_buffer.update(cx, |buffer, _| {
 687        assert_eq!(
 688            buffer
 689                .completion_triggers()
 690                .into_iter()
 691                .cloned()
 692                .collect::<Vec<_>>(),
 693            &[".".to_string(), "::".to_string()]
 694        );
 695    });
 696    toml_buffer.update(cx, |buffer, _| {
 697        assert!(buffer.completion_triggers().is_empty());
 698    });
 699
 700    // Edit a buffer. The changes are reported to the language server.
 701    rust_buffer.update(cx, |buffer, cx| buffer.edit([(16..16, "2")], None, cx));
 702    assert_eq!(
 703        fake_rust_server
 704            .receive_notification::<lsp::notification::DidChangeTextDocument>()
 705            .await
 706            .text_document,
 707        lsp::VersionedTextDocumentIdentifier::new(
 708            lsp::Url::from_file_path(path!("/dir/test.rs")).unwrap(),
 709            1
 710        )
 711    );
 712
 713    // Open a third buffer with a different associated language server.
 714    let (json_buffer, _json_handle) = project
 715        .update(cx, |project, cx| {
 716            project.open_local_buffer_with_lsp(path!("/dir/package.json"), cx)
 717        })
 718        .await
 719        .unwrap();
 720
 721    // A json language server is started up and is only notified about the json buffer.
 722    let mut fake_json_server = fake_json_servers.next().await.unwrap();
 723    assert_eq!(
 724        fake_json_server
 725            .receive_notification::<lsp::notification::DidOpenTextDocument>()
 726            .await
 727            .text_document,
 728        lsp::TextDocumentItem {
 729            uri: lsp::Url::from_file_path(path!("/dir/package.json")).unwrap(),
 730            version: 0,
 731            text: "{\"a\": 1}".to_string(),
 732            language_id: "json".to_string(),
 733        }
 734    );
 735
 736    // This buffer is configured based on the second language server's
 737    // capabilities.
 738    json_buffer.update(cx, |buffer, _| {
 739        assert_eq!(
 740            buffer
 741                .completion_triggers()
 742                .into_iter()
 743                .cloned()
 744                .collect::<Vec<_>>(),
 745            &[":".to_string()]
 746        );
 747    });
 748
 749    // When opening another buffer whose language server is already running,
 750    // it is also configured based on the existing language server's capabilities.
 751    let (rust_buffer2, _handle4) = project
 752        .update(cx, |project, cx| {
 753            project.open_local_buffer_with_lsp(path!("/dir/test2.rs"), cx)
 754        })
 755        .await
 756        .unwrap();
 757    rust_buffer2.update(cx, |buffer, _| {
 758        assert_eq!(
 759            buffer
 760                .completion_triggers()
 761                .into_iter()
 762                .cloned()
 763                .collect::<Vec<_>>(),
 764            &[".".to_string(), "::".to_string()]
 765        );
 766    });
 767
 768    // Changes are reported only to servers matching the buffer's language.
 769    toml_buffer.update(cx, |buffer, cx| buffer.edit([(5..5, "23")], None, cx));
 770    rust_buffer2.update(cx, |buffer, cx| {
 771        buffer.edit([(0..0, "let x = 1;")], None, cx)
 772    });
 773    assert_eq!(
 774        fake_rust_server
 775            .receive_notification::<lsp::notification::DidChangeTextDocument>()
 776            .await
 777            .text_document,
 778        lsp::VersionedTextDocumentIdentifier::new(
 779            lsp::Url::from_file_path(path!("/dir/test2.rs")).unwrap(),
 780            1
 781        )
 782    );
 783
 784    // Save notifications are reported to all servers.
 785    project
 786        .update(cx, |project, cx| project.save_buffer(toml_buffer, cx))
 787        .await
 788        .unwrap();
 789    assert_eq!(
 790        fake_rust_server
 791            .receive_notification::<lsp::notification::DidSaveTextDocument>()
 792            .await
 793            .text_document,
 794        lsp::TextDocumentIdentifier::new(
 795            lsp::Url::from_file_path(path!("/dir/Cargo.toml")).unwrap()
 796        )
 797    );
 798    assert_eq!(
 799        fake_json_server
 800            .receive_notification::<lsp::notification::DidSaveTextDocument>()
 801            .await
 802            .text_document,
 803        lsp::TextDocumentIdentifier::new(
 804            lsp::Url::from_file_path(path!("/dir/Cargo.toml")).unwrap()
 805        )
 806    );
 807
 808    // Renames are reported only to servers matching the buffer's language.
 809    fs.rename(
 810        Path::new(path!("/dir/test2.rs")),
 811        Path::new(path!("/dir/test3.rs")),
 812        Default::default(),
 813    )
 814    .await
 815    .unwrap();
 816    assert_eq!(
 817        fake_rust_server
 818            .receive_notification::<lsp::notification::DidCloseTextDocument>()
 819            .await
 820            .text_document,
 821        lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path(path!("/dir/test2.rs")).unwrap()),
 822    );
 823    assert_eq!(
 824        fake_rust_server
 825            .receive_notification::<lsp::notification::DidOpenTextDocument>()
 826            .await
 827            .text_document,
 828        lsp::TextDocumentItem {
 829            uri: lsp::Url::from_file_path(path!("/dir/test3.rs")).unwrap(),
 830            version: 0,
 831            text: rust_buffer2.update(cx, |buffer, _| buffer.text()),
 832            language_id: "rust".to_string(),
 833        },
 834    );
 835
 836    rust_buffer2.update(cx, |buffer, cx| {
 837        buffer.update_diagnostics(
 838            LanguageServerId(0),
 839            DiagnosticSet::from_sorted_entries(
 840                vec![DiagnosticEntry {
 841                    diagnostic: Default::default(),
 842                    range: Anchor::MIN..Anchor::MAX,
 843                }],
 844                &buffer.snapshot(),
 845            ),
 846            cx,
 847        );
 848        assert_eq!(
 849            buffer
 850                .snapshot()
 851                .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
 852                .count(),
 853            1
 854        );
 855    });
 856
 857    // When the rename changes the extension of the file, the buffer gets closed on the old
 858    // language server and gets opened on the new one.
 859    fs.rename(
 860        Path::new(path!("/dir/test3.rs")),
 861        Path::new(path!("/dir/test3.json")),
 862        Default::default(),
 863    )
 864    .await
 865    .unwrap();
 866    assert_eq!(
 867        fake_rust_server
 868            .receive_notification::<lsp::notification::DidCloseTextDocument>()
 869            .await
 870            .text_document,
 871        lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path(path!("/dir/test3.rs")).unwrap()),
 872    );
 873    assert_eq!(
 874        fake_json_server
 875            .receive_notification::<lsp::notification::DidOpenTextDocument>()
 876            .await
 877            .text_document,
 878        lsp::TextDocumentItem {
 879            uri: lsp::Url::from_file_path(path!("/dir/test3.json")).unwrap(),
 880            version: 0,
 881            text: rust_buffer2.update(cx, |buffer, _| buffer.text()),
 882            language_id: "json".to_string(),
 883        },
 884    );
 885
 886    // We clear the diagnostics, since the language has changed.
 887    rust_buffer2.update(cx, |buffer, _| {
 888        assert_eq!(
 889            buffer
 890                .snapshot()
 891                .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
 892                .count(),
 893            0
 894        );
 895    });
 896
 897    // The renamed file's version resets after changing language server.
 898    rust_buffer2.update(cx, |buffer, cx| buffer.edit([(0..0, "// ")], None, cx));
 899    assert_eq!(
 900        fake_json_server
 901            .receive_notification::<lsp::notification::DidChangeTextDocument>()
 902            .await
 903            .text_document,
 904        lsp::VersionedTextDocumentIdentifier::new(
 905            lsp::Url::from_file_path(path!("/dir/test3.json")).unwrap(),
 906            1
 907        )
 908    );
 909
 910    // Restart language servers
 911    project.update(cx, |project, cx| {
 912        project.restart_language_servers_for_buffers(
 913            vec![rust_buffer.clone(), json_buffer.clone()],
 914            cx,
 915        );
 916    });
 917
 918    let mut rust_shutdown_requests = fake_rust_server
 919        .set_request_handler::<lsp::request::Shutdown, _, _>(|_, _| future::ready(Ok(())));
 920    let mut json_shutdown_requests = fake_json_server
 921        .set_request_handler::<lsp::request::Shutdown, _, _>(|_, _| future::ready(Ok(())));
 922    futures::join!(rust_shutdown_requests.next(), json_shutdown_requests.next());
 923
 924    let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
 925    let mut fake_json_server = fake_json_servers.next().await.unwrap();
 926
 927    // Ensure rust document is reopened in new rust language server
 928    assert_eq!(
 929        fake_rust_server
 930            .receive_notification::<lsp::notification::DidOpenTextDocument>()
 931            .await
 932            .text_document,
 933        lsp::TextDocumentItem {
 934            uri: lsp::Url::from_file_path(path!("/dir/test.rs")).unwrap(),
 935            version: 0,
 936            text: rust_buffer.update(cx, |buffer, _| buffer.text()),
 937            language_id: "rust".to_string(),
 938        }
 939    );
 940
 941    // Ensure json documents are reopened in new json language server
 942    assert_set_eq!(
 943        [
 944            fake_json_server
 945                .receive_notification::<lsp::notification::DidOpenTextDocument>()
 946                .await
 947                .text_document,
 948            fake_json_server
 949                .receive_notification::<lsp::notification::DidOpenTextDocument>()
 950                .await
 951                .text_document,
 952        ],
 953        [
 954            lsp::TextDocumentItem {
 955                uri: lsp::Url::from_file_path(path!("/dir/package.json")).unwrap(),
 956                version: 0,
 957                text: json_buffer.update(cx, |buffer, _| buffer.text()),
 958                language_id: "json".to_string(),
 959            },
 960            lsp::TextDocumentItem {
 961                uri: lsp::Url::from_file_path(path!("/dir/test3.json")).unwrap(),
 962                version: 0,
 963                text: rust_buffer2.update(cx, |buffer, _| buffer.text()),
 964                language_id: "json".to_string(),
 965            }
 966        ]
 967    );
 968
 969    // Close notifications are reported only to servers matching the buffer's language.
 970    cx.update(|_| drop(_json_handle));
 971    let close_message = lsp::DidCloseTextDocumentParams {
 972        text_document: lsp::TextDocumentIdentifier::new(
 973            lsp::Url::from_file_path(path!("/dir/package.json")).unwrap(),
 974        ),
 975    };
 976    assert_eq!(
 977        fake_json_server
 978            .receive_notification::<lsp::notification::DidCloseTextDocument>()
 979            .await,
 980        close_message,
 981    );
 982}
 983
 984#[gpui::test]
 985async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) {
 986    init_test(cx);
 987
 988    let fs = FakeFs::new(cx.executor());
 989    fs.insert_tree(
 990        path!("/the-root"),
 991        json!({
 992            ".gitignore": "target\n",
 993            "Cargo.lock": "",
 994            "src": {
 995                "a.rs": "",
 996                "b.rs": "",
 997            },
 998            "target": {
 999                "x": {
1000                    "out": {
1001                        "x.rs": ""
1002                    }
1003                },
1004                "y": {
1005                    "out": {
1006                        "y.rs": "",
1007                    }
1008                },
1009                "z": {
1010                    "out": {
1011                        "z.rs": ""
1012                    }
1013                }
1014            }
1015        }),
1016    )
1017    .await;
1018    fs.insert_tree(
1019        path!("/the-registry"),
1020        json!({
1021            "dep1": {
1022                "src": {
1023                    "dep1.rs": "",
1024                }
1025            },
1026            "dep2": {
1027                "src": {
1028                    "dep2.rs": "",
1029                }
1030            },
1031        }),
1032    )
1033    .await;
1034    fs.insert_tree(
1035        path!("/the/stdlib"),
1036        json!({
1037            "LICENSE": "",
1038            "src": {
1039                "string.rs": "",
1040            }
1041        }),
1042    )
1043    .await;
1044
1045    let project = Project::test(fs.clone(), [path!("/the-root").as_ref()], cx).await;
1046    let (language_registry, lsp_store) = project.read_with(cx, |project, _| {
1047        (project.languages().clone(), project.lsp_store())
1048    });
1049    language_registry.add(rust_lang());
1050    let mut fake_servers = language_registry.register_fake_lsp(
1051        "Rust",
1052        FakeLspAdapter {
1053            name: "the-language-server",
1054            ..Default::default()
1055        },
1056    );
1057
1058    cx.executor().run_until_parked();
1059
1060    // Start the language server by opening a buffer with a compatible file extension.
1061    project
1062        .update(cx, |project, cx| {
1063            project.open_local_buffer_with_lsp(path!("/the-root/src/a.rs"), cx)
1064        })
1065        .await
1066        .unwrap();
1067
1068    // Initially, we don't load ignored files because the language server has not explicitly asked us to watch them.
1069    project.update(cx, |project, cx| {
1070        let worktree = project.worktrees(cx).next().unwrap();
1071        assert_eq!(
1072            worktree
1073                .read(cx)
1074                .snapshot()
1075                .entries(true, 0)
1076                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
1077                .collect::<Vec<_>>(),
1078            &[
1079                (Path::new(""), false),
1080                (Path::new(".gitignore"), false),
1081                (Path::new("Cargo.lock"), false),
1082                (Path::new("src"), false),
1083                (Path::new("src/a.rs"), false),
1084                (Path::new("src/b.rs"), false),
1085                (Path::new("target"), true),
1086            ]
1087        );
1088    });
1089
1090    let prev_read_dir_count = fs.read_dir_call_count();
1091
1092    let fake_server = fake_servers.next().await.unwrap();
1093    let (server_id, server_name) = lsp_store.read_with(cx, |lsp_store, _| {
1094        let (id, status) = lsp_store.language_server_statuses().next().unwrap();
1095        (id, LanguageServerName::from(status.name.as_str()))
1096    });
1097
1098    // Simulate jumping to a definition in a dependency outside of the worktree.
1099    let _out_of_worktree_buffer = project
1100        .update(cx, |project, cx| {
1101            project.open_local_buffer_via_lsp(
1102                lsp::Url::from_file_path(path!("/the-registry/dep1/src/dep1.rs")).unwrap(),
1103                server_id,
1104                server_name.clone(),
1105                cx,
1106            )
1107        })
1108        .await
1109        .unwrap();
1110
1111    // Keep track of the FS events reported to the language server.
1112    let file_changes = Arc::new(Mutex::new(Vec::new()));
1113    fake_server
1114        .request::<lsp::request::RegisterCapability>(lsp::RegistrationParams {
1115            registrations: vec![lsp::Registration {
1116                id: Default::default(),
1117                method: "workspace/didChangeWatchedFiles".to_string(),
1118                register_options: serde_json::to_value(
1119                    lsp::DidChangeWatchedFilesRegistrationOptions {
1120                        watchers: vec![
1121                            lsp::FileSystemWatcher {
1122                                glob_pattern: lsp::GlobPattern::String(
1123                                    path!("/the-root/Cargo.toml").to_string(),
1124                                ),
1125                                kind: None,
1126                            },
1127                            lsp::FileSystemWatcher {
1128                                glob_pattern: lsp::GlobPattern::String(
1129                                    path!("/the-root/src/*.{rs,c}").to_string(),
1130                                ),
1131                                kind: None,
1132                            },
1133                            lsp::FileSystemWatcher {
1134                                glob_pattern: lsp::GlobPattern::String(
1135                                    path!("/the-root/target/y/**/*.rs").to_string(),
1136                                ),
1137                                kind: None,
1138                            },
1139                            lsp::FileSystemWatcher {
1140                                glob_pattern: lsp::GlobPattern::String(
1141                                    path!("/the/stdlib/src/**/*.rs").to_string(),
1142                                ),
1143                                kind: None,
1144                            },
1145                            lsp::FileSystemWatcher {
1146                                glob_pattern: lsp::GlobPattern::String(
1147                                    path!("**/Cargo.lock").to_string(),
1148                                ),
1149                                kind: None,
1150                            },
1151                        ],
1152                    },
1153                )
1154                .ok(),
1155            }],
1156        })
1157        .await
1158        .into_response()
1159        .unwrap();
1160    fake_server.handle_notification::<lsp::notification::DidChangeWatchedFiles, _>({
1161        let file_changes = file_changes.clone();
1162        move |params, _| {
1163            let mut file_changes = file_changes.lock();
1164            file_changes.extend(params.changes);
1165            file_changes.sort_by(|a, b| a.uri.cmp(&b.uri));
1166        }
1167    });
1168
1169    cx.executor().run_until_parked();
1170    assert_eq!(mem::take(&mut *file_changes.lock()), &[]);
1171    assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 5);
1172
1173    let mut new_watched_paths = fs.watched_paths();
1174    new_watched_paths.retain(|path| !path.starts_with(config_dir()));
1175    assert_eq!(
1176        &new_watched_paths,
1177        &[
1178            Path::new(path!("/the-root")),
1179            Path::new(path!("/the-registry/dep1/src/dep1.rs")),
1180            Path::new(path!("/the/stdlib/src"))
1181        ]
1182    );
1183
1184    // Now the language server has asked us to watch an ignored directory path,
1185    // so we recursively load it.
1186    project.update(cx, |project, cx| {
1187        let worktree = project.visible_worktrees(cx).next().unwrap();
1188        assert_eq!(
1189            worktree
1190                .read(cx)
1191                .snapshot()
1192                .entries(true, 0)
1193                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
1194                .collect::<Vec<_>>(),
1195            &[
1196                (Path::new(""), false),
1197                (Path::new(".gitignore"), false),
1198                (Path::new("Cargo.lock"), false),
1199                (Path::new("src"), false),
1200                (Path::new("src/a.rs"), false),
1201                (Path::new("src/b.rs"), false),
1202                (Path::new("target"), true),
1203                (Path::new("target/x"), true),
1204                (Path::new("target/y"), true),
1205                (Path::new("target/y/out"), true),
1206                (Path::new("target/y/out/y.rs"), true),
1207                (Path::new("target/z"), true),
1208            ]
1209        );
1210    });
1211
1212    // Perform some file system mutations, two of which match the watched patterns,
1213    // and one of which does not.
1214    fs.create_file(path!("/the-root/src/c.rs").as_ref(), Default::default())
1215        .await
1216        .unwrap();
1217    fs.create_file(path!("/the-root/src/d.txt").as_ref(), Default::default())
1218        .await
1219        .unwrap();
1220    fs.remove_file(path!("/the-root/src/b.rs").as_ref(), Default::default())
1221        .await
1222        .unwrap();
1223    fs.create_file(
1224        path!("/the-root/target/x/out/x2.rs").as_ref(),
1225        Default::default(),
1226    )
1227    .await
1228    .unwrap();
1229    fs.create_file(
1230        path!("/the-root/target/y/out/y2.rs").as_ref(),
1231        Default::default(),
1232    )
1233    .await
1234    .unwrap();
1235    fs.save(
1236        path!("/the-root/Cargo.lock").as_ref(),
1237        &"".into(),
1238        Default::default(),
1239    )
1240    .await
1241    .unwrap();
1242    fs.save(
1243        path!("/the-stdlib/LICENSE").as_ref(),
1244        &"".into(),
1245        Default::default(),
1246    )
1247    .await
1248    .unwrap();
1249    fs.save(
1250        path!("/the/stdlib/src/string.rs").as_ref(),
1251        &"".into(),
1252        Default::default(),
1253    )
1254    .await
1255    .unwrap();
1256
1257    // The language server receives events for the FS mutations that match its watch patterns.
1258    cx.executor().run_until_parked();
1259    assert_eq!(
1260        &*file_changes.lock(),
1261        &[
1262            lsp::FileEvent {
1263                uri: lsp::Url::from_file_path(path!("/the-root/Cargo.lock")).unwrap(),
1264                typ: lsp::FileChangeType::CHANGED,
1265            },
1266            lsp::FileEvent {
1267                uri: lsp::Url::from_file_path(path!("/the-root/src/b.rs")).unwrap(),
1268                typ: lsp::FileChangeType::DELETED,
1269            },
1270            lsp::FileEvent {
1271                uri: lsp::Url::from_file_path(path!("/the-root/src/c.rs")).unwrap(),
1272                typ: lsp::FileChangeType::CREATED,
1273            },
1274            lsp::FileEvent {
1275                uri: lsp::Url::from_file_path(path!("/the-root/target/y/out/y2.rs")).unwrap(),
1276                typ: lsp::FileChangeType::CREATED,
1277            },
1278            lsp::FileEvent {
1279                uri: lsp::Url::from_file_path(path!("/the/stdlib/src/string.rs")).unwrap(),
1280                typ: lsp::FileChangeType::CHANGED,
1281            },
1282        ]
1283    );
1284}
1285
1286#[gpui::test]
1287async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
1288    init_test(cx);
1289
1290    let fs = FakeFs::new(cx.executor());
1291    fs.insert_tree(
1292        path!("/dir"),
1293        json!({
1294            "a.rs": "let a = 1;",
1295            "b.rs": "let b = 2;"
1296        }),
1297    )
1298    .await;
1299
1300    let project = Project::test(
1301        fs,
1302        [path!("/dir/a.rs").as_ref(), path!("/dir/b.rs").as_ref()],
1303        cx,
1304    )
1305    .await;
1306    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1307
1308    let buffer_a = project
1309        .update(cx, |project, cx| {
1310            project.open_local_buffer(path!("/dir/a.rs"), cx)
1311        })
1312        .await
1313        .unwrap();
1314    let buffer_b = project
1315        .update(cx, |project, cx| {
1316            project.open_local_buffer(path!("/dir/b.rs"), cx)
1317        })
1318        .await
1319        .unwrap();
1320
1321    lsp_store.update(cx, |lsp_store, cx| {
1322        lsp_store
1323            .update_diagnostics(
1324                LanguageServerId(0),
1325                lsp::PublishDiagnosticsParams {
1326                    uri: Url::from_file_path(path!("/dir/a.rs")).unwrap(),
1327                    version: None,
1328                    diagnostics: vec![lsp::Diagnostic {
1329                        range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)),
1330                        severity: Some(lsp::DiagnosticSeverity::ERROR),
1331                        message: "error 1".to_string(),
1332                        ..Default::default()
1333                    }],
1334                },
1335                &[],
1336                cx,
1337            )
1338            .unwrap();
1339        lsp_store
1340            .update_diagnostics(
1341                LanguageServerId(0),
1342                lsp::PublishDiagnosticsParams {
1343                    uri: Url::from_file_path(path!("/dir/b.rs")).unwrap(),
1344                    version: None,
1345                    diagnostics: vec![lsp::Diagnostic {
1346                        range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)),
1347                        severity: Some(DiagnosticSeverity::WARNING),
1348                        message: "error 2".to_string(),
1349                        ..Default::default()
1350                    }],
1351                },
1352                &[],
1353                cx,
1354            )
1355            .unwrap();
1356    });
1357
1358    buffer_a.update(cx, |buffer, _| {
1359        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
1360        assert_eq!(
1361            chunks
1362                .iter()
1363                .map(|(s, d)| (s.as_str(), *d))
1364                .collect::<Vec<_>>(),
1365            &[
1366                ("let ", None),
1367                ("a", Some(DiagnosticSeverity::ERROR)),
1368                (" = 1;", None),
1369            ]
1370        );
1371    });
1372    buffer_b.update(cx, |buffer, _| {
1373        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
1374        assert_eq!(
1375            chunks
1376                .iter()
1377                .map(|(s, d)| (s.as_str(), *d))
1378                .collect::<Vec<_>>(),
1379            &[
1380                ("let ", None),
1381                ("b", Some(DiagnosticSeverity::WARNING)),
1382                (" = 2;", None),
1383            ]
1384        );
1385    });
1386}
1387
1388#[gpui::test]
1389async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
1390    init_test(cx);
1391
1392    let fs = FakeFs::new(cx.executor());
1393    fs.insert_tree(
1394        path!("/root"),
1395        json!({
1396            "dir": {
1397                ".git": {
1398                    "HEAD": "ref: refs/heads/main",
1399                },
1400                ".gitignore": "b.rs",
1401                "a.rs": "let a = 1;",
1402                "b.rs": "let b = 2;",
1403            },
1404            "other.rs": "let b = c;"
1405        }),
1406    )
1407    .await;
1408
1409    let project = Project::test(fs, [path!("/root/dir").as_ref()], cx).await;
1410    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1411    let (worktree, _) = project
1412        .update(cx, |project, cx| {
1413            project.find_or_create_worktree(path!("/root/dir"), true, cx)
1414        })
1415        .await
1416        .unwrap();
1417    let main_worktree_id = worktree.read_with(cx, |tree, _| tree.id());
1418
1419    let (worktree, _) = project
1420        .update(cx, |project, cx| {
1421            project.find_or_create_worktree(path!("/root/other.rs"), false, cx)
1422        })
1423        .await
1424        .unwrap();
1425    let other_worktree_id = worktree.update(cx, |tree, _| tree.id());
1426
1427    let server_id = LanguageServerId(0);
1428    lsp_store.update(cx, |lsp_store, cx| {
1429        lsp_store
1430            .update_diagnostics(
1431                server_id,
1432                lsp::PublishDiagnosticsParams {
1433                    uri: Url::from_file_path(path!("/root/dir/b.rs")).unwrap(),
1434                    version: None,
1435                    diagnostics: vec![lsp::Diagnostic {
1436                        range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)),
1437                        severity: Some(lsp::DiagnosticSeverity::ERROR),
1438                        message: "unused variable 'b'".to_string(),
1439                        ..Default::default()
1440                    }],
1441                },
1442                &[],
1443                cx,
1444            )
1445            .unwrap();
1446        lsp_store
1447            .update_diagnostics(
1448                server_id,
1449                lsp::PublishDiagnosticsParams {
1450                    uri: Url::from_file_path(path!("/root/other.rs")).unwrap(),
1451                    version: None,
1452                    diagnostics: vec![lsp::Diagnostic {
1453                        range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 9)),
1454                        severity: Some(lsp::DiagnosticSeverity::ERROR),
1455                        message: "unknown variable 'c'".to_string(),
1456                        ..Default::default()
1457                    }],
1458                },
1459                &[],
1460                cx,
1461            )
1462            .unwrap();
1463    });
1464
1465    let main_ignored_buffer = project
1466        .update(cx, |project, cx| {
1467            project.open_buffer((main_worktree_id, "b.rs"), cx)
1468        })
1469        .await
1470        .unwrap();
1471    main_ignored_buffer.update(cx, |buffer, _| {
1472        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
1473        assert_eq!(
1474            chunks
1475                .iter()
1476                .map(|(s, d)| (s.as_str(), *d))
1477                .collect::<Vec<_>>(),
1478            &[
1479                ("let ", None),
1480                ("b", Some(DiagnosticSeverity::ERROR)),
1481                (" = 2;", None),
1482            ],
1483            "Gigitnored buffers should still get in-buffer diagnostics",
1484        );
1485    });
1486    let other_buffer = project
1487        .update(cx, |project, cx| {
1488            project.open_buffer((other_worktree_id, ""), cx)
1489        })
1490        .await
1491        .unwrap();
1492    other_buffer.update(cx, |buffer, _| {
1493        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
1494        assert_eq!(
1495            chunks
1496                .iter()
1497                .map(|(s, d)| (s.as_str(), *d))
1498                .collect::<Vec<_>>(),
1499            &[
1500                ("let b = ", None),
1501                ("c", Some(DiagnosticSeverity::ERROR)),
1502                (";", None),
1503            ],
1504            "Buffers from hidden projects should still get in-buffer diagnostics"
1505        );
1506    });
1507
1508    project.update(cx, |project, cx| {
1509        assert_eq!(project.diagnostic_summaries(false, cx).next(), None);
1510        assert_eq!(
1511            project.diagnostic_summaries(true, cx).collect::<Vec<_>>(),
1512            vec![(
1513                ProjectPath {
1514                    worktree_id: main_worktree_id,
1515                    path: Arc::from(Path::new("b.rs")),
1516                },
1517                server_id,
1518                DiagnosticSummary {
1519                    error_count: 1,
1520                    warning_count: 0,
1521                }
1522            )]
1523        );
1524        assert_eq!(project.diagnostic_summary(false, cx).error_count, 0);
1525        assert_eq!(project.diagnostic_summary(true, cx).error_count, 1);
1526    });
1527}
1528
1529#[gpui::test]
1530async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
1531    init_test(cx);
1532
1533    let progress_token = "the-progress-token";
1534
1535    let fs = FakeFs::new(cx.executor());
1536    fs.insert_tree(
1537        path!("/dir"),
1538        json!({
1539            "a.rs": "fn a() { A }",
1540            "b.rs": "const y: i32 = 1",
1541        }),
1542    )
1543    .await;
1544
1545    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
1546    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1547
1548    language_registry.add(rust_lang());
1549    let mut fake_servers = language_registry.register_fake_lsp(
1550        "Rust",
1551        FakeLspAdapter {
1552            disk_based_diagnostics_progress_token: Some(progress_token.into()),
1553            disk_based_diagnostics_sources: vec!["disk".into()],
1554            ..Default::default()
1555        },
1556    );
1557
1558    let worktree_id = project.update(cx, |p, cx| p.worktrees(cx).next().unwrap().read(cx).id());
1559
1560    // Cause worktree to start the fake language server
1561    let _ = project
1562        .update(cx, |project, cx| {
1563            project.open_local_buffer_with_lsp(path!("/dir/b.rs"), cx)
1564        })
1565        .await
1566        .unwrap();
1567
1568    let mut events = cx.events(&project);
1569
1570    let fake_server = fake_servers.next().await.unwrap();
1571    assert_eq!(
1572        events.next().await.unwrap(),
1573        Event::LanguageServerAdded(
1574            LanguageServerId(0),
1575            fake_server.server.name(),
1576            Some(worktree_id)
1577        ),
1578    );
1579
1580    fake_server
1581        .start_progress(format!("{}/0", progress_token))
1582        .await;
1583    assert_eq!(events.next().await.unwrap(), Event::RefreshInlayHints);
1584    assert_eq!(
1585        events.next().await.unwrap(),
1586        Event::DiskBasedDiagnosticsStarted {
1587            language_server_id: LanguageServerId(0),
1588        }
1589    );
1590
1591    fake_server.notify::<lsp::notification::PublishDiagnostics>(&lsp::PublishDiagnosticsParams {
1592        uri: Url::from_file_path(path!("/dir/a.rs")).unwrap(),
1593        version: None,
1594        diagnostics: vec![lsp::Diagnostic {
1595            range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
1596            severity: Some(lsp::DiagnosticSeverity::ERROR),
1597            message: "undefined variable 'A'".to_string(),
1598            ..Default::default()
1599        }],
1600    });
1601    assert_eq!(
1602        events.next().await.unwrap(),
1603        Event::DiagnosticsUpdated {
1604            language_server_id: LanguageServerId(0),
1605            path: (worktree_id, Path::new("a.rs")).into()
1606        }
1607    );
1608
1609    fake_server.end_progress(format!("{}/0", progress_token));
1610    assert_eq!(
1611        events.next().await.unwrap(),
1612        Event::DiskBasedDiagnosticsFinished {
1613            language_server_id: LanguageServerId(0)
1614        }
1615    );
1616
1617    let buffer = project
1618        .update(cx, |p, cx| p.open_local_buffer(path!("/dir/a.rs"), cx))
1619        .await
1620        .unwrap();
1621
1622    buffer.update(cx, |buffer, _| {
1623        let snapshot = buffer.snapshot();
1624        let diagnostics = snapshot
1625            .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
1626            .collect::<Vec<_>>();
1627        assert_eq!(
1628            diagnostics,
1629            &[DiagnosticEntry {
1630                range: Point::new(0, 9)..Point::new(0, 10),
1631                diagnostic: Diagnostic {
1632                    severity: lsp::DiagnosticSeverity::ERROR,
1633                    message: "undefined variable 'A'".to_string(),
1634                    group_id: 0,
1635                    is_primary: true,
1636                    ..Default::default()
1637                }
1638            }]
1639        )
1640    });
1641
1642    // Ensure publishing empty diagnostics twice only results in one update event.
1643    fake_server.notify::<lsp::notification::PublishDiagnostics>(&lsp::PublishDiagnosticsParams {
1644        uri: Url::from_file_path(path!("/dir/a.rs")).unwrap(),
1645        version: None,
1646        diagnostics: Default::default(),
1647    });
1648    assert_eq!(
1649        events.next().await.unwrap(),
1650        Event::DiagnosticsUpdated {
1651            language_server_id: LanguageServerId(0),
1652            path: (worktree_id, Path::new("a.rs")).into()
1653        }
1654    );
1655
1656    fake_server.notify::<lsp::notification::PublishDiagnostics>(&lsp::PublishDiagnosticsParams {
1657        uri: Url::from_file_path(path!("/dir/a.rs")).unwrap(),
1658        version: None,
1659        diagnostics: Default::default(),
1660    });
1661    cx.executor().run_until_parked();
1662    assert_eq!(futures::poll!(events.next()), Poll::Pending);
1663}
1664
1665#[gpui::test]
1666async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppContext) {
1667    init_test(cx);
1668
1669    let progress_token = "the-progress-token";
1670
1671    let fs = FakeFs::new(cx.executor());
1672    fs.insert_tree(path!("/dir"), json!({ "a.rs": "" })).await;
1673
1674    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
1675
1676    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1677    language_registry.add(rust_lang());
1678    let mut fake_servers = language_registry.register_fake_lsp(
1679        "Rust",
1680        FakeLspAdapter {
1681            name: "the-language-server",
1682            disk_based_diagnostics_sources: vec!["disk".into()],
1683            disk_based_diagnostics_progress_token: Some(progress_token.into()),
1684            ..Default::default()
1685        },
1686    );
1687
1688    let worktree_id = project.update(cx, |p, cx| p.worktrees(cx).next().unwrap().read(cx).id());
1689
1690    let (buffer, _handle) = project
1691        .update(cx, |project, cx| {
1692            project.open_local_buffer_with_lsp(path!("/dir/a.rs"), cx)
1693        })
1694        .await
1695        .unwrap();
1696    // Simulate diagnostics starting to update.
1697    let fake_server = fake_servers.next().await.unwrap();
1698    fake_server.start_progress(progress_token).await;
1699
1700    // Restart the server before the diagnostics finish updating.
1701    project.update(cx, |project, cx| {
1702        project.restart_language_servers_for_buffers(vec![buffer], cx);
1703    });
1704    let mut events = cx.events(&project);
1705
1706    // Simulate the newly started server sending more diagnostics.
1707    let fake_server = fake_servers.next().await.unwrap();
1708    assert_eq!(
1709        events.next().await.unwrap(),
1710        Event::LanguageServerAdded(
1711            LanguageServerId(1),
1712            fake_server.server.name(),
1713            Some(worktree_id)
1714        )
1715    );
1716    assert_eq!(events.next().await.unwrap(), Event::RefreshInlayHints);
1717    fake_server.start_progress(progress_token).await;
1718    assert_eq!(
1719        events.next().await.unwrap(),
1720        Event::DiskBasedDiagnosticsStarted {
1721            language_server_id: LanguageServerId(1)
1722        }
1723    );
1724    project.update(cx, |project, cx| {
1725        assert_eq!(
1726            project
1727                .language_servers_running_disk_based_diagnostics(cx)
1728                .collect::<Vec<_>>(),
1729            [LanguageServerId(1)]
1730        );
1731    });
1732
1733    // All diagnostics are considered done, despite the old server's diagnostic
1734    // task never completing.
1735    fake_server.end_progress(progress_token);
1736    assert_eq!(
1737        events.next().await.unwrap(),
1738        Event::DiskBasedDiagnosticsFinished {
1739            language_server_id: LanguageServerId(1)
1740        }
1741    );
1742    project.update(cx, |project, cx| {
1743        assert_eq!(
1744            project
1745                .language_servers_running_disk_based_diagnostics(cx)
1746                .collect::<Vec<_>>(),
1747            [] as [language::LanguageServerId; 0]
1748        );
1749    });
1750}
1751
1752#[gpui::test]
1753async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAppContext) {
1754    init_test(cx);
1755
1756    let fs = FakeFs::new(cx.executor());
1757    fs.insert_tree(path!("/dir"), json!({ "a.rs": "x" })).await;
1758
1759    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
1760
1761    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1762    language_registry.add(rust_lang());
1763    let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
1764
1765    let (buffer, _) = project
1766        .update(cx, |project, cx| {
1767            project.open_local_buffer_with_lsp(path!("/dir/a.rs"), cx)
1768        })
1769        .await
1770        .unwrap();
1771
1772    // Publish diagnostics
1773    let fake_server = fake_servers.next().await.unwrap();
1774    fake_server.notify::<lsp::notification::PublishDiagnostics>(&lsp::PublishDiagnosticsParams {
1775        uri: Url::from_file_path(path!("/dir/a.rs")).unwrap(),
1776        version: None,
1777        diagnostics: vec![lsp::Diagnostic {
1778            range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
1779            severity: Some(lsp::DiagnosticSeverity::ERROR),
1780            message: "the message".to_string(),
1781            ..Default::default()
1782        }],
1783    });
1784
1785    cx.executor().run_until_parked();
1786    buffer.update(cx, |buffer, _| {
1787        assert_eq!(
1788            buffer
1789                .snapshot()
1790                .diagnostics_in_range::<_, usize>(0..1, false)
1791                .map(|entry| entry.diagnostic.message.clone())
1792                .collect::<Vec<_>>(),
1793            ["the message".to_string()]
1794        );
1795    });
1796    project.update(cx, |project, cx| {
1797        assert_eq!(
1798            project.diagnostic_summary(false, cx),
1799            DiagnosticSummary {
1800                error_count: 1,
1801                warning_count: 0,
1802            }
1803        );
1804    });
1805
1806    project.update(cx, |project, cx| {
1807        project.restart_language_servers_for_buffers(vec![buffer.clone()], cx);
1808    });
1809
1810    // The diagnostics are cleared.
1811    cx.executor().run_until_parked();
1812    buffer.update(cx, |buffer, _| {
1813        assert_eq!(
1814            buffer
1815                .snapshot()
1816                .diagnostics_in_range::<_, usize>(0..1, false)
1817                .map(|entry| entry.diagnostic.message.clone())
1818                .collect::<Vec<_>>(),
1819            Vec::<String>::new(),
1820        );
1821    });
1822    project.update(cx, |project, cx| {
1823        assert_eq!(
1824            project.diagnostic_summary(false, cx),
1825            DiagnosticSummary {
1826                error_count: 0,
1827                warning_count: 0,
1828            }
1829        );
1830    });
1831}
1832
1833#[gpui::test]
1834async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::TestAppContext) {
1835    init_test(cx);
1836
1837    let fs = FakeFs::new(cx.executor());
1838    fs.insert_tree(path!("/dir"), json!({ "a.rs": "" })).await;
1839
1840    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
1841    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1842
1843    language_registry.add(rust_lang());
1844    let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
1845
1846    let (buffer, _handle) = project
1847        .update(cx, |project, cx| {
1848            project.open_local_buffer_with_lsp(path!("/dir/a.rs"), cx)
1849        })
1850        .await
1851        .unwrap();
1852
1853    // Before restarting the server, report diagnostics with an unknown buffer version.
1854    let fake_server = fake_servers.next().await.unwrap();
1855    fake_server.notify::<lsp::notification::PublishDiagnostics>(&lsp::PublishDiagnosticsParams {
1856        uri: lsp::Url::from_file_path(path!("/dir/a.rs")).unwrap(),
1857        version: Some(10000),
1858        diagnostics: Vec::new(),
1859    });
1860    cx.executor().run_until_parked();
1861    project.update(cx, |project, cx| {
1862        project.restart_language_servers_for_buffers(vec![buffer.clone()], cx);
1863    });
1864
1865    let mut fake_server = fake_servers.next().await.unwrap();
1866    let notification = fake_server
1867        .receive_notification::<lsp::notification::DidOpenTextDocument>()
1868        .await
1869        .text_document;
1870    assert_eq!(notification.version, 0);
1871}
1872
1873#[gpui::test]
1874async fn test_cancel_language_server_work(cx: &mut gpui::TestAppContext) {
1875    init_test(cx);
1876
1877    let progress_token = "the-progress-token";
1878
1879    let fs = FakeFs::new(cx.executor());
1880    fs.insert_tree(path!("/dir"), json!({ "a.rs": "" })).await;
1881
1882    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
1883
1884    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1885    language_registry.add(rust_lang());
1886    let mut fake_servers = language_registry.register_fake_lsp(
1887        "Rust",
1888        FakeLspAdapter {
1889            name: "the-language-server",
1890            disk_based_diagnostics_sources: vec!["disk".into()],
1891            disk_based_diagnostics_progress_token: Some(progress_token.into()),
1892            ..Default::default()
1893        },
1894    );
1895
1896    let (buffer, _handle) = project
1897        .update(cx, |project, cx| {
1898            project.open_local_buffer_with_lsp(path!("/dir/a.rs"), cx)
1899        })
1900        .await
1901        .unwrap();
1902
1903    // Simulate diagnostics starting to update.
1904    let mut fake_server = fake_servers.next().await.unwrap();
1905    fake_server
1906        .start_progress_with(
1907            "another-token",
1908            lsp::WorkDoneProgressBegin {
1909                cancellable: Some(false),
1910                ..Default::default()
1911            },
1912        )
1913        .await;
1914    fake_server
1915        .start_progress_with(
1916            progress_token,
1917            lsp::WorkDoneProgressBegin {
1918                cancellable: Some(true),
1919                ..Default::default()
1920            },
1921        )
1922        .await;
1923    cx.executor().run_until_parked();
1924
1925    project.update(cx, |project, cx| {
1926        project.cancel_language_server_work_for_buffers([buffer.clone()], cx)
1927    });
1928
1929    let cancel_notification = fake_server
1930        .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
1931        .await;
1932    assert_eq!(
1933        cancel_notification.token,
1934        NumberOrString::String(progress_token.into())
1935    );
1936}
1937
1938#[gpui::test]
1939async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
1940    init_test(cx);
1941
1942    let fs = FakeFs::new(cx.executor());
1943    fs.insert_tree(path!("/dir"), json!({ "a.rs": "", "b.js": "" }))
1944        .await;
1945
1946    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
1947    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1948
1949    let mut fake_rust_servers = language_registry.register_fake_lsp(
1950        "Rust",
1951        FakeLspAdapter {
1952            name: "rust-lsp",
1953            ..Default::default()
1954        },
1955    );
1956    let mut fake_js_servers = language_registry.register_fake_lsp(
1957        "JavaScript",
1958        FakeLspAdapter {
1959            name: "js-lsp",
1960            ..Default::default()
1961        },
1962    );
1963    language_registry.add(rust_lang());
1964    language_registry.add(js_lang());
1965
1966    let _rs_buffer = project
1967        .update(cx, |project, cx| {
1968            project.open_local_buffer_with_lsp(path!("/dir/a.rs"), cx)
1969        })
1970        .await
1971        .unwrap();
1972    let _js_buffer = project
1973        .update(cx, |project, cx| {
1974            project.open_local_buffer_with_lsp(path!("/dir/b.js"), cx)
1975        })
1976        .await
1977        .unwrap();
1978
1979    let mut fake_rust_server_1 = fake_rust_servers.next().await.unwrap();
1980    assert_eq!(
1981        fake_rust_server_1
1982            .receive_notification::<lsp::notification::DidOpenTextDocument>()
1983            .await
1984            .text_document
1985            .uri
1986            .as_str(),
1987        uri!("file:///dir/a.rs")
1988    );
1989
1990    let mut fake_js_server = fake_js_servers.next().await.unwrap();
1991    assert_eq!(
1992        fake_js_server
1993            .receive_notification::<lsp::notification::DidOpenTextDocument>()
1994            .await
1995            .text_document
1996            .uri
1997            .as_str(),
1998        uri!("file:///dir/b.js")
1999    );
2000
2001    // Disable Rust language server, ensuring only that server gets stopped.
2002    cx.update(|cx| {
2003        SettingsStore::update_global(cx, |settings, cx| {
2004            settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
2005                settings.languages.insert(
2006                    "Rust".into(),
2007                    LanguageSettingsContent {
2008                        enable_language_server: Some(false),
2009                        ..Default::default()
2010                    },
2011                );
2012            });
2013        })
2014    });
2015    fake_rust_server_1
2016        .receive_notification::<lsp::notification::Exit>()
2017        .await;
2018
2019    // Enable Rust and disable JavaScript language servers, ensuring that the
2020    // former gets started again and that the latter stops.
2021    cx.update(|cx| {
2022        SettingsStore::update_global(cx, |settings, cx| {
2023            settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
2024                settings.languages.insert(
2025                    LanguageName::new("Rust"),
2026                    LanguageSettingsContent {
2027                        enable_language_server: Some(true),
2028                        ..Default::default()
2029                    },
2030                );
2031                settings.languages.insert(
2032                    LanguageName::new("JavaScript"),
2033                    LanguageSettingsContent {
2034                        enable_language_server: Some(false),
2035                        ..Default::default()
2036                    },
2037                );
2038            });
2039        })
2040    });
2041    let mut fake_rust_server_2 = fake_rust_servers.next().await.unwrap();
2042    assert_eq!(
2043        fake_rust_server_2
2044            .receive_notification::<lsp::notification::DidOpenTextDocument>()
2045            .await
2046            .text_document
2047            .uri
2048            .as_str(),
2049        uri!("file:///dir/a.rs")
2050    );
2051    fake_js_server
2052        .receive_notification::<lsp::notification::Exit>()
2053        .await;
2054}
2055
2056#[gpui::test(iterations = 3)]
2057async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
2058    init_test(cx);
2059
2060    let text = "
2061        fn a() { A }
2062        fn b() { BB }
2063        fn c() { CCC }
2064    "
2065    .unindent();
2066
2067    let fs = FakeFs::new(cx.executor());
2068    fs.insert_tree(path!("/dir"), json!({ "a.rs": text })).await;
2069
2070    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
2071    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2072
2073    language_registry.add(rust_lang());
2074    let mut fake_servers = language_registry.register_fake_lsp(
2075        "Rust",
2076        FakeLspAdapter {
2077            disk_based_diagnostics_sources: vec!["disk".into()],
2078            ..Default::default()
2079        },
2080    );
2081
2082    let buffer = project
2083        .update(cx, |project, cx| {
2084            project.open_local_buffer(path!("/dir/a.rs"), cx)
2085        })
2086        .await
2087        .unwrap();
2088
2089    let _handle = project.update(cx, |project, cx| {
2090        project.register_buffer_with_language_servers(&buffer, cx)
2091    });
2092
2093    let mut fake_server = fake_servers.next().await.unwrap();
2094    let open_notification = fake_server
2095        .receive_notification::<lsp::notification::DidOpenTextDocument>()
2096        .await;
2097
2098    // Edit the buffer, moving the content down
2099    buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "\n\n")], None, cx));
2100    let change_notification_1 = fake_server
2101        .receive_notification::<lsp::notification::DidChangeTextDocument>()
2102        .await;
2103    assert!(change_notification_1.text_document.version > open_notification.text_document.version);
2104
2105    // Report some diagnostics for the initial version of the buffer
2106    fake_server.notify::<lsp::notification::PublishDiagnostics>(&lsp::PublishDiagnosticsParams {
2107        uri: lsp::Url::from_file_path(path!("/dir/a.rs")).unwrap(),
2108        version: Some(open_notification.text_document.version),
2109        diagnostics: vec![
2110            lsp::Diagnostic {
2111                range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
2112                severity: Some(DiagnosticSeverity::ERROR),
2113                message: "undefined variable 'A'".to_string(),
2114                source: Some("disk".to_string()),
2115                ..Default::default()
2116            },
2117            lsp::Diagnostic {
2118                range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
2119                severity: Some(DiagnosticSeverity::ERROR),
2120                message: "undefined variable 'BB'".to_string(),
2121                source: Some("disk".to_string()),
2122                ..Default::default()
2123            },
2124            lsp::Diagnostic {
2125                range: lsp::Range::new(lsp::Position::new(2, 9), lsp::Position::new(2, 12)),
2126                severity: Some(DiagnosticSeverity::ERROR),
2127                source: Some("disk".to_string()),
2128                message: "undefined variable 'CCC'".to_string(),
2129                ..Default::default()
2130            },
2131        ],
2132    });
2133
2134    // The diagnostics have moved down since they were created.
2135    cx.executor().run_until_parked();
2136    buffer.update(cx, |buffer, _| {
2137        assert_eq!(
2138            buffer
2139                .snapshot()
2140                .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0), false)
2141                .collect::<Vec<_>>(),
2142            &[
2143                DiagnosticEntry {
2144                    range: Point::new(3, 9)..Point::new(3, 11),
2145                    diagnostic: Diagnostic {
2146                        source: Some("disk".into()),
2147                        severity: DiagnosticSeverity::ERROR,
2148                        message: "undefined variable 'BB'".to_string(),
2149                        is_disk_based: true,
2150                        group_id: 1,
2151                        is_primary: true,
2152                        ..Default::default()
2153                    },
2154                },
2155                DiagnosticEntry {
2156                    range: Point::new(4, 9)..Point::new(4, 12),
2157                    diagnostic: Diagnostic {
2158                        source: Some("disk".into()),
2159                        severity: DiagnosticSeverity::ERROR,
2160                        message: "undefined variable 'CCC'".to_string(),
2161                        is_disk_based: true,
2162                        group_id: 2,
2163                        is_primary: true,
2164                        ..Default::default()
2165                    }
2166                }
2167            ]
2168        );
2169        assert_eq!(
2170            chunks_with_diagnostics(buffer, 0..buffer.len()),
2171            [
2172                ("\n\nfn a() { ".to_string(), None),
2173                ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
2174                (" }\nfn b() { ".to_string(), None),
2175                ("BB".to_string(), Some(DiagnosticSeverity::ERROR)),
2176                (" }\nfn c() { ".to_string(), None),
2177                ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)),
2178                (" }\n".to_string(), None),
2179            ]
2180        );
2181        assert_eq!(
2182            chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)),
2183            [
2184                ("B".to_string(), Some(DiagnosticSeverity::ERROR)),
2185                (" }\nfn c() { ".to_string(), None),
2186                ("CC".to_string(), Some(DiagnosticSeverity::ERROR)),
2187            ]
2188        );
2189    });
2190
2191    // Ensure overlapping diagnostics are highlighted correctly.
2192    fake_server.notify::<lsp::notification::PublishDiagnostics>(&lsp::PublishDiagnosticsParams {
2193        uri: lsp::Url::from_file_path(path!("/dir/a.rs")).unwrap(),
2194        version: Some(open_notification.text_document.version),
2195        diagnostics: vec![
2196            lsp::Diagnostic {
2197                range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
2198                severity: Some(DiagnosticSeverity::ERROR),
2199                message: "undefined variable 'A'".to_string(),
2200                source: Some("disk".to_string()),
2201                ..Default::default()
2202            },
2203            lsp::Diagnostic {
2204                range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 12)),
2205                severity: Some(DiagnosticSeverity::WARNING),
2206                message: "unreachable statement".to_string(),
2207                source: Some("disk".to_string()),
2208                ..Default::default()
2209            },
2210        ],
2211    });
2212
2213    cx.executor().run_until_parked();
2214    buffer.update(cx, |buffer, _| {
2215        assert_eq!(
2216            buffer
2217                .snapshot()
2218                .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0), false)
2219                .collect::<Vec<_>>(),
2220            &[
2221                DiagnosticEntry {
2222                    range: Point::new(2, 9)..Point::new(2, 12),
2223                    diagnostic: Diagnostic {
2224                        source: Some("disk".into()),
2225                        severity: DiagnosticSeverity::WARNING,
2226                        message: "unreachable statement".to_string(),
2227                        is_disk_based: true,
2228                        group_id: 4,
2229                        is_primary: true,
2230                        ..Default::default()
2231                    }
2232                },
2233                DiagnosticEntry {
2234                    range: Point::new(2, 9)..Point::new(2, 10),
2235                    diagnostic: Diagnostic {
2236                        source: Some("disk".into()),
2237                        severity: DiagnosticSeverity::ERROR,
2238                        message: "undefined variable 'A'".to_string(),
2239                        is_disk_based: true,
2240                        group_id: 3,
2241                        is_primary: true,
2242                        ..Default::default()
2243                    },
2244                }
2245            ]
2246        );
2247        assert_eq!(
2248            chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)),
2249            [
2250                ("fn a() { ".to_string(), None),
2251                ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
2252                (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
2253                ("\n".to_string(), None),
2254            ]
2255        );
2256        assert_eq!(
2257            chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)),
2258            [
2259                (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
2260                ("\n".to_string(), None),
2261            ]
2262        );
2263    });
2264
2265    // Keep editing the buffer and ensure disk-based diagnostics get translated according to the
2266    // changes since the last save.
2267    buffer.update(cx, |buffer, cx| {
2268        buffer.edit([(Point::new(2, 0)..Point::new(2, 0), "    ")], None, cx);
2269        buffer.edit(
2270            [(Point::new(2, 8)..Point::new(2, 10), "(x: usize)")],
2271            None,
2272            cx,
2273        );
2274        buffer.edit([(Point::new(3, 10)..Point::new(3, 10), "xxx")], None, cx);
2275    });
2276    let change_notification_2 = fake_server
2277        .receive_notification::<lsp::notification::DidChangeTextDocument>()
2278        .await;
2279    assert!(
2280        change_notification_2.text_document.version > change_notification_1.text_document.version
2281    );
2282
2283    // Handle out-of-order diagnostics
2284    fake_server.notify::<lsp::notification::PublishDiagnostics>(&lsp::PublishDiagnosticsParams {
2285        uri: lsp::Url::from_file_path(path!("/dir/a.rs")).unwrap(),
2286        version: Some(change_notification_2.text_document.version),
2287        diagnostics: vec![
2288            lsp::Diagnostic {
2289                range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
2290                severity: Some(DiagnosticSeverity::ERROR),
2291                message: "undefined variable 'BB'".to_string(),
2292                source: Some("disk".to_string()),
2293                ..Default::default()
2294            },
2295            lsp::Diagnostic {
2296                range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
2297                severity: Some(DiagnosticSeverity::WARNING),
2298                message: "undefined variable 'A'".to_string(),
2299                source: Some("disk".to_string()),
2300                ..Default::default()
2301            },
2302        ],
2303    });
2304
2305    cx.executor().run_until_parked();
2306    buffer.update(cx, |buffer, _| {
2307        assert_eq!(
2308            buffer
2309                .snapshot()
2310                .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
2311                .collect::<Vec<_>>(),
2312            &[
2313                DiagnosticEntry {
2314                    range: Point::new(2, 21)..Point::new(2, 22),
2315                    diagnostic: Diagnostic {
2316                        source: Some("disk".into()),
2317                        severity: DiagnosticSeverity::WARNING,
2318                        message: "undefined variable 'A'".to_string(),
2319                        is_disk_based: true,
2320                        group_id: 6,
2321                        is_primary: true,
2322                        ..Default::default()
2323                    }
2324                },
2325                DiagnosticEntry {
2326                    range: Point::new(3, 9)..Point::new(3, 14),
2327                    diagnostic: Diagnostic {
2328                        source: Some("disk".into()),
2329                        severity: DiagnosticSeverity::ERROR,
2330                        message: "undefined variable 'BB'".to_string(),
2331                        is_disk_based: true,
2332                        group_id: 5,
2333                        is_primary: true,
2334                        ..Default::default()
2335                    },
2336                }
2337            ]
2338        );
2339    });
2340}
2341
2342#[gpui::test]
2343async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
2344    init_test(cx);
2345
2346    let text = concat!(
2347        "let one = ;\n", //
2348        "let two = \n",
2349        "let three = 3;\n",
2350    );
2351
2352    let fs = FakeFs::new(cx.executor());
2353    fs.insert_tree("/dir", json!({ "a.rs": text })).await;
2354
2355    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2356    let buffer = project
2357        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
2358        .await
2359        .unwrap();
2360
2361    project.update(cx, |project, cx| {
2362        project.lsp_store.update(cx, |lsp_store, cx| {
2363            lsp_store
2364                .update_diagnostic_entries(
2365                    LanguageServerId(0),
2366                    PathBuf::from("/dir/a.rs"),
2367                    None,
2368                    vec![
2369                        DiagnosticEntry {
2370                            range: Unclipped(PointUtf16::new(0, 10))
2371                                ..Unclipped(PointUtf16::new(0, 10)),
2372                            diagnostic: Diagnostic {
2373                                severity: DiagnosticSeverity::ERROR,
2374                                message: "syntax error 1".to_string(),
2375                                ..Default::default()
2376                            },
2377                        },
2378                        DiagnosticEntry {
2379                            range: Unclipped(PointUtf16::new(1, 10))
2380                                ..Unclipped(PointUtf16::new(1, 10)),
2381                            diagnostic: Diagnostic {
2382                                severity: DiagnosticSeverity::ERROR,
2383                                message: "syntax error 2".to_string(),
2384                                ..Default::default()
2385                            },
2386                        },
2387                    ],
2388                    cx,
2389                )
2390                .unwrap();
2391        })
2392    });
2393
2394    // An empty range is extended forward to include the following character.
2395    // At the end of a line, an empty range is extended backward to include
2396    // the preceding character.
2397    buffer.update(cx, |buffer, _| {
2398        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
2399        assert_eq!(
2400            chunks
2401                .iter()
2402                .map(|(s, d)| (s.as_str(), *d))
2403                .collect::<Vec<_>>(),
2404            &[
2405                ("let one = ", None),
2406                (";", Some(DiagnosticSeverity::ERROR)),
2407                ("\nlet two =", None),
2408                (" ", Some(DiagnosticSeverity::ERROR)),
2409                ("\nlet three = 3;\n", None)
2410            ]
2411        );
2412    });
2413}
2414
2415#[gpui::test]
2416async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppContext) {
2417    init_test(cx);
2418
2419    let fs = FakeFs::new(cx.executor());
2420    fs.insert_tree("/dir", json!({ "a.rs": "one two three" }))
2421        .await;
2422
2423    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2424    let lsp_store = project.read_with(cx, |project, _| project.lsp_store.clone());
2425
2426    lsp_store.update(cx, |lsp_store, cx| {
2427        lsp_store
2428            .update_diagnostic_entries(
2429                LanguageServerId(0),
2430                Path::new("/dir/a.rs").to_owned(),
2431                None,
2432                vec![DiagnosticEntry {
2433                    range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
2434                    diagnostic: Diagnostic {
2435                        severity: DiagnosticSeverity::ERROR,
2436                        is_primary: true,
2437                        message: "syntax error a1".to_string(),
2438                        ..Default::default()
2439                    },
2440                }],
2441                cx,
2442            )
2443            .unwrap();
2444        lsp_store
2445            .update_diagnostic_entries(
2446                LanguageServerId(1),
2447                Path::new("/dir/a.rs").to_owned(),
2448                None,
2449                vec![DiagnosticEntry {
2450                    range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
2451                    diagnostic: Diagnostic {
2452                        severity: DiagnosticSeverity::ERROR,
2453                        is_primary: true,
2454                        message: "syntax error b1".to_string(),
2455                        ..Default::default()
2456                    },
2457                }],
2458                cx,
2459            )
2460            .unwrap();
2461
2462        assert_eq!(
2463            lsp_store.diagnostic_summary(false, cx),
2464            DiagnosticSummary {
2465                error_count: 2,
2466                warning_count: 0,
2467            }
2468        );
2469    });
2470}
2471
2472#[gpui::test]
2473async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) {
2474    init_test(cx);
2475
2476    let text = "
2477        fn a() {
2478            f1();
2479        }
2480        fn b() {
2481            f2();
2482        }
2483        fn c() {
2484            f3();
2485        }
2486    "
2487    .unindent();
2488
2489    let fs = FakeFs::new(cx.executor());
2490    fs.insert_tree(
2491        path!("/dir"),
2492        json!({
2493            "a.rs": text.clone(),
2494        }),
2495    )
2496    .await;
2497
2498    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
2499    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
2500
2501    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2502    language_registry.add(rust_lang());
2503    let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
2504
2505    let (buffer, _handle) = project
2506        .update(cx, |project, cx| {
2507            project.open_local_buffer_with_lsp(path!("/dir/a.rs"), cx)
2508        })
2509        .await
2510        .unwrap();
2511
2512    let mut fake_server = fake_servers.next().await.unwrap();
2513    let lsp_document_version = fake_server
2514        .receive_notification::<lsp::notification::DidOpenTextDocument>()
2515        .await
2516        .text_document
2517        .version;
2518
2519    // Simulate editing the buffer after the language server computes some edits.
2520    buffer.update(cx, |buffer, cx| {
2521        buffer.edit(
2522            [(
2523                Point::new(0, 0)..Point::new(0, 0),
2524                "// above first function\n",
2525            )],
2526            None,
2527            cx,
2528        );
2529        buffer.edit(
2530            [(
2531                Point::new(2, 0)..Point::new(2, 0),
2532                "    // inside first function\n",
2533            )],
2534            None,
2535            cx,
2536        );
2537        buffer.edit(
2538            [(
2539                Point::new(6, 4)..Point::new(6, 4),
2540                "// inside second function ",
2541            )],
2542            None,
2543            cx,
2544        );
2545
2546        assert_eq!(
2547            buffer.text(),
2548            "
2549                // above first function
2550                fn a() {
2551                    // inside first function
2552                    f1();
2553                }
2554                fn b() {
2555                    // inside second function f2();
2556                }
2557                fn c() {
2558                    f3();
2559                }
2560            "
2561            .unindent()
2562        );
2563    });
2564
2565    let edits = lsp_store
2566        .update(cx, |lsp_store, cx| {
2567            lsp_store.as_local_mut().unwrap().edits_from_lsp(
2568                &buffer,
2569                vec![
2570                    // replace body of first function
2571                    lsp::TextEdit {
2572                        range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(3, 0)),
2573                        new_text: "
2574                            fn a() {
2575                                f10();
2576                            }
2577                            "
2578                        .unindent(),
2579                    },
2580                    // edit inside second function
2581                    lsp::TextEdit {
2582                        range: lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 6)),
2583                        new_text: "00".into(),
2584                    },
2585                    // edit inside third function via two distinct edits
2586                    lsp::TextEdit {
2587                        range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 5)),
2588                        new_text: "4000".into(),
2589                    },
2590                    lsp::TextEdit {
2591                        range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 6)),
2592                        new_text: "".into(),
2593                    },
2594                ],
2595                LanguageServerId(0),
2596                Some(lsp_document_version),
2597                cx,
2598            )
2599        })
2600        .await
2601        .unwrap();
2602
2603    buffer.update(cx, |buffer, cx| {
2604        for (range, new_text) in edits {
2605            buffer.edit([(range, new_text)], None, cx);
2606        }
2607        assert_eq!(
2608            buffer.text(),
2609            "
2610                // above first function
2611                fn a() {
2612                    // inside first function
2613                    f10();
2614                }
2615                fn b() {
2616                    // inside second function f200();
2617                }
2618                fn c() {
2619                    f4000();
2620                }
2621                "
2622            .unindent()
2623        );
2624    });
2625}
2626
2627#[gpui::test]
2628async fn test_edits_from_lsp2_with_edits_on_adjacent_lines(cx: &mut gpui::TestAppContext) {
2629    init_test(cx);
2630
2631    let text = "
2632        use a::b;
2633        use a::c;
2634
2635        fn f() {
2636            b();
2637            c();
2638        }
2639    "
2640    .unindent();
2641
2642    let fs = FakeFs::new(cx.executor());
2643    fs.insert_tree(
2644        path!("/dir"),
2645        json!({
2646            "a.rs": text.clone(),
2647        }),
2648    )
2649    .await;
2650
2651    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
2652    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
2653    let buffer = project
2654        .update(cx, |project, cx| {
2655            project.open_local_buffer(path!("/dir/a.rs"), cx)
2656        })
2657        .await
2658        .unwrap();
2659
2660    // Simulate the language server sending us a small edit in the form of a very large diff.
2661    // Rust-analyzer does this when performing a merge-imports code action.
2662    let edits = lsp_store
2663        .update(cx, |lsp_store, cx| {
2664            lsp_store.as_local_mut().unwrap().edits_from_lsp(
2665                &buffer,
2666                [
2667                    // Replace the first use statement without editing the semicolon.
2668                    lsp::TextEdit {
2669                        range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 8)),
2670                        new_text: "a::{b, c}".into(),
2671                    },
2672                    // Reinsert the remainder of the file between the semicolon and the final
2673                    // newline of the file.
2674                    lsp::TextEdit {
2675                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
2676                        new_text: "\n\n".into(),
2677                    },
2678                    lsp::TextEdit {
2679                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
2680                        new_text: "
2681                            fn f() {
2682                                b();
2683                                c();
2684                            }"
2685                        .unindent(),
2686                    },
2687                    // Delete everything after the first newline of the file.
2688                    lsp::TextEdit {
2689                        range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(7, 0)),
2690                        new_text: "".into(),
2691                    },
2692                ],
2693                LanguageServerId(0),
2694                None,
2695                cx,
2696            )
2697        })
2698        .await
2699        .unwrap();
2700
2701    buffer.update(cx, |buffer, cx| {
2702        let edits = edits
2703            .into_iter()
2704            .map(|(range, text)| {
2705                (
2706                    range.start.to_point(buffer)..range.end.to_point(buffer),
2707                    text,
2708                )
2709            })
2710            .collect::<Vec<_>>();
2711
2712        assert_eq!(
2713            edits,
2714            [
2715                (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
2716                (Point::new(1, 0)..Point::new(2, 0), "".into())
2717            ]
2718        );
2719
2720        for (range, new_text) in edits {
2721            buffer.edit([(range, new_text)], None, cx);
2722        }
2723        assert_eq!(
2724            buffer.text(),
2725            "
2726                use a::{b, c};
2727
2728                fn f() {
2729                    b();
2730                    c();
2731                }
2732            "
2733            .unindent()
2734        );
2735    });
2736}
2737
2738#[gpui::test]
2739async fn test_edits_from_lsp_with_replacement_followed_by_adjacent_insertion(
2740    cx: &mut gpui::TestAppContext,
2741) {
2742    init_test(cx);
2743
2744    let text = "Path()";
2745
2746    let fs = FakeFs::new(cx.executor());
2747    fs.insert_tree(
2748        path!("/dir"),
2749        json!({
2750            "a.rs": text
2751        }),
2752    )
2753    .await;
2754
2755    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
2756    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
2757    let buffer = project
2758        .update(cx, |project, cx| {
2759            project.open_local_buffer(path!("/dir/a.rs"), cx)
2760        })
2761        .await
2762        .unwrap();
2763
2764    // Simulate the language server sending us a pair of edits at the same location,
2765    // with an insertion following a replacement (which violates the LSP spec).
2766    let edits = lsp_store
2767        .update(cx, |lsp_store, cx| {
2768            lsp_store.as_local_mut().unwrap().edits_from_lsp(
2769                &buffer,
2770                [
2771                    lsp::TextEdit {
2772                        range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
2773                        new_text: "Path".into(),
2774                    },
2775                    lsp::TextEdit {
2776                        range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
2777                        new_text: "from path import Path\n\n\n".into(),
2778                    },
2779                ],
2780                LanguageServerId(0),
2781                None,
2782                cx,
2783            )
2784        })
2785        .await
2786        .unwrap();
2787
2788    buffer.update(cx, |buffer, cx| {
2789        buffer.edit(edits, None, cx);
2790        assert_eq!(buffer.text(), "from path import Path\n\n\nPath()")
2791    });
2792}
2793
2794#[gpui::test]
2795async fn test_invalid_edits_from_lsp2(cx: &mut gpui::TestAppContext) {
2796    init_test(cx);
2797
2798    let text = "
2799        use a::b;
2800        use a::c;
2801
2802        fn f() {
2803            b();
2804            c();
2805        }
2806    "
2807    .unindent();
2808
2809    let fs = FakeFs::new(cx.executor());
2810    fs.insert_tree(
2811        path!("/dir"),
2812        json!({
2813            "a.rs": text.clone(),
2814        }),
2815    )
2816    .await;
2817
2818    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
2819    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
2820    let buffer = project
2821        .update(cx, |project, cx| {
2822            project.open_local_buffer(path!("/dir/a.rs"), cx)
2823        })
2824        .await
2825        .unwrap();
2826
2827    // Simulate the language server sending us edits in a non-ordered fashion,
2828    // with ranges sometimes being inverted or pointing to invalid locations.
2829    let edits = lsp_store
2830        .update(cx, |lsp_store, cx| {
2831            lsp_store.as_local_mut().unwrap().edits_from_lsp(
2832                &buffer,
2833                [
2834                    lsp::TextEdit {
2835                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
2836                        new_text: "\n\n".into(),
2837                    },
2838                    lsp::TextEdit {
2839                        range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 4)),
2840                        new_text: "a::{b, c}".into(),
2841                    },
2842                    lsp::TextEdit {
2843                        range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(99, 0)),
2844                        new_text: "".into(),
2845                    },
2846                    lsp::TextEdit {
2847                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
2848                        new_text: "
2849                            fn f() {
2850                                b();
2851                                c();
2852                            }"
2853                        .unindent(),
2854                    },
2855                ],
2856                LanguageServerId(0),
2857                None,
2858                cx,
2859            )
2860        })
2861        .await
2862        .unwrap();
2863
2864    buffer.update(cx, |buffer, cx| {
2865        let edits = edits
2866            .into_iter()
2867            .map(|(range, text)| {
2868                (
2869                    range.start.to_point(buffer)..range.end.to_point(buffer),
2870                    text,
2871                )
2872            })
2873            .collect::<Vec<_>>();
2874
2875        assert_eq!(
2876            edits,
2877            [
2878                (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
2879                (Point::new(1, 0)..Point::new(2, 0), "".into())
2880            ]
2881        );
2882
2883        for (range, new_text) in edits {
2884            buffer.edit([(range, new_text)], None, cx);
2885        }
2886        assert_eq!(
2887            buffer.text(),
2888            "
2889                use a::{b, c};
2890
2891                fn f() {
2892                    b();
2893                    c();
2894                }
2895            "
2896            .unindent()
2897        );
2898    });
2899}
2900
2901fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
2902    buffer: &Buffer,
2903    range: Range<T>,
2904) -> Vec<(String, Option<DiagnosticSeverity>)> {
2905    let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
2906    for chunk in buffer.snapshot().chunks(range, true) {
2907        if chunks.last().map_or(false, |prev_chunk| {
2908            prev_chunk.1 == chunk.diagnostic_severity
2909        }) {
2910            chunks.last_mut().unwrap().0.push_str(chunk.text);
2911        } else {
2912            chunks.push((chunk.text.to_string(), chunk.diagnostic_severity));
2913        }
2914    }
2915    chunks
2916}
2917
2918#[gpui::test(iterations = 10)]
2919async fn test_definition(cx: &mut gpui::TestAppContext) {
2920    init_test(cx);
2921
2922    let fs = FakeFs::new(cx.executor());
2923    fs.insert_tree(
2924        path!("/dir"),
2925        json!({
2926            "a.rs": "const fn a() { A }",
2927            "b.rs": "const y: i32 = crate::a()",
2928        }),
2929    )
2930    .await;
2931
2932    let project = Project::test(fs, [path!("/dir/b.rs").as_ref()], cx).await;
2933
2934    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2935    language_registry.add(rust_lang());
2936    let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
2937
2938    let (buffer, _handle) = project
2939        .update(cx, |project, cx| {
2940            project.open_local_buffer_with_lsp(path!("/dir/b.rs"), cx)
2941        })
2942        .await
2943        .unwrap();
2944
2945    let fake_server = fake_servers.next().await.unwrap();
2946    fake_server.set_request_handler::<lsp::request::GotoDefinition, _, _>(|params, _| async move {
2947        let params = params.text_document_position_params;
2948        assert_eq!(
2949            params.text_document.uri.to_file_path().unwrap(),
2950            Path::new(path!("/dir/b.rs")),
2951        );
2952        assert_eq!(params.position, lsp::Position::new(0, 22));
2953
2954        Ok(Some(lsp::GotoDefinitionResponse::Scalar(
2955            lsp::Location::new(
2956                lsp::Url::from_file_path(path!("/dir/a.rs")).unwrap(),
2957                lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
2958            ),
2959        )))
2960    });
2961    let mut definitions = project
2962        .update(cx, |project, cx| project.definition(&buffer, 22, cx))
2963        .await
2964        .unwrap();
2965
2966    // Assert no new language server started
2967    cx.executor().run_until_parked();
2968    assert!(fake_servers.try_next().is_err());
2969
2970    assert_eq!(definitions.len(), 1);
2971    let definition = definitions.pop().unwrap();
2972    cx.update(|cx| {
2973        let target_buffer = definition.target.buffer.read(cx);
2974        assert_eq!(
2975            target_buffer
2976                .file()
2977                .unwrap()
2978                .as_local()
2979                .unwrap()
2980                .abs_path(cx),
2981            Path::new(path!("/dir/a.rs")),
2982        );
2983        assert_eq!(definition.target.range.to_offset(target_buffer), 9..10);
2984        assert_eq!(
2985            list_worktrees(&project, cx),
2986            [
2987                (path!("/dir/a.rs").as_ref(), false),
2988                (path!("/dir/b.rs").as_ref(), true)
2989            ],
2990        );
2991
2992        drop(definition);
2993    });
2994    cx.update(|cx| {
2995        assert_eq!(
2996            list_worktrees(&project, cx),
2997            [(path!("/dir/b.rs").as_ref(), true)]
2998        );
2999    });
3000
3001    fn list_worktrees<'a>(project: &'a Entity<Project>, cx: &'a App) -> Vec<(&'a Path, bool)> {
3002        project
3003            .read(cx)
3004            .worktrees(cx)
3005            .map(|worktree| {
3006                let worktree = worktree.read(cx);
3007                (
3008                    worktree.as_local().unwrap().abs_path().as_ref(),
3009                    worktree.is_visible(),
3010                )
3011            })
3012            .collect::<Vec<_>>()
3013    }
3014}
3015
3016#[gpui::test]
3017async fn test_completions_with_text_edit(cx: &mut gpui::TestAppContext) {
3018    init_test(cx);
3019
3020    let fs = FakeFs::new(cx.executor());
3021    fs.insert_tree(
3022        path!("/dir"),
3023        json!({
3024            "a.ts": "",
3025        }),
3026    )
3027    .await;
3028
3029    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
3030
3031    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3032    language_registry.add(typescript_lang());
3033    let mut fake_language_servers = language_registry.register_fake_lsp(
3034        "TypeScript",
3035        FakeLspAdapter {
3036            capabilities: lsp::ServerCapabilities {
3037                completion_provider: Some(lsp::CompletionOptions {
3038                    trigger_characters: Some(vec![".".to_string()]),
3039                    ..Default::default()
3040                }),
3041                ..Default::default()
3042            },
3043            ..Default::default()
3044        },
3045    );
3046
3047    let (buffer, _handle) = project
3048        .update(cx, |p, cx| {
3049            p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
3050        })
3051        .await
3052        .unwrap();
3053
3054    let fake_server = fake_language_servers.next().await.unwrap();
3055
3056    // When text_edit exists, it takes precedence over insert_text and label
3057    let text = "let a = obj.fqn";
3058    buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
3059    let completions = project.update(cx, |project, cx| {
3060        project.completions(&buffer, text.len(), DEFAULT_COMPLETION_CONTEXT, cx)
3061    });
3062
3063    fake_server
3064        .set_request_handler::<lsp::request::Completion, _, _>(|_, _| async {
3065            Ok(Some(lsp::CompletionResponse::Array(vec![
3066                lsp::CompletionItem {
3067                    label: "labelText".into(),
3068                    insert_text: Some("insertText".into()),
3069                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
3070                        range: lsp::Range::new(
3071                            lsp::Position::new(0, text.len() as u32 - 3),
3072                            lsp::Position::new(0, text.len() as u32),
3073                        ),
3074                        new_text: "textEditText".into(),
3075                    })),
3076                    ..Default::default()
3077                },
3078            ])))
3079        })
3080        .next()
3081        .await;
3082
3083    let completions = completions
3084        .await
3085        .unwrap()
3086        .into_iter()
3087        .flat_map(|response| response.completions)
3088        .collect::<Vec<_>>();
3089    let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
3090
3091    assert_eq!(completions.len(), 1);
3092    assert_eq!(completions[0].new_text, "textEditText");
3093    assert_eq!(
3094        completions[0].replace_range.to_offset(&snapshot),
3095        text.len() - 3..text.len()
3096    );
3097}
3098
3099#[gpui::test]
3100async fn test_completions_with_edit_ranges(cx: &mut gpui::TestAppContext) {
3101    init_test(cx);
3102
3103    let fs = FakeFs::new(cx.executor());
3104    fs.insert_tree(
3105        path!("/dir"),
3106        json!({
3107            "a.ts": "",
3108        }),
3109    )
3110    .await;
3111
3112    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
3113
3114    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3115    language_registry.add(typescript_lang());
3116    let mut fake_language_servers = language_registry.register_fake_lsp(
3117        "TypeScript",
3118        FakeLspAdapter {
3119            capabilities: lsp::ServerCapabilities {
3120                completion_provider: Some(lsp::CompletionOptions {
3121                    trigger_characters: Some(vec![".".to_string()]),
3122                    ..Default::default()
3123                }),
3124                ..Default::default()
3125            },
3126            ..Default::default()
3127        },
3128    );
3129
3130    let (buffer, _handle) = project
3131        .update(cx, |p, cx| {
3132            p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
3133        })
3134        .await
3135        .unwrap();
3136
3137    let fake_server = fake_language_servers.next().await.unwrap();
3138    let text = "let a = obj.fqn";
3139
3140    // Test 1: When text_edit is None but insert_text exists with default edit_range
3141    {
3142        buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
3143        let completions = project.update(cx, |project, cx| {
3144            project.completions(&buffer, text.len(), DEFAULT_COMPLETION_CONTEXT, cx)
3145        });
3146
3147        fake_server
3148            .set_request_handler::<lsp::request::Completion, _, _>(|_, _| async {
3149                Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
3150                    is_incomplete: false,
3151                    item_defaults: Some(lsp::CompletionListItemDefaults {
3152                        edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
3153                            lsp::Range::new(
3154                                lsp::Position::new(0, text.len() as u32 - 3),
3155                                lsp::Position::new(0, text.len() as u32),
3156                            ),
3157                        )),
3158                        ..Default::default()
3159                    }),
3160                    items: vec![lsp::CompletionItem {
3161                        label: "labelText".into(),
3162                        insert_text: Some("insertText".into()),
3163                        text_edit: None,
3164                        ..Default::default()
3165                    }],
3166                })))
3167            })
3168            .next()
3169            .await;
3170
3171        let completions = completions
3172            .await
3173            .unwrap()
3174            .into_iter()
3175            .flat_map(|response| response.completions)
3176            .collect::<Vec<_>>();
3177        let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
3178
3179        assert_eq!(completions.len(), 1);
3180        assert_eq!(completions[0].new_text, "insertText");
3181        assert_eq!(
3182            completions[0].replace_range.to_offset(&snapshot),
3183            text.len() - 3..text.len()
3184        );
3185    }
3186
3187    // Test 2: When both text_edit and insert_text are None with default edit_range
3188    {
3189        buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
3190        let completions = project.update(cx, |project, cx| {
3191            project.completions(&buffer, text.len(), DEFAULT_COMPLETION_CONTEXT, cx)
3192        });
3193
3194        fake_server
3195            .set_request_handler::<lsp::request::Completion, _, _>(|_, _| async {
3196                Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
3197                    is_incomplete: false,
3198                    item_defaults: Some(lsp::CompletionListItemDefaults {
3199                        edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
3200                            lsp::Range::new(
3201                                lsp::Position::new(0, text.len() as u32 - 3),
3202                                lsp::Position::new(0, text.len() as u32),
3203                            ),
3204                        )),
3205                        ..Default::default()
3206                    }),
3207                    items: vec![lsp::CompletionItem {
3208                        label: "labelText".into(),
3209                        insert_text: None,
3210                        text_edit: None,
3211                        ..Default::default()
3212                    }],
3213                })))
3214            })
3215            .next()
3216            .await;
3217
3218        let completions = completions
3219            .await
3220            .unwrap()
3221            .into_iter()
3222            .flat_map(|response| response.completions)
3223            .collect::<Vec<_>>();
3224        let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
3225
3226        assert_eq!(completions.len(), 1);
3227        assert_eq!(completions[0].new_text, "labelText");
3228        assert_eq!(
3229            completions[0].replace_range.to_offset(&snapshot),
3230            text.len() - 3..text.len()
3231        );
3232    }
3233}
3234
3235#[gpui::test]
3236async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
3237    init_test(cx);
3238
3239    let fs = FakeFs::new(cx.executor());
3240    fs.insert_tree(
3241        path!("/dir"),
3242        json!({
3243            "a.ts": "",
3244        }),
3245    )
3246    .await;
3247
3248    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
3249
3250    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3251    language_registry.add(typescript_lang());
3252    let mut fake_language_servers = language_registry.register_fake_lsp(
3253        "TypeScript",
3254        FakeLspAdapter {
3255            capabilities: lsp::ServerCapabilities {
3256                completion_provider: Some(lsp::CompletionOptions {
3257                    trigger_characters: Some(vec![":".to_string()]),
3258                    ..Default::default()
3259                }),
3260                ..Default::default()
3261            },
3262            ..Default::default()
3263        },
3264    );
3265
3266    let (buffer, _handle) = project
3267        .update(cx, |p, cx| {
3268            p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
3269        })
3270        .await
3271        .unwrap();
3272
3273    let fake_server = fake_language_servers.next().await.unwrap();
3274
3275    // Test 1: When text_edit is None but insert_text exists (no edit_range in defaults)
3276    let text = "let a = b.fqn";
3277    buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
3278    let completions = project.update(cx, |project, cx| {
3279        project.completions(&buffer, text.len(), DEFAULT_COMPLETION_CONTEXT, cx)
3280    });
3281
3282    fake_server
3283        .set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
3284            Ok(Some(lsp::CompletionResponse::Array(vec![
3285                lsp::CompletionItem {
3286                    label: "fullyQualifiedName?".into(),
3287                    insert_text: Some("fullyQualifiedName".into()),
3288                    ..Default::default()
3289                },
3290            ])))
3291        })
3292        .next()
3293        .await;
3294    let completions = completions
3295        .await
3296        .unwrap()
3297        .into_iter()
3298        .flat_map(|response| response.completions)
3299        .collect::<Vec<_>>();
3300    let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
3301    assert_eq!(completions.len(), 1);
3302    assert_eq!(completions[0].new_text, "fullyQualifiedName");
3303    assert_eq!(
3304        completions[0].replace_range.to_offset(&snapshot),
3305        text.len() - 3..text.len()
3306    );
3307
3308    // Test 2: When both text_edit and insert_text are None (no edit_range in defaults)
3309    let text = "let a = \"atoms/cmp\"";
3310    buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
3311    let completions = project.update(cx, |project, cx| {
3312        project.completions(&buffer, text.len() - 1, DEFAULT_COMPLETION_CONTEXT, cx)
3313    });
3314
3315    fake_server
3316        .set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
3317            Ok(Some(lsp::CompletionResponse::Array(vec![
3318                lsp::CompletionItem {
3319                    label: "component".into(),
3320                    ..Default::default()
3321                },
3322            ])))
3323        })
3324        .next()
3325        .await;
3326    let completions = completions
3327        .await
3328        .unwrap()
3329        .into_iter()
3330        .flat_map(|response| response.completions)
3331        .collect::<Vec<_>>();
3332    let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
3333    assert_eq!(completions.len(), 1);
3334    assert_eq!(completions[0].new_text, "component");
3335    assert_eq!(
3336        completions[0].replace_range.to_offset(&snapshot),
3337        text.len() - 4..text.len() - 1
3338    );
3339}
3340
3341#[gpui::test]
3342async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
3343    init_test(cx);
3344
3345    let fs = FakeFs::new(cx.executor());
3346    fs.insert_tree(
3347        path!("/dir"),
3348        json!({
3349            "a.ts": "",
3350        }),
3351    )
3352    .await;
3353
3354    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
3355
3356    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3357    language_registry.add(typescript_lang());
3358    let mut fake_language_servers = language_registry.register_fake_lsp(
3359        "TypeScript",
3360        FakeLspAdapter {
3361            capabilities: lsp::ServerCapabilities {
3362                completion_provider: Some(lsp::CompletionOptions {
3363                    trigger_characters: Some(vec![":".to_string()]),
3364                    ..Default::default()
3365                }),
3366                ..Default::default()
3367            },
3368            ..Default::default()
3369        },
3370    );
3371
3372    let (buffer, _handle) = project
3373        .update(cx, |p, cx| {
3374            p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
3375        })
3376        .await
3377        .unwrap();
3378
3379    let fake_server = fake_language_servers.next().await.unwrap();
3380
3381    let text = "let a = b.fqn";
3382    buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
3383    let completions = project.update(cx, |project, cx| {
3384        project.completions(&buffer, text.len(), DEFAULT_COMPLETION_CONTEXT, cx)
3385    });
3386
3387    fake_server
3388        .set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
3389            Ok(Some(lsp::CompletionResponse::Array(vec![
3390                lsp::CompletionItem {
3391                    label: "fullyQualifiedName?".into(),
3392                    insert_text: Some("fully\rQualified\r\nName".into()),
3393                    ..Default::default()
3394                },
3395            ])))
3396        })
3397        .next()
3398        .await;
3399    let completions = completions
3400        .await
3401        .unwrap()
3402        .into_iter()
3403        .flat_map(|response| response.completions)
3404        .collect::<Vec<_>>();
3405    assert_eq!(completions.len(), 1);
3406    assert_eq!(completions[0].new_text, "fully\nQualified\nName");
3407}
3408
3409#[gpui::test(iterations = 10)]
3410async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
3411    init_test(cx);
3412
3413    let fs = FakeFs::new(cx.executor());
3414    fs.insert_tree(
3415        path!("/dir"),
3416        json!({
3417            "a.ts": "a",
3418        }),
3419    )
3420    .await;
3421
3422    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
3423
3424    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3425    language_registry.add(typescript_lang());
3426    let mut fake_language_servers = language_registry.register_fake_lsp(
3427        "TypeScript",
3428        FakeLspAdapter {
3429            capabilities: lsp::ServerCapabilities {
3430                code_action_provider: Some(lsp::CodeActionProviderCapability::Options(
3431                    lsp::CodeActionOptions {
3432                        resolve_provider: Some(true),
3433                        ..lsp::CodeActionOptions::default()
3434                    },
3435                )),
3436                execute_command_provider: Some(lsp::ExecuteCommandOptions {
3437                    commands: vec!["_the/command".to_string()],
3438                    ..lsp::ExecuteCommandOptions::default()
3439                }),
3440                ..lsp::ServerCapabilities::default()
3441            },
3442            ..FakeLspAdapter::default()
3443        },
3444    );
3445
3446    let (buffer, _handle) = project
3447        .update(cx, |p, cx| {
3448            p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
3449        })
3450        .await
3451        .unwrap();
3452
3453    let fake_server = fake_language_servers.next().await.unwrap();
3454
3455    // Language server returns code actions that contain commands, and not edits.
3456    let actions = project.update(cx, |project, cx| {
3457        project.code_actions(&buffer, 0..0, None, cx)
3458    });
3459    fake_server
3460        .set_request_handler::<lsp::request::CodeActionRequest, _, _>(|_, _| async move {
3461            Ok(Some(vec![
3462                lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
3463                    title: "The code action".into(),
3464                    data: Some(serde_json::json!({
3465                        "command": "_the/command",
3466                    })),
3467                    ..lsp::CodeAction::default()
3468                }),
3469                lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
3470                    title: "two".into(),
3471                    ..lsp::CodeAction::default()
3472                }),
3473            ]))
3474        })
3475        .next()
3476        .await;
3477
3478    let action = actions.await.unwrap()[0].clone();
3479    let apply = project.update(cx, |project, cx| {
3480        project.apply_code_action(buffer.clone(), action, true, cx)
3481    });
3482
3483    // Resolving the code action does not populate its edits. In absence of
3484    // edits, we must execute the given command.
3485    fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>(
3486        |mut action, _| async move {
3487            if action.data.is_some() {
3488                action.command = Some(lsp::Command {
3489                    title: "The command".into(),
3490                    command: "_the/command".into(),
3491                    arguments: Some(vec![json!("the-argument")]),
3492                });
3493            }
3494            Ok(action)
3495        },
3496    );
3497
3498    // While executing the command, the language server sends the editor
3499    // a `workspaceEdit` request.
3500    fake_server
3501        .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
3502            let fake = fake_server.clone();
3503            move |params, _| {
3504                assert_eq!(params.command, "_the/command");
3505                let fake = fake.clone();
3506                async move {
3507                    fake.server
3508                        .request::<lsp::request::ApplyWorkspaceEdit>(
3509                            lsp::ApplyWorkspaceEditParams {
3510                                label: None,
3511                                edit: lsp::WorkspaceEdit {
3512                                    changes: Some(
3513                                        [(
3514                                            lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
3515                                            vec![lsp::TextEdit {
3516                                                range: lsp::Range::new(
3517                                                    lsp::Position::new(0, 0),
3518                                                    lsp::Position::new(0, 0),
3519                                                ),
3520                                                new_text: "X".into(),
3521                                            }],
3522                                        )]
3523                                        .into_iter()
3524                                        .collect(),
3525                                    ),
3526                                    ..Default::default()
3527                                },
3528                            },
3529                        )
3530                        .await
3531                        .into_response()
3532                        .unwrap();
3533                    Ok(Some(json!(null)))
3534                }
3535            }
3536        })
3537        .next()
3538        .await;
3539
3540    // Applying the code action returns a project transaction containing the edits
3541    // sent by the language server in its `workspaceEdit` request.
3542    let transaction = apply.await.unwrap();
3543    assert!(transaction.0.contains_key(&buffer));
3544    buffer.update(cx, |buffer, cx| {
3545        assert_eq!(buffer.text(), "Xa");
3546        buffer.undo(cx);
3547        assert_eq!(buffer.text(), "a");
3548    });
3549}
3550
3551#[gpui::test(iterations = 10)]
3552async fn test_save_file(cx: &mut gpui::TestAppContext) {
3553    init_test(cx);
3554
3555    let fs = FakeFs::new(cx.executor());
3556    fs.insert_tree(
3557        path!("/dir"),
3558        json!({
3559            "file1": "the old contents",
3560        }),
3561    )
3562    .await;
3563
3564    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3565    let buffer = project
3566        .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file1"), cx))
3567        .await
3568        .unwrap();
3569    buffer.update(cx, |buffer, cx| {
3570        assert_eq!(buffer.text(), "the old contents");
3571        buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
3572    });
3573
3574    project
3575        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
3576        .await
3577        .unwrap();
3578
3579    let new_text = fs
3580        .load(Path::new(path!("/dir/file1")))
3581        .await
3582        .unwrap()
3583        .replace("\r\n", "\n");
3584    assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text()));
3585}
3586
3587#[gpui::test(iterations = 30)]
3588async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) {
3589    init_test(cx);
3590
3591    let fs = FakeFs::new(cx.executor().clone());
3592    fs.insert_tree(
3593        path!("/dir"),
3594        json!({
3595            "file1": "the original contents",
3596        }),
3597    )
3598    .await;
3599
3600    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3601    let worktree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
3602    let buffer = project
3603        .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file1"), cx))
3604        .await
3605        .unwrap();
3606
3607    // Simulate buffer diffs being slow, so that they don't complete before
3608    // the next file change occurs.
3609    cx.executor().deprioritize(*language::BUFFER_DIFF_TASK);
3610
3611    // Change the buffer's file on disk, and then wait for the file change
3612    // to be detected by the worktree, so that the buffer starts reloading.
3613    fs.save(
3614        path!("/dir/file1").as_ref(),
3615        &"the first contents".into(),
3616        Default::default(),
3617    )
3618    .await
3619    .unwrap();
3620    worktree.next_event(cx).await;
3621
3622    // Change the buffer's file again. Depending on the random seed, the
3623    // previous file change may still be in progress.
3624    fs.save(
3625        path!("/dir/file1").as_ref(),
3626        &"the second contents".into(),
3627        Default::default(),
3628    )
3629    .await
3630    .unwrap();
3631    worktree.next_event(cx).await;
3632
3633    cx.executor().run_until_parked();
3634    let on_disk_text = fs.load(Path::new(path!("/dir/file1"))).await.unwrap();
3635    buffer.read_with(cx, |buffer, _| {
3636        assert_eq!(buffer.text(), on_disk_text);
3637        assert!(!buffer.is_dirty(), "buffer should not be dirty");
3638        assert!(!buffer.has_conflict(), "buffer should not be dirty");
3639    });
3640}
3641
3642#[gpui::test(iterations = 30)]
3643async fn test_edit_buffer_while_it_reloads(cx: &mut gpui::TestAppContext) {
3644    init_test(cx);
3645
3646    let fs = FakeFs::new(cx.executor().clone());
3647    fs.insert_tree(
3648        path!("/dir"),
3649        json!({
3650            "file1": "the original contents",
3651        }),
3652    )
3653    .await;
3654
3655    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3656    let worktree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
3657    let buffer = project
3658        .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file1"), cx))
3659        .await
3660        .unwrap();
3661
3662    // Simulate buffer diffs being slow, so that they don't complete before
3663    // the next file change occurs.
3664    cx.executor().deprioritize(*language::BUFFER_DIFF_TASK);
3665
3666    // Change the buffer's file on disk, and then wait for the file change
3667    // to be detected by the worktree, so that the buffer starts reloading.
3668    fs.save(
3669        path!("/dir/file1").as_ref(),
3670        &"the first contents".into(),
3671        Default::default(),
3672    )
3673    .await
3674    .unwrap();
3675    worktree.next_event(cx).await;
3676
3677    cx.executor()
3678        .spawn(cx.executor().simulate_random_delay())
3679        .await;
3680
3681    // Perform a noop edit, causing the buffer's version to increase.
3682    buffer.update(cx, |buffer, cx| {
3683        buffer.edit([(0..0, " ")], None, cx);
3684        buffer.undo(cx);
3685    });
3686
3687    cx.executor().run_until_parked();
3688    let on_disk_text = fs.load(Path::new(path!("/dir/file1"))).await.unwrap();
3689    buffer.read_with(cx, |buffer, _| {
3690        let buffer_text = buffer.text();
3691        if buffer_text == on_disk_text {
3692            assert!(
3693                !buffer.is_dirty() && !buffer.has_conflict(),
3694                "buffer shouldn't be dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}",
3695            );
3696        }
3697        // If the file change occurred while the buffer was processing the first
3698        // change, the buffer will be in a conflicting state.
3699        else {
3700            assert!(buffer.is_dirty(), "buffer should report that it is dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}");
3701            assert!(buffer.has_conflict(), "buffer should report that it is dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}");
3702        }
3703    });
3704}
3705
3706#[gpui::test]
3707async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) {
3708    init_test(cx);
3709
3710    let fs = FakeFs::new(cx.executor());
3711    fs.insert_tree(
3712        path!("/dir"),
3713        json!({
3714            "file1": "the old contents",
3715        }),
3716    )
3717    .await;
3718
3719    let project = Project::test(fs.clone(), [path!("/dir/file1").as_ref()], cx).await;
3720    let buffer = project
3721        .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file1"), cx))
3722        .await
3723        .unwrap();
3724    buffer.update(cx, |buffer, cx| {
3725        buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
3726    });
3727
3728    project
3729        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
3730        .await
3731        .unwrap();
3732
3733    let new_text = fs
3734        .load(Path::new(path!("/dir/file1")))
3735        .await
3736        .unwrap()
3737        .replace("\r\n", "\n");
3738    assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text()));
3739}
3740
3741#[gpui::test]
3742async fn test_save_as(cx: &mut gpui::TestAppContext) {
3743    init_test(cx);
3744
3745    let fs = FakeFs::new(cx.executor());
3746    fs.insert_tree("/dir", json!({})).await;
3747
3748    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3749
3750    let languages = project.update(cx, |project, _| project.languages().clone());
3751    languages.add(rust_lang());
3752
3753    let buffer = project.update(cx, |project, cx| project.create_local_buffer("", None, cx));
3754    buffer.update(cx, |buffer, cx| {
3755        buffer.edit([(0..0, "abc")], None, cx);
3756        assert!(buffer.is_dirty());
3757        assert!(!buffer.has_conflict());
3758        assert_eq!(buffer.language().unwrap().name(), "Plain Text".into());
3759    });
3760    project
3761        .update(cx, |project, cx| {
3762            let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3763            let path = ProjectPath {
3764                worktree_id,
3765                path: Arc::from(Path::new("file1.rs")),
3766            };
3767            project.save_buffer_as(buffer.clone(), path, cx)
3768        })
3769        .await
3770        .unwrap();
3771    assert_eq!(fs.load(Path::new("/dir/file1.rs")).await.unwrap(), "abc");
3772
3773    cx.executor().run_until_parked();
3774    buffer.update(cx, |buffer, cx| {
3775        assert_eq!(
3776            buffer.file().unwrap().full_path(cx),
3777            Path::new("dir/file1.rs")
3778        );
3779        assert!(!buffer.is_dirty());
3780        assert!(!buffer.has_conflict());
3781        assert_eq!(buffer.language().unwrap().name(), "Rust".into());
3782    });
3783
3784    let opened_buffer = project
3785        .update(cx, |project, cx| {
3786            project.open_local_buffer("/dir/file1.rs", cx)
3787        })
3788        .await
3789        .unwrap();
3790    assert_eq!(opened_buffer, buffer);
3791}
3792
3793#[gpui::test(retries = 5)]
3794async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
3795    use worktree::WorktreeModelHandle as _;
3796
3797    init_test(cx);
3798    cx.executor().allow_parking();
3799
3800    let dir = TempTree::new(json!({
3801        "a": {
3802            "file1": "",
3803            "file2": "",
3804            "file3": "",
3805        },
3806        "b": {
3807            "c": {
3808                "file4": "",
3809                "file5": "",
3810            }
3811        }
3812    }));
3813
3814    let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [dir.path()], cx).await;
3815
3816    let buffer_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
3817        let buffer = project.update(cx, |p, cx| p.open_local_buffer(dir.path().join(path), cx));
3818        async move { buffer.await.unwrap() }
3819    };
3820    let id_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
3821        project.update(cx, |project, cx| {
3822            let tree = project.worktrees(cx).next().unwrap();
3823            tree.read(cx)
3824                .entry_for_path(path)
3825                .unwrap_or_else(|| panic!("no entry for path {}", path))
3826                .id
3827        })
3828    };
3829
3830    let buffer2 = buffer_for_path("a/file2", cx).await;
3831    let buffer3 = buffer_for_path("a/file3", cx).await;
3832    let buffer4 = buffer_for_path("b/c/file4", cx).await;
3833    let buffer5 = buffer_for_path("b/c/file5", cx).await;
3834
3835    let file2_id = id_for_path("a/file2", cx);
3836    let file3_id = id_for_path("a/file3", cx);
3837    let file4_id = id_for_path("b/c/file4", cx);
3838
3839    // Create a remote copy of this worktree.
3840    let tree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
3841    let metadata = tree.update(cx, |tree, _| tree.metadata_proto());
3842
3843    let updates = Arc::new(Mutex::new(Vec::new()));
3844    tree.update(cx, |tree, cx| {
3845        let updates = updates.clone();
3846        tree.observe_updates(0, cx, move |update| {
3847            updates.lock().push(update);
3848            async { true }
3849        });
3850    });
3851
3852    let remote =
3853        cx.update(|cx| Worktree::remote(0, 1, metadata, project.read(cx).client().into(), cx));
3854
3855    cx.executor().run_until_parked();
3856
3857    cx.update(|cx| {
3858        assert!(!buffer2.read(cx).is_dirty());
3859        assert!(!buffer3.read(cx).is_dirty());
3860        assert!(!buffer4.read(cx).is_dirty());
3861        assert!(!buffer5.read(cx).is_dirty());
3862    });
3863
3864    // Rename and delete files and directories.
3865    tree.flush_fs_events(cx).await;
3866    std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap();
3867    std::fs::remove_file(dir.path().join("b/c/file5")).unwrap();
3868    std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap();
3869    std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap();
3870    tree.flush_fs_events(cx).await;
3871
3872    cx.update(|app| {
3873        assert_eq!(
3874            tree.read(app)
3875                .paths()
3876                .map(|p| p.to_str().unwrap())
3877                .collect::<Vec<_>>(),
3878            vec![
3879                "a",
3880                separator!("a/file1"),
3881                separator!("a/file2.new"),
3882                "b",
3883                "d",
3884                separator!("d/file3"),
3885                separator!("d/file4"),
3886            ]
3887        );
3888    });
3889
3890    assert_eq!(id_for_path("a/file2.new", cx), file2_id);
3891    assert_eq!(id_for_path("d/file3", cx), file3_id);
3892    assert_eq!(id_for_path("d/file4", cx), file4_id);
3893
3894    cx.update(|cx| {
3895        assert_eq!(
3896            buffer2.read(cx).file().unwrap().path().as_ref(),
3897            Path::new("a/file2.new")
3898        );
3899        assert_eq!(
3900            buffer3.read(cx).file().unwrap().path().as_ref(),
3901            Path::new("d/file3")
3902        );
3903        assert_eq!(
3904            buffer4.read(cx).file().unwrap().path().as_ref(),
3905            Path::new("d/file4")
3906        );
3907        assert_eq!(
3908            buffer5.read(cx).file().unwrap().path().as_ref(),
3909            Path::new("b/c/file5")
3910        );
3911
3912        assert_matches!(
3913            buffer2.read(cx).file().unwrap().disk_state(),
3914            DiskState::Present { .. }
3915        );
3916        assert_matches!(
3917            buffer3.read(cx).file().unwrap().disk_state(),
3918            DiskState::Present { .. }
3919        );
3920        assert_matches!(
3921            buffer4.read(cx).file().unwrap().disk_state(),
3922            DiskState::Present { .. }
3923        );
3924        assert_eq!(
3925            buffer5.read(cx).file().unwrap().disk_state(),
3926            DiskState::Deleted
3927        );
3928    });
3929
3930    // Update the remote worktree. Check that it becomes consistent with the
3931    // local worktree.
3932    cx.executor().run_until_parked();
3933
3934    remote.update(cx, |remote, _| {
3935        for update in updates.lock().drain(..) {
3936            remote.as_remote_mut().unwrap().update_from_remote(update);
3937        }
3938    });
3939    cx.executor().run_until_parked();
3940    remote.update(cx, |remote, _| {
3941        assert_eq!(
3942            remote
3943                .paths()
3944                .map(|p| p.to_str().unwrap())
3945                .collect::<Vec<_>>(),
3946            vec![
3947                "a",
3948                separator!("a/file1"),
3949                separator!("a/file2.new"),
3950                "b",
3951                "d",
3952                separator!("d/file3"),
3953                separator!("d/file4"),
3954            ]
3955        );
3956    });
3957}
3958
3959#[gpui::test(iterations = 10)]
3960async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) {
3961    init_test(cx);
3962
3963    let fs = FakeFs::new(cx.executor());
3964    fs.insert_tree(
3965        path!("/dir"),
3966        json!({
3967            "a": {
3968                "file1": "",
3969            }
3970        }),
3971    )
3972    .await;
3973
3974    let project = Project::test(fs, [Path::new(path!("/dir"))], cx).await;
3975    let tree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
3976    let tree_id = tree.update(cx, |tree, _| tree.id());
3977
3978    let id_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
3979        project.update(cx, |project, cx| {
3980            let tree = project.worktrees(cx).next().unwrap();
3981            tree.read(cx)
3982                .entry_for_path(path)
3983                .unwrap_or_else(|| panic!("no entry for path {}", path))
3984                .id
3985        })
3986    };
3987
3988    let dir_id = id_for_path("a", cx);
3989    let file_id = id_for_path("a/file1", cx);
3990    let buffer = project
3991        .update(cx, |p, cx| p.open_buffer((tree_id, "a/file1"), cx))
3992        .await
3993        .unwrap();
3994    buffer.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
3995
3996    project
3997        .update(cx, |project, cx| {
3998            project.rename_entry(dir_id, Path::new("b"), cx)
3999        })
4000        .unwrap()
4001        .await
4002        .to_included()
4003        .unwrap();
4004    cx.executor().run_until_parked();
4005
4006    assert_eq!(id_for_path("b", cx), dir_id);
4007    assert_eq!(id_for_path("b/file1", cx), file_id);
4008    buffer.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
4009}
4010
4011#[gpui::test]
4012async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) {
4013    init_test(cx);
4014
4015    let fs = FakeFs::new(cx.executor());
4016    fs.insert_tree(
4017        "/dir",
4018        json!({
4019            "a.txt": "a-contents",
4020            "b.txt": "b-contents",
4021        }),
4022    )
4023    .await;
4024
4025    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
4026
4027    // Spawn multiple tasks to open paths, repeating some paths.
4028    let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| {
4029        (
4030            p.open_local_buffer("/dir/a.txt", cx),
4031            p.open_local_buffer("/dir/b.txt", cx),
4032            p.open_local_buffer("/dir/a.txt", cx),
4033        )
4034    });
4035
4036    let buffer_a_1 = buffer_a_1.await.unwrap();
4037    let buffer_a_2 = buffer_a_2.await.unwrap();
4038    let buffer_b = buffer_b.await.unwrap();
4039    assert_eq!(buffer_a_1.update(cx, |b, _| b.text()), "a-contents");
4040    assert_eq!(buffer_b.update(cx, |b, _| b.text()), "b-contents");
4041
4042    // There is only one buffer per path.
4043    let buffer_a_id = buffer_a_1.entity_id();
4044    assert_eq!(buffer_a_2.entity_id(), buffer_a_id);
4045
4046    // Open the same path again while it is still open.
4047    drop(buffer_a_1);
4048    let buffer_a_3 = project
4049        .update(cx, |p, cx| p.open_local_buffer("/dir/a.txt", cx))
4050        .await
4051        .unwrap();
4052
4053    // There's still only one buffer per path.
4054    assert_eq!(buffer_a_3.entity_id(), buffer_a_id);
4055}
4056
4057#[gpui::test]
4058async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
4059    init_test(cx);
4060
4061    let fs = FakeFs::new(cx.executor());
4062    fs.insert_tree(
4063        path!("/dir"),
4064        json!({
4065            "file1": "abc",
4066            "file2": "def",
4067            "file3": "ghi",
4068        }),
4069    )
4070    .await;
4071
4072    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4073
4074    let buffer1 = project
4075        .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file1"), cx))
4076        .await
4077        .unwrap();
4078    let events = Arc::new(Mutex::new(Vec::new()));
4079
4080    // initially, the buffer isn't dirty.
4081    buffer1.update(cx, |buffer, cx| {
4082        cx.subscribe(&buffer1, {
4083            let events = events.clone();
4084            move |_, _, event, _| match event {
4085                BufferEvent::Operation { .. } => {}
4086                _ => events.lock().push(event.clone()),
4087            }
4088        })
4089        .detach();
4090
4091        assert!(!buffer.is_dirty());
4092        assert!(events.lock().is_empty());
4093
4094        buffer.edit([(1..2, "")], None, cx);
4095    });
4096
4097    // after the first edit, the buffer is dirty, and emits a dirtied event.
4098    buffer1.update(cx, |buffer, cx| {
4099        assert!(buffer.text() == "ac");
4100        assert!(buffer.is_dirty());
4101        assert_eq!(
4102            *events.lock(),
4103            &[
4104                language::BufferEvent::Edited,
4105                language::BufferEvent::DirtyChanged
4106            ]
4107        );
4108        events.lock().clear();
4109        buffer.did_save(
4110            buffer.version(),
4111            buffer.file().unwrap().disk_state().mtime(),
4112            cx,
4113        );
4114    });
4115
4116    // after saving, the buffer is not dirty, and emits a saved event.
4117    buffer1.update(cx, |buffer, cx| {
4118        assert!(!buffer.is_dirty());
4119        assert_eq!(*events.lock(), &[language::BufferEvent::Saved]);
4120        events.lock().clear();
4121
4122        buffer.edit([(1..1, "B")], None, cx);
4123        buffer.edit([(2..2, "D")], None, cx);
4124    });
4125
4126    // after editing again, the buffer is dirty, and emits another dirty event.
4127    buffer1.update(cx, |buffer, cx| {
4128        assert!(buffer.text() == "aBDc");
4129        assert!(buffer.is_dirty());
4130        assert_eq!(
4131            *events.lock(),
4132            &[
4133                language::BufferEvent::Edited,
4134                language::BufferEvent::DirtyChanged,
4135                language::BufferEvent::Edited,
4136            ],
4137        );
4138        events.lock().clear();
4139
4140        // After restoring the buffer to its previously-saved state,
4141        // the buffer is not considered dirty anymore.
4142        buffer.edit([(1..3, "")], None, cx);
4143        assert!(buffer.text() == "ac");
4144        assert!(!buffer.is_dirty());
4145    });
4146
4147    assert_eq!(
4148        *events.lock(),
4149        &[
4150            language::BufferEvent::Edited,
4151            language::BufferEvent::DirtyChanged
4152        ]
4153    );
4154
4155    // When a file is deleted, it is not considered dirty.
4156    let events = Arc::new(Mutex::new(Vec::new()));
4157    let buffer2 = project
4158        .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file2"), cx))
4159        .await
4160        .unwrap();
4161    buffer2.update(cx, |_, cx| {
4162        cx.subscribe(&buffer2, {
4163            let events = events.clone();
4164            move |_, _, event, _| match event {
4165                BufferEvent::Operation { .. } => {}
4166                _ => events.lock().push(event.clone()),
4167            }
4168        })
4169        .detach();
4170    });
4171
4172    fs.remove_file(path!("/dir/file2").as_ref(), Default::default())
4173        .await
4174        .unwrap();
4175    cx.executor().run_until_parked();
4176    buffer2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
4177    assert_eq!(
4178        mem::take(&mut *events.lock()),
4179        &[language::BufferEvent::FileHandleChanged]
4180    );
4181
4182    // Buffer becomes dirty when edited.
4183    buffer2.update(cx, |buffer, cx| {
4184        buffer.edit([(2..3, "")], None, cx);
4185        assert_eq!(buffer.is_dirty(), true);
4186    });
4187    assert_eq!(
4188        mem::take(&mut *events.lock()),
4189        &[
4190            language::BufferEvent::Edited,
4191            language::BufferEvent::DirtyChanged
4192        ]
4193    );
4194
4195    // Buffer becomes clean again when all of its content is removed, because
4196    // the file was deleted.
4197    buffer2.update(cx, |buffer, cx| {
4198        buffer.edit([(0..2, "")], None, cx);
4199        assert_eq!(buffer.is_empty(), true);
4200        assert_eq!(buffer.is_dirty(), false);
4201    });
4202    assert_eq!(
4203        *events.lock(),
4204        &[
4205            language::BufferEvent::Edited,
4206            language::BufferEvent::DirtyChanged
4207        ]
4208    );
4209
4210    // When a file is already dirty when deleted, we don't emit a Dirtied event.
4211    let events = Arc::new(Mutex::new(Vec::new()));
4212    let buffer3 = project
4213        .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file3"), cx))
4214        .await
4215        .unwrap();
4216    buffer3.update(cx, |_, cx| {
4217        cx.subscribe(&buffer3, {
4218            let events = events.clone();
4219            move |_, _, event, _| match event {
4220                BufferEvent::Operation { .. } => {}
4221                _ => events.lock().push(event.clone()),
4222            }
4223        })
4224        .detach();
4225    });
4226
4227    buffer3.update(cx, |buffer, cx| {
4228        buffer.edit([(0..0, "x")], None, cx);
4229    });
4230    events.lock().clear();
4231    fs.remove_file(path!("/dir/file3").as_ref(), Default::default())
4232        .await
4233        .unwrap();
4234    cx.executor().run_until_parked();
4235    assert_eq!(*events.lock(), &[language::BufferEvent::FileHandleChanged]);
4236    cx.update(|cx| assert!(buffer3.read(cx).is_dirty()));
4237}
4238
4239#[gpui::test]
4240async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
4241    init_test(cx);
4242
4243    let (initial_contents, initial_offsets) =
4244        marked_text_offsets("one twoˇ\nthree ˇfourˇ five\nsixˇ seven\n");
4245    let fs = FakeFs::new(cx.executor());
4246    fs.insert_tree(
4247        path!("/dir"),
4248        json!({
4249            "the-file": initial_contents,
4250        }),
4251    )
4252    .await;
4253    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4254    let buffer = project
4255        .update(cx, |p, cx| p.open_local_buffer(path!("/dir/the-file"), cx))
4256        .await
4257        .unwrap();
4258
4259    let anchors = initial_offsets
4260        .iter()
4261        .map(|offset| buffer.update(cx, |b, _| b.anchor_before(offset)))
4262        .collect::<Vec<_>>();
4263
4264    // Change the file on disk, adding two new lines of text, and removing
4265    // one line.
4266    buffer.update(cx, |buffer, _| {
4267        assert!(!buffer.is_dirty());
4268        assert!(!buffer.has_conflict());
4269    });
4270
4271    let (new_contents, new_offsets) =
4272        marked_text_offsets("oneˇ\nthree ˇFOURˇ five\nsixtyˇ seven\n");
4273    fs.save(
4274        path!("/dir/the-file").as_ref(),
4275        &new_contents.as_str().into(),
4276        LineEnding::Unix,
4277    )
4278    .await
4279    .unwrap();
4280
4281    // Because the buffer was not modified, it is reloaded from disk. Its
4282    // contents are edited according to the diff between the old and new
4283    // file contents.
4284    cx.executor().run_until_parked();
4285    buffer.update(cx, |buffer, _| {
4286        assert_eq!(buffer.text(), new_contents);
4287        assert!(!buffer.is_dirty());
4288        assert!(!buffer.has_conflict());
4289
4290        let anchor_offsets = anchors
4291            .iter()
4292            .map(|anchor| anchor.to_offset(&*buffer))
4293            .collect::<Vec<_>>();
4294        assert_eq!(anchor_offsets, new_offsets);
4295    });
4296
4297    // Modify the buffer
4298    buffer.update(cx, |buffer, cx| {
4299        buffer.edit([(0..0, " ")], None, cx);
4300        assert!(buffer.is_dirty());
4301        assert!(!buffer.has_conflict());
4302    });
4303
4304    // Change the file on disk again, adding blank lines to the beginning.
4305    fs.save(
4306        path!("/dir/the-file").as_ref(),
4307        &"\n\n\nAAAA\naaa\nBB\nbbbbb\n".into(),
4308        LineEnding::Unix,
4309    )
4310    .await
4311    .unwrap();
4312
4313    // Because the buffer is modified, it doesn't reload from disk, but is
4314    // marked as having a conflict.
4315    cx.executor().run_until_parked();
4316    buffer.update(cx, |buffer, _| {
4317        assert_eq!(buffer.text(), " ".to_string() + &new_contents);
4318        assert!(buffer.has_conflict());
4319    });
4320}
4321
4322#[gpui::test]
4323async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) {
4324    init_test(cx);
4325
4326    let fs = FakeFs::new(cx.executor());
4327    fs.insert_tree(
4328        path!("/dir"),
4329        json!({
4330            "file1": "a\nb\nc\n",
4331            "file2": "one\r\ntwo\r\nthree\r\n",
4332        }),
4333    )
4334    .await;
4335
4336    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4337    let buffer1 = project
4338        .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file1"), cx))
4339        .await
4340        .unwrap();
4341    let buffer2 = project
4342        .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file2"), cx))
4343        .await
4344        .unwrap();
4345
4346    buffer1.update(cx, |buffer, _| {
4347        assert_eq!(buffer.text(), "a\nb\nc\n");
4348        assert_eq!(buffer.line_ending(), LineEnding::Unix);
4349    });
4350    buffer2.update(cx, |buffer, _| {
4351        assert_eq!(buffer.text(), "one\ntwo\nthree\n");
4352        assert_eq!(buffer.line_ending(), LineEnding::Windows);
4353    });
4354
4355    // Change a file's line endings on disk from unix to windows. The buffer's
4356    // state updates correctly.
4357    fs.save(
4358        path!("/dir/file1").as_ref(),
4359        &"aaa\nb\nc\n".into(),
4360        LineEnding::Windows,
4361    )
4362    .await
4363    .unwrap();
4364    cx.executor().run_until_parked();
4365    buffer1.update(cx, |buffer, _| {
4366        assert_eq!(buffer.text(), "aaa\nb\nc\n");
4367        assert_eq!(buffer.line_ending(), LineEnding::Windows);
4368    });
4369
4370    // Save a file with windows line endings. The file is written correctly.
4371    buffer2.update(cx, |buffer, cx| {
4372        buffer.set_text("one\ntwo\nthree\nfour\n", cx);
4373    });
4374    project
4375        .update(cx, |project, cx| project.save_buffer(buffer2, cx))
4376        .await
4377        .unwrap();
4378    assert_eq!(
4379        fs.load(path!("/dir/file2").as_ref()).await.unwrap(),
4380        "one\r\ntwo\r\nthree\r\nfour\r\n",
4381    );
4382}
4383
4384#[gpui::test]
4385async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
4386    init_test(cx);
4387
4388    let fs = FakeFs::new(cx.executor());
4389    fs.insert_tree(
4390        path!("/dir"),
4391        json!({
4392            "a.rs": "
4393                fn foo(mut v: Vec<usize>) {
4394                    for x in &v {
4395                        v.push(1);
4396                    }
4397                }
4398            "
4399            .unindent(),
4400        }),
4401    )
4402    .await;
4403
4404    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4405    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
4406    let buffer = project
4407        .update(cx, |p, cx| p.open_local_buffer(path!("/dir/a.rs"), cx))
4408        .await
4409        .unwrap();
4410
4411    let buffer_uri = Url::from_file_path(path!("/dir/a.rs")).unwrap();
4412    let message = lsp::PublishDiagnosticsParams {
4413        uri: buffer_uri.clone(),
4414        diagnostics: vec![
4415            lsp::Diagnostic {
4416                range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
4417                severity: Some(DiagnosticSeverity::WARNING),
4418                message: "error 1".to_string(),
4419                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
4420                    location: lsp::Location {
4421                        uri: buffer_uri.clone(),
4422                        range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
4423                    },
4424                    message: "error 1 hint 1".to_string(),
4425                }]),
4426                ..Default::default()
4427            },
4428            lsp::Diagnostic {
4429                range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
4430                severity: Some(DiagnosticSeverity::HINT),
4431                message: "error 1 hint 1".to_string(),
4432                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
4433                    location: lsp::Location {
4434                        uri: buffer_uri.clone(),
4435                        range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
4436                    },
4437                    message: "original diagnostic".to_string(),
4438                }]),
4439                ..Default::default()
4440            },
4441            lsp::Diagnostic {
4442                range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
4443                severity: Some(DiagnosticSeverity::ERROR),
4444                message: "error 2".to_string(),
4445                related_information: Some(vec![
4446                    lsp::DiagnosticRelatedInformation {
4447                        location: lsp::Location {
4448                            uri: buffer_uri.clone(),
4449                            range: lsp::Range::new(
4450                                lsp::Position::new(1, 13),
4451                                lsp::Position::new(1, 15),
4452                            ),
4453                        },
4454                        message: "error 2 hint 1".to_string(),
4455                    },
4456                    lsp::DiagnosticRelatedInformation {
4457                        location: lsp::Location {
4458                            uri: buffer_uri.clone(),
4459                            range: lsp::Range::new(
4460                                lsp::Position::new(1, 13),
4461                                lsp::Position::new(1, 15),
4462                            ),
4463                        },
4464                        message: "error 2 hint 2".to_string(),
4465                    },
4466                ]),
4467                ..Default::default()
4468            },
4469            lsp::Diagnostic {
4470                range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
4471                severity: Some(DiagnosticSeverity::HINT),
4472                message: "error 2 hint 1".to_string(),
4473                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
4474                    location: lsp::Location {
4475                        uri: buffer_uri.clone(),
4476                        range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
4477                    },
4478                    message: "original diagnostic".to_string(),
4479                }]),
4480                ..Default::default()
4481            },
4482            lsp::Diagnostic {
4483                range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
4484                severity: Some(DiagnosticSeverity::HINT),
4485                message: "error 2 hint 2".to_string(),
4486                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
4487                    location: lsp::Location {
4488                        uri: buffer_uri,
4489                        range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
4490                    },
4491                    message: "original diagnostic".to_string(),
4492                }]),
4493                ..Default::default()
4494            },
4495        ],
4496        version: None,
4497    };
4498
4499    lsp_store
4500        .update(cx, |lsp_store, cx| {
4501            lsp_store.update_diagnostics(LanguageServerId(0), message, &[], cx)
4502        })
4503        .unwrap();
4504    let buffer = buffer.update(cx, |buffer, _| buffer.snapshot());
4505
4506    assert_eq!(
4507        buffer
4508            .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
4509            .collect::<Vec<_>>(),
4510        &[
4511            DiagnosticEntry {
4512                range: Point::new(1, 8)..Point::new(1, 9),
4513                diagnostic: Diagnostic {
4514                    severity: DiagnosticSeverity::WARNING,
4515                    message: "error 1".to_string(),
4516                    group_id: 1,
4517                    is_primary: true,
4518                    ..Default::default()
4519                }
4520            },
4521            DiagnosticEntry {
4522                range: Point::new(1, 8)..Point::new(1, 9),
4523                diagnostic: Diagnostic {
4524                    severity: DiagnosticSeverity::HINT,
4525                    message: "error 1 hint 1".to_string(),
4526                    group_id: 1,
4527                    is_primary: false,
4528                    ..Default::default()
4529                }
4530            },
4531            DiagnosticEntry {
4532                range: Point::new(1, 13)..Point::new(1, 15),
4533                diagnostic: Diagnostic {
4534                    severity: DiagnosticSeverity::HINT,
4535                    message: "error 2 hint 1".to_string(),
4536                    group_id: 0,
4537                    is_primary: false,
4538                    ..Default::default()
4539                }
4540            },
4541            DiagnosticEntry {
4542                range: Point::new(1, 13)..Point::new(1, 15),
4543                diagnostic: Diagnostic {
4544                    severity: DiagnosticSeverity::HINT,
4545                    message: "error 2 hint 2".to_string(),
4546                    group_id: 0,
4547                    is_primary: false,
4548                    ..Default::default()
4549                }
4550            },
4551            DiagnosticEntry {
4552                range: Point::new(2, 8)..Point::new(2, 17),
4553                diagnostic: Diagnostic {
4554                    severity: DiagnosticSeverity::ERROR,
4555                    message: "error 2".to_string(),
4556                    group_id: 0,
4557                    is_primary: true,
4558                    ..Default::default()
4559                }
4560            }
4561        ]
4562    );
4563
4564    assert_eq!(
4565        buffer.diagnostic_group::<Point>(0).collect::<Vec<_>>(),
4566        &[
4567            DiagnosticEntry {
4568                range: Point::new(1, 13)..Point::new(1, 15),
4569                diagnostic: Diagnostic {
4570                    severity: DiagnosticSeverity::HINT,
4571                    message: "error 2 hint 1".to_string(),
4572                    group_id: 0,
4573                    is_primary: false,
4574                    ..Default::default()
4575                }
4576            },
4577            DiagnosticEntry {
4578                range: Point::new(1, 13)..Point::new(1, 15),
4579                diagnostic: Diagnostic {
4580                    severity: DiagnosticSeverity::HINT,
4581                    message: "error 2 hint 2".to_string(),
4582                    group_id: 0,
4583                    is_primary: false,
4584                    ..Default::default()
4585                }
4586            },
4587            DiagnosticEntry {
4588                range: Point::new(2, 8)..Point::new(2, 17),
4589                diagnostic: Diagnostic {
4590                    severity: DiagnosticSeverity::ERROR,
4591                    message: "error 2".to_string(),
4592                    group_id: 0,
4593                    is_primary: true,
4594                    ..Default::default()
4595                }
4596            }
4597        ]
4598    );
4599
4600    assert_eq!(
4601        buffer.diagnostic_group::<Point>(1).collect::<Vec<_>>(),
4602        &[
4603            DiagnosticEntry {
4604                range: Point::new(1, 8)..Point::new(1, 9),
4605                diagnostic: Diagnostic {
4606                    severity: DiagnosticSeverity::WARNING,
4607                    message: "error 1".to_string(),
4608                    group_id: 1,
4609                    is_primary: true,
4610                    ..Default::default()
4611                }
4612            },
4613            DiagnosticEntry {
4614                range: Point::new(1, 8)..Point::new(1, 9),
4615                diagnostic: Diagnostic {
4616                    severity: DiagnosticSeverity::HINT,
4617                    message: "error 1 hint 1".to_string(),
4618                    group_id: 1,
4619                    is_primary: false,
4620                    ..Default::default()
4621                }
4622            },
4623        ]
4624    );
4625}
4626
4627#[gpui::test]
4628async fn test_lsp_rename_notifications(cx: &mut gpui::TestAppContext) {
4629    init_test(cx);
4630
4631    let fs = FakeFs::new(cx.executor());
4632    fs.insert_tree(
4633        path!("/dir"),
4634        json!({
4635            "one.rs": "const ONE: usize = 1;",
4636            "two": {
4637                "two.rs": "const TWO: usize = one::ONE + one::ONE;"
4638            }
4639
4640        }),
4641    )
4642    .await;
4643    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4644
4645    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
4646    language_registry.add(rust_lang());
4647    let watched_paths = lsp::FileOperationRegistrationOptions {
4648        filters: vec![
4649            FileOperationFilter {
4650                scheme: Some("file".to_owned()),
4651                pattern: lsp::FileOperationPattern {
4652                    glob: "**/*.rs".to_owned(),
4653                    matches: Some(lsp::FileOperationPatternKind::File),
4654                    options: None,
4655                },
4656            },
4657            FileOperationFilter {
4658                scheme: Some("file".to_owned()),
4659                pattern: lsp::FileOperationPattern {
4660                    glob: "**/**".to_owned(),
4661                    matches: Some(lsp::FileOperationPatternKind::Folder),
4662                    options: None,
4663                },
4664            },
4665        ],
4666    };
4667    let mut fake_servers = language_registry.register_fake_lsp(
4668        "Rust",
4669        FakeLspAdapter {
4670            capabilities: lsp::ServerCapabilities {
4671                workspace: Some(lsp::WorkspaceServerCapabilities {
4672                    workspace_folders: None,
4673                    file_operations: Some(lsp::WorkspaceFileOperationsServerCapabilities {
4674                        did_rename: Some(watched_paths.clone()),
4675                        will_rename: Some(watched_paths),
4676                        ..Default::default()
4677                    }),
4678                }),
4679                ..Default::default()
4680            },
4681            ..Default::default()
4682        },
4683    );
4684
4685    let _ = project
4686        .update(cx, |project, cx| {
4687            project.open_local_buffer_with_lsp(path!("/dir/one.rs"), cx)
4688        })
4689        .await
4690        .unwrap();
4691
4692    let fake_server = fake_servers.next().await.unwrap();
4693    let response = project.update(cx, |project, cx| {
4694        let worktree = project.worktrees(cx).next().unwrap();
4695        let entry = worktree.read(cx).entry_for_path("one.rs").unwrap();
4696        project.rename_entry(entry.id, "three.rs".as_ref(), cx)
4697    });
4698    let expected_edit = lsp::WorkspaceEdit {
4699        changes: None,
4700        document_changes: Some(DocumentChanges::Edits({
4701            vec![TextDocumentEdit {
4702                edits: vec![lsp::Edit::Plain(lsp::TextEdit {
4703                    range: lsp::Range {
4704                        start: lsp::Position {
4705                            line: 0,
4706                            character: 1,
4707                        },
4708                        end: lsp::Position {
4709                            line: 0,
4710                            character: 3,
4711                        },
4712                    },
4713                    new_text: "This is not a drill".to_owned(),
4714                })],
4715                text_document: lsp::OptionalVersionedTextDocumentIdentifier {
4716                    uri: Url::from_str(uri!("file:///dir/two/two.rs")).unwrap(),
4717                    version: Some(1337),
4718                },
4719            }]
4720        })),
4721        change_annotations: None,
4722    };
4723    let resolved_workspace_edit = Arc::new(OnceLock::new());
4724    fake_server
4725        .set_request_handler::<WillRenameFiles, _, _>({
4726            let resolved_workspace_edit = resolved_workspace_edit.clone();
4727            let expected_edit = expected_edit.clone();
4728            move |params, _| {
4729                let resolved_workspace_edit = resolved_workspace_edit.clone();
4730                let expected_edit = expected_edit.clone();
4731                async move {
4732                    assert_eq!(params.files.len(), 1);
4733                    assert_eq!(params.files[0].old_uri, uri!("file:///dir/one.rs"));
4734                    assert_eq!(params.files[0].new_uri, uri!("file:///dir/three.rs"));
4735                    resolved_workspace_edit.set(expected_edit.clone()).unwrap();
4736                    Ok(Some(expected_edit))
4737                }
4738            }
4739        })
4740        .next()
4741        .await
4742        .unwrap();
4743    let _ = response.await.unwrap();
4744    fake_server
4745        .handle_notification::<DidRenameFiles, _>(|params, _| {
4746            assert_eq!(params.files.len(), 1);
4747            assert_eq!(params.files[0].old_uri, uri!("file:///dir/one.rs"));
4748            assert_eq!(params.files[0].new_uri, uri!("file:///dir/three.rs"));
4749        })
4750        .next()
4751        .await
4752        .unwrap();
4753    assert_eq!(resolved_workspace_edit.get(), Some(&expected_edit));
4754}
4755
4756#[gpui::test]
4757async fn test_rename(cx: &mut gpui::TestAppContext) {
4758    // hi
4759    init_test(cx);
4760
4761    let fs = FakeFs::new(cx.executor());
4762    fs.insert_tree(
4763        path!("/dir"),
4764        json!({
4765            "one.rs": "const ONE: usize = 1;",
4766            "two.rs": "const TWO: usize = one::ONE + one::ONE;"
4767        }),
4768    )
4769    .await;
4770
4771    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4772
4773    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
4774    language_registry.add(rust_lang());
4775    let mut fake_servers = language_registry.register_fake_lsp(
4776        "Rust",
4777        FakeLspAdapter {
4778            capabilities: lsp::ServerCapabilities {
4779                rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
4780                    prepare_provider: Some(true),
4781                    work_done_progress_options: Default::default(),
4782                })),
4783                ..Default::default()
4784            },
4785            ..Default::default()
4786        },
4787    );
4788
4789    let (buffer, _handle) = project
4790        .update(cx, |project, cx| {
4791            project.open_local_buffer_with_lsp(path!("/dir/one.rs"), cx)
4792        })
4793        .await
4794        .unwrap();
4795
4796    let fake_server = fake_servers.next().await.unwrap();
4797
4798    let response = project.update(cx, |project, cx| {
4799        project.prepare_rename(buffer.clone(), 7, cx)
4800    });
4801    fake_server
4802        .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
4803            assert_eq!(
4804                params.text_document.uri.as_str(),
4805                uri!("file:///dir/one.rs")
4806            );
4807            assert_eq!(params.position, lsp::Position::new(0, 7));
4808            Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
4809                lsp::Position::new(0, 6),
4810                lsp::Position::new(0, 9),
4811            ))))
4812        })
4813        .next()
4814        .await
4815        .unwrap();
4816    let response = response.await.unwrap();
4817    let PrepareRenameResponse::Success(range) = response else {
4818        panic!("{:?}", response);
4819    };
4820    let range = buffer.update(cx, |buffer, _| range.to_offset(buffer));
4821    assert_eq!(range, 6..9);
4822
4823    let response = project.update(cx, |project, cx| {
4824        project.perform_rename(buffer.clone(), 7, "THREE".to_string(), cx)
4825    });
4826    fake_server
4827        .set_request_handler::<lsp::request::Rename, _, _>(|params, _| async move {
4828            assert_eq!(
4829                params.text_document_position.text_document.uri.as_str(),
4830                uri!("file:///dir/one.rs")
4831            );
4832            assert_eq!(
4833                params.text_document_position.position,
4834                lsp::Position::new(0, 7)
4835            );
4836            assert_eq!(params.new_name, "THREE");
4837            Ok(Some(lsp::WorkspaceEdit {
4838                changes: Some(
4839                    [
4840                        (
4841                            lsp::Url::from_file_path(path!("/dir/one.rs")).unwrap(),
4842                            vec![lsp::TextEdit::new(
4843                                lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4844                                "THREE".to_string(),
4845                            )],
4846                        ),
4847                        (
4848                            lsp::Url::from_file_path(path!("/dir/two.rs")).unwrap(),
4849                            vec![
4850                                lsp::TextEdit::new(
4851                                    lsp::Range::new(
4852                                        lsp::Position::new(0, 24),
4853                                        lsp::Position::new(0, 27),
4854                                    ),
4855                                    "THREE".to_string(),
4856                                ),
4857                                lsp::TextEdit::new(
4858                                    lsp::Range::new(
4859                                        lsp::Position::new(0, 35),
4860                                        lsp::Position::new(0, 38),
4861                                    ),
4862                                    "THREE".to_string(),
4863                                ),
4864                            ],
4865                        ),
4866                    ]
4867                    .into_iter()
4868                    .collect(),
4869                ),
4870                ..Default::default()
4871            }))
4872        })
4873        .next()
4874        .await
4875        .unwrap();
4876    let mut transaction = response.await.unwrap().0;
4877    assert_eq!(transaction.len(), 2);
4878    assert_eq!(
4879        transaction
4880            .remove_entry(&buffer)
4881            .unwrap()
4882            .0
4883            .update(cx, |buffer, _| buffer.text()),
4884        "const THREE: usize = 1;"
4885    );
4886    assert_eq!(
4887        transaction
4888            .into_keys()
4889            .next()
4890            .unwrap()
4891            .update(cx, |buffer, _| buffer.text()),
4892        "const TWO: usize = one::THREE + one::THREE;"
4893    );
4894}
4895
4896#[gpui::test]
4897async fn test_search(cx: &mut gpui::TestAppContext) {
4898    init_test(cx);
4899
4900    let fs = FakeFs::new(cx.executor());
4901    fs.insert_tree(
4902        path!("/dir"),
4903        json!({
4904            "one.rs": "const ONE: usize = 1;",
4905            "two.rs": "const TWO: usize = one::ONE + one::ONE;",
4906            "three.rs": "const THREE: usize = one::ONE + two::TWO;",
4907            "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
4908        }),
4909    )
4910    .await;
4911    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4912    assert_eq!(
4913        search(
4914            &project,
4915            SearchQuery::text(
4916                "TWO",
4917                false,
4918                true,
4919                false,
4920                Default::default(),
4921                Default::default(),
4922                false,
4923                None
4924            )
4925            .unwrap(),
4926            cx
4927        )
4928        .await
4929        .unwrap(),
4930        HashMap::from_iter([
4931            (separator!("dir/two.rs").to_string(), vec![6..9]),
4932            (separator!("dir/three.rs").to_string(), vec![37..40])
4933        ])
4934    );
4935
4936    let buffer_4 = project
4937        .update(cx, |project, cx| {
4938            project.open_local_buffer(path!("/dir/four.rs"), cx)
4939        })
4940        .await
4941        .unwrap();
4942    buffer_4.update(cx, |buffer, cx| {
4943        let text = "two::TWO";
4944        buffer.edit([(20..28, text), (31..43, text)], None, cx);
4945    });
4946
4947    assert_eq!(
4948        search(
4949            &project,
4950            SearchQuery::text(
4951                "TWO",
4952                false,
4953                true,
4954                false,
4955                Default::default(),
4956                Default::default(),
4957                false,
4958                None,
4959            )
4960            .unwrap(),
4961            cx
4962        )
4963        .await
4964        .unwrap(),
4965        HashMap::from_iter([
4966            (separator!("dir/two.rs").to_string(), vec![6..9]),
4967            (separator!("dir/three.rs").to_string(), vec![37..40]),
4968            (separator!("dir/four.rs").to_string(), vec![25..28, 36..39])
4969        ])
4970    );
4971}
4972
4973#[gpui::test]
4974async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
4975    init_test(cx);
4976
4977    let search_query = "file";
4978
4979    let fs = FakeFs::new(cx.executor());
4980    fs.insert_tree(
4981        path!("/dir"),
4982        json!({
4983            "one.rs": r#"// Rust file one"#,
4984            "one.ts": r#"// TypeScript file one"#,
4985            "two.rs": r#"// Rust file two"#,
4986            "two.ts": r#"// TypeScript file two"#,
4987        }),
4988    )
4989    .await;
4990    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4991
4992    assert!(
4993        search(
4994            &project,
4995            SearchQuery::text(
4996                search_query,
4997                false,
4998                true,
4999                false,
5000                PathMatcher::new(&["*.odd".to_owned()]).unwrap(),
5001                Default::default(),
5002                false,
5003                None
5004            )
5005            .unwrap(),
5006            cx
5007        )
5008        .await
5009        .unwrap()
5010        .is_empty(),
5011        "If no inclusions match, no files should be returned"
5012    );
5013
5014    assert_eq!(
5015        search(
5016            &project,
5017            SearchQuery::text(
5018                search_query,
5019                false,
5020                true,
5021                false,
5022                PathMatcher::new(&["*.rs".to_owned()]).unwrap(),
5023                Default::default(),
5024                false,
5025                None
5026            )
5027            .unwrap(),
5028            cx
5029        )
5030        .await
5031        .unwrap(),
5032        HashMap::from_iter([
5033            (separator!("dir/one.rs").to_string(), vec![8..12]),
5034            (separator!("dir/two.rs").to_string(), vec![8..12]),
5035        ]),
5036        "Rust only search should give only Rust files"
5037    );
5038
5039    assert_eq!(
5040        search(
5041            &project,
5042            SearchQuery::text(
5043                search_query,
5044                false,
5045                true,
5046                false,
5047                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
5048                Default::default(),
5049                false,
5050                None,
5051            )
5052            .unwrap(),
5053            cx
5054        )
5055        .await
5056        .unwrap(),
5057        HashMap::from_iter([
5058            (separator!("dir/one.ts").to_string(), vec![14..18]),
5059            (separator!("dir/two.ts").to_string(), vec![14..18]),
5060        ]),
5061        "TypeScript only search should give only TypeScript files, even if other inclusions don't match anything"
5062    );
5063
5064    assert_eq!(
5065        search(
5066            &project,
5067            SearchQuery::text(
5068                search_query,
5069                false,
5070                true,
5071                false,
5072                PathMatcher::new(&["*.rs".to_owned(), "*.ts".to_owned(), "*.odd".to_owned()])
5073                    .unwrap(),
5074                Default::default(),
5075                false,
5076                None,
5077            )
5078            .unwrap(),
5079            cx
5080        )
5081        .await
5082        .unwrap(),
5083        HashMap::from_iter([
5084            (separator!("dir/two.ts").to_string(), vec![14..18]),
5085            (separator!("dir/one.rs").to_string(), vec![8..12]),
5086            (separator!("dir/one.ts").to_string(), vec![14..18]),
5087            (separator!("dir/two.rs").to_string(), vec![8..12]),
5088        ]),
5089        "Rust and typescript search should give both Rust and TypeScript files, even if other inclusions don't match anything"
5090    );
5091}
5092
5093#[gpui::test]
5094async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
5095    init_test(cx);
5096
5097    let search_query = "file";
5098
5099    let fs = FakeFs::new(cx.executor());
5100    fs.insert_tree(
5101        path!("/dir"),
5102        json!({
5103            "one.rs": r#"// Rust file one"#,
5104            "one.ts": r#"// TypeScript file one"#,
5105            "two.rs": r#"// Rust file two"#,
5106            "two.ts": r#"// TypeScript file two"#,
5107        }),
5108    )
5109    .await;
5110    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
5111
5112    assert_eq!(
5113        search(
5114            &project,
5115            SearchQuery::text(
5116                search_query,
5117                false,
5118                true,
5119                false,
5120                Default::default(),
5121                PathMatcher::new(&["*.odd".to_owned()]).unwrap(),
5122                false,
5123                None,
5124            )
5125            .unwrap(),
5126            cx
5127        )
5128        .await
5129        .unwrap(),
5130        HashMap::from_iter([
5131            (separator!("dir/one.rs").to_string(), vec![8..12]),
5132            (separator!("dir/one.ts").to_string(), vec![14..18]),
5133            (separator!("dir/two.rs").to_string(), vec![8..12]),
5134            (separator!("dir/two.ts").to_string(), vec![14..18]),
5135        ]),
5136        "If no exclusions match, all files should be returned"
5137    );
5138
5139    assert_eq!(
5140        search(
5141            &project,
5142            SearchQuery::text(
5143                search_query,
5144                false,
5145                true,
5146                false,
5147                Default::default(),
5148                PathMatcher::new(&["*.rs".to_owned()]).unwrap(),
5149                false,
5150                None,
5151            )
5152            .unwrap(),
5153            cx
5154        )
5155        .await
5156        .unwrap(),
5157        HashMap::from_iter([
5158            (separator!("dir/one.ts").to_string(), vec![14..18]),
5159            (separator!("dir/two.ts").to_string(), vec![14..18]),
5160        ]),
5161        "Rust exclusion search should give only TypeScript files"
5162    );
5163
5164    assert_eq!(
5165        search(
5166            &project,
5167            SearchQuery::text(
5168                search_query,
5169                false,
5170                true,
5171                false,
5172                Default::default(),
5173                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
5174                false,
5175                None,
5176            )
5177            .unwrap(),
5178            cx
5179        )
5180        .await
5181        .unwrap(),
5182        HashMap::from_iter([
5183            (separator!("dir/one.rs").to_string(), vec![8..12]),
5184            (separator!("dir/two.rs").to_string(), vec![8..12]),
5185        ]),
5186        "TypeScript exclusion search should give only Rust files, even if other exclusions don't match anything"
5187    );
5188
5189    assert!(
5190        search(
5191            &project,
5192            SearchQuery::text(
5193                search_query,
5194                false,
5195                true,
5196                false,
5197                Default::default(),
5198                PathMatcher::new(&["*.rs".to_owned(), "*.ts".to_owned(), "*.odd".to_owned()])
5199                    .unwrap(),
5200                false,
5201                None,
5202            )
5203            .unwrap(),
5204            cx
5205        )
5206        .await
5207        .unwrap()
5208        .is_empty(),
5209        "Rust and typescript exclusion should give no files, even if other exclusions don't match anything"
5210    );
5211}
5212
5213#[gpui::test]
5214async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContext) {
5215    init_test(cx);
5216
5217    let search_query = "file";
5218
5219    let fs = FakeFs::new(cx.executor());
5220    fs.insert_tree(
5221        path!("/dir"),
5222        json!({
5223            "one.rs": r#"// Rust file one"#,
5224            "one.ts": r#"// TypeScript file one"#,
5225            "two.rs": r#"// Rust file two"#,
5226            "two.ts": r#"// TypeScript file two"#,
5227        }),
5228    )
5229    .await;
5230    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
5231
5232    assert!(
5233        search(
5234            &project,
5235            SearchQuery::text(
5236                search_query,
5237                false,
5238                true,
5239                false,
5240                PathMatcher::new(&["*.odd".to_owned()]).unwrap(),
5241                PathMatcher::new(&["*.odd".to_owned()]).unwrap(),
5242                false,
5243                None,
5244            )
5245            .unwrap(),
5246            cx
5247        )
5248        .await
5249        .unwrap()
5250        .is_empty(),
5251        "If both no exclusions and inclusions match, exclusions should win and return nothing"
5252    );
5253
5254    assert!(
5255        search(
5256            &project,
5257            SearchQuery::text(
5258                search_query,
5259                false,
5260                true,
5261                false,
5262                PathMatcher::new(&["*.ts".to_owned()]).unwrap(),
5263                PathMatcher::new(&["*.ts".to_owned()]).unwrap(),
5264                false,
5265                None,
5266            )
5267            .unwrap(),
5268            cx
5269        )
5270        .await
5271        .unwrap()
5272        .is_empty(),
5273        "If both TypeScript exclusions and inclusions match, exclusions should win and return nothing files."
5274    );
5275
5276    assert!(
5277        search(
5278            &project,
5279            SearchQuery::text(
5280                search_query,
5281                false,
5282                true,
5283                false,
5284                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
5285                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
5286                false,
5287                None,
5288            )
5289            .unwrap(),
5290            cx
5291        )
5292        .await
5293        .unwrap()
5294        .is_empty(),
5295        "Non-matching inclusions and exclusions should not change that."
5296    );
5297
5298    assert_eq!(
5299        search(
5300            &project,
5301            SearchQuery::text(
5302                search_query,
5303                false,
5304                true,
5305                false,
5306                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
5307                PathMatcher::new(&["*.rs".to_owned(), "*.odd".to_owned()]).unwrap(),
5308                false,
5309                None,
5310            )
5311            .unwrap(),
5312            cx
5313        )
5314        .await
5315        .unwrap(),
5316        HashMap::from_iter([
5317            (separator!("dir/one.ts").to_string(), vec![14..18]),
5318            (separator!("dir/two.ts").to_string(), vec![14..18]),
5319        ]),
5320        "Non-intersecting TypeScript inclusions and Rust exclusions should return TypeScript files"
5321    );
5322}
5323
5324#[gpui::test]
5325async fn test_search_multiple_worktrees_with_inclusions(cx: &mut gpui::TestAppContext) {
5326    init_test(cx);
5327
5328    let fs = FakeFs::new(cx.executor());
5329    fs.insert_tree(
5330        path!("/worktree-a"),
5331        json!({
5332            "haystack.rs": r#"// NEEDLE"#,
5333            "haystack.ts": r#"// NEEDLE"#,
5334        }),
5335    )
5336    .await;
5337    fs.insert_tree(
5338        path!("/worktree-b"),
5339        json!({
5340            "haystack.rs": r#"// NEEDLE"#,
5341            "haystack.ts": r#"// NEEDLE"#,
5342        }),
5343    )
5344    .await;
5345
5346    let project = Project::test(
5347        fs.clone(),
5348        [path!("/worktree-a").as_ref(), path!("/worktree-b").as_ref()],
5349        cx,
5350    )
5351    .await;
5352
5353    assert_eq!(
5354        search(
5355            &project,
5356            SearchQuery::text(
5357                "NEEDLE",
5358                false,
5359                true,
5360                false,
5361                PathMatcher::new(&["worktree-a/*.rs".to_owned()]).unwrap(),
5362                Default::default(),
5363                true,
5364                None,
5365            )
5366            .unwrap(),
5367            cx
5368        )
5369        .await
5370        .unwrap(),
5371        HashMap::from_iter([(separator!("worktree-a/haystack.rs").to_string(), vec![3..9])]),
5372        "should only return results from included worktree"
5373    );
5374    assert_eq!(
5375        search(
5376            &project,
5377            SearchQuery::text(
5378                "NEEDLE",
5379                false,
5380                true,
5381                false,
5382                PathMatcher::new(&["worktree-b/*.rs".to_owned()]).unwrap(),
5383                Default::default(),
5384                true,
5385                None,
5386            )
5387            .unwrap(),
5388            cx
5389        )
5390        .await
5391        .unwrap(),
5392        HashMap::from_iter([(separator!("worktree-b/haystack.rs").to_string(), vec![3..9])]),
5393        "should only return results from included worktree"
5394    );
5395
5396    assert_eq!(
5397        search(
5398            &project,
5399            SearchQuery::text(
5400                "NEEDLE",
5401                false,
5402                true,
5403                false,
5404                PathMatcher::new(&["*.ts".to_owned()]).unwrap(),
5405                Default::default(),
5406                false,
5407                None,
5408            )
5409            .unwrap(),
5410            cx
5411        )
5412        .await
5413        .unwrap(),
5414        HashMap::from_iter([
5415            (separator!("worktree-a/haystack.ts").to_string(), vec![3..9]),
5416            (separator!("worktree-b/haystack.ts").to_string(), vec![3..9])
5417        ]),
5418        "should return results from both worktrees"
5419    );
5420}
5421
5422#[gpui::test]
5423async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) {
5424    init_test(cx);
5425
5426    let fs = FakeFs::new(cx.background_executor.clone());
5427    fs.insert_tree(
5428        path!("/dir"),
5429        json!({
5430            ".git": {},
5431            ".gitignore": "**/target\n/node_modules\n",
5432            "target": {
5433                "index.txt": "index_key:index_value"
5434            },
5435            "node_modules": {
5436                "eslint": {
5437                    "index.ts": "const eslint_key = 'eslint value'",
5438                    "package.json": r#"{ "some_key": "some value" }"#,
5439                },
5440                "prettier": {
5441                    "index.ts": "const prettier_key = 'prettier value'",
5442                    "package.json": r#"{ "other_key": "other value" }"#,
5443                },
5444            },
5445            "package.json": r#"{ "main_key": "main value" }"#,
5446        }),
5447    )
5448    .await;
5449    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
5450
5451    let query = "key";
5452    assert_eq!(
5453        search(
5454            &project,
5455            SearchQuery::text(
5456                query,
5457                false,
5458                false,
5459                false,
5460                Default::default(),
5461                Default::default(),
5462                false,
5463                None,
5464            )
5465            .unwrap(),
5466            cx
5467        )
5468        .await
5469        .unwrap(),
5470        HashMap::from_iter([(separator!("dir/package.json").to_string(), vec![8..11])]),
5471        "Only one non-ignored file should have the query"
5472    );
5473
5474    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
5475    assert_eq!(
5476        search(
5477            &project,
5478            SearchQuery::text(
5479                query,
5480                false,
5481                false,
5482                true,
5483                Default::default(),
5484                Default::default(),
5485                false,
5486                None,
5487            )
5488            .unwrap(),
5489            cx
5490        )
5491        .await
5492        .unwrap(),
5493        HashMap::from_iter([
5494            (separator!("dir/package.json").to_string(), vec![8..11]),
5495            (separator!("dir/target/index.txt").to_string(), vec![6..9]),
5496            (
5497                separator!("dir/node_modules/prettier/package.json").to_string(),
5498                vec![9..12]
5499            ),
5500            (
5501                separator!("dir/node_modules/prettier/index.ts").to_string(),
5502                vec![15..18]
5503            ),
5504            (
5505                separator!("dir/node_modules/eslint/index.ts").to_string(),
5506                vec![13..16]
5507            ),
5508            (
5509                separator!("dir/node_modules/eslint/package.json").to_string(),
5510                vec![8..11]
5511            ),
5512        ]),
5513        "Unrestricted search with ignored directories should find every file with the query"
5514    );
5515
5516    let files_to_include = PathMatcher::new(&["node_modules/prettier/**".to_owned()]).unwrap();
5517    let files_to_exclude = PathMatcher::new(&["*.ts".to_owned()]).unwrap();
5518    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
5519    assert_eq!(
5520        search(
5521            &project,
5522            SearchQuery::text(
5523                query,
5524                false,
5525                false,
5526                true,
5527                files_to_include,
5528                files_to_exclude,
5529                false,
5530                None,
5531            )
5532            .unwrap(),
5533            cx
5534        )
5535        .await
5536        .unwrap(),
5537        HashMap::from_iter([(
5538            separator!("dir/node_modules/prettier/package.json").to_string(),
5539            vec![9..12]
5540        )]),
5541        "With search including ignored prettier directory and excluding TS files, only one file should be found"
5542    );
5543}
5544
5545#[gpui::test]
5546async fn test_search_with_unicode(cx: &mut gpui::TestAppContext) {
5547    init_test(cx);
5548
5549    let fs = FakeFs::new(cx.executor());
5550    fs.insert_tree(
5551        path!("/dir"),
5552        json!({
5553            "one.rs": "// ПРИВЕТ? привет!",
5554            "two.rs": "// ПРИВЕТ.",
5555            "three.rs": "// привет",
5556        }),
5557    )
5558    .await;
5559    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
5560
5561    let unicode_case_sensitive_query = SearchQuery::text(
5562        "привет",
5563        false,
5564        true,
5565        false,
5566        Default::default(),
5567        Default::default(),
5568        false,
5569        None,
5570    );
5571    assert_matches!(unicode_case_sensitive_query, Ok(SearchQuery::Text { .. }));
5572    assert_eq!(
5573        search(&project, unicode_case_sensitive_query.unwrap(), cx)
5574            .await
5575            .unwrap(),
5576        HashMap::from_iter([
5577            (separator!("dir/one.rs").to_string(), vec![17..29]),
5578            (separator!("dir/three.rs").to_string(), vec![3..15]),
5579        ])
5580    );
5581
5582    let unicode_case_insensitive_query = SearchQuery::text(
5583        "привет",
5584        false,
5585        false,
5586        false,
5587        Default::default(),
5588        Default::default(),
5589        false,
5590        None,
5591    );
5592    assert_matches!(
5593        unicode_case_insensitive_query,
5594        Ok(SearchQuery::Regex { .. })
5595    );
5596    assert_eq!(
5597        search(&project, unicode_case_insensitive_query.unwrap(), cx)
5598            .await
5599            .unwrap(),
5600        HashMap::from_iter([
5601            (separator!("dir/one.rs").to_string(), vec![3..15, 17..29]),
5602            (separator!("dir/two.rs").to_string(), vec![3..15]),
5603            (separator!("dir/three.rs").to_string(), vec![3..15]),
5604        ])
5605    );
5606
5607    assert_eq!(
5608        search(
5609            &project,
5610            SearchQuery::text(
5611                "привет.",
5612                false,
5613                false,
5614                false,
5615                Default::default(),
5616                Default::default(),
5617                false,
5618                None,
5619            )
5620            .unwrap(),
5621            cx
5622        )
5623        .await
5624        .unwrap(),
5625        HashMap::from_iter([(separator!("dir/two.rs").to_string(), vec![3..16]),])
5626    );
5627}
5628
5629#[gpui::test]
5630async fn test_create_entry(cx: &mut gpui::TestAppContext) {
5631    init_test(cx);
5632
5633    let fs = FakeFs::new(cx.executor().clone());
5634    fs.insert_tree(
5635        "/one/two",
5636        json!({
5637            "three": {
5638                "a.txt": "",
5639                "four": {}
5640            },
5641            "c.rs": ""
5642        }),
5643    )
5644    .await;
5645
5646    let project = Project::test(fs.clone(), ["/one/two/three".as_ref()], cx).await;
5647    project
5648        .update(cx, |project, cx| {
5649            let id = project.worktrees(cx).next().unwrap().read(cx).id();
5650            project.create_entry((id, "b.."), true, cx)
5651        })
5652        .await
5653        .unwrap()
5654        .to_included()
5655        .unwrap();
5656
5657    // Can't create paths outside the project
5658    let result = project
5659        .update(cx, |project, cx| {
5660            let id = project.worktrees(cx).next().unwrap().read(cx).id();
5661            project.create_entry((id, "../../boop"), true, cx)
5662        })
5663        .await;
5664    assert!(result.is_err());
5665
5666    // Can't create paths with '..'
5667    let result = project
5668        .update(cx, |project, cx| {
5669            let id = project.worktrees(cx).next().unwrap().read(cx).id();
5670            project.create_entry((id, "four/../beep"), true, cx)
5671        })
5672        .await;
5673    assert!(result.is_err());
5674
5675    assert_eq!(
5676        fs.paths(true),
5677        vec![
5678            PathBuf::from(path!("/")),
5679            PathBuf::from(path!("/one")),
5680            PathBuf::from(path!("/one/two")),
5681            PathBuf::from(path!("/one/two/c.rs")),
5682            PathBuf::from(path!("/one/two/three")),
5683            PathBuf::from(path!("/one/two/three/a.txt")),
5684            PathBuf::from(path!("/one/two/three/b..")),
5685            PathBuf::from(path!("/one/two/three/four")),
5686        ]
5687    );
5688
5689    // And we cannot open buffers with '..'
5690    let result = project
5691        .update(cx, |project, cx| {
5692            let id = project.worktrees(cx).next().unwrap().read(cx).id();
5693            project.open_buffer((id, "../c.rs"), cx)
5694        })
5695        .await;
5696    assert!(result.is_err())
5697}
5698
5699#[gpui::test]
5700async fn test_multiple_language_server_hovers(cx: &mut gpui::TestAppContext) {
5701    init_test(cx);
5702
5703    let fs = FakeFs::new(cx.executor());
5704    fs.insert_tree(
5705        path!("/dir"),
5706        json!({
5707            "a.tsx": "a",
5708        }),
5709    )
5710    .await;
5711
5712    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
5713
5714    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
5715    language_registry.add(tsx_lang());
5716    let language_server_names = [
5717        "TypeScriptServer",
5718        "TailwindServer",
5719        "ESLintServer",
5720        "NoHoverCapabilitiesServer",
5721    ];
5722    let mut language_servers = [
5723        language_registry.register_fake_lsp(
5724            "tsx",
5725            FakeLspAdapter {
5726                name: language_server_names[0],
5727                capabilities: lsp::ServerCapabilities {
5728                    hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
5729                    ..lsp::ServerCapabilities::default()
5730                },
5731                ..FakeLspAdapter::default()
5732            },
5733        ),
5734        language_registry.register_fake_lsp(
5735            "tsx",
5736            FakeLspAdapter {
5737                name: language_server_names[1],
5738                capabilities: lsp::ServerCapabilities {
5739                    hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
5740                    ..lsp::ServerCapabilities::default()
5741                },
5742                ..FakeLspAdapter::default()
5743            },
5744        ),
5745        language_registry.register_fake_lsp(
5746            "tsx",
5747            FakeLspAdapter {
5748                name: language_server_names[2],
5749                capabilities: lsp::ServerCapabilities {
5750                    hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
5751                    ..lsp::ServerCapabilities::default()
5752                },
5753                ..FakeLspAdapter::default()
5754            },
5755        ),
5756        language_registry.register_fake_lsp(
5757            "tsx",
5758            FakeLspAdapter {
5759                name: language_server_names[3],
5760                capabilities: lsp::ServerCapabilities {
5761                    hover_provider: None,
5762                    ..lsp::ServerCapabilities::default()
5763                },
5764                ..FakeLspAdapter::default()
5765            },
5766        ),
5767    ];
5768
5769    let (buffer, _handle) = project
5770        .update(cx, |p, cx| {
5771            p.open_local_buffer_with_lsp(path!("/dir/a.tsx"), cx)
5772        })
5773        .await
5774        .unwrap();
5775    cx.executor().run_until_parked();
5776
5777    let mut servers_with_hover_requests = HashMap::default();
5778    for i in 0..language_server_names.len() {
5779        let new_server = language_servers[i].next().await.unwrap_or_else(|| {
5780            panic!(
5781                "Failed to get language server #{i} with name {}",
5782                &language_server_names[i]
5783            )
5784        });
5785        let new_server_name = new_server.server.name();
5786        assert!(
5787            !servers_with_hover_requests.contains_key(&new_server_name),
5788            "Unexpected: initialized server with the same name twice. Name: `{new_server_name}`"
5789        );
5790        match new_server_name.as_ref() {
5791            "TailwindServer" | "TypeScriptServer" => {
5792                servers_with_hover_requests.insert(
5793                    new_server_name.clone(),
5794                    new_server.set_request_handler::<lsp::request::HoverRequest, _, _>(
5795                        move |_, _| {
5796                            let name = new_server_name.clone();
5797                            async move {
5798                                Ok(Some(lsp::Hover {
5799                                    contents: lsp::HoverContents::Scalar(
5800                                        lsp::MarkedString::String(format!("{name} hover")),
5801                                    ),
5802                                    range: None,
5803                                }))
5804                            }
5805                        },
5806                    ),
5807                );
5808            }
5809            "ESLintServer" => {
5810                servers_with_hover_requests.insert(
5811                    new_server_name,
5812                    new_server.set_request_handler::<lsp::request::HoverRequest, _, _>(
5813                        |_, _| async move { Ok(None) },
5814                    ),
5815                );
5816            }
5817            "NoHoverCapabilitiesServer" => {
5818                let _never_handled = new_server
5819                    .set_request_handler::<lsp::request::HoverRequest, _, _>(|_, _| async move {
5820                        panic!(
5821                            "Should not call for hovers server with no corresponding capabilities"
5822                        )
5823                    });
5824            }
5825            unexpected => panic!("Unexpected server name: {unexpected}"),
5826        }
5827    }
5828
5829    let hover_task = project.update(cx, |project, cx| {
5830        project.hover(&buffer, Point::new(0, 0), cx)
5831    });
5832    let _: Vec<()> = futures::future::join_all(servers_with_hover_requests.into_values().map(
5833        |mut hover_request| async move {
5834            hover_request
5835                .next()
5836                .await
5837                .expect("All hover requests should have been triggered")
5838        },
5839    ))
5840    .await;
5841    assert_eq!(
5842        vec!["TailwindServer hover", "TypeScriptServer hover"],
5843        hover_task
5844            .await
5845            .into_iter()
5846            .map(|hover| hover.contents.iter().map(|block| &block.text).join("|"))
5847            .sorted()
5848            .collect::<Vec<_>>(),
5849        "Should receive hover responses from all related servers with hover capabilities"
5850    );
5851}
5852
5853#[gpui::test]
5854async fn test_hovers_with_empty_parts(cx: &mut gpui::TestAppContext) {
5855    init_test(cx);
5856
5857    let fs = FakeFs::new(cx.executor());
5858    fs.insert_tree(
5859        path!("/dir"),
5860        json!({
5861            "a.ts": "a",
5862        }),
5863    )
5864    .await;
5865
5866    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
5867
5868    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
5869    language_registry.add(typescript_lang());
5870    let mut fake_language_servers = language_registry.register_fake_lsp(
5871        "TypeScript",
5872        FakeLspAdapter {
5873            capabilities: lsp::ServerCapabilities {
5874                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
5875                ..lsp::ServerCapabilities::default()
5876            },
5877            ..FakeLspAdapter::default()
5878        },
5879    );
5880
5881    let (buffer, _handle) = project
5882        .update(cx, |p, cx| {
5883            p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
5884        })
5885        .await
5886        .unwrap();
5887    cx.executor().run_until_parked();
5888
5889    let fake_server = fake_language_servers
5890        .next()
5891        .await
5892        .expect("failed to get the language server");
5893
5894    let mut request_handled = fake_server.set_request_handler::<lsp::request::HoverRequest, _, _>(
5895        move |_, _| async move {
5896            Ok(Some(lsp::Hover {
5897                contents: lsp::HoverContents::Array(vec![
5898                    lsp::MarkedString::String("".to_string()),
5899                    lsp::MarkedString::String("      ".to_string()),
5900                    lsp::MarkedString::String("\n\n\n".to_string()),
5901                ]),
5902                range: None,
5903            }))
5904        },
5905    );
5906
5907    let hover_task = project.update(cx, |project, cx| {
5908        project.hover(&buffer, Point::new(0, 0), cx)
5909    });
5910    let () = request_handled
5911        .next()
5912        .await
5913        .expect("All hover requests should have been triggered");
5914    assert_eq!(
5915        Vec::<String>::new(),
5916        hover_task
5917            .await
5918            .into_iter()
5919            .map(|hover| hover.contents.iter().map(|block| &block.text).join("|"))
5920            .sorted()
5921            .collect::<Vec<_>>(),
5922        "Empty hover parts should be ignored"
5923    );
5924}
5925
5926#[gpui::test]
5927async fn test_code_actions_only_kinds(cx: &mut gpui::TestAppContext) {
5928    init_test(cx);
5929
5930    let fs = FakeFs::new(cx.executor());
5931    fs.insert_tree(
5932        path!("/dir"),
5933        json!({
5934            "a.ts": "a",
5935        }),
5936    )
5937    .await;
5938
5939    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
5940
5941    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
5942    language_registry.add(typescript_lang());
5943    let mut fake_language_servers = language_registry.register_fake_lsp(
5944        "TypeScript",
5945        FakeLspAdapter {
5946            capabilities: lsp::ServerCapabilities {
5947                code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
5948                ..lsp::ServerCapabilities::default()
5949            },
5950            ..FakeLspAdapter::default()
5951        },
5952    );
5953
5954    let (buffer, _handle) = project
5955        .update(cx, |p, cx| {
5956            p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
5957        })
5958        .await
5959        .unwrap();
5960    cx.executor().run_until_parked();
5961
5962    let fake_server = fake_language_servers
5963        .next()
5964        .await
5965        .expect("failed to get the language server");
5966
5967    let mut request_handled = fake_server
5968        .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |_, _| async move {
5969            Ok(Some(vec![
5970                lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
5971                    title: "organize imports".to_string(),
5972                    kind: Some(CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
5973                    ..lsp::CodeAction::default()
5974                }),
5975                lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
5976                    title: "fix code".to_string(),
5977                    kind: Some(CodeActionKind::SOURCE_FIX_ALL),
5978                    ..lsp::CodeAction::default()
5979                }),
5980            ]))
5981        });
5982
5983    let code_actions_task = project.update(cx, |project, cx| {
5984        project.code_actions(
5985            &buffer,
5986            0..buffer.read(cx).len(),
5987            Some(vec![CodeActionKind::SOURCE_ORGANIZE_IMPORTS]),
5988            cx,
5989        )
5990    });
5991
5992    let () = request_handled
5993        .next()
5994        .await
5995        .expect("The code action request should have been triggered");
5996
5997    let code_actions = code_actions_task.await.unwrap();
5998    assert_eq!(code_actions.len(), 1);
5999    assert_eq!(
6000        code_actions[0].lsp_action.action_kind(),
6001        Some(CodeActionKind::SOURCE_ORGANIZE_IMPORTS)
6002    );
6003}
6004
6005#[gpui::test]
6006async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) {
6007    init_test(cx);
6008
6009    let fs = FakeFs::new(cx.executor());
6010    fs.insert_tree(
6011        path!("/dir"),
6012        json!({
6013            "a.tsx": "a",
6014        }),
6015    )
6016    .await;
6017
6018    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
6019
6020    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
6021    language_registry.add(tsx_lang());
6022    let language_server_names = [
6023        "TypeScriptServer",
6024        "TailwindServer",
6025        "ESLintServer",
6026        "NoActionsCapabilitiesServer",
6027    ];
6028
6029    let mut language_server_rxs = [
6030        language_registry.register_fake_lsp(
6031            "tsx",
6032            FakeLspAdapter {
6033                name: language_server_names[0],
6034                capabilities: lsp::ServerCapabilities {
6035                    code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
6036                    ..lsp::ServerCapabilities::default()
6037                },
6038                ..FakeLspAdapter::default()
6039            },
6040        ),
6041        language_registry.register_fake_lsp(
6042            "tsx",
6043            FakeLspAdapter {
6044                name: language_server_names[1],
6045                capabilities: lsp::ServerCapabilities {
6046                    code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
6047                    ..lsp::ServerCapabilities::default()
6048                },
6049                ..FakeLspAdapter::default()
6050            },
6051        ),
6052        language_registry.register_fake_lsp(
6053            "tsx",
6054            FakeLspAdapter {
6055                name: language_server_names[2],
6056                capabilities: lsp::ServerCapabilities {
6057                    code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
6058                    ..lsp::ServerCapabilities::default()
6059                },
6060                ..FakeLspAdapter::default()
6061            },
6062        ),
6063        language_registry.register_fake_lsp(
6064            "tsx",
6065            FakeLspAdapter {
6066                name: language_server_names[3],
6067                capabilities: lsp::ServerCapabilities {
6068                    code_action_provider: None,
6069                    ..lsp::ServerCapabilities::default()
6070                },
6071                ..FakeLspAdapter::default()
6072            },
6073        ),
6074    ];
6075
6076    let (buffer, _handle) = project
6077        .update(cx, |p, cx| {
6078            p.open_local_buffer_with_lsp(path!("/dir/a.tsx"), cx)
6079        })
6080        .await
6081        .unwrap();
6082    cx.executor().run_until_parked();
6083
6084    let mut servers_with_actions_requests = HashMap::default();
6085    for i in 0..language_server_names.len() {
6086        let new_server = language_server_rxs[i].next().await.unwrap_or_else(|| {
6087            panic!(
6088                "Failed to get language server #{i} with name {}",
6089                &language_server_names[i]
6090            )
6091        });
6092        let new_server_name = new_server.server.name();
6093
6094        assert!(
6095            !servers_with_actions_requests.contains_key(&new_server_name),
6096            "Unexpected: initialized server with the same name twice. Name: `{new_server_name}`"
6097        );
6098        match new_server_name.0.as_ref() {
6099            "TailwindServer" | "TypeScriptServer" => {
6100                servers_with_actions_requests.insert(
6101                    new_server_name.clone(),
6102                    new_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
6103                        move |_, _| {
6104                            let name = new_server_name.clone();
6105                            async move {
6106                                Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
6107                                    lsp::CodeAction {
6108                                        title: format!("{name} code action"),
6109                                        ..lsp::CodeAction::default()
6110                                    },
6111                                )]))
6112                            }
6113                        },
6114                    ),
6115                );
6116            }
6117            "ESLintServer" => {
6118                servers_with_actions_requests.insert(
6119                    new_server_name,
6120                    new_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
6121                        |_, _| async move { Ok(None) },
6122                    ),
6123                );
6124            }
6125            "NoActionsCapabilitiesServer" => {
6126                let _never_handled = new_server
6127                    .set_request_handler::<lsp::request::CodeActionRequest, _, _>(|_, _| async move {
6128                        panic!(
6129                            "Should not call for code actions server with no corresponding capabilities"
6130                        )
6131                    });
6132            }
6133            unexpected => panic!("Unexpected server name: {unexpected}"),
6134        }
6135    }
6136
6137    let code_actions_task = project.update(cx, |project, cx| {
6138        project.code_actions(&buffer, 0..buffer.read(cx).len(), None, cx)
6139    });
6140
6141    // cx.run_until_parked();
6142    let _: Vec<()> = futures::future::join_all(servers_with_actions_requests.into_values().map(
6143        |mut code_actions_request| async move {
6144            code_actions_request
6145                .next()
6146                .await
6147                .expect("All code actions requests should have been triggered")
6148        },
6149    ))
6150    .await;
6151    assert_eq!(
6152        vec!["TailwindServer code action", "TypeScriptServer code action"],
6153        code_actions_task
6154            .await
6155            .unwrap()
6156            .into_iter()
6157            .map(|code_action| code_action.lsp_action.title().to_owned())
6158            .sorted()
6159            .collect::<Vec<_>>(),
6160        "Should receive code actions responses from all related servers with hover capabilities"
6161    );
6162}
6163
6164#[gpui::test]
6165async fn test_reordering_worktrees(cx: &mut gpui::TestAppContext) {
6166    init_test(cx);
6167
6168    let fs = FakeFs::new(cx.executor());
6169    fs.insert_tree(
6170        "/dir",
6171        json!({
6172            "a.rs": "let a = 1;",
6173            "b.rs": "let b = 2;",
6174            "c.rs": "let c = 2;",
6175        }),
6176    )
6177    .await;
6178
6179    let project = Project::test(
6180        fs,
6181        [
6182            "/dir/a.rs".as_ref(),
6183            "/dir/b.rs".as_ref(),
6184            "/dir/c.rs".as_ref(),
6185        ],
6186        cx,
6187    )
6188    .await;
6189
6190    // check the initial state and get the worktrees
6191    let (worktree_a, worktree_b, worktree_c) = project.update(cx, |project, cx| {
6192        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
6193        assert_eq!(worktrees.len(), 3);
6194
6195        let worktree_a = worktrees[0].read(cx);
6196        let worktree_b = worktrees[1].read(cx);
6197        let worktree_c = worktrees[2].read(cx);
6198
6199        // check they start in the right order
6200        assert_eq!(worktree_a.abs_path().to_str().unwrap(), "/dir/a.rs");
6201        assert_eq!(worktree_b.abs_path().to_str().unwrap(), "/dir/b.rs");
6202        assert_eq!(worktree_c.abs_path().to_str().unwrap(), "/dir/c.rs");
6203
6204        (
6205            worktrees[0].clone(),
6206            worktrees[1].clone(),
6207            worktrees[2].clone(),
6208        )
6209    });
6210
6211    // move first worktree to after the second
6212    // [a, b, c] -> [b, a, c]
6213    project
6214        .update(cx, |project, cx| {
6215            let first = worktree_a.read(cx);
6216            let second = worktree_b.read(cx);
6217            project.move_worktree(first.id(), second.id(), cx)
6218        })
6219        .expect("moving first after second");
6220
6221    // check the state after moving
6222    project.update(cx, |project, cx| {
6223        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
6224        assert_eq!(worktrees.len(), 3);
6225
6226        let first = worktrees[0].read(cx);
6227        let second = worktrees[1].read(cx);
6228        let third = worktrees[2].read(cx);
6229
6230        // check they are now in the right order
6231        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/b.rs");
6232        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/a.rs");
6233        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
6234    });
6235
6236    // move the second worktree to before the first
6237    // [b, a, c] -> [a, b, c]
6238    project
6239        .update(cx, |project, cx| {
6240            let second = worktree_a.read(cx);
6241            let first = worktree_b.read(cx);
6242            project.move_worktree(first.id(), second.id(), cx)
6243        })
6244        .expect("moving second before first");
6245
6246    // check the state after moving
6247    project.update(cx, |project, cx| {
6248        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
6249        assert_eq!(worktrees.len(), 3);
6250
6251        let first = worktrees[0].read(cx);
6252        let second = worktrees[1].read(cx);
6253        let third = worktrees[2].read(cx);
6254
6255        // check they are now in the right order
6256        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
6257        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/b.rs");
6258        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
6259    });
6260
6261    // move the second worktree to after the third
6262    // [a, b, c] -> [a, c, b]
6263    project
6264        .update(cx, |project, cx| {
6265            let second = worktree_b.read(cx);
6266            let third = worktree_c.read(cx);
6267            project.move_worktree(second.id(), third.id(), cx)
6268        })
6269        .expect("moving second after third");
6270
6271    // check the state after moving
6272    project.update(cx, |project, cx| {
6273        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
6274        assert_eq!(worktrees.len(), 3);
6275
6276        let first = worktrees[0].read(cx);
6277        let second = worktrees[1].read(cx);
6278        let third = worktrees[2].read(cx);
6279
6280        // check they are now in the right order
6281        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
6282        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/c.rs");
6283        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/b.rs");
6284    });
6285
6286    // move the third worktree to before the second
6287    // [a, c, b] -> [a, b, c]
6288    project
6289        .update(cx, |project, cx| {
6290            let third = worktree_c.read(cx);
6291            let second = worktree_b.read(cx);
6292            project.move_worktree(third.id(), second.id(), cx)
6293        })
6294        .expect("moving third before second");
6295
6296    // check the state after moving
6297    project.update(cx, |project, cx| {
6298        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
6299        assert_eq!(worktrees.len(), 3);
6300
6301        let first = worktrees[0].read(cx);
6302        let second = worktrees[1].read(cx);
6303        let third = worktrees[2].read(cx);
6304
6305        // check they are now in the right order
6306        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
6307        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/b.rs");
6308        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
6309    });
6310
6311    // move the first worktree to after the third
6312    // [a, b, c] -> [b, c, a]
6313    project
6314        .update(cx, |project, cx| {
6315            let first = worktree_a.read(cx);
6316            let third = worktree_c.read(cx);
6317            project.move_worktree(first.id(), third.id(), cx)
6318        })
6319        .expect("moving first after third");
6320
6321    // check the state after moving
6322    project.update(cx, |project, cx| {
6323        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
6324        assert_eq!(worktrees.len(), 3);
6325
6326        let first = worktrees[0].read(cx);
6327        let second = worktrees[1].read(cx);
6328        let third = worktrees[2].read(cx);
6329
6330        // check they are now in the right order
6331        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/b.rs");
6332        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/c.rs");
6333        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/a.rs");
6334    });
6335
6336    // move the third worktree to before the first
6337    // [b, c, a] -> [a, b, c]
6338    project
6339        .update(cx, |project, cx| {
6340            let third = worktree_a.read(cx);
6341            let first = worktree_b.read(cx);
6342            project.move_worktree(third.id(), first.id(), cx)
6343        })
6344        .expect("moving third before first");
6345
6346    // check the state after moving
6347    project.update(cx, |project, cx| {
6348        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
6349        assert_eq!(worktrees.len(), 3);
6350
6351        let first = worktrees[0].read(cx);
6352        let second = worktrees[1].read(cx);
6353        let third = worktrees[2].read(cx);
6354
6355        // check they are now in the right order
6356        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
6357        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/b.rs");
6358        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
6359    });
6360}
6361
6362#[gpui::test]
6363async fn test_unstaged_diff_for_buffer(cx: &mut gpui::TestAppContext) {
6364    init_test(cx);
6365
6366    let staged_contents = r#"
6367        fn main() {
6368            println!("hello world");
6369        }
6370    "#
6371    .unindent();
6372    let file_contents = r#"
6373        // print goodbye
6374        fn main() {
6375            println!("goodbye world");
6376        }
6377    "#
6378    .unindent();
6379
6380    let fs = FakeFs::new(cx.background_executor.clone());
6381    fs.insert_tree(
6382        "/dir",
6383        json!({
6384            ".git": {},
6385           "src": {
6386               "main.rs": file_contents,
6387           }
6388        }),
6389    )
6390    .await;
6391
6392    fs.set_index_for_repo(
6393        Path::new("/dir/.git"),
6394        &[("src/main.rs".into(), staged_contents)],
6395    );
6396
6397    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
6398
6399    let buffer = project
6400        .update(cx, |project, cx| {
6401            project.open_local_buffer("/dir/src/main.rs", cx)
6402        })
6403        .await
6404        .unwrap();
6405    let unstaged_diff = project
6406        .update(cx, |project, cx| {
6407            project.open_unstaged_diff(buffer.clone(), cx)
6408        })
6409        .await
6410        .unwrap();
6411
6412    cx.run_until_parked();
6413    unstaged_diff.update(cx, |unstaged_diff, cx| {
6414        let snapshot = buffer.read(cx).snapshot();
6415        assert_hunks(
6416            unstaged_diff.hunks(&snapshot, cx),
6417            &snapshot,
6418            &unstaged_diff.base_text_string().unwrap(),
6419            &[
6420                (0..1, "", "// print goodbye\n", DiffHunkStatus::added_none()),
6421                (
6422                    2..3,
6423                    "    println!(\"hello world\");\n",
6424                    "    println!(\"goodbye world\");\n",
6425                    DiffHunkStatus::modified_none(),
6426                ),
6427            ],
6428        );
6429    });
6430
6431    let staged_contents = r#"
6432        // print goodbye
6433        fn main() {
6434        }
6435    "#
6436    .unindent();
6437
6438    fs.set_index_for_repo(
6439        Path::new("/dir/.git"),
6440        &[("src/main.rs".into(), staged_contents)],
6441    );
6442
6443    cx.run_until_parked();
6444    unstaged_diff.update(cx, |unstaged_diff, cx| {
6445        let snapshot = buffer.read(cx).snapshot();
6446        assert_hunks(
6447            unstaged_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
6448            &snapshot,
6449            &unstaged_diff.base_text().text(),
6450            &[(
6451                2..3,
6452                "",
6453                "    println!(\"goodbye world\");\n",
6454                DiffHunkStatus::added_none(),
6455            )],
6456        );
6457    });
6458}
6459
6460#[gpui::test]
6461async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) {
6462    init_test(cx);
6463
6464    let committed_contents = r#"
6465        fn main() {
6466            println!("hello world");
6467        }
6468    "#
6469    .unindent();
6470    let staged_contents = r#"
6471        fn main() {
6472            println!("goodbye world");
6473        }
6474    "#
6475    .unindent();
6476    let file_contents = r#"
6477        // print goodbye
6478        fn main() {
6479            println!("goodbye world");
6480        }
6481    "#
6482    .unindent();
6483
6484    let fs = FakeFs::new(cx.background_executor.clone());
6485    fs.insert_tree(
6486        "/dir",
6487        json!({
6488            ".git": {},
6489           "src": {
6490               "modification.rs": file_contents,
6491           }
6492        }),
6493    )
6494    .await;
6495
6496    fs.set_head_for_repo(
6497        Path::new("/dir/.git"),
6498        &[
6499            ("src/modification.rs".into(), committed_contents),
6500            ("src/deletion.rs".into(), "// the-deleted-contents\n".into()),
6501        ],
6502    );
6503    fs.set_index_for_repo(
6504        Path::new("/dir/.git"),
6505        &[
6506            ("src/modification.rs".into(), staged_contents),
6507            ("src/deletion.rs".into(), "// the-deleted-contents\n".into()),
6508        ],
6509    );
6510
6511    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
6512    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
6513    let language = rust_lang();
6514    language_registry.add(language.clone());
6515
6516    let buffer_1 = project
6517        .update(cx, |project, cx| {
6518            project.open_local_buffer("/dir/src/modification.rs", cx)
6519        })
6520        .await
6521        .unwrap();
6522    let diff_1 = project
6523        .update(cx, |project, cx| {
6524            project.open_uncommitted_diff(buffer_1.clone(), cx)
6525        })
6526        .await
6527        .unwrap();
6528    diff_1.read_with(cx, |diff, _| {
6529        assert_eq!(diff.base_text().language().cloned(), Some(language))
6530    });
6531    cx.run_until_parked();
6532    diff_1.update(cx, |diff, cx| {
6533        let snapshot = buffer_1.read(cx).snapshot();
6534        assert_hunks(
6535            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
6536            &snapshot,
6537            &diff.base_text_string().unwrap(),
6538            &[
6539                (
6540                    0..1,
6541                    "",
6542                    "// print goodbye\n",
6543                    DiffHunkStatus::added(DiffHunkSecondaryStatus::HasSecondaryHunk),
6544                ),
6545                (
6546                    2..3,
6547                    "    println!(\"hello world\");\n",
6548                    "    println!(\"goodbye world\");\n",
6549                    DiffHunkStatus::modified_none(),
6550                ),
6551            ],
6552        );
6553    });
6554
6555    // Reset HEAD to a version that differs from both the buffer and the index.
6556    let committed_contents = r#"
6557        // print goodbye
6558        fn main() {
6559        }
6560    "#
6561    .unindent();
6562    fs.set_head_for_repo(
6563        Path::new("/dir/.git"),
6564        &[
6565            ("src/modification.rs".into(), committed_contents.clone()),
6566            ("src/deletion.rs".into(), "// the-deleted-contents\n".into()),
6567        ],
6568    );
6569
6570    // Buffer now has an unstaged hunk.
6571    cx.run_until_parked();
6572    diff_1.update(cx, |diff, cx| {
6573        let snapshot = buffer_1.read(cx).snapshot();
6574        assert_hunks(
6575            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
6576            &snapshot,
6577            &diff.base_text().text(),
6578            &[(
6579                2..3,
6580                "",
6581                "    println!(\"goodbye world\");\n",
6582                DiffHunkStatus::added_none(),
6583            )],
6584        );
6585    });
6586
6587    // Open a buffer for a file that's been deleted.
6588    let buffer_2 = project
6589        .update(cx, |project, cx| {
6590            project.open_local_buffer("/dir/src/deletion.rs", cx)
6591        })
6592        .await
6593        .unwrap();
6594    let diff_2 = project
6595        .update(cx, |project, cx| {
6596            project.open_uncommitted_diff(buffer_2.clone(), cx)
6597        })
6598        .await
6599        .unwrap();
6600    cx.run_until_parked();
6601    diff_2.update(cx, |diff, cx| {
6602        let snapshot = buffer_2.read(cx).snapshot();
6603        assert_hunks(
6604            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
6605            &snapshot,
6606            &diff.base_text_string().unwrap(),
6607            &[(
6608                0..0,
6609                "// the-deleted-contents\n",
6610                "",
6611                DiffHunkStatus::deleted(DiffHunkSecondaryStatus::HasSecondaryHunk),
6612            )],
6613        );
6614    });
6615
6616    // Stage the deletion of this file
6617    fs.set_index_for_repo(
6618        Path::new("/dir/.git"),
6619        &[("src/modification.rs".into(), committed_contents.clone())],
6620    );
6621    cx.run_until_parked();
6622    diff_2.update(cx, |diff, cx| {
6623        let snapshot = buffer_2.read(cx).snapshot();
6624        assert_hunks(
6625            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
6626            &snapshot,
6627            &diff.base_text_string().unwrap(),
6628            &[(
6629                0..0,
6630                "// the-deleted-contents\n",
6631                "",
6632                DiffHunkStatus::deleted(DiffHunkSecondaryStatus::NoSecondaryHunk),
6633            )],
6634        );
6635    });
6636}
6637
6638#[gpui::test]
6639async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
6640    use DiffHunkSecondaryStatus::*;
6641    init_test(cx);
6642
6643    let committed_contents = r#"
6644        zero
6645        one
6646        two
6647        three
6648        four
6649        five
6650    "#
6651    .unindent();
6652    let file_contents = r#"
6653        one
6654        TWO
6655        three
6656        FOUR
6657        five
6658    "#
6659    .unindent();
6660
6661    let fs = FakeFs::new(cx.background_executor.clone());
6662    fs.insert_tree(
6663        "/dir",
6664        json!({
6665            ".git": {},
6666            "file.txt": file_contents.clone()
6667        }),
6668    )
6669    .await;
6670
6671    fs.set_head_and_index_for_repo(
6672        "/dir/.git".as_ref(),
6673        &[("file.txt".into(), committed_contents.clone())],
6674    );
6675
6676    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
6677
6678    let buffer = project
6679        .update(cx, |project, cx| {
6680            project.open_local_buffer("/dir/file.txt", cx)
6681        })
6682        .await
6683        .unwrap();
6684    let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
6685    let uncommitted_diff = project
6686        .update(cx, |project, cx| {
6687            project.open_uncommitted_diff(buffer.clone(), cx)
6688        })
6689        .await
6690        .unwrap();
6691    let mut diff_events = cx.events(&uncommitted_diff);
6692
6693    // The hunks are initially unstaged.
6694    uncommitted_diff.read_with(cx, |diff, cx| {
6695        assert_hunks(
6696            diff.hunks(&snapshot, cx),
6697            &snapshot,
6698            &diff.base_text_string().unwrap(),
6699            &[
6700                (
6701                    0..0,
6702                    "zero\n",
6703                    "",
6704                    DiffHunkStatus::deleted(HasSecondaryHunk),
6705                ),
6706                (
6707                    1..2,
6708                    "two\n",
6709                    "TWO\n",
6710                    DiffHunkStatus::modified(HasSecondaryHunk),
6711                ),
6712                (
6713                    3..4,
6714                    "four\n",
6715                    "FOUR\n",
6716                    DiffHunkStatus::modified(HasSecondaryHunk),
6717                ),
6718            ],
6719        );
6720    });
6721
6722    // Stage a hunk. It appears as optimistically staged.
6723    uncommitted_diff.update(cx, |diff, cx| {
6724        let range =
6725            snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_before(Point::new(2, 0));
6726        let hunks = diff
6727            .hunks_intersecting_range(range, &snapshot, cx)
6728            .collect::<Vec<_>>();
6729        diff.stage_or_unstage_hunks(true, &hunks, &snapshot, true, cx);
6730
6731        assert_hunks(
6732            diff.hunks(&snapshot, cx),
6733            &snapshot,
6734            &diff.base_text_string().unwrap(),
6735            &[
6736                (
6737                    0..0,
6738                    "zero\n",
6739                    "",
6740                    DiffHunkStatus::deleted(HasSecondaryHunk),
6741                ),
6742                (
6743                    1..2,
6744                    "two\n",
6745                    "TWO\n",
6746                    DiffHunkStatus::modified(SecondaryHunkRemovalPending),
6747                ),
6748                (
6749                    3..4,
6750                    "four\n",
6751                    "FOUR\n",
6752                    DiffHunkStatus::modified(HasSecondaryHunk),
6753                ),
6754            ],
6755        );
6756    });
6757
6758    // The diff emits a change event for the range of the staged hunk.
6759    assert!(matches!(
6760        diff_events.next().await.unwrap(),
6761        BufferDiffEvent::HunksStagedOrUnstaged(_)
6762    ));
6763    let event = diff_events.next().await.unwrap();
6764    if let BufferDiffEvent::DiffChanged {
6765        changed_range: Some(changed_range),
6766    } = event
6767    {
6768        let changed_range = changed_range.to_point(&snapshot);
6769        assert_eq!(changed_range, Point::new(1, 0)..Point::new(2, 0));
6770    } else {
6771        panic!("Unexpected event {event:?}");
6772    }
6773
6774    // When the write to the index completes, it appears as staged.
6775    cx.run_until_parked();
6776    uncommitted_diff.update(cx, |diff, cx| {
6777        assert_hunks(
6778            diff.hunks(&snapshot, cx),
6779            &snapshot,
6780            &diff.base_text_string().unwrap(),
6781            &[
6782                (
6783                    0..0,
6784                    "zero\n",
6785                    "",
6786                    DiffHunkStatus::deleted(HasSecondaryHunk),
6787                ),
6788                (
6789                    1..2,
6790                    "two\n",
6791                    "TWO\n",
6792                    DiffHunkStatus::modified(NoSecondaryHunk),
6793                ),
6794                (
6795                    3..4,
6796                    "four\n",
6797                    "FOUR\n",
6798                    DiffHunkStatus::modified(HasSecondaryHunk),
6799                ),
6800            ],
6801        );
6802    });
6803
6804    // The diff emits a change event for the changed index text.
6805    let event = diff_events.next().await.unwrap();
6806    if let BufferDiffEvent::DiffChanged {
6807        changed_range: Some(changed_range),
6808    } = event
6809    {
6810        let changed_range = changed_range.to_point(&snapshot);
6811        assert_eq!(changed_range, Point::new(0, 0)..Point::new(4, 0));
6812    } else {
6813        panic!("Unexpected event {event:?}");
6814    }
6815
6816    // Simulate a problem writing to the git index.
6817    fs.set_error_message_for_index_write(
6818        "/dir/.git".as_ref(),
6819        Some("failed to write git index".into()),
6820    );
6821
6822    // Stage another hunk.
6823    uncommitted_diff.update(cx, |diff, cx| {
6824        let range =
6825            snapshot.anchor_before(Point::new(3, 0))..snapshot.anchor_before(Point::new(4, 0));
6826        let hunks = diff
6827            .hunks_intersecting_range(range, &snapshot, cx)
6828            .collect::<Vec<_>>();
6829        diff.stage_or_unstage_hunks(true, &hunks, &snapshot, true, cx);
6830
6831        assert_hunks(
6832            diff.hunks(&snapshot, cx),
6833            &snapshot,
6834            &diff.base_text_string().unwrap(),
6835            &[
6836                (
6837                    0..0,
6838                    "zero\n",
6839                    "",
6840                    DiffHunkStatus::deleted(HasSecondaryHunk),
6841                ),
6842                (
6843                    1..2,
6844                    "two\n",
6845                    "TWO\n",
6846                    DiffHunkStatus::modified(NoSecondaryHunk),
6847                ),
6848                (
6849                    3..4,
6850                    "four\n",
6851                    "FOUR\n",
6852                    DiffHunkStatus::modified(SecondaryHunkRemovalPending),
6853                ),
6854            ],
6855        );
6856    });
6857    assert!(matches!(
6858        diff_events.next().await.unwrap(),
6859        BufferDiffEvent::HunksStagedOrUnstaged(_)
6860    ));
6861    let event = diff_events.next().await.unwrap();
6862    if let BufferDiffEvent::DiffChanged {
6863        changed_range: Some(changed_range),
6864    } = event
6865    {
6866        let changed_range = changed_range.to_point(&snapshot);
6867        assert_eq!(changed_range, Point::new(3, 0)..Point::new(4, 0));
6868    } else {
6869        panic!("Unexpected event {event:?}");
6870    }
6871
6872    // When the write fails, the hunk returns to being unstaged.
6873    cx.run_until_parked();
6874    uncommitted_diff.update(cx, |diff, cx| {
6875        assert_hunks(
6876            diff.hunks(&snapshot, cx),
6877            &snapshot,
6878            &diff.base_text_string().unwrap(),
6879            &[
6880                (
6881                    0..0,
6882                    "zero\n",
6883                    "",
6884                    DiffHunkStatus::deleted(HasSecondaryHunk),
6885                ),
6886                (
6887                    1..2,
6888                    "two\n",
6889                    "TWO\n",
6890                    DiffHunkStatus::modified(NoSecondaryHunk),
6891                ),
6892                (
6893                    3..4,
6894                    "four\n",
6895                    "FOUR\n",
6896                    DiffHunkStatus::modified(HasSecondaryHunk),
6897                ),
6898            ],
6899        );
6900    });
6901
6902    let event = diff_events.next().await.unwrap();
6903    if let BufferDiffEvent::DiffChanged {
6904        changed_range: Some(changed_range),
6905    } = event
6906    {
6907        let changed_range = changed_range.to_point(&snapshot);
6908        assert_eq!(changed_range, Point::new(0, 0)..Point::new(5, 0));
6909    } else {
6910        panic!("Unexpected event {event:?}");
6911    }
6912
6913    // Allow writing to the git index to succeed again.
6914    fs.set_error_message_for_index_write("/dir/.git".as_ref(), None);
6915
6916    // Stage two hunks with separate operations.
6917    uncommitted_diff.update(cx, |diff, cx| {
6918        let hunks = diff.hunks(&snapshot, cx).collect::<Vec<_>>();
6919        diff.stage_or_unstage_hunks(true, &hunks[0..1], &snapshot, true, cx);
6920        diff.stage_or_unstage_hunks(true, &hunks[2..3], &snapshot, true, cx);
6921    });
6922
6923    // Both staged hunks appear as pending.
6924    uncommitted_diff.update(cx, |diff, cx| {
6925        assert_hunks(
6926            diff.hunks(&snapshot, cx),
6927            &snapshot,
6928            &diff.base_text_string().unwrap(),
6929            &[
6930                (
6931                    0..0,
6932                    "zero\n",
6933                    "",
6934                    DiffHunkStatus::deleted(SecondaryHunkRemovalPending),
6935                ),
6936                (
6937                    1..2,
6938                    "two\n",
6939                    "TWO\n",
6940                    DiffHunkStatus::modified(NoSecondaryHunk),
6941                ),
6942                (
6943                    3..4,
6944                    "four\n",
6945                    "FOUR\n",
6946                    DiffHunkStatus::modified(SecondaryHunkRemovalPending),
6947                ),
6948            ],
6949        );
6950    });
6951
6952    // Both staging operations take effect.
6953    cx.run_until_parked();
6954    uncommitted_diff.update(cx, |diff, cx| {
6955        assert_hunks(
6956            diff.hunks(&snapshot, cx),
6957            &snapshot,
6958            &diff.base_text_string().unwrap(),
6959            &[
6960                (0..0, "zero\n", "", DiffHunkStatus::deleted(NoSecondaryHunk)),
6961                (
6962                    1..2,
6963                    "two\n",
6964                    "TWO\n",
6965                    DiffHunkStatus::modified(NoSecondaryHunk),
6966                ),
6967                (
6968                    3..4,
6969                    "four\n",
6970                    "FOUR\n",
6971                    DiffHunkStatus::modified(NoSecondaryHunk),
6972                ),
6973            ],
6974        );
6975    });
6976}
6977
6978#[gpui::test(seeds(340, 472))]
6979async fn test_staging_hunks_with_delayed_fs_event(cx: &mut gpui::TestAppContext) {
6980    use DiffHunkSecondaryStatus::*;
6981    init_test(cx);
6982
6983    let committed_contents = r#"
6984        zero
6985        one
6986        two
6987        three
6988        four
6989        five
6990    "#
6991    .unindent();
6992    let file_contents = r#"
6993        one
6994        TWO
6995        three
6996        FOUR
6997        five
6998    "#
6999    .unindent();
7000
7001    let fs = FakeFs::new(cx.background_executor.clone());
7002    fs.insert_tree(
7003        "/dir",
7004        json!({
7005            ".git": {},
7006            "file.txt": file_contents.clone()
7007        }),
7008    )
7009    .await;
7010
7011    fs.set_head_for_repo(
7012        "/dir/.git".as_ref(),
7013        &[("file.txt".into(), committed_contents.clone())],
7014    );
7015    fs.set_index_for_repo(
7016        "/dir/.git".as_ref(),
7017        &[("file.txt".into(), committed_contents.clone())],
7018    );
7019
7020    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
7021
7022    let buffer = project
7023        .update(cx, |project, cx| {
7024            project.open_local_buffer("/dir/file.txt", cx)
7025        })
7026        .await
7027        .unwrap();
7028    let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
7029    let uncommitted_diff = project
7030        .update(cx, |project, cx| {
7031            project.open_uncommitted_diff(buffer.clone(), cx)
7032        })
7033        .await
7034        .unwrap();
7035
7036    // The hunks are initially unstaged.
7037    uncommitted_diff.read_with(cx, |diff, cx| {
7038        assert_hunks(
7039            diff.hunks(&snapshot, cx),
7040            &snapshot,
7041            &diff.base_text_string().unwrap(),
7042            &[
7043                (
7044                    0..0,
7045                    "zero\n",
7046                    "",
7047                    DiffHunkStatus::deleted(HasSecondaryHunk),
7048                ),
7049                (
7050                    1..2,
7051                    "two\n",
7052                    "TWO\n",
7053                    DiffHunkStatus::modified(HasSecondaryHunk),
7054                ),
7055                (
7056                    3..4,
7057                    "four\n",
7058                    "FOUR\n",
7059                    DiffHunkStatus::modified(HasSecondaryHunk),
7060                ),
7061            ],
7062        );
7063    });
7064
7065    // Pause IO events
7066    fs.pause_events();
7067
7068    // Stage the first hunk.
7069    uncommitted_diff.update(cx, |diff, cx| {
7070        let hunk = diff.hunks(&snapshot, cx).next().unwrap();
7071        diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx);
7072        assert_hunks(
7073            diff.hunks(&snapshot, cx),
7074            &snapshot,
7075            &diff.base_text_string().unwrap(),
7076            &[
7077                (
7078                    0..0,
7079                    "zero\n",
7080                    "",
7081                    DiffHunkStatus::deleted(SecondaryHunkRemovalPending),
7082                ),
7083                (
7084                    1..2,
7085                    "two\n",
7086                    "TWO\n",
7087                    DiffHunkStatus::modified(HasSecondaryHunk),
7088                ),
7089                (
7090                    3..4,
7091                    "four\n",
7092                    "FOUR\n",
7093                    DiffHunkStatus::modified(HasSecondaryHunk),
7094                ),
7095            ],
7096        );
7097    });
7098
7099    // Stage the second hunk *before* receiving the FS event for the first hunk.
7100    cx.run_until_parked();
7101    uncommitted_diff.update(cx, |diff, cx| {
7102        let hunk = diff.hunks(&snapshot, cx).nth(1).unwrap();
7103        diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx);
7104        assert_hunks(
7105            diff.hunks(&snapshot, cx),
7106            &snapshot,
7107            &diff.base_text_string().unwrap(),
7108            &[
7109                (
7110                    0..0,
7111                    "zero\n",
7112                    "",
7113                    DiffHunkStatus::deleted(SecondaryHunkRemovalPending),
7114                ),
7115                (
7116                    1..2,
7117                    "two\n",
7118                    "TWO\n",
7119                    DiffHunkStatus::modified(SecondaryHunkRemovalPending),
7120                ),
7121                (
7122                    3..4,
7123                    "four\n",
7124                    "FOUR\n",
7125                    DiffHunkStatus::modified(HasSecondaryHunk),
7126                ),
7127            ],
7128        );
7129    });
7130
7131    // Process the FS event for staging the first hunk (second event is still pending).
7132    fs.flush_events(1);
7133    cx.run_until_parked();
7134
7135    // Stage the third hunk before receiving the second FS event.
7136    uncommitted_diff.update(cx, |diff, cx| {
7137        let hunk = diff.hunks(&snapshot, cx).nth(2).unwrap();
7138        diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx);
7139    });
7140
7141    // Wait for all remaining IO.
7142    cx.run_until_parked();
7143    fs.flush_events(fs.buffered_event_count());
7144
7145    // Now all hunks are staged.
7146    cx.run_until_parked();
7147    uncommitted_diff.update(cx, |diff, cx| {
7148        assert_hunks(
7149            diff.hunks(&snapshot, cx),
7150            &snapshot,
7151            &diff.base_text_string().unwrap(),
7152            &[
7153                (0..0, "zero\n", "", DiffHunkStatus::deleted(NoSecondaryHunk)),
7154                (
7155                    1..2,
7156                    "two\n",
7157                    "TWO\n",
7158                    DiffHunkStatus::modified(NoSecondaryHunk),
7159                ),
7160                (
7161                    3..4,
7162                    "four\n",
7163                    "FOUR\n",
7164                    DiffHunkStatus::modified(NoSecondaryHunk),
7165                ),
7166            ],
7167        );
7168    });
7169}
7170
7171#[gpui::test(iterations = 25)]
7172async fn test_staging_random_hunks(
7173    mut rng: StdRng,
7174    executor: BackgroundExecutor,
7175    cx: &mut gpui::TestAppContext,
7176) {
7177    let operations = env::var("OPERATIONS")
7178        .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
7179        .unwrap_or(20);
7180
7181    // Try to induce races between diff recalculation and index writes.
7182    if rng.gen_bool(0.5) {
7183        executor.deprioritize(*CALCULATE_DIFF_TASK);
7184    }
7185
7186    use DiffHunkSecondaryStatus::*;
7187    init_test(cx);
7188
7189    let committed_text = (0..30).map(|i| format!("line {i}\n")).collect::<String>();
7190    let index_text = committed_text.clone();
7191    let buffer_text = (0..30)
7192        .map(|i| match i % 5 {
7193            0 => format!("line {i} (modified)\n"),
7194            _ => format!("line {i}\n"),
7195        })
7196        .collect::<String>();
7197
7198    let fs = FakeFs::new(cx.background_executor.clone());
7199    fs.insert_tree(
7200        path!("/dir"),
7201        json!({
7202            ".git": {},
7203            "file.txt": buffer_text.clone()
7204        }),
7205    )
7206    .await;
7207    fs.set_head_for_repo(
7208        path!("/dir/.git").as_ref(),
7209        &[("file.txt".into(), committed_text.clone())],
7210    );
7211    fs.set_index_for_repo(
7212        path!("/dir/.git").as_ref(),
7213        &[("file.txt".into(), index_text.clone())],
7214    );
7215    let repo = fs.open_repo(path!("/dir/.git").as_ref()).unwrap();
7216
7217    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
7218    let buffer = project
7219        .update(cx, |project, cx| {
7220            project.open_local_buffer(path!("/dir/file.txt"), cx)
7221        })
7222        .await
7223        .unwrap();
7224    let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
7225    let uncommitted_diff = project
7226        .update(cx, |project, cx| {
7227            project.open_uncommitted_diff(buffer.clone(), cx)
7228        })
7229        .await
7230        .unwrap();
7231
7232    let mut hunks =
7233        uncommitted_diff.update(cx, |diff, cx| diff.hunks(&snapshot, cx).collect::<Vec<_>>());
7234    assert_eq!(hunks.len(), 6);
7235
7236    for _i in 0..operations {
7237        let hunk_ix = rng.gen_range(0..hunks.len());
7238        let hunk = &mut hunks[hunk_ix];
7239        let row = hunk.range.start.row;
7240
7241        if hunk.status().has_secondary_hunk() {
7242            log::info!("staging hunk at {row}");
7243            uncommitted_diff.update(cx, |diff, cx| {
7244                diff.stage_or_unstage_hunks(true, &[hunk.clone()], &snapshot, true, cx);
7245            });
7246            hunk.secondary_status = SecondaryHunkRemovalPending;
7247        } else {
7248            log::info!("unstaging hunk at {row}");
7249            uncommitted_diff.update(cx, |diff, cx| {
7250                diff.stage_or_unstage_hunks(false, &[hunk.clone()], &snapshot, true, cx);
7251            });
7252            hunk.secondary_status = SecondaryHunkAdditionPending;
7253        }
7254
7255        for _ in 0..rng.gen_range(0..10) {
7256            log::info!("yielding");
7257            cx.executor().simulate_random_delay().await;
7258        }
7259    }
7260
7261    cx.executor().run_until_parked();
7262
7263    for hunk in &mut hunks {
7264        if hunk.secondary_status == SecondaryHunkRemovalPending {
7265            hunk.secondary_status = NoSecondaryHunk;
7266        } else if hunk.secondary_status == SecondaryHunkAdditionPending {
7267            hunk.secondary_status = HasSecondaryHunk;
7268        }
7269    }
7270
7271    log::info!(
7272        "index text:\n{}",
7273        repo.load_index_text("file.txt".into()).await.unwrap()
7274    );
7275
7276    uncommitted_diff.update(cx, |diff, cx| {
7277        let expected_hunks = hunks
7278            .iter()
7279            .map(|hunk| (hunk.range.start.row, hunk.secondary_status))
7280            .collect::<Vec<_>>();
7281        let actual_hunks = diff
7282            .hunks(&snapshot, cx)
7283            .map(|hunk| (hunk.range.start.row, hunk.secondary_status))
7284            .collect::<Vec<_>>();
7285        assert_eq!(actual_hunks, expected_hunks);
7286    });
7287}
7288
7289#[gpui::test]
7290async fn test_single_file_diffs(cx: &mut gpui::TestAppContext) {
7291    init_test(cx);
7292
7293    let committed_contents = r#"
7294        fn main() {
7295            println!("hello from HEAD");
7296        }
7297    "#
7298    .unindent();
7299    let file_contents = r#"
7300        fn main() {
7301            println!("hello from the working copy");
7302        }
7303    "#
7304    .unindent();
7305
7306    let fs = FakeFs::new(cx.background_executor.clone());
7307    fs.insert_tree(
7308        "/dir",
7309        json!({
7310            ".git": {},
7311           "src": {
7312               "main.rs": file_contents,
7313           }
7314        }),
7315    )
7316    .await;
7317
7318    fs.set_head_for_repo(
7319        Path::new("/dir/.git"),
7320        &[("src/main.rs".into(), committed_contents.clone())],
7321    );
7322    fs.set_index_for_repo(
7323        Path::new("/dir/.git"),
7324        &[("src/main.rs".into(), committed_contents.clone())],
7325    );
7326
7327    let project = Project::test(fs.clone(), ["/dir/src/main.rs".as_ref()], cx).await;
7328
7329    let buffer = project
7330        .update(cx, |project, cx| {
7331            project.open_local_buffer("/dir/src/main.rs", cx)
7332        })
7333        .await
7334        .unwrap();
7335    let uncommitted_diff = project
7336        .update(cx, |project, cx| {
7337            project.open_uncommitted_diff(buffer.clone(), cx)
7338        })
7339        .await
7340        .unwrap();
7341
7342    cx.run_until_parked();
7343    uncommitted_diff.update(cx, |uncommitted_diff, cx| {
7344        let snapshot = buffer.read(cx).snapshot();
7345        assert_hunks(
7346            uncommitted_diff.hunks(&snapshot, cx),
7347            &snapshot,
7348            &uncommitted_diff.base_text_string().unwrap(),
7349            &[(
7350                1..2,
7351                "    println!(\"hello from HEAD\");\n",
7352                "    println!(\"hello from the working copy\");\n",
7353                DiffHunkStatus {
7354                    kind: DiffHunkStatusKind::Modified,
7355                    secondary: DiffHunkSecondaryStatus::HasSecondaryHunk,
7356                },
7357            )],
7358        );
7359    });
7360}
7361
7362#[gpui::test]
7363async fn test_repository_and_path_for_project_path(
7364    background_executor: BackgroundExecutor,
7365    cx: &mut gpui::TestAppContext,
7366) {
7367    init_test(cx);
7368    let fs = FakeFs::new(background_executor);
7369    fs.insert_tree(
7370        path!("/root"),
7371        json!({
7372            "c.txt": "",
7373            "dir1": {
7374                ".git": {},
7375                "deps": {
7376                    "dep1": {
7377                        ".git": {},
7378                        "src": {
7379                            "a.txt": ""
7380                        }
7381                    }
7382                },
7383                "src": {
7384                    "b.txt": ""
7385                }
7386            },
7387        }),
7388    )
7389    .await;
7390
7391    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
7392    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7393    let tree_id = tree.read_with(cx, |tree, _| tree.id());
7394    tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
7395        .await;
7396    cx.run_until_parked();
7397
7398    project.read_with(cx, |project, cx| {
7399        let git_store = project.git_store().read(cx);
7400        let pairs = [
7401            ("c.txt", None),
7402            ("dir1/src/b.txt", Some((path!("/root/dir1"), "src/b.txt"))),
7403            (
7404                "dir1/deps/dep1/src/a.txt",
7405                Some((path!("/root/dir1/deps/dep1"), "src/a.txt")),
7406            ),
7407        ];
7408        let expected = pairs
7409            .iter()
7410            .map(|(path, result)| {
7411                (
7412                    path,
7413                    result.map(|(repo, repo_path)| {
7414                        (Path::new(repo).into(), RepoPath::from(repo_path))
7415                    }),
7416                )
7417            })
7418            .collect::<Vec<_>>();
7419        let actual = pairs
7420            .iter()
7421            .map(|(path, _)| {
7422                let project_path = (tree_id, Path::new(path)).into();
7423                let result = maybe!({
7424                    let (repo, repo_path) =
7425                        git_store.repository_and_path_for_project_path(&project_path, cx)?;
7426                    Some((repo.read(cx).work_directory_abs_path.clone(), repo_path))
7427                });
7428                (path, result)
7429            })
7430            .collect::<Vec<_>>();
7431        pretty_assertions::assert_eq!(expected, actual);
7432    });
7433
7434    fs.remove_dir(path!("/root/dir1/.git").as_ref(), RemoveOptions::default())
7435        .await
7436        .unwrap();
7437    cx.run_until_parked();
7438
7439    project.read_with(cx, |project, cx| {
7440        let git_store = project.git_store().read(cx);
7441        assert_eq!(
7442            git_store.repository_and_path_for_project_path(
7443                &(tree_id, Path::new("dir1/src/b.txt")).into(),
7444                cx
7445            ),
7446            None
7447        );
7448    });
7449}
7450
7451#[gpui::test]
7452async fn test_home_dir_as_git_repository(cx: &mut gpui::TestAppContext) {
7453    init_test(cx);
7454    let fs = FakeFs::new(cx.background_executor.clone());
7455    fs.insert_tree(
7456        path!("/root"),
7457        json!({
7458            "home": {
7459                ".git": {},
7460                "project": {
7461                    "a.txt": "A"
7462                },
7463            },
7464        }),
7465    )
7466    .await;
7467    fs.set_home_dir(Path::new(path!("/root/home")).to_owned());
7468
7469    let project = Project::test(fs.clone(), [path!("/root/home/project").as_ref()], cx).await;
7470    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7471    let tree_id = tree.read_with(cx, |tree, _| tree.id());
7472    tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
7473        .await;
7474    tree.flush_fs_events(cx).await;
7475
7476    project.read_with(cx, |project, cx| {
7477        let containing = project
7478            .git_store()
7479            .read(cx)
7480            .repository_and_path_for_project_path(&(tree_id, "a.txt").into(), cx);
7481        assert!(containing.is_none());
7482    });
7483
7484    let project = Project::test(fs.clone(), [path!("/root/home").as_ref()], cx).await;
7485    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7486    let tree_id = tree.read_with(cx, |tree, _| tree.id());
7487    tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
7488        .await;
7489    tree.flush_fs_events(cx).await;
7490
7491    project.read_with(cx, |project, cx| {
7492        let containing = project
7493            .git_store()
7494            .read(cx)
7495            .repository_and_path_for_project_path(&(tree_id, "project/a.txt").into(), cx);
7496        assert_eq!(
7497            containing
7498                .unwrap()
7499                .0
7500                .read(cx)
7501                .work_directory_abs_path
7502                .as_ref(),
7503            Path::new(path!("/root/home"))
7504        );
7505    });
7506}
7507
7508#[gpui::test]
7509async fn test_git_repository_status(cx: &mut gpui::TestAppContext) {
7510    init_test(cx);
7511    cx.executor().allow_parking();
7512
7513    let root = TempTree::new(json!({
7514        "project": {
7515            "a.txt": "a",    // Modified
7516            "b.txt": "bb",   // Added
7517            "c.txt": "ccc",  // Unchanged
7518            "d.txt": "dddd", // Deleted
7519        },
7520    }));
7521
7522    // Set up git repository before creating the project.
7523    let work_dir = root.path().join("project");
7524    let repo = git_init(work_dir.as_path());
7525    git_add("a.txt", &repo);
7526    git_add("c.txt", &repo);
7527    git_add("d.txt", &repo);
7528    git_commit("Initial commit", &repo);
7529    std::fs::remove_file(work_dir.join("d.txt")).unwrap();
7530    std::fs::write(work_dir.join("a.txt"), "aa").unwrap();
7531
7532    let project = Project::test(
7533        Arc::new(RealFs::new(None, cx.executor())),
7534        [root.path()],
7535        cx,
7536    )
7537    .await;
7538
7539    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7540    tree.flush_fs_events(cx).await;
7541    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7542        .await;
7543    cx.executor().run_until_parked();
7544
7545    let repository = project.read_with(cx, |project, cx| {
7546        project.repositories(cx).values().next().unwrap().clone()
7547    });
7548
7549    // Check that the right git state is observed on startup
7550    repository.read_with(cx, |repository, _| {
7551        let entries = repository.cached_status().collect::<Vec<_>>();
7552        assert_eq!(
7553            entries,
7554            [
7555                StatusEntry {
7556                    repo_path: "a.txt".into(),
7557                    status: StatusCode::Modified.worktree(),
7558                },
7559                StatusEntry {
7560                    repo_path: "b.txt".into(),
7561                    status: FileStatus::Untracked,
7562                },
7563                StatusEntry {
7564                    repo_path: "d.txt".into(),
7565                    status: StatusCode::Deleted.worktree(),
7566                },
7567            ]
7568        );
7569    });
7570
7571    std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
7572
7573    tree.flush_fs_events(cx).await;
7574    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7575        .await;
7576    cx.executor().run_until_parked();
7577
7578    repository.read_with(cx, |repository, _| {
7579        let entries = repository.cached_status().collect::<Vec<_>>();
7580        assert_eq!(
7581            entries,
7582            [
7583                StatusEntry {
7584                    repo_path: "a.txt".into(),
7585                    status: StatusCode::Modified.worktree(),
7586                },
7587                StatusEntry {
7588                    repo_path: "b.txt".into(),
7589                    status: FileStatus::Untracked,
7590                },
7591                StatusEntry {
7592                    repo_path: "c.txt".into(),
7593                    status: StatusCode::Modified.worktree(),
7594                },
7595                StatusEntry {
7596                    repo_path: "d.txt".into(),
7597                    status: StatusCode::Deleted.worktree(),
7598                },
7599            ]
7600        );
7601    });
7602
7603    git_add("a.txt", &repo);
7604    git_add("c.txt", &repo);
7605    git_remove_index(Path::new("d.txt"), &repo);
7606    git_commit("Another commit", &repo);
7607    tree.flush_fs_events(cx).await;
7608    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7609        .await;
7610    cx.executor().run_until_parked();
7611
7612    std::fs::remove_file(work_dir.join("a.txt")).unwrap();
7613    std::fs::remove_file(work_dir.join("b.txt")).unwrap();
7614    tree.flush_fs_events(cx).await;
7615    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7616        .await;
7617    cx.executor().run_until_parked();
7618
7619    repository.read_with(cx, |repository, _cx| {
7620        let entries = repository.cached_status().collect::<Vec<_>>();
7621
7622        // Deleting an untracked entry, b.txt, should leave no status
7623        // a.txt was tracked, and so should have a status
7624        assert_eq!(
7625            entries,
7626            [StatusEntry {
7627                repo_path: "a.txt".into(),
7628                status: StatusCode::Deleted.worktree(),
7629            }]
7630        );
7631    });
7632}
7633
7634#[gpui::test]
7635async fn test_git_status_postprocessing(cx: &mut gpui::TestAppContext) {
7636    init_test(cx);
7637    cx.executor().allow_parking();
7638
7639    let root = TempTree::new(json!({
7640        "project": {
7641            "sub": {},
7642            "a.txt": "",
7643        },
7644    }));
7645
7646    let work_dir = root.path().join("project");
7647    let repo = git_init(work_dir.as_path());
7648    // a.txt exists in HEAD and the working copy but is deleted in the index.
7649    git_add("a.txt", &repo);
7650    git_commit("Initial commit", &repo);
7651    git_remove_index("a.txt".as_ref(), &repo);
7652    // `sub` is a nested git repository.
7653    let _sub = git_init(&work_dir.join("sub"));
7654
7655    let project = Project::test(
7656        Arc::new(RealFs::new(None, cx.executor())),
7657        [root.path()],
7658        cx,
7659    )
7660    .await;
7661
7662    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7663    tree.flush_fs_events(cx).await;
7664    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7665        .await;
7666    cx.executor().run_until_parked();
7667
7668    let repository = project.read_with(cx, |project, cx| {
7669        project
7670            .repositories(cx)
7671            .values()
7672            .find(|repo| repo.read(cx).work_directory_abs_path.ends_with("project"))
7673            .unwrap()
7674            .clone()
7675    });
7676
7677    repository.read_with(cx, |repository, _cx| {
7678        let entries = repository.cached_status().collect::<Vec<_>>();
7679
7680        // `sub` doesn't appear in our computed statuses.
7681        // a.txt appears with a combined `DA` status.
7682        assert_eq!(
7683            entries,
7684            [StatusEntry {
7685                repo_path: "a.txt".into(),
7686                status: TrackedStatus {
7687                    index_status: StatusCode::Deleted,
7688                    worktree_status: StatusCode::Added
7689                }
7690                .into(),
7691            }]
7692        )
7693    });
7694}
7695
7696#[gpui::test]
7697async fn test_repository_subfolder_git_status(
7698    executor: gpui::BackgroundExecutor,
7699    cx: &mut gpui::TestAppContext,
7700) {
7701    init_test(cx);
7702
7703    let fs = FakeFs::new(executor);
7704    fs.insert_tree(
7705        path!("/root"),
7706        json!({
7707            "my-repo": {
7708                ".git": {},
7709                "a.txt": "a",
7710                "sub-folder-1": {
7711                    "sub-folder-2": {
7712                        "c.txt": "cc",
7713                        "d": {
7714                            "e.txt": "eee"
7715                        }
7716                    },
7717                }
7718            },
7719        }),
7720    )
7721    .await;
7722
7723    const C_TXT: &str = "sub-folder-1/sub-folder-2/c.txt";
7724    const E_TXT: &str = "sub-folder-1/sub-folder-2/d/e.txt";
7725
7726    fs.set_status_for_repo(
7727        path!("/root/my-repo/.git").as_ref(),
7728        &[(E_TXT.as_ref(), FileStatus::Untracked)],
7729    );
7730
7731    let project = Project::test(
7732        fs.clone(),
7733        [path!("/root/my-repo/sub-folder-1/sub-folder-2").as_ref()],
7734        cx,
7735    )
7736    .await;
7737
7738    project
7739        .update(cx, |project, cx| project.git_scans_complete(cx))
7740        .await;
7741    cx.run_until_parked();
7742
7743    let repository = project.read_with(cx, |project, cx| {
7744        project.repositories(cx).values().next().unwrap().clone()
7745    });
7746
7747    // Ensure that the git status is loaded correctly
7748    repository.read_with(cx, |repository, _cx| {
7749        assert_eq!(
7750            repository.work_directory_abs_path,
7751            Path::new(path!("/root/my-repo")).into()
7752        );
7753
7754        assert_eq!(repository.status_for_path(&C_TXT.into()), None);
7755        assert_eq!(
7756            repository.status_for_path(&E_TXT.into()).unwrap().status,
7757            FileStatus::Untracked
7758        );
7759    });
7760
7761    fs.set_status_for_repo(path!("/root/my-repo/.git").as_ref(), &[]);
7762    project
7763        .update(cx, |project, cx| project.git_scans_complete(cx))
7764        .await;
7765    cx.run_until_parked();
7766
7767    repository.read_with(cx, |repository, _cx| {
7768        assert_eq!(repository.status_for_path(&C_TXT.into()), None);
7769        assert_eq!(repository.status_for_path(&E_TXT.into()), None);
7770    });
7771}
7772
7773// TODO: this test is flaky (especially on Windows but at least sometimes on all platforms).
7774#[cfg(any())]
7775#[gpui::test]
7776async fn test_conflicted_cherry_pick(cx: &mut gpui::TestAppContext) {
7777    init_test(cx);
7778    cx.executor().allow_parking();
7779
7780    let root = TempTree::new(json!({
7781        "project": {
7782            "a.txt": "a",
7783        },
7784    }));
7785    let root_path = root.path();
7786
7787    let repo = git_init(&root_path.join("project"));
7788    git_add("a.txt", &repo);
7789    git_commit("init", &repo);
7790
7791    let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [root_path], cx).await;
7792
7793    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7794    tree.flush_fs_events(cx).await;
7795    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7796        .await;
7797    cx.executor().run_until_parked();
7798
7799    let repository = project.read_with(cx, |project, cx| {
7800        project.repositories(cx).values().next().unwrap().clone()
7801    });
7802
7803    git_branch("other-branch", &repo);
7804    git_checkout("refs/heads/other-branch", &repo);
7805    std::fs::write(root_path.join("project/a.txt"), "A").unwrap();
7806    git_add("a.txt", &repo);
7807    git_commit("capitalize", &repo);
7808    let commit = repo
7809        .head()
7810        .expect("Failed to get HEAD")
7811        .peel_to_commit()
7812        .expect("HEAD is not a commit");
7813    git_checkout("refs/heads/main", &repo);
7814    std::fs::write(root_path.join("project/a.txt"), "b").unwrap();
7815    git_add("a.txt", &repo);
7816    git_commit("improve letter", &repo);
7817    git_cherry_pick(&commit, &repo);
7818    std::fs::read_to_string(root_path.join("project/.git/CHERRY_PICK_HEAD"))
7819        .expect("No CHERRY_PICK_HEAD");
7820    pretty_assertions::assert_eq!(
7821        git_status(&repo),
7822        collections::HashMap::from_iter([("a.txt".to_owned(), git2::Status::CONFLICTED)])
7823    );
7824    tree.flush_fs_events(cx).await;
7825    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7826        .await;
7827    cx.executor().run_until_parked();
7828    let conflicts = repository.update(cx, |repository, _| {
7829        repository
7830            .merge_conflicts
7831            .iter()
7832            .cloned()
7833            .collect::<Vec<_>>()
7834    });
7835    pretty_assertions::assert_eq!(conflicts, [RepoPath::from("a.txt")]);
7836
7837    git_add("a.txt", &repo);
7838    // Attempt to manually simulate what `git cherry-pick --continue` would do.
7839    git_commit("whatevs", &repo);
7840    std::fs::remove_file(root.path().join("project/.git/CHERRY_PICK_HEAD"))
7841        .expect("Failed to remove CHERRY_PICK_HEAD");
7842    pretty_assertions::assert_eq!(git_status(&repo), collections::HashMap::default());
7843    tree.flush_fs_events(cx).await;
7844    let conflicts = repository.update(cx, |repository, _| {
7845        repository
7846            .merge_conflicts
7847            .iter()
7848            .cloned()
7849            .collect::<Vec<_>>()
7850    });
7851    pretty_assertions::assert_eq!(conflicts, []);
7852}
7853
7854#[gpui::test]
7855async fn test_update_gitignore(cx: &mut gpui::TestAppContext) {
7856    init_test(cx);
7857    let fs = FakeFs::new(cx.background_executor.clone());
7858    fs.insert_tree(
7859        path!("/root"),
7860        json!({
7861            ".git": {},
7862            ".gitignore": "*.txt\n",
7863            "a.xml": "<a></a>",
7864            "b.txt": "Some text"
7865        }),
7866    )
7867    .await;
7868
7869    fs.set_head_and_index_for_repo(
7870        path!("/root/.git").as_ref(),
7871        &[
7872            (".gitignore".into(), "*.txt\n".into()),
7873            ("a.xml".into(), "<a></a>".into()),
7874        ],
7875    );
7876
7877    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
7878
7879    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7880    tree.flush_fs_events(cx).await;
7881    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7882        .await;
7883    cx.executor().run_until_parked();
7884
7885    let repository = project.read_with(cx, |project, cx| {
7886        project.repositories(cx).values().next().unwrap().clone()
7887    });
7888
7889    // One file is unmodified, the other is ignored.
7890    cx.read(|cx| {
7891        assert_entry_git_state(tree.read(cx), repository.read(cx), "a.xml", None, false);
7892        assert_entry_git_state(tree.read(cx), repository.read(cx), "b.txt", None, true);
7893    });
7894
7895    // Change the gitignore, and stage the newly non-ignored file.
7896    fs.atomic_write(path!("/root/.gitignore").into(), "*.xml\n".into())
7897        .await
7898        .unwrap();
7899    fs.set_index_for_repo(
7900        Path::new(path!("/root/.git")),
7901        &[
7902            (".gitignore".into(), "*.txt\n".into()),
7903            ("a.xml".into(), "<a></a>".into()),
7904            ("b.txt".into(), "Some text".into()),
7905        ],
7906    );
7907
7908    cx.executor().run_until_parked();
7909    cx.read(|cx| {
7910        assert_entry_git_state(tree.read(cx), repository.read(cx), "a.xml", None, true);
7911        assert_entry_git_state(
7912            tree.read(cx),
7913            repository.read(cx),
7914            "b.txt",
7915            Some(StatusCode::Added),
7916            false,
7917        );
7918    });
7919}
7920
7921// NOTE:
7922// This test always fails on Windows, because on Windows, unlike on Unix, you can't rename
7923// a directory which some program has already open.
7924// This is a limitation of the Windows.
7925// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
7926#[gpui::test]
7927#[cfg_attr(target_os = "windows", ignore)]
7928async fn test_rename_work_directory(cx: &mut gpui::TestAppContext) {
7929    init_test(cx);
7930    cx.executor().allow_parking();
7931    let root = TempTree::new(json!({
7932        "projects": {
7933            "project1": {
7934                "a": "",
7935                "b": "",
7936            }
7937        },
7938
7939    }));
7940    let root_path = root.path();
7941
7942    let repo = git_init(&root_path.join("projects/project1"));
7943    git_add("a", &repo);
7944    git_commit("init", &repo);
7945    std::fs::write(root_path.join("projects/project1/a"), "aa").unwrap();
7946
7947    let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [root_path], cx).await;
7948
7949    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7950    tree.flush_fs_events(cx).await;
7951    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7952        .await;
7953    cx.executor().run_until_parked();
7954
7955    let repository = project.read_with(cx, |project, cx| {
7956        project.repositories(cx).values().next().unwrap().clone()
7957    });
7958
7959    repository.read_with(cx, |repository, _| {
7960        assert_eq!(
7961            repository.work_directory_abs_path.as_ref(),
7962            root_path.join("projects/project1").as_path()
7963        );
7964        assert_eq!(
7965            repository
7966                .status_for_path(&"a".into())
7967                .map(|entry| entry.status),
7968            Some(StatusCode::Modified.worktree()),
7969        );
7970        assert_eq!(
7971            repository
7972                .status_for_path(&"b".into())
7973                .map(|entry| entry.status),
7974            Some(FileStatus::Untracked),
7975        );
7976    });
7977
7978    std::fs::rename(
7979        root_path.join("projects/project1"),
7980        root_path.join("projects/project2"),
7981    )
7982    .unwrap();
7983    tree.flush_fs_events(cx).await;
7984
7985    repository.read_with(cx, |repository, _| {
7986        assert_eq!(
7987            repository.work_directory_abs_path.as_ref(),
7988            root_path.join("projects/project2").as_path()
7989        );
7990        assert_eq!(
7991            repository.status_for_path(&"a".into()).unwrap().status,
7992            StatusCode::Modified.worktree(),
7993        );
7994        assert_eq!(
7995            repository.status_for_path(&"b".into()).unwrap().status,
7996            FileStatus::Untracked,
7997        );
7998    });
7999}
8000
8001// NOTE: This test always fails on Windows, because on Windows, unlike on Unix,
8002// you can't rename a directory which some program has already open. This is a
8003// limitation of the Windows. See:
8004// https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
8005#[gpui::test]
8006#[cfg_attr(target_os = "windows", ignore)]
8007async fn test_file_status(cx: &mut gpui::TestAppContext) {
8008    init_test(cx);
8009    cx.executor().allow_parking();
8010    const IGNORE_RULE: &str = "**/target";
8011
8012    let root = TempTree::new(json!({
8013        "project": {
8014            "a.txt": "a",
8015            "b.txt": "bb",
8016            "c": {
8017                "d": {
8018                    "e.txt": "eee"
8019                }
8020            },
8021            "f.txt": "ffff",
8022            "target": {
8023                "build_file": "???"
8024            },
8025            ".gitignore": IGNORE_RULE
8026        },
8027
8028    }));
8029    let root_path = root.path();
8030
8031    const A_TXT: &str = "a.txt";
8032    const B_TXT: &str = "b.txt";
8033    const E_TXT: &str = "c/d/e.txt";
8034    const F_TXT: &str = "f.txt";
8035    const DOTGITIGNORE: &str = ".gitignore";
8036    const BUILD_FILE: &str = "target/build_file";
8037
8038    // Set up git repository before creating the worktree.
8039    let work_dir = root.path().join("project");
8040    let mut repo = git_init(work_dir.as_path());
8041    repo.add_ignore_rule(IGNORE_RULE).unwrap();
8042    git_add(A_TXT, &repo);
8043    git_add(E_TXT, &repo);
8044    git_add(DOTGITIGNORE, &repo);
8045    git_commit("Initial commit", &repo);
8046
8047    let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [root_path], cx).await;
8048
8049    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
8050    tree.flush_fs_events(cx).await;
8051    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
8052        .await;
8053    cx.executor().run_until_parked();
8054
8055    let repository = project.read_with(cx, |project, cx| {
8056        project.repositories(cx).values().next().unwrap().clone()
8057    });
8058
8059    // Check that the right git state is observed on startup
8060    repository.read_with(cx, |repository, _cx| {
8061        assert_eq!(
8062            repository.work_directory_abs_path.as_ref(),
8063            root_path.join("project").as_path()
8064        );
8065
8066        assert_eq!(
8067            repository.status_for_path(&B_TXT.into()).unwrap().status,
8068            FileStatus::Untracked,
8069        );
8070        assert_eq!(
8071            repository.status_for_path(&F_TXT.into()).unwrap().status,
8072            FileStatus::Untracked,
8073        );
8074    });
8075
8076    // Modify a file in the working copy.
8077    std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
8078    tree.flush_fs_events(cx).await;
8079    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
8080        .await;
8081    cx.executor().run_until_parked();
8082
8083    // The worktree detects that the file's git status has changed.
8084    repository.read_with(cx, |repository, _| {
8085        assert_eq!(
8086            repository.status_for_path(&A_TXT.into()).unwrap().status,
8087            StatusCode::Modified.worktree(),
8088        );
8089    });
8090
8091    // Create a commit in the git repository.
8092    git_add(A_TXT, &repo);
8093    git_add(B_TXT, &repo);
8094    git_commit("Committing modified and added", &repo);
8095    tree.flush_fs_events(cx).await;
8096    cx.executor().run_until_parked();
8097
8098    // The worktree detects that the files' git status have changed.
8099    repository.read_with(cx, |repository, _cx| {
8100        assert_eq!(
8101            repository.status_for_path(&F_TXT.into()).unwrap().status,
8102            FileStatus::Untracked,
8103        );
8104        assert_eq!(repository.status_for_path(&B_TXT.into()), None);
8105        assert_eq!(repository.status_for_path(&A_TXT.into()), None);
8106    });
8107
8108    // Modify files in the working copy and perform git operations on other files.
8109    git_reset(0, &repo);
8110    git_remove_index(Path::new(B_TXT), &repo);
8111    git_stash(&mut repo);
8112    std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
8113    std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
8114    tree.flush_fs_events(cx).await;
8115    cx.executor().run_until_parked();
8116
8117    // Check that more complex repo changes are tracked
8118    repository.read_with(cx, |repository, _cx| {
8119        assert_eq!(repository.status_for_path(&A_TXT.into()), None);
8120        assert_eq!(
8121            repository.status_for_path(&B_TXT.into()).unwrap().status,
8122            FileStatus::Untracked,
8123        );
8124        assert_eq!(
8125            repository.status_for_path(&E_TXT.into()).unwrap().status,
8126            StatusCode::Modified.worktree(),
8127        );
8128    });
8129
8130    std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
8131    std::fs::remove_dir_all(work_dir.join("c")).unwrap();
8132    std::fs::write(
8133        work_dir.join(DOTGITIGNORE),
8134        [IGNORE_RULE, "f.txt"].join("\n"),
8135    )
8136    .unwrap();
8137
8138    git_add(Path::new(DOTGITIGNORE), &repo);
8139    git_commit("Committing modified git ignore", &repo);
8140
8141    tree.flush_fs_events(cx).await;
8142    cx.executor().run_until_parked();
8143
8144    let mut renamed_dir_name = "first_directory/second_directory";
8145    const RENAMED_FILE: &str = "rf.txt";
8146
8147    std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
8148    std::fs::write(
8149        work_dir.join(renamed_dir_name).join(RENAMED_FILE),
8150        "new-contents",
8151    )
8152    .unwrap();
8153
8154    tree.flush_fs_events(cx).await;
8155    cx.executor().run_until_parked();
8156
8157    repository.read_with(cx, |repository, _cx| {
8158        assert_eq!(
8159            repository
8160                .status_for_path(&Path::new(renamed_dir_name).join(RENAMED_FILE).into())
8161                .unwrap()
8162                .status,
8163            FileStatus::Untracked,
8164        );
8165    });
8166
8167    renamed_dir_name = "new_first_directory/second_directory";
8168
8169    std::fs::rename(
8170        work_dir.join("first_directory"),
8171        work_dir.join("new_first_directory"),
8172    )
8173    .unwrap();
8174
8175    tree.flush_fs_events(cx).await;
8176    cx.executor().run_until_parked();
8177
8178    repository.read_with(cx, |repository, _cx| {
8179        assert_eq!(
8180            repository
8181                .status_for_path(&Path::new(renamed_dir_name).join(RENAMED_FILE).into())
8182                .unwrap()
8183                .status,
8184            FileStatus::Untracked,
8185        );
8186    });
8187}
8188
8189#[gpui::test]
8190async fn test_repos_in_invisible_worktrees(
8191    executor: BackgroundExecutor,
8192    cx: &mut gpui::TestAppContext,
8193) {
8194    init_test(cx);
8195    let fs = FakeFs::new(executor);
8196    fs.insert_tree(
8197        path!("/root"),
8198        json!({
8199            "dir1": {
8200                ".git": {},
8201                "dep1": {
8202                    ".git": {},
8203                    "src": {
8204                        "a.txt": "",
8205                    },
8206                },
8207                "b.txt": "",
8208            },
8209        }),
8210    )
8211    .await;
8212
8213    let project = Project::test(fs.clone(), [path!("/root/dir1/dep1").as_ref()], cx).await;
8214    let visible_worktree =
8215        project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
8216    visible_worktree
8217        .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
8218        .await;
8219
8220    let repos = project.read_with(cx, |project, cx| {
8221        project
8222            .repositories(cx)
8223            .values()
8224            .map(|repo| repo.read(cx).work_directory_abs_path.clone())
8225            .collect::<Vec<_>>()
8226    });
8227    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root/dir1/dep1")).into()]);
8228
8229    let (invisible_worktree, _) = project
8230        .update(cx, |project, cx| {
8231            project.worktree_store.update(cx, |worktree_store, cx| {
8232                worktree_store.find_or_create_worktree(path!("/root/dir1/b.txt"), false, cx)
8233            })
8234        })
8235        .await
8236        .expect("failed to create worktree");
8237    invisible_worktree
8238        .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
8239        .await;
8240
8241    let repos = project.read_with(cx, |project, cx| {
8242        project
8243            .repositories(cx)
8244            .values()
8245            .map(|repo| repo.read(cx).work_directory_abs_path.clone())
8246            .collect::<Vec<_>>()
8247    });
8248    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root/dir1/dep1")).into()]);
8249}
8250
8251#[gpui::test(iterations = 10)]
8252async fn test_rescan_with_gitignore(cx: &mut gpui::TestAppContext) {
8253    init_test(cx);
8254    cx.update(|cx| {
8255        cx.update_global::<SettingsStore, _>(|store, cx| {
8256            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
8257                project_settings.file_scan_exclusions = Some(Vec::new());
8258            });
8259        });
8260    });
8261    let fs = FakeFs::new(cx.background_executor.clone());
8262    fs.insert_tree(
8263        path!("/root"),
8264        json!({
8265            ".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
8266            "tree": {
8267                ".git": {},
8268                ".gitignore": "ignored-dir\n",
8269                "tracked-dir": {
8270                    "tracked-file1": "",
8271                    "ancestor-ignored-file1": "",
8272                },
8273                "ignored-dir": {
8274                    "ignored-file1": ""
8275                }
8276            }
8277        }),
8278    )
8279    .await;
8280    fs.set_head_and_index_for_repo(
8281        path!("/root/tree/.git").as_ref(),
8282        &[
8283            (".gitignore".into(), "ignored-dir\n".into()),
8284            ("tracked-dir/tracked-file1".into(), "".into()),
8285        ],
8286    );
8287
8288    let project = Project::test(fs.clone(), [path!("/root/tree").as_ref()], cx).await;
8289
8290    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
8291    tree.flush_fs_events(cx).await;
8292    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
8293        .await;
8294    cx.executor().run_until_parked();
8295
8296    let repository = project.read_with(cx, |project, cx| {
8297        project.repositories(cx).values().next().unwrap().clone()
8298    });
8299
8300    tree.read_with(cx, |tree, _| {
8301        tree.as_local()
8302            .unwrap()
8303            .manually_refresh_entries_for_paths(vec![Path::new("ignored-dir").into()])
8304    })
8305    .recv()
8306    .await;
8307
8308    cx.read(|cx| {
8309        assert_entry_git_state(
8310            tree.read(cx),
8311            repository.read(cx),
8312            "tracked-dir/tracked-file1",
8313            None,
8314            false,
8315        );
8316        assert_entry_git_state(
8317            tree.read(cx),
8318            repository.read(cx),
8319            "tracked-dir/ancestor-ignored-file1",
8320            None,
8321            false,
8322        );
8323        assert_entry_git_state(
8324            tree.read(cx),
8325            repository.read(cx),
8326            "ignored-dir/ignored-file1",
8327            None,
8328            true,
8329        );
8330    });
8331
8332    fs.create_file(
8333        path!("/root/tree/tracked-dir/tracked-file2").as_ref(),
8334        Default::default(),
8335    )
8336    .await
8337    .unwrap();
8338    fs.set_index_for_repo(
8339        path!("/root/tree/.git").as_ref(),
8340        &[
8341            (".gitignore".into(), "ignored-dir\n".into()),
8342            ("tracked-dir/tracked-file1".into(), "".into()),
8343            ("tracked-dir/tracked-file2".into(), "".into()),
8344        ],
8345    );
8346    fs.create_file(
8347        path!("/root/tree/tracked-dir/ancestor-ignored-file2").as_ref(),
8348        Default::default(),
8349    )
8350    .await
8351    .unwrap();
8352    fs.create_file(
8353        path!("/root/tree/ignored-dir/ignored-file2").as_ref(),
8354        Default::default(),
8355    )
8356    .await
8357    .unwrap();
8358
8359    cx.executor().run_until_parked();
8360    cx.read(|cx| {
8361        assert_entry_git_state(
8362            tree.read(cx),
8363            repository.read(cx),
8364            "tracked-dir/tracked-file2",
8365            Some(StatusCode::Added),
8366            false,
8367        );
8368        assert_entry_git_state(
8369            tree.read(cx),
8370            repository.read(cx),
8371            "tracked-dir/ancestor-ignored-file2",
8372            None,
8373            false,
8374        );
8375        assert_entry_git_state(
8376            tree.read(cx),
8377            repository.read(cx),
8378            "ignored-dir/ignored-file2",
8379            None,
8380            true,
8381        );
8382        assert!(tree.read(cx).entry_for_path(".git").unwrap().is_ignored);
8383    });
8384}
8385
8386#[gpui::test]
8387async fn test_git_worktrees_and_submodules(cx: &mut gpui::TestAppContext) {
8388    init_test(cx);
8389
8390    let fs = FakeFs::new(cx.executor());
8391    fs.insert_tree(
8392        path!("/project"),
8393        json!({
8394            ".git": {
8395                "worktrees": {
8396                    "some-worktree": {
8397                        "commondir": "../..\n",
8398                        // For is_git_dir
8399                        "HEAD": "",
8400                        "config": ""
8401                    }
8402                },
8403                "modules": {
8404                    "subdir": {
8405                        "some-submodule": {
8406                            // For is_git_dir
8407                            "HEAD": "",
8408                            "config": "",
8409                        }
8410                    }
8411                }
8412            },
8413            "src": {
8414                "a.txt": "A",
8415            },
8416            "some-worktree": {
8417                ".git": "gitdir: ../.git/worktrees/some-worktree\n",
8418                "src": {
8419                    "b.txt": "B",
8420                }
8421            },
8422            "subdir": {
8423                "some-submodule": {
8424                    ".git": "gitdir: ../../.git/modules/subdir/some-submodule\n",
8425                    "c.txt": "C",
8426                }
8427            }
8428        }),
8429    )
8430    .await;
8431
8432    let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
8433    let scan_complete = project.update(cx, |project, cx| {
8434        project
8435            .worktrees(cx)
8436            .next()
8437            .unwrap()
8438            .read(cx)
8439            .as_local()
8440            .unwrap()
8441            .scan_complete()
8442    });
8443    scan_complete.await;
8444
8445    let mut repositories = project.update(cx, |project, cx| {
8446        project
8447            .repositories(cx)
8448            .values()
8449            .map(|repo| repo.read(cx).work_directory_abs_path.clone())
8450            .collect::<Vec<_>>()
8451    });
8452    repositories.sort();
8453    pretty_assertions::assert_eq!(
8454        repositories,
8455        [
8456            Path::new(path!("/project")).into(),
8457            Path::new(path!("/project/some-worktree")).into(),
8458            Path::new(path!("/project/subdir/some-submodule")).into(),
8459        ]
8460    );
8461
8462    // Generate a git-related event for the worktree and check that it's refreshed.
8463    fs.with_git_state(
8464        path!("/project/some-worktree/.git").as_ref(),
8465        true,
8466        |state| {
8467            state
8468                .head_contents
8469                .insert("src/b.txt".into(), "b".to_owned());
8470            state
8471                .index_contents
8472                .insert("src/b.txt".into(), "b".to_owned());
8473        },
8474    )
8475    .unwrap();
8476    cx.run_until_parked();
8477
8478    let buffer = project
8479        .update(cx, |project, cx| {
8480            project.open_local_buffer(path!("/project/some-worktree/src/b.txt"), cx)
8481        })
8482        .await
8483        .unwrap();
8484    let (worktree_repo, barrier) = project.update(cx, |project, cx| {
8485        let (repo, _) = project
8486            .git_store()
8487            .read(cx)
8488            .repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
8489            .unwrap();
8490        pretty_assertions::assert_eq!(
8491            repo.read(cx).work_directory_abs_path,
8492            Path::new(path!("/project/some-worktree")).into(),
8493        );
8494        let barrier = repo.update(cx, |repo, _| repo.barrier());
8495        (repo.clone(), barrier)
8496    });
8497    barrier.await.unwrap();
8498    worktree_repo.update(cx, |repo, _| {
8499        pretty_assertions::assert_eq!(
8500            repo.status_for_path(&"src/b.txt".into()).unwrap().status,
8501            StatusCode::Modified.worktree(),
8502        );
8503    });
8504
8505    // The same for the submodule.
8506    fs.with_git_state(
8507        path!("/project/subdir/some-submodule/.git").as_ref(),
8508        true,
8509        |state| {
8510            state.head_contents.insert("c.txt".into(), "c".to_owned());
8511            state.index_contents.insert("c.txt".into(), "c".to_owned());
8512        },
8513    )
8514    .unwrap();
8515    cx.run_until_parked();
8516
8517    let buffer = project
8518        .update(cx, |project, cx| {
8519            project.open_local_buffer(path!("/project/subdir/some-submodule/c.txt"), cx)
8520        })
8521        .await
8522        .unwrap();
8523    let (submodule_repo, barrier) = project.update(cx, |project, cx| {
8524        let (repo, _) = project
8525            .git_store()
8526            .read(cx)
8527            .repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
8528            .unwrap();
8529        pretty_assertions::assert_eq!(
8530            repo.read(cx).work_directory_abs_path,
8531            Path::new(path!("/project/subdir/some-submodule")).into(),
8532        );
8533        let barrier = repo.update(cx, |repo, _| repo.barrier());
8534        (repo.clone(), barrier)
8535    });
8536    barrier.await.unwrap();
8537    submodule_repo.update(cx, |repo, _| {
8538        pretty_assertions::assert_eq!(
8539            repo.status_for_path(&"c.txt".into()).unwrap().status,
8540            StatusCode::Modified.worktree(),
8541        );
8542    });
8543}
8544
8545#[gpui::test]
8546async fn test_repository_deduplication(cx: &mut gpui::TestAppContext) {
8547    init_test(cx);
8548    let fs = FakeFs::new(cx.background_executor.clone());
8549    fs.insert_tree(
8550        path!("/root"),
8551        json!({
8552            "project": {
8553                ".git": {},
8554                "child1": {
8555                    "a.txt": "A",
8556                },
8557                "child2": {
8558                    "b.txt": "B",
8559                }
8560            }
8561        }),
8562    )
8563    .await;
8564
8565    let project = Project::test(
8566        fs.clone(),
8567        [
8568            path!("/root/project/child1").as_ref(),
8569            path!("/root/project/child2").as_ref(),
8570        ],
8571        cx,
8572    )
8573    .await;
8574
8575    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
8576    tree.flush_fs_events(cx).await;
8577    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
8578        .await;
8579    cx.executor().run_until_parked();
8580
8581    let repos = project.read_with(cx, |project, cx| {
8582        project
8583            .repositories(cx)
8584            .values()
8585            .map(|repo| repo.read(cx).work_directory_abs_path.clone())
8586            .collect::<Vec<_>>()
8587    });
8588    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root/project")).into()]);
8589}
8590
8591async fn search(
8592    project: &Entity<Project>,
8593    query: SearchQuery,
8594    cx: &mut gpui::TestAppContext,
8595) -> Result<HashMap<String, Vec<Range<usize>>>> {
8596    let search_rx = project.update(cx, |project, cx| project.search(query, cx));
8597    let mut results = HashMap::default();
8598    while let Ok(search_result) = search_rx.recv().await {
8599        match search_result {
8600            SearchResult::Buffer { buffer, ranges } => {
8601                results.entry(buffer).or_insert(ranges);
8602            }
8603            SearchResult::LimitReached => {}
8604        }
8605    }
8606    Ok(results
8607        .into_iter()
8608        .map(|(buffer, ranges)| {
8609            buffer.update(cx, |buffer, cx| {
8610                let path = buffer
8611                    .file()
8612                    .unwrap()
8613                    .full_path(cx)
8614                    .to_string_lossy()
8615                    .to_string();
8616                let ranges = ranges
8617                    .into_iter()
8618                    .map(|range| range.to_offset(buffer))
8619                    .collect::<Vec<_>>();
8620                (path, ranges)
8621            })
8622        })
8623        .collect())
8624}
8625
8626pub fn init_test(cx: &mut gpui::TestAppContext) {
8627    zlog::init_test();
8628
8629    cx.update(|cx| {
8630        let settings_store = SettingsStore::test(cx);
8631        cx.set_global(settings_store);
8632        release_channel::init(SemanticVersion::default(), cx);
8633        language::init(cx);
8634        Project::init_settings(cx);
8635    });
8636}
8637
8638fn json_lang() -> Arc<Language> {
8639    Arc::new(Language::new(
8640        LanguageConfig {
8641            name: "JSON".into(),
8642            matcher: LanguageMatcher {
8643                path_suffixes: vec!["json".to_string()],
8644                ..Default::default()
8645            },
8646            ..Default::default()
8647        },
8648        None,
8649    ))
8650}
8651
8652fn js_lang() -> Arc<Language> {
8653    Arc::new(Language::new(
8654        LanguageConfig {
8655            name: "JavaScript".into(),
8656            matcher: LanguageMatcher {
8657                path_suffixes: vec!["js".to_string()],
8658                ..Default::default()
8659            },
8660            ..Default::default()
8661        },
8662        None,
8663    ))
8664}
8665
8666fn rust_lang() -> Arc<Language> {
8667    Arc::new(Language::new(
8668        LanguageConfig {
8669            name: "Rust".into(),
8670            matcher: LanguageMatcher {
8671                path_suffixes: vec!["rs".to_string()],
8672                ..Default::default()
8673            },
8674            ..Default::default()
8675        },
8676        Some(tree_sitter_rust::LANGUAGE.into()),
8677    ))
8678}
8679
8680fn typescript_lang() -> Arc<Language> {
8681    Arc::new(Language::new(
8682        LanguageConfig {
8683            name: "TypeScript".into(),
8684            matcher: LanguageMatcher {
8685                path_suffixes: vec!["ts".to_string()],
8686                ..Default::default()
8687            },
8688            ..Default::default()
8689        },
8690        Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8691    ))
8692}
8693
8694fn tsx_lang() -> Arc<Language> {
8695    Arc::new(Language::new(
8696        LanguageConfig {
8697            name: "tsx".into(),
8698            matcher: LanguageMatcher {
8699                path_suffixes: vec!["tsx".to_string()],
8700                ..Default::default()
8701            },
8702            ..Default::default()
8703        },
8704        Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8705    ))
8706}
8707
8708fn get_all_tasks(
8709    project: &Entity<Project>,
8710    task_contexts: &TaskContexts,
8711    cx: &mut App,
8712) -> Vec<(TaskSourceKind, ResolvedTask)> {
8713    let (mut old, new) = project.update(cx, |project, cx| {
8714        project
8715            .task_store
8716            .read(cx)
8717            .task_inventory()
8718            .unwrap()
8719            .read(cx)
8720            .used_and_current_resolved_tasks(task_contexts, cx)
8721    });
8722    old.extend(new);
8723    old
8724}
8725
8726#[track_caller]
8727fn assert_entry_git_state(
8728    tree: &Worktree,
8729    repository: &Repository,
8730    path: &str,
8731    index_status: Option<StatusCode>,
8732    is_ignored: bool,
8733) {
8734    assert_eq!(tree.abs_path(), repository.work_directory_abs_path);
8735    let entry = tree
8736        .entry_for_path(path)
8737        .unwrap_or_else(|| panic!("entry {path} not found"));
8738    let status = repository
8739        .status_for_path(&path.into())
8740        .map(|entry| entry.status);
8741    let expected = index_status.map(|index_status| {
8742        TrackedStatus {
8743            index_status,
8744            worktree_status: StatusCode::Unmodified,
8745        }
8746        .into()
8747    });
8748    assert_eq!(
8749        status, expected,
8750        "expected {path} to have git status: {expected:?}"
8751    );
8752    assert_eq!(
8753        entry.is_ignored, is_ignored,
8754        "expected {path} to have is_ignored: {is_ignored}"
8755    );
8756}
8757
8758#[track_caller]
8759fn git_init(path: &Path) -> git2::Repository {
8760    let mut init_opts = RepositoryInitOptions::new();
8761    init_opts.initial_head("main");
8762    git2::Repository::init_opts(path, &init_opts).expect("Failed to initialize git repository")
8763}
8764
8765#[track_caller]
8766fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
8767    let path = path.as_ref();
8768    let mut index = repo.index().expect("Failed to get index");
8769    index.add_path(path).expect("Failed to add file");
8770    index.write().expect("Failed to write index");
8771}
8772
8773#[track_caller]
8774fn git_remove_index(path: &Path, repo: &git2::Repository) {
8775    let mut index = repo.index().expect("Failed to get index");
8776    index.remove_path(path).expect("Failed to add file");
8777    index.write().expect("Failed to write index");
8778}
8779
8780#[track_caller]
8781fn git_commit(msg: &'static str, repo: &git2::Repository) {
8782    use git2::Signature;
8783
8784    let signature = Signature::now("test", "test@zed.dev").unwrap();
8785    let oid = repo.index().unwrap().write_tree().unwrap();
8786    let tree = repo.find_tree(oid).unwrap();
8787    if let Ok(head) = repo.head() {
8788        let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
8789
8790        let parent_commit = parent_obj.as_commit().unwrap();
8791
8792        repo.commit(
8793            Some("HEAD"),
8794            &signature,
8795            &signature,
8796            msg,
8797            &tree,
8798            &[parent_commit],
8799        )
8800        .expect("Failed to commit with parent");
8801    } else {
8802        repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
8803            .expect("Failed to commit");
8804    }
8805}
8806
8807#[cfg(any())]
8808#[track_caller]
8809fn git_cherry_pick(commit: &git2::Commit<'_>, repo: &git2::Repository) {
8810    repo.cherrypick(commit, None).expect("Failed to cherrypick");
8811}
8812
8813#[track_caller]
8814fn git_stash(repo: &mut git2::Repository) {
8815    use git2::Signature;
8816
8817    let signature = Signature::now("test", "test@zed.dev").unwrap();
8818    repo.stash_save(&signature, "N/A", None)
8819        .expect("Failed to stash");
8820}
8821
8822#[track_caller]
8823fn git_reset(offset: usize, repo: &git2::Repository) {
8824    let head = repo.head().expect("Couldn't get repo head");
8825    let object = head.peel(git2::ObjectType::Commit).unwrap();
8826    let commit = object.as_commit().unwrap();
8827    let new_head = commit
8828        .parents()
8829        .inspect(|parnet| {
8830            parnet.message();
8831        })
8832        .nth(offset)
8833        .expect("Not enough history");
8834    repo.reset(new_head.as_object(), git2::ResetType::Soft, None)
8835        .expect("Could not reset");
8836}
8837
8838#[cfg(any())]
8839#[track_caller]
8840fn git_branch(name: &str, repo: &git2::Repository) {
8841    let head = repo
8842        .head()
8843        .expect("Couldn't get repo head")
8844        .peel_to_commit()
8845        .expect("HEAD is not a commit");
8846    repo.branch(name, &head, false).expect("Failed to commit");
8847}
8848
8849#[cfg(any())]
8850#[track_caller]
8851fn git_checkout(name: &str, repo: &git2::Repository) {
8852    repo.set_head(name).expect("Failed to set head");
8853    repo.checkout_head(None).expect("Failed to check out head");
8854}
8855
8856#[cfg(any())]
8857#[track_caller]
8858fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
8859    repo.statuses(None)
8860        .unwrap()
8861        .iter()
8862        .map(|status| (status.path().unwrap().to_string(), status.status()))
8863        .collect()
8864}
8865
8866#[gpui::test]
8867async fn test_find_project_path_abs(
8868    background_executor: BackgroundExecutor,
8869    cx: &mut gpui::TestAppContext,
8870) {
8871    // find_project_path should work with absolute paths
8872    init_test(cx);
8873
8874    let fs = FakeFs::new(background_executor);
8875    fs.insert_tree(
8876        path!("/root"),
8877        json!({
8878            "project1": {
8879                "file1.txt": "content1",
8880                "subdir": {
8881                    "file2.txt": "content2"
8882                }
8883            },
8884            "project2": {
8885                "file3.txt": "content3"
8886            }
8887        }),
8888    )
8889    .await;
8890
8891    let project = Project::test(
8892        fs.clone(),
8893        [
8894            path!("/root/project1").as_ref(),
8895            path!("/root/project2").as_ref(),
8896        ],
8897        cx,
8898    )
8899    .await;
8900
8901    // Make sure the worktrees are fully initialized
8902    for worktree in project.read_with(cx, |project, cx| project.worktrees(cx).collect::<Vec<_>>()) {
8903        worktree
8904            .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
8905            .await;
8906    }
8907    cx.run_until_parked();
8908
8909    let (project1_abs_path, project1_id, project2_abs_path, project2_id) =
8910        project.read_with(cx, |project, cx| {
8911            let worktrees: Vec<_> = project.worktrees(cx).collect();
8912            let abs_path1 = worktrees[0].read(cx).abs_path().to_path_buf();
8913            let id1 = worktrees[0].read(cx).id();
8914            let abs_path2 = worktrees[1].read(cx).abs_path().to_path_buf();
8915            let id2 = worktrees[1].read(cx).id();
8916            (abs_path1, id1, abs_path2, id2)
8917        });
8918
8919    project.update(cx, |project, cx| {
8920        let abs_path = project1_abs_path.join("file1.txt");
8921        let found_path = project.find_project_path(abs_path, cx).unwrap();
8922        assert_eq!(found_path.worktree_id, project1_id);
8923        assert_eq!(found_path.path.as_ref(), Path::new("file1.txt"));
8924
8925        let abs_path = project1_abs_path.join("subdir").join("file2.txt");
8926        let found_path = project.find_project_path(abs_path, cx).unwrap();
8927        assert_eq!(found_path.worktree_id, project1_id);
8928        assert_eq!(found_path.path.as_ref(), Path::new("subdir/file2.txt"));
8929
8930        let abs_path = project2_abs_path.join("file3.txt");
8931        let found_path = project.find_project_path(abs_path, cx).unwrap();
8932        assert_eq!(found_path.worktree_id, project2_id);
8933        assert_eq!(found_path.path.as_ref(), Path::new("file3.txt"));
8934
8935        let abs_path = project1_abs_path.join("nonexistent.txt");
8936        let found_path = project.find_project_path(abs_path, cx);
8937        assert!(
8938            found_path.is_some(),
8939            "Should find project path for nonexistent file in worktree"
8940        );
8941
8942        // Test with an absolute path outside any worktree
8943        let abs_path = Path::new("/some/other/path");
8944        let found_path = project.find_project_path(abs_path, cx);
8945        assert!(
8946            found_path.is_none(),
8947            "Should not find project path for path outside any worktree"
8948        );
8949    });
8950}