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        "deadbeef",
6503    );
6504    fs.set_index_for_repo(
6505        Path::new("/dir/.git"),
6506        &[
6507            ("src/modification.rs".into(), staged_contents),
6508            ("src/deletion.rs".into(), "// the-deleted-contents\n".into()),
6509        ],
6510    );
6511
6512    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
6513    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
6514    let language = rust_lang();
6515    language_registry.add(language.clone());
6516
6517    let buffer_1 = project
6518        .update(cx, |project, cx| {
6519            project.open_local_buffer("/dir/src/modification.rs", cx)
6520        })
6521        .await
6522        .unwrap();
6523    let diff_1 = project
6524        .update(cx, |project, cx| {
6525            project.open_uncommitted_diff(buffer_1.clone(), cx)
6526        })
6527        .await
6528        .unwrap();
6529    diff_1.read_with(cx, |diff, _| {
6530        assert_eq!(diff.base_text().language().cloned(), Some(language))
6531    });
6532    cx.run_until_parked();
6533    diff_1.update(cx, |diff, cx| {
6534        let snapshot = buffer_1.read(cx).snapshot();
6535        assert_hunks(
6536            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
6537            &snapshot,
6538            &diff.base_text_string().unwrap(),
6539            &[
6540                (
6541                    0..1,
6542                    "",
6543                    "// print goodbye\n",
6544                    DiffHunkStatus::added(DiffHunkSecondaryStatus::HasSecondaryHunk),
6545                ),
6546                (
6547                    2..3,
6548                    "    println!(\"hello world\");\n",
6549                    "    println!(\"goodbye world\");\n",
6550                    DiffHunkStatus::modified_none(),
6551                ),
6552            ],
6553        );
6554    });
6555
6556    // Reset HEAD to a version that differs from both the buffer and the index.
6557    let committed_contents = r#"
6558        // print goodbye
6559        fn main() {
6560        }
6561    "#
6562    .unindent();
6563    fs.set_head_for_repo(
6564        Path::new("/dir/.git"),
6565        &[
6566            ("src/modification.rs".into(), committed_contents.clone()),
6567            ("src/deletion.rs".into(), "// the-deleted-contents\n".into()),
6568        ],
6569        "deadbeef",
6570    );
6571
6572    // Buffer now has an unstaged hunk.
6573    cx.run_until_parked();
6574    diff_1.update(cx, |diff, cx| {
6575        let snapshot = buffer_1.read(cx).snapshot();
6576        assert_hunks(
6577            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
6578            &snapshot,
6579            &diff.base_text().text(),
6580            &[(
6581                2..3,
6582                "",
6583                "    println!(\"goodbye world\");\n",
6584                DiffHunkStatus::added_none(),
6585            )],
6586        );
6587    });
6588
6589    // Open a buffer for a file that's been deleted.
6590    let buffer_2 = project
6591        .update(cx, |project, cx| {
6592            project.open_local_buffer("/dir/src/deletion.rs", cx)
6593        })
6594        .await
6595        .unwrap();
6596    let diff_2 = project
6597        .update(cx, |project, cx| {
6598            project.open_uncommitted_diff(buffer_2.clone(), cx)
6599        })
6600        .await
6601        .unwrap();
6602    cx.run_until_parked();
6603    diff_2.update(cx, |diff, cx| {
6604        let snapshot = buffer_2.read(cx).snapshot();
6605        assert_hunks(
6606            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
6607            &snapshot,
6608            &diff.base_text_string().unwrap(),
6609            &[(
6610                0..0,
6611                "// the-deleted-contents\n",
6612                "",
6613                DiffHunkStatus::deleted(DiffHunkSecondaryStatus::HasSecondaryHunk),
6614            )],
6615        );
6616    });
6617
6618    // Stage the deletion of this file
6619    fs.set_index_for_repo(
6620        Path::new("/dir/.git"),
6621        &[("src/modification.rs".into(), committed_contents.clone())],
6622    );
6623    cx.run_until_parked();
6624    diff_2.update(cx, |diff, cx| {
6625        let snapshot = buffer_2.read(cx).snapshot();
6626        assert_hunks(
6627            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
6628            &snapshot,
6629            &diff.base_text_string().unwrap(),
6630            &[(
6631                0..0,
6632                "// the-deleted-contents\n",
6633                "",
6634                DiffHunkStatus::deleted(DiffHunkSecondaryStatus::NoSecondaryHunk),
6635            )],
6636        );
6637    });
6638}
6639
6640#[gpui::test]
6641async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
6642    use DiffHunkSecondaryStatus::*;
6643    init_test(cx);
6644
6645    let committed_contents = r#"
6646        zero
6647        one
6648        two
6649        three
6650        four
6651        five
6652    "#
6653    .unindent();
6654    let file_contents = r#"
6655        one
6656        TWO
6657        three
6658        FOUR
6659        five
6660    "#
6661    .unindent();
6662
6663    let fs = FakeFs::new(cx.background_executor.clone());
6664    fs.insert_tree(
6665        "/dir",
6666        json!({
6667            ".git": {},
6668            "file.txt": file_contents.clone()
6669        }),
6670    )
6671    .await;
6672
6673    fs.set_head_and_index_for_repo(
6674        "/dir/.git".as_ref(),
6675        &[("file.txt".into(), committed_contents.clone())],
6676    );
6677
6678    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
6679
6680    let buffer = project
6681        .update(cx, |project, cx| {
6682            project.open_local_buffer("/dir/file.txt", cx)
6683        })
6684        .await
6685        .unwrap();
6686    let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
6687    let uncommitted_diff = project
6688        .update(cx, |project, cx| {
6689            project.open_uncommitted_diff(buffer.clone(), cx)
6690        })
6691        .await
6692        .unwrap();
6693    let mut diff_events = cx.events(&uncommitted_diff);
6694
6695    // The hunks are initially unstaged.
6696    uncommitted_diff.read_with(cx, |diff, cx| {
6697        assert_hunks(
6698            diff.hunks(&snapshot, cx),
6699            &snapshot,
6700            &diff.base_text_string().unwrap(),
6701            &[
6702                (
6703                    0..0,
6704                    "zero\n",
6705                    "",
6706                    DiffHunkStatus::deleted(HasSecondaryHunk),
6707                ),
6708                (
6709                    1..2,
6710                    "two\n",
6711                    "TWO\n",
6712                    DiffHunkStatus::modified(HasSecondaryHunk),
6713                ),
6714                (
6715                    3..4,
6716                    "four\n",
6717                    "FOUR\n",
6718                    DiffHunkStatus::modified(HasSecondaryHunk),
6719                ),
6720            ],
6721        );
6722    });
6723
6724    // Stage a hunk. It appears as optimistically staged.
6725    uncommitted_diff.update(cx, |diff, cx| {
6726        let range =
6727            snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_before(Point::new(2, 0));
6728        let hunks = diff
6729            .hunks_intersecting_range(range, &snapshot, cx)
6730            .collect::<Vec<_>>();
6731        diff.stage_or_unstage_hunks(true, &hunks, &snapshot, true, cx);
6732
6733        assert_hunks(
6734            diff.hunks(&snapshot, cx),
6735            &snapshot,
6736            &diff.base_text_string().unwrap(),
6737            &[
6738                (
6739                    0..0,
6740                    "zero\n",
6741                    "",
6742                    DiffHunkStatus::deleted(HasSecondaryHunk),
6743                ),
6744                (
6745                    1..2,
6746                    "two\n",
6747                    "TWO\n",
6748                    DiffHunkStatus::modified(SecondaryHunkRemovalPending),
6749                ),
6750                (
6751                    3..4,
6752                    "four\n",
6753                    "FOUR\n",
6754                    DiffHunkStatus::modified(HasSecondaryHunk),
6755                ),
6756            ],
6757        );
6758    });
6759
6760    // The diff emits a change event for the range of the staged hunk.
6761    assert!(matches!(
6762        diff_events.next().await.unwrap(),
6763        BufferDiffEvent::HunksStagedOrUnstaged(_)
6764    ));
6765    let event = diff_events.next().await.unwrap();
6766    if let BufferDiffEvent::DiffChanged {
6767        changed_range: Some(changed_range),
6768    } = event
6769    {
6770        let changed_range = changed_range.to_point(&snapshot);
6771        assert_eq!(changed_range, Point::new(1, 0)..Point::new(2, 0));
6772    } else {
6773        panic!("Unexpected event {event:?}");
6774    }
6775
6776    // When the write to the index completes, it appears as staged.
6777    cx.run_until_parked();
6778    uncommitted_diff.update(cx, |diff, cx| {
6779        assert_hunks(
6780            diff.hunks(&snapshot, cx),
6781            &snapshot,
6782            &diff.base_text_string().unwrap(),
6783            &[
6784                (
6785                    0..0,
6786                    "zero\n",
6787                    "",
6788                    DiffHunkStatus::deleted(HasSecondaryHunk),
6789                ),
6790                (
6791                    1..2,
6792                    "two\n",
6793                    "TWO\n",
6794                    DiffHunkStatus::modified(NoSecondaryHunk),
6795                ),
6796                (
6797                    3..4,
6798                    "four\n",
6799                    "FOUR\n",
6800                    DiffHunkStatus::modified(HasSecondaryHunk),
6801                ),
6802            ],
6803        );
6804    });
6805
6806    // The diff emits a change event for the changed index text.
6807    let event = diff_events.next().await.unwrap();
6808    if let BufferDiffEvent::DiffChanged {
6809        changed_range: Some(changed_range),
6810    } = event
6811    {
6812        let changed_range = changed_range.to_point(&snapshot);
6813        assert_eq!(changed_range, Point::new(0, 0)..Point::new(4, 0));
6814    } else {
6815        panic!("Unexpected event {event:?}");
6816    }
6817
6818    // Simulate a problem writing to the git index.
6819    fs.set_error_message_for_index_write(
6820        "/dir/.git".as_ref(),
6821        Some("failed to write git index".into()),
6822    );
6823
6824    // Stage another hunk.
6825    uncommitted_diff.update(cx, |diff, cx| {
6826        let range =
6827            snapshot.anchor_before(Point::new(3, 0))..snapshot.anchor_before(Point::new(4, 0));
6828        let hunks = diff
6829            .hunks_intersecting_range(range, &snapshot, cx)
6830            .collect::<Vec<_>>();
6831        diff.stage_or_unstage_hunks(true, &hunks, &snapshot, true, cx);
6832
6833        assert_hunks(
6834            diff.hunks(&snapshot, cx),
6835            &snapshot,
6836            &diff.base_text_string().unwrap(),
6837            &[
6838                (
6839                    0..0,
6840                    "zero\n",
6841                    "",
6842                    DiffHunkStatus::deleted(HasSecondaryHunk),
6843                ),
6844                (
6845                    1..2,
6846                    "two\n",
6847                    "TWO\n",
6848                    DiffHunkStatus::modified(NoSecondaryHunk),
6849                ),
6850                (
6851                    3..4,
6852                    "four\n",
6853                    "FOUR\n",
6854                    DiffHunkStatus::modified(SecondaryHunkRemovalPending),
6855                ),
6856            ],
6857        );
6858    });
6859    assert!(matches!(
6860        diff_events.next().await.unwrap(),
6861        BufferDiffEvent::HunksStagedOrUnstaged(_)
6862    ));
6863    let event = diff_events.next().await.unwrap();
6864    if let BufferDiffEvent::DiffChanged {
6865        changed_range: Some(changed_range),
6866    } = event
6867    {
6868        let changed_range = changed_range.to_point(&snapshot);
6869        assert_eq!(changed_range, Point::new(3, 0)..Point::new(4, 0));
6870    } else {
6871        panic!("Unexpected event {event:?}");
6872    }
6873
6874    // When the write fails, the hunk returns to being unstaged.
6875    cx.run_until_parked();
6876    uncommitted_diff.update(cx, |diff, cx| {
6877        assert_hunks(
6878            diff.hunks(&snapshot, cx),
6879            &snapshot,
6880            &diff.base_text_string().unwrap(),
6881            &[
6882                (
6883                    0..0,
6884                    "zero\n",
6885                    "",
6886                    DiffHunkStatus::deleted(HasSecondaryHunk),
6887                ),
6888                (
6889                    1..2,
6890                    "two\n",
6891                    "TWO\n",
6892                    DiffHunkStatus::modified(NoSecondaryHunk),
6893                ),
6894                (
6895                    3..4,
6896                    "four\n",
6897                    "FOUR\n",
6898                    DiffHunkStatus::modified(HasSecondaryHunk),
6899                ),
6900            ],
6901        );
6902    });
6903
6904    let event = diff_events.next().await.unwrap();
6905    if let BufferDiffEvent::DiffChanged {
6906        changed_range: Some(changed_range),
6907    } = event
6908    {
6909        let changed_range = changed_range.to_point(&snapshot);
6910        assert_eq!(changed_range, Point::new(0, 0)..Point::new(5, 0));
6911    } else {
6912        panic!("Unexpected event {event:?}");
6913    }
6914
6915    // Allow writing to the git index to succeed again.
6916    fs.set_error_message_for_index_write("/dir/.git".as_ref(), None);
6917
6918    // Stage two hunks with separate operations.
6919    uncommitted_diff.update(cx, |diff, cx| {
6920        let hunks = diff.hunks(&snapshot, cx).collect::<Vec<_>>();
6921        diff.stage_or_unstage_hunks(true, &hunks[0..1], &snapshot, true, cx);
6922        diff.stage_or_unstage_hunks(true, &hunks[2..3], &snapshot, true, cx);
6923    });
6924
6925    // Both staged hunks appear as pending.
6926    uncommitted_diff.update(cx, |diff, cx| {
6927        assert_hunks(
6928            diff.hunks(&snapshot, cx),
6929            &snapshot,
6930            &diff.base_text_string().unwrap(),
6931            &[
6932                (
6933                    0..0,
6934                    "zero\n",
6935                    "",
6936                    DiffHunkStatus::deleted(SecondaryHunkRemovalPending),
6937                ),
6938                (
6939                    1..2,
6940                    "two\n",
6941                    "TWO\n",
6942                    DiffHunkStatus::modified(NoSecondaryHunk),
6943                ),
6944                (
6945                    3..4,
6946                    "four\n",
6947                    "FOUR\n",
6948                    DiffHunkStatus::modified(SecondaryHunkRemovalPending),
6949                ),
6950            ],
6951        );
6952    });
6953
6954    // Both staging operations take effect.
6955    cx.run_until_parked();
6956    uncommitted_diff.update(cx, |diff, cx| {
6957        assert_hunks(
6958            diff.hunks(&snapshot, cx),
6959            &snapshot,
6960            &diff.base_text_string().unwrap(),
6961            &[
6962                (0..0, "zero\n", "", DiffHunkStatus::deleted(NoSecondaryHunk)),
6963                (
6964                    1..2,
6965                    "two\n",
6966                    "TWO\n",
6967                    DiffHunkStatus::modified(NoSecondaryHunk),
6968                ),
6969                (
6970                    3..4,
6971                    "four\n",
6972                    "FOUR\n",
6973                    DiffHunkStatus::modified(NoSecondaryHunk),
6974                ),
6975            ],
6976        );
6977    });
6978}
6979
6980#[gpui::test(seeds(340, 472))]
6981async fn test_staging_hunks_with_delayed_fs_event(cx: &mut gpui::TestAppContext) {
6982    use DiffHunkSecondaryStatus::*;
6983    init_test(cx);
6984
6985    let committed_contents = r#"
6986        zero
6987        one
6988        two
6989        three
6990        four
6991        five
6992    "#
6993    .unindent();
6994    let file_contents = r#"
6995        one
6996        TWO
6997        three
6998        FOUR
6999        five
7000    "#
7001    .unindent();
7002
7003    let fs = FakeFs::new(cx.background_executor.clone());
7004    fs.insert_tree(
7005        "/dir",
7006        json!({
7007            ".git": {},
7008            "file.txt": file_contents.clone()
7009        }),
7010    )
7011    .await;
7012
7013    fs.set_head_for_repo(
7014        "/dir/.git".as_ref(),
7015        &[("file.txt".into(), committed_contents.clone())],
7016        "deadbeef",
7017    );
7018    fs.set_index_for_repo(
7019        "/dir/.git".as_ref(),
7020        &[("file.txt".into(), committed_contents.clone())],
7021    );
7022
7023    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
7024
7025    let buffer = project
7026        .update(cx, |project, cx| {
7027            project.open_local_buffer("/dir/file.txt", cx)
7028        })
7029        .await
7030        .unwrap();
7031    let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
7032    let uncommitted_diff = project
7033        .update(cx, |project, cx| {
7034            project.open_uncommitted_diff(buffer.clone(), cx)
7035        })
7036        .await
7037        .unwrap();
7038
7039    // The hunks are initially unstaged.
7040    uncommitted_diff.read_with(cx, |diff, cx| {
7041        assert_hunks(
7042            diff.hunks(&snapshot, cx),
7043            &snapshot,
7044            &diff.base_text_string().unwrap(),
7045            &[
7046                (
7047                    0..0,
7048                    "zero\n",
7049                    "",
7050                    DiffHunkStatus::deleted(HasSecondaryHunk),
7051                ),
7052                (
7053                    1..2,
7054                    "two\n",
7055                    "TWO\n",
7056                    DiffHunkStatus::modified(HasSecondaryHunk),
7057                ),
7058                (
7059                    3..4,
7060                    "four\n",
7061                    "FOUR\n",
7062                    DiffHunkStatus::modified(HasSecondaryHunk),
7063                ),
7064            ],
7065        );
7066    });
7067
7068    // Pause IO events
7069    fs.pause_events();
7070
7071    // Stage the first hunk.
7072    uncommitted_diff.update(cx, |diff, cx| {
7073        let hunk = diff.hunks(&snapshot, cx).next().unwrap();
7074        diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx);
7075        assert_hunks(
7076            diff.hunks(&snapshot, cx),
7077            &snapshot,
7078            &diff.base_text_string().unwrap(),
7079            &[
7080                (
7081                    0..0,
7082                    "zero\n",
7083                    "",
7084                    DiffHunkStatus::deleted(SecondaryHunkRemovalPending),
7085                ),
7086                (
7087                    1..2,
7088                    "two\n",
7089                    "TWO\n",
7090                    DiffHunkStatus::modified(HasSecondaryHunk),
7091                ),
7092                (
7093                    3..4,
7094                    "four\n",
7095                    "FOUR\n",
7096                    DiffHunkStatus::modified(HasSecondaryHunk),
7097                ),
7098            ],
7099        );
7100    });
7101
7102    // Stage the second hunk *before* receiving the FS event for the first hunk.
7103    cx.run_until_parked();
7104    uncommitted_diff.update(cx, |diff, cx| {
7105        let hunk = diff.hunks(&snapshot, cx).nth(1).unwrap();
7106        diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx);
7107        assert_hunks(
7108            diff.hunks(&snapshot, cx),
7109            &snapshot,
7110            &diff.base_text_string().unwrap(),
7111            &[
7112                (
7113                    0..0,
7114                    "zero\n",
7115                    "",
7116                    DiffHunkStatus::deleted(SecondaryHunkRemovalPending),
7117                ),
7118                (
7119                    1..2,
7120                    "two\n",
7121                    "TWO\n",
7122                    DiffHunkStatus::modified(SecondaryHunkRemovalPending),
7123                ),
7124                (
7125                    3..4,
7126                    "four\n",
7127                    "FOUR\n",
7128                    DiffHunkStatus::modified(HasSecondaryHunk),
7129                ),
7130            ],
7131        );
7132    });
7133
7134    // Process the FS event for staging the first hunk (second event is still pending).
7135    fs.flush_events(1);
7136    cx.run_until_parked();
7137
7138    // Stage the third hunk before receiving the second FS event.
7139    uncommitted_diff.update(cx, |diff, cx| {
7140        let hunk = diff.hunks(&snapshot, cx).nth(2).unwrap();
7141        diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx);
7142    });
7143
7144    // Wait for all remaining IO.
7145    cx.run_until_parked();
7146    fs.flush_events(fs.buffered_event_count());
7147
7148    // Now all hunks are staged.
7149    cx.run_until_parked();
7150    uncommitted_diff.update(cx, |diff, cx| {
7151        assert_hunks(
7152            diff.hunks(&snapshot, cx),
7153            &snapshot,
7154            &diff.base_text_string().unwrap(),
7155            &[
7156                (0..0, "zero\n", "", DiffHunkStatus::deleted(NoSecondaryHunk)),
7157                (
7158                    1..2,
7159                    "two\n",
7160                    "TWO\n",
7161                    DiffHunkStatus::modified(NoSecondaryHunk),
7162                ),
7163                (
7164                    3..4,
7165                    "four\n",
7166                    "FOUR\n",
7167                    DiffHunkStatus::modified(NoSecondaryHunk),
7168                ),
7169            ],
7170        );
7171    });
7172}
7173
7174#[gpui::test(iterations = 25)]
7175async fn test_staging_random_hunks(
7176    mut rng: StdRng,
7177    executor: BackgroundExecutor,
7178    cx: &mut gpui::TestAppContext,
7179) {
7180    let operations = env::var("OPERATIONS")
7181        .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
7182        .unwrap_or(20);
7183
7184    // Try to induce races between diff recalculation and index writes.
7185    if rng.gen_bool(0.5) {
7186        executor.deprioritize(*CALCULATE_DIFF_TASK);
7187    }
7188
7189    use DiffHunkSecondaryStatus::*;
7190    init_test(cx);
7191
7192    let committed_text = (0..30).map(|i| format!("line {i}\n")).collect::<String>();
7193    let index_text = committed_text.clone();
7194    let buffer_text = (0..30)
7195        .map(|i| match i % 5 {
7196            0 => format!("line {i} (modified)\n"),
7197            _ => format!("line {i}\n"),
7198        })
7199        .collect::<String>();
7200
7201    let fs = FakeFs::new(cx.background_executor.clone());
7202    fs.insert_tree(
7203        path!("/dir"),
7204        json!({
7205            ".git": {},
7206            "file.txt": buffer_text.clone()
7207        }),
7208    )
7209    .await;
7210    fs.set_head_for_repo(
7211        path!("/dir/.git").as_ref(),
7212        &[("file.txt".into(), committed_text.clone())],
7213        "deadbeef",
7214    );
7215    fs.set_index_for_repo(
7216        path!("/dir/.git").as_ref(),
7217        &[("file.txt".into(), index_text.clone())],
7218    );
7219    let repo = fs.open_repo(path!("/dir/.git").as_ref()).unwrap();
7220
7221    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
7222    let buffer = project
7223        .update(cx, |project, cx| {
7224            project.open_local_buffer(path!("/dir/file.txt"), cx)
7225        })
7226        .await
7227        .unwrap();
7228    let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
7229    let uncommitted_diff = project
7230        .update(cx, |project, cx| {
7231            project.open_uncommitted_diff(buffer.clone(), cx)
7232        })
7233        .await
7234        .unwrap();
7235
7236    let mut hunks =
7237        uncommitted_diff.update(cx, |diff, cx| diff.hunks(&snapshot, cx).collect::<Vec<_>>());
7238    assert_eq!(hunks.len(), 6);
7239
7240    for _i in 0..operations {
7241        let hunk_ix = rng.gen_range(0..hunks.len());
7242        let hunk = &mut hunks[hunk_ix];
7243        let row = hunk.range.start.row;
7244
7245        if hunk.status().has_secondary_hunk() {
7246            log::info!("staging hunk at {row}");
7247            uncommitted_diff.update(cx, |diff, cx| {
7248                diff.stage_or_unstage_hunks(true, &[hunk.clone()], &snapshot, true, cx);
7249            });
7250            hunk.secondary_status = SecondaryHunkRemovalPending;
7251        } else {
7252            log::info!("unstaging hunk at {row}");
7253            uncommitted_diff.update(cx, |diff, cx| {
7254                diff.stage_or_unstage_hunks(false, &[hunk.clone()], &snapshot, true, cx);
7255            });
7256            hunk.secondary_status = SecondaryHunkAdditionPending;
7257        }
7258
7259        for _ in 0..rng.gen_range(0..10) {
7260            log::info!("yielding");
7261            cx.executor().simulate_random_delay().await;
7262        }
7263    }
7264
7265    cx.executor().run_until_parked();
7266
7267    for hunk in &mut hunks {
7268        if hunk.secondary_status == SecondaryHunkRemovalPending {
7269            hunk.secondary_status = NoSecondaryHunk;
7270        } else if hunk.secondary_status == SecondaryHunkAdditionPending {
7271            hunk.secondary_status = HasSecondaryHunk;
7272        }
7273    }
7274
7275    log::info!(
7276        "index text:\n{}",
7277        repo.load_index_text("file.txt".into()).await.unwrap()
7278    );
7279
7280    uncommitted_diff.update(cx, |diff, cx| {
7281        let expected_hunks = hunks
7282            .iter()
7283            .map(|hunk| (hunk.range.start.row, hunk.secondary_status))
7284            .collect::<Vec<_>>();
7285        let actual_hunks = diff
7286            .hunks(&snapshot, cx)
7287            .map(|hunk| (hunk.range.start.row, hunk.secondary_status))
7288            .collect::<Vec<_>>();
7289        assert_eq!(actual_hunks, expected_hunks);
7290    });
7291}
7292
7293#[gpui::test]
7294async fn test_single_file_diffs(cx: &mut gpui::TestAppContext) {
7295    init_test(cx);
7296
7297    let committed_contents = r#"
7298        fn main() {
7299            println!("hello from HEAD");
7300        }
7301    "#
7302    .unindent();
7303    let file_contents = r#"
7304        fn main() {
7305            println!("hello from the working copy");
7306        }
7307    "#
7308    .unindent();
7309
7310    let fs = FakeFs::new(cx.background_executor.clone());
7311    fs.insert_tree(
7312        "/dir",
7313        json!({
7314            ".git": {},
7315           "src": {
7316               "main.rs": file_contents,
7317           }
7318        }),
7319    )
7320    .await;
7321
7322    fs.set_head_for_repo(
7323        Path::new("/dir/.git"),
7324        &[("src/main.rs".into(), committed_contents.clone())],
7325        "deadbeef",
7326    );
7327    fs.set_index_for_repo(
7328        Path::new("/dir/.git"),
7329        &[("src/main.rs".into(), committed_contents.clone())],
7330    );
7331
7332    let project = Project::test(fs.clone(), ["/dir/src/main.rs".as_ref()], cx).await;
7333
7334    let buffer = project
7335        .update(cx, |project, cx| {
7336            project.open_local_buffer("/dir/src/main.rs", cx)
7337        })
7338        .await
7339        .unwrap();
7340    let uncommitted_diff = project
7341        .update(cx, |project, cx| {
7342            project.open_uncommitted_diff(buffer.clone(), cx)
7343        })
7344        .await
7345        .unwrap();
7346
7347    cx.run_until_parked();
7348    uncommitted_diff.update(cx, |uncommitted_diff, cx| {
7349        let snapshot = buffer.read(cx).snapshot();
7350        assert_hunks(
7351            uncommitted_diff.hunks(&snapshot, cx),
7352            &snapshot,
7353            &uncommitted_diff.base_text_string().unwrap(),
7354            &[(
7355                1..2,
7356                "    println!(\"hello from HEAD\");\n",
7357                "    println!(\"hello from the working copy\");\n",
7358                DiffHunkStatus {
7359                    kind: DiffHunkStatusKind::Modified,
7360                    secondary: DiffHunkSecondaryStatus::HasSecondaryHunk,
7361                },
7362            )],
7363        );
7364    });
7365}
7366
7367#[gpui::test]
7368async fn test_repository_and_path_for_project_path(
7369    background_executor: BackgroundExecutor,
7370    cx: &mut gpui::TestAppContext,
7371) {
7372    init_test(cx);
7373    let fs = FakeFs::new(background_executor);
7374    fs.insert_tree(
7375        path!("/root"),
7376        json!({
7377            "c.txt": "",
7378            "dir1": {
7379                ".git": {},
7380                "deps": {
7381                    "dep1": {
7382                        ".git": {},
7383                        "src": {
7384                            "a.txt": ""
7385                        }
7386                    }
7387                },
7388                "src": {
7389                    "b.txt": ""
7390                }
7391            },
7392        }),
7393    )
7394    .await;
7395
7396    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
7397    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7398    let tree_id = tree.read_with(cx, |tree, _| tree.id());
7399    tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
7400        .await;
7401    cx.run_until_parked();
7402
7403    project.read_with(cx, |project, cx| {
7404        let git_store = project.git_store().read(cx);
7405        let pairs = [
7406            ("c.txt", None),
7407            ("dir1/src/b.txt", Some((path!("/root/dir1"), "src/b.txt"))),
7408            (
7409                "dir1/deps/dep1/src/a.txt",
7410                Some((path!("/root/dir1/deps/dep1"), "src/a.txt")),
7411            ),
7412        ];
7413        let expected = pairs
7414            .iter()
7415            .map(|(path, result)| {
7416                (
7417                    path,
7418                    result.map(|(repo, repo_path)| {
7419                        (Path::new(repo).into(), RepoPath::from(repo_path))
7420                    }),
7421                )
7422            })
7423            .collect::<Vec<_>>();
7424        let actual = pairs
7425            .iter()
7426            .map(|(path, _)| {
7427                let project_path = (tree_id, Path::new(path)).into();
7428                let result = maybe!({
7429                    let (repo, repo_path) =
7430                        git_store.repository_and_path_for_project_path(&project_path, cx)?;
7431                    Some((repo.read(cx).work_directory_abs_path.clone(), repo_path))
7432                });
7433                (path, result)
7434            })
7435            .collect::<Vec<_>>();
7436        pretty_assertions::assert_eq!(expected, actual);
7437    });
7438
7439    fs.remove_dir(path!("/root/dir1/.git").as_ref(), RemoveOptions::default())
7440        .await
7441        .unwrap();
7442    cx.run_until_parked();
7443
7444    project.read_with(cx, |project, cx| {
7445        let git_store = project.git_store().read(cx);
7446        assert_eq!(
7447            git_store.repository_and_path_for_project_path(
7448                &(tree_id, Path::new("dir1/src/b.txt")).into(),
7449                cx
7450            ),
7451            None
7452        );
7453    });
7454}
7455
7456#[gpui::test]
7457async fn test_home_dir_as_git_repository(cx: &mut gpui::TestAppContext) {
7458    init_test(cx);
7459    let fs = FakeFs::new(cx.background_executor.clone());
7460    fs.insert_tree(
7461        path!("/root"),
7462        json!({
7463            "home": {
7464                ".git": {},
7465                "project": {
7466                    "a.txt": "A"
7467                },
7468            },
7469        }),
7470    )
7471    .await;
7472    fs.set_home_dir(Path::new(path!("/root/home")).to_owned());
7473
7474    let project = Project::test(fs.clone(), [path!("/root/home/project").as_ref()], cx).await;
7475    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7476    let tree_id = tree.read_with(cx, |tree, _| tree.id());
7477    tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
7478        .await;
7479    tree.flush_fs_events(cx).await;
7480
7481    project.read_with(cx, |project, cx| {
7482        let containing = project
7483            .git_store()
7484            .read(cx)
7485            .repository_and_path_for_project_path(&(tree_id, "a.txt").into(), cx);
7486        assert!(containing.is_none());
7487    });
7488
7489    let project = Project::test(fs.clone(), [path!("/root/home").as_ref()], cx).await;
7490    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7491    let tree_id = tree.read_with(cx, |tree, _| tree.id());
7492    tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
7493        .await;
7494    tree.flush_fs_events(cx).await;
7495
7496    project.read_with(cx, |project, cx| {
7497        let containing = project
7498            .git_store()
7499            .read(cx)
7500            .repository_and_path_for_project_path(&(tree_id, "project/a.txt").into(), cx);
7501        assert_eq!(
7502            containing
7503                .unwrap()
7504                .0
7505                .read(cx)
7506                .work_directory_abs_path
7507                .as_ref(),
7508            Path::new(path!("/root/home"))
7509        );
7510    });
7511}
7512
7513#[gpui::test]
7514async fn test_git_repository_status(cx: &mut gpui::TestAppContext) {
7515    init_test(cx);
7516    cx.executor().allow_parking();
7517
7518    let root = TempTree::new(json!({
7519        "project": {
7520            "a.txt": "a",    // Modified
7521            "b.txt": "bb",   // Added
7522            "c.txt": "ccc",  // Unchanged
7523            "d.txt": "dddd", // Deleted
7524        },
7525    }));
7526
7527    // Set up git repository before creating the project.
7528    let work_dir = root.path().join("project");
7529    let repo = git_init(work_dir.as_path());
7530    git_add("a.txt", &repo);
7531    git_add("c.txt", &repo);
7532    git_add("d.txt", &repo);
7533    git_commit("Initial commit", &repo);
7534    std::fs::remove_file(work_dir.join("d.txt")).unwrap();
7535    std::fs::write(work_dir.join("a.txt"), "aa").unwrap();
7536
7537    let project = Project::test(
7538        Arc::new(RealFs::new(None, cx.executor())),
7539        [root.path()],
7540        cx,
7541    )
7542    .await;
7543
7544    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7545    tree.flush_fs_events(cx).await;
7546    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7547        .await;
7548    cx.executor().run_until_parked();
7549
7550    let repository = project.read_with(cx, |project, cx| {
7551        project.repositories(cx).values().next().unwrap().clone()
7552    });
7553
7554    // Check that the right git state is observed on startup
7555    repository.read_with(cx, |repository, _| {
7556        let entries = repository.cached_status().collect::<Vec<_>>();
7557        assert_eq!(
7558            entries,
7559            [
7560                StatusEntry {
7561                    repo_path: "a.txt".into(),
7562                    status: StatusCode::Modified.worktree(),
7563                },
7564                StatusEntry {
7565                    repo_path: "b.txt".into(),
7566                    status: FileStatus::Untracked,
7567                },
7568                StatusEntry {
7569                    repo_path: "d.txt".into(),
7570                    status: StatusCode::Deleted.worktree(),
7571                },
7572            ]
7573        );
7574    });
7575
7576    std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
7577
7578    tree.flush_fs_events(cx).await;
7579    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7580        .await;
7581    cx.executor().run_until_parked();
7582
7583    repository.read_with(cx, |repository, _| {
7584        let entries = repository.cached_status().collect::<Vec<_>>();
7585        assert_eq!(
7586            entries,
7587            [
7588                StatusEntry {
7589                    repo_path: "a.txt".into(),
7590                    status: StatusCode::Modified.worktree(),
7591                },
7592                StatusEntry {
7593                    repo_path: "b.txt".into(),
7594                    status: FileStatus::Untracked,
7595                },
7596                StatusEntry {
7597                    repo_path: "c.txt".into(),
7598                    status: StatusCode::Modified.worktree(),
7599                },
7600                StatusEntry {
7601                    repo_path: "d.txt".into(),
7602                    status: StatusCode::Deleted.worktree(),
7603                },
7604            ]
7605        );
7606    });
7607
7608    git_add("a.txt", &repo);
7609    git_add("c.txt", &repo);
7610    git_remove_index(Path::new("d.txt"), &repo);
7611    git_commit("Another commit", &repo);
7612    tree.flush_fs_events(cx).await;
7613    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7614        .await;
7615    cx.executor().run_until_parked();
7616
7617    std::fs::remove_file(work_dir.join("a.txt")).unwrap();
7618    std::fs::remove_file(work_dir.join("b.txt")).unwrap();
7619    tree.flush_fs_events(cx).await;
7620    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7621        .await;
7622    cx.executor().run_until_parked();
7623
7624    repository.read_with(cx, |repository, _cx| {
7625        let entries = repository.cached_status().collect::<Vec<_>>();
7626
7627        // Deleting an untracked entry, b.txt, should leave no status
7628        // a.txt was tracked, and so should have a status
7629        assert_eq!(
7630            entries,
7631            [StatusEntry {
7632                repo_path: "a.txt".into(),
7633                status: StatusCode::Deleted.worktree(),
7634            }]
7635        );
7636    });
7637}
7638
7639#[gpui::test]
7640async fn test_git_status_postprocessing(cx: &mut gpui::TestAppContext) {
7641    init_test(cx);
7642    cx.executor().allow_parking();
7643
7644    let root = TempTree::new(json!({
7645        "project": {
7646            "sub": {},
7647            "a.txt": "",
7648        },
7649    }));
7650
7651    let work_dir = root.path().join("project");
7652    let repo = git_init(work_dir.as_path());
7653    // a.txt exists in HEAD and the working copy but is deleted in the index.
7654    git_add("a.txt", &repo);
7655    git_commit("Initial commit", &repo);
7656    git_remove_index("a.txt".as_ref(), &repo);
7657    // `sub` is a nested git repository.
7658    let _sub = git_init(&work_dir.join("sub"));
7659
7660    let project = Project::test(
7661        Arc::new(RealFs::new(None, cx.executor())),
7662        [root.path()],
7663        cx,
7664    )
7665    .await;
7666
7667    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7668    tree.flush_fs_events(cx).await;
7669    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7670        .await;
7671    cx.executor().run_until_parked();
7672
7673    let repository = project.read_with(cx, |project, cx| {
7674        project
7675            .repositories(cx)
7676            .values()
7677            .find(|repo| repo.read(cx).work_directory_abs_path.ends_with("project"))
7678            .unwrap()
7679            .clone()
7680    });
7681
7682    repository.read_with(cx, |repository, _cx| {
7683        let entries = repository.cached_status().collect::<Vec<_>>();
7684
7685        // `sub` doesn't appear in our computed statuses.
7686        // a.txt appears with a combined `DA` status.
7687        assert_eq!(
7688            entries,
7689            [StatusEntry {
7690                repo_path: "a.txt".into(),
7691                status: TrackedStatus {
7692                    index_status: StatusCode::Deleted,
7693                    worktree_status: StatusCode::Added
7694                }
7695                .into(),
7696            }]
7697        )
7698    });
7699}
7700
7701#[gpui::test]
7702async fn test_repository_subfolder_git_status(
7703    executor: gpui::BackgroundExecutor,
7704    cx: &mut gpui::TestAppContext,
7705) {
7706    init_test(cx);
7707
7708    let fs = FakeFs::new(executor);
7709    fs.insert_tree(
7710        path!("/root"),
7711        json!({
7712            "my-repo": {
7713                ".git": {},
7714                "a.txt": "a",
7715                "sub-folder-1": {
7716                    "sub-folder-2": {
7717                        "c.txt": "cc",
7718                        "d": {
7719                            "e.txt": "eee"
7720                        }
7721                    },
7722                }
7723            },
7724        }),
7725    )
7726    .await;
7727
7728    const C_TXT: &str = "sub-folder-1/sub-folder-2/c.txt";
7729    const E_TXT: &str = "sub-folder-1/sub-folder-2/d/e.txt";
7730
7731    fs.set_status_for_repo(
7732        path!("/root/my-repo/.git").as_ref(),
7733        &[(E_TXT.as_ref(), FileStatus::Untracked)],
7734    );
7735
7736    let project = Project::test(
7737        fs.clone(),
7738        [path!("/root/my-repo/sub-folder-1/sub-folder-2").as_ref()],
7739        cx,
7740    )
7741    .await;
7742
7743    project
7744        .update(cx, |project, cx| project.git_scans_complete(cx))
7745        .await;
7746    cx.run_until_parked();
7747
7748    let repository = project.read_with(cx, |project, cx| {
7749        project.repositories(cx).values().next().unwrap().clone()
7750    });
7751
7752    // Ensure that the git status is loaded correctly
7753    repository.read_with(cx, |repository, _cx| {
7754        assert_eq!(
7755            repository.work_directory_abs_path,
7756            Path::new(path!("/root/my-repo")).into()
7757        );
7758
7759        assert_eq!(repository.status_for_path(&C_TXT.into()), None);
7760        assert_eq!(
7761            repository.status_for_path(&E_TXT.into()).unwrap().status,
7762            FileStatus::Untracked
7763        );
7764    });
7765
7766    fs.set_status_for_repo(path!("/root/my-repo/.git").as_ref(), &[]);
7767    project
7768        .update(cx, |project, cx| project.git_scans_complete(cx))
7769        .await;
7770    cx.run_until_parked();
7771
7772    repository.read_with(cx, |repository, _cx| {
7773        assert_eq!(repository.status_for_path(&C_TXT.into()), None);
7774        assert_eq!(repository.status_for_path(&E_TXT.into()), None);
7775    });
7776}
7777
7778// TODO: this test is flaky (especially on Windows but at least sometimes on all platforms).
7779#[cfg(any())]
7780#[gpui::test]
7781async fn test_conflicted_cherry_pick(cx: &mut gpui::TestAppContext) {
7782    init_test(cx);
7783    cx.executor().allow_parking();
7784
7785    let root = TempTree::new(json!({
7786        "project": {
7787            "a.txt": "a",
7788        },
7789    }));
7790    let root_path = root.path();
7791
7792    let repo = git_init(&root_path.join("project"));
7793    git_add("a.txt", &repo);
7794    git_commit("init", &repo);
7795
7796    let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [root_path], cx).await;
7797
7798    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7799    tree.flush_fs_events(cx).await;
7800    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7801        .await;
7802    cx.executor().run_until_parked();
7803
7804    let repository = project.read_with(cx, |project, cx| {
7805        project.repositories(cx).values().next().unwrap().clone()
7806    });
7807
7808    git_branch("other-branch", &repo);
7809    git_checkout("refs/heads/other-branch", &repo);
7810    std::fs::write(root_path.join("project/a.txt"), "A").unwrap();
7811    git_add("a.txt", &repo);
7812    git_commit("capitalize", &repo);
7813    let commit = repo
7814        .head()
7815        .expect("Failed to get HEAD")
7816        .peel_to_commit()
7817        .expect("HEAD is not a commit");
7818    git_checkout("refs/heads/main", &repo);
7819    std::fs::write(root_path.join("project/a.txt"), "b").unwrap();
7820    git_add("a.txt", &repo);
7821    git_commit("improve letter", &repo);
7822    git_cherry_pick(&commit, &repo);
7823    std::fs::read_to_string(root_path.join("project/.git/CHERRY_PICK_HEAD"))
7824        .expect("No CHERRY_PICK_HEAD");
7825    pretty_assertions::assert_eq!(
7826        git_status(&repo),
7827        collections::HashMap::from_iter([("a.txt".to_owned(), git2::Status::CONFLICTED)])
7828    );
7829    tree.flush_fs_events(cx).await;
7830    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7831        .await;
7832    cx.executor().run_until_parked();
7833    let conflicts = repository.update(cx, |repository, _| {
7834        repository
7835            .merge_conflicts
7836            .iter()
7837            .cloned()
7838            .collect::<Vec<_>>()
7839    });
7840    pretty_assertions::assert_eq!(conflicts, [RepoPath::from("a.txt")]);
7841
7842    git_add("a.txt", &repo);
7843    // Attempt to manually simulate what `git cherry-pick --continue` would do.
7844    git_commit("whatevs", &repo);
7845    std::fs::remove_file(root.path().join("project/.git/CHERRY_PICK_HEAD"))
7846        .expect("Failed to remove CHERRY_PICK_HEAD");
7847    pretty_assertions::assert_eq!(git_status(&repo), collections::HashMap::default());
7848    tree.flush_fs_events(cx).await;
7849    let conflicts = repository.update(cx, |repository, _| {
7850        repository
7851            .merge_conflicts
7852            .iter()
7853            .cloned()
7854            .collect::<Vec<_>>()
7855    });
7856    pretty_assertions::assert_eq!(conflicts, []);
7857}
7858
7859#[gpui::test]
7860async fn test_update_gitignore(cx: &mut gpui::TestAppContext) {
7861    init_test(cx);
7862    let fs = FakeFs::new(cx.background_executor.clone());
7863    fs.insert_tree(
7864        path!("/root"),
7865        json!({
7866            ".git": {},
7867            ".gitignore": "*.txt\n",
7868            "a.xml": "<a></a>",
7869            "b.txt": "Some text"
7870        }),
7871    )
7872    .await;
7873
7874    fs.set_head_and_index_for_repo(
7875        path!("/root/.git").as_ref(),
7876        &[
7877            (".gitignore".into(), "*.txt\n".into()),
7878            ("a.xml".into(), "<a></a>".into()),
7879        ],
7880    );
7881
7882    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
7883
7884    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7885    tree.flush_fs_events(cx).await;
7886    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7887        .await;
7888    cx.executor().run_until_parked();
7889
7890    let repository = project.read_with(cx, |project, cx| {
7891        project.repositories(cx).values().next().unwrap().clone()
7892    });
7893
7894    // One file is unmodified, the other is ignored.
7895    cx.read(|cx| {
7896        assert_entry_git_state(tree.read(cx), repository.read(cx), "a.xml", None, false);
7897        assert_entry_git_state(tree.read(cx), repository.read(cx), "b.txt", None, true);
7898    });
7899
7900    // Change the gitignore, and stage the newly non-ignored file.
7901    fs.atomic_write(path!("/root/.gitignore").into(), "*.xml\n".into())
7902        .await
7903        .unwrap();
7904    fs.set_index_for_repo(
7905        Path::new(path!("/root/.git")),
7906        &[
7907            (".gitignore".into(), "*.txt\n".into()),
7908            ("a.xml".into(), "<a></a>".into()),
7909            ("b.txt".into(), "Some text".into()),
7910        ],
7911    );
7912
7913    cx.executor().run_until_parked();
7914    cx.read(|cx| {
7915        assert_entry_git_state(tree.read(cx), repository.read(cx), "a.xml", None, true);
7916        assert_entry_git_state(
7917            tree.read(cx),
7918            repository.read(cx),
7919            "b.txt",
7920            Some(StatusCode::Added),
7921            false,
7922        );
7923    });
7924}
7925
7926// NOTE:
7927// This test always fails on Windows, because on Windows, unlike on Unix, you can't rename
7928// a directory which some program has already open.
7929// This is a limitation of the Windows.
7930// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
7931#[gpui::test]
7932#[cfg_attr(target_os = "windows", ignore)]
7933async fn test_rename_work_directory(cx: &mut gpui::TestAppContext) {
7934    init_test(cx);
7935    cx.executor().allow_parking();
7936    let root = TempTree::new(json!({
7937        "projects": {
7938            "project1": {
7939                "a": "",
7940                "b": "",
7941            }
7942        },
7943
7944    }));
7945    let root_path = root.path();
7946
7947    let repo = git_init(&root_path.join("projects/project1"));
7948    git_add("a", &repo);
7949    git_commit("init", &repo);
7950    std::fs::write(root_path.join("projects/project1/a"), "aa").unwrap();
7951
7952    let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [root_path], cx).await;
7953
7954    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7955    tree.flush_fs_events(cx).await;
7956    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7957        .await;
7958    cx.executor().run_until_parked();
7959
7960    let repository = project.read_with(cx, |project, cx| {
7961        project.repositories(cx).values().next().unwrap().clone()
7962    });
7963
7964    repository.read_with(cx, |repository, _| {
7965        assert_eq!(
7966            repository.work_directory_abs_path.as_ref(),
7967            root_path.join("projects/project1").as_path()
7968        );
7969        assert_eq!(
7970            repository
7971                .status_for_path(&"a".into())
7972                .map(|entry| entry.status),
7973            Some(StatusCode::Modified.worktree()),
7974        );
7975        assert_eq!(
7976            repository
7977                .status_for_path(&"b".into())
7978                .map(|entry| entry.status),
7979            Some(FileStatus::Untracked),
7980        );
7981    });
7982
7983    std::fs::rename(
7984        root_path.join("projects/project1"),
7985        root_path.join("projects/project2"),
7986    )
7987    .unwrap();
7988    tree.flush_fs_events(cx).await;
7989
7990    repository.read_with(cx, |repository, _| {
7991        assert_eq!(
7992            repository.work_directory_abs_path.as_ref(),
7993            root_path.join("projects/project2").as_path()
7994        );
7995        assert_eq!(
7996            repository.status_for_path(&"a".into()).unwrap().status,
7997            StatusCode::Modified.worktree(),
7998        );
7999        assert_eq!(
8000            repository.status_for_path(&"b".into()).unwrap().status,
8001            FileStatus::Untracked,
8002        );
8003    });
8004}
8005
8006// NOTE: This test always fails on Windows, because on Windows, unlike on Unix,
8007// you can't rename a directory which some program has already open. This is a
8008// limitation of the Windows. See:
8009// https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
8010#[gpui::test]
8011#[cfg_attr(target_os = "windows", ignore)]
8012async fn test_file_status(cx: &mut gpui::TestAppContext) {
8013    init_test(cx);
8014    cx.executor().allow_parking();
8015    const IGNORE_RULE: &str = "**/target";
8016
8017    let root = TempTree::new(json!({
8018        "project": {
8019            "a.txt": "a",
8020            "b.txt": "bb",
8021            "c": {
8022                "d": {
8023                    "e.txt": "eee"
8024                }
8025            },
8026            "f.txt": "ffff",
8027            "target": {
8028                "build_file": "???"
8029            },
8030            ".gitignore": IGNORE_RULE
8031        },
8032
8033    }));
8034    let root_path = root.path();
8035
8036    const A_TXT: &str = "a.txt";
8037    const B_TXT: &str = "b.txt";
8038    const E_TXT: &str = "c/d/e.txt";
8039    const F_TXT: &str = "f.txt";
8040    const DOTGITIGNORE: &str = ".gitignore";
8041    const BUILD_FILE: &str = "target/build_file";
8042
8043    // Set up git repository before creating the worktree.
8044    let work_dir = root.path().join("project");
8045    let mut repo = git_init(work_dir.as_path());
8046    repo.add_ignore_rule(IGNORE_RULE).unwrap();
8047    git_add(A_TXT, &repo);
8048    git_add(E_TXT, &repo);
8049    git_add(DOTGITIGNORE, &repo);
8050    git_commit("Initial commit", &repo);
8051
8052    let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [root_path], cx).await;
8053
8054    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
8055    tree.flush_fs_events(cx).await;
8056    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
8057        .await;
8058    cx.executor().run_until_parked();
8059
8060    let repository = project.read_with(cx, |project, cx| {
8061        project.repositories(cx).values().next().unwrap().clone()
8062    });
8063
8064    // Check that the right git state is observed on startup
8065    repository.read_with(cx, |repository, _cx| {
8066        assert_eq!(
8067            repository.work_directory_abs_path.as_ref(),
8068            root_path.join("project").as_path()
8069        );
8070
8071        assert_eq!(
8072            repository.status_for_path(&B_TXT.into()).unwrap().status,
8073            FileStatus::Untracked,
8074        );
8075        assert_eq!(
8076            repository.status_for_path(&F_TXT.into()).unwrap().status,
8077            FileStatus::Untracked,
8078        );
8079    });
8080
8081    // Modify a file in the working copy.
8082    std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
8083    tree.flush_fs_events(cx).await;
8084    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
8085        .await;
8086    cx.executor().run_until_parked();
8087
8088    // The worktree detects that the file's git status has changed.
8089    repository.read_with(cx, |repository, _| {
8090        assert_eq!(
8091            repository.status_for_path(&A_TXT.into()).unwrap().status,
8092            StatusCode::Modified.worktree(),
8093        );
8094    });
8095
8096    // Create a commit in the git repository.
8097    git_add(A_TXT, &repo);
8098    git_add(B_TXT, &repo);
8099    git_commit("Committing modified and added", &repo);
8100    tree.flush_fs_events(cx).await;
8101    cx.executor().run_until_parked();
8102
8103    // The worktree detects that the files' git status have changed.
8104    repository.read_with(cx, |repository, _cx| {
8105        assert_eq!(
8106            repository.status_for_path(&F_TXT.into()).unwrap().status,
8107            FileStatus::Untracked,
8108        );
8109        assert_eq!(repository.status_for_path(&B_TXT.into()), None);
8110        assert_eq!(repository.status_for_path(&A_TXT.into()), None);
8111    });
8112
8113    // Modify files in the working copy and perform git operations on other files.
8114    git_reset(0, &repo);
8115    git_remove_index(Path::new(B_TXT), &repo);
8116    git_stash(&mut repo);
8117    std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
8118    std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
8119    tree.flush_fs_events(cx).await;
8120    cx.executor().run_until_parked();
8121
8122    // Check that more complex repo changes are tracked
8123    repository.read_with(cx, |repository, _cx| {
8124        assert_eq!(repository.status_for_path(&A_TXT.into()), None);
8125        assert_eq!(
8126            repository.status_for_path(&B_TXT.into()).unwrap().status,
8127            FileStatus::Untracked,
8128        );
8129        assert_eq!(
8130            repository.status_for_path(&E_TXT.into()).unwrap().status,
8131            StatusCode::Modified.worktree(),
8132        );
8133    });
8134
8135    std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
8136    std::fs::remove_dir_all(work_dir.join("c")).unwrap();
8137    std::fs::write(
8138        work_dir.join(DOTGITIGNORE),
8139        [IGNORE_RULE, "f.txt"].join("\n"),
8140    )
8141    .unwrap();
8142
8143    git_add(Path::new(DOTGITIGNORE), &repo);
8144    git_commit("Committing modified git ignore", &repo);
8145
8146    tree.flush_fs_events(cx).await;
8147    cx.executor().run_until_parked();
8148
8149    let mut renamed_dir_name = "first_directory/second_directory";
8150    const RENAMED_FILE: &str = "rf.txt";
8151
8152    std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
8153    std::fs::write(
8154        work_dir.join(renamed_dir_name).join(RENAMED_FILE),
8155        "new-contents",
8156    )
8157    .unwrap();
8158
8159    tree.flush_fs_events(cx).await;
8160    cx.executor().run_until_parked();
8161
8162    repository.read_with(cx, |repository, _cx| {
8163        assert_eq!(
8164            repository
8165                .status_for_path(&Path::new(renamed_dir_name).join(RENAMED_FILE).into())
8166                .unwrap()
8167                .status,
8168            FileStatus::Untracked,
8169        );
8170    });
8171
8172    renamed_dir_name = "new_first_directory/second_directory";
8173
8174    std::fs::rename(
8175        work_dir.join("first_directory"),
8176        work_dir.join("new_first_directory"),
8177    )
8178    .unwrap();
8179
8180    tree.flush_fs_events(cx).await;
8181    cx.executor().run_until_parked();
8182
8183    repository.read_with(cx, |repository, _cx| {
8184        assert_eq!(
8185            repository
8186                .status_for_path(&Path::new(renamed_dir_name).join(RENAMED_FILE).into())
8187                .unwrap()
8188                .status,
8189            FileStatus::Untracked,
8190        );
8191    });
8192}
8193
8194#[gpui::test]
8195async fn test_repos_in_invisible_worktrees(
8196    executor: BackgroundExecutor,
8197    cx: &mut gpui::TestAppContext,
8198) {
8199    init_test(cx);
8200    let fs = FakeFs::new(executor);
8201    fs.insert_tree(
8202        path!("/root"),
8203        json!({
8204            "dir1": {
8205                ".git": {},
8206                "dep1": {
8207                    ".git": {},
8208                    "src": {
8209                        "a.txt": "",
8210                    },
8211                },
8212                "b.txt": "",
8213            },
8214        }),
8215    )
8216    .await;
8217
8218    let project = Project::test(fs.clone(), [path!("/root/dir1/dep1").as_ref()], cx).await;
8219    let visible_worktree =
8220        project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
8221    visible_worktree
8222        .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
8223        .await;
8224
8225    let repos = project.read_with(cx, |project, cx| {
8226        project
8227            .repositories(cx)
8228            .values()
8229            .map(|repo| repo.read(cx).work_directory_abs_path.clone())
8230            .collect::<Vec<_>>()
8231    });
8232    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root/dir1/dep1")).into()]);
8233
8234    let (invisible_worktree, _) = project
8235        .update(cx, |project, cx| {
8236            project.worktree_store.update(cx, |worktree_store, cx| {
8237                worktree_store.find_or_create_worktree(path!("/root/dir1/b.txt"), false, cx)
8238            })
8239        })
8240        .await
8241        .expect("failed to create worktree");
8242    invisible_worktree
8243        .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
8244        .await;
8245
8246    let repos = project.read_with(cx, |project, cx| {
8247        project
8248            .repositories(cx)
8249            .values()
8250            .map(|repo| repo.read(cx).work_directory_abs_path.clone())
8251            .collect::<Vec<_>>()
8252    });
8253    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root/dir1/dep1")).into()]);
8254}
8255
8256#[gpui::test(iterations = 10)]
8257async fn test_rescan_with_gitignore(cx: &mut gpui::TestAppContext) {
8258    init_test(cx);
8259    cx.update(|cx| {
8260        cx.update_global::<SettingsStore, _>(|store, cx| {
8261            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
8262                project_settings.file_scan_exclusions = Some(Vec::new());
8263            });
8264        });
8265    });
8266    let fs = FakeFs::new(cx.background_executor.clone());
8267    fs.insert_tree(
8268        path!("/root"),
8269        json!({
8270            ".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
8271            "tree": {
8272                ".git": {},
8273                ".gitignore": "ignored-dir\n",
8274                "tracked-dir": {
8275                    "tracked-file1": "",
8276                    "ancestor-ignored-file1": "",
8277                },
8278                "ignored-dir": {
8279                    "ignored-file1": ""
8280                }
8281            }
8282        }),
8283    )
8284    .await;
8285    fs.set_head_and_index_for_repo(
8286        path!("/root/tree/.git").as_ref(),
8287        &[
8288            (".gitignore".into(), "ignored-dir\n".into()),
8289            ("tracked-dir/tracked-file1".into(), "".into()),
8290        ],
8291    );
8292
8293    let project = Project::test(fs.clone(), [path!("/root/tree").as_ref()], cx).await;
8294
8295    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
8296    tree.flush_fs_events(cx).await;
8297    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
8298        .await;
8299    cx.executor().run_until_parked();
8300
8301    let repository = project.read_with(cx, |project, cx| {
8302        project.repositories(cx).values().next().unwrap().clone()
8303    });
8304
8305    tree.read_with(cx, |tree, _| {
8306        tree.as_local()
8307            .unwrap()
8308            .manually_refresh_entries_for_paths(vec![Path::new("ignored-dir").into()])
8309    })
8310    .recv()
8311    .await;
8312
8313    cx.read(|cx| {
8314        assert_entry_git_state(
8315            tree.read(cx),
8316            repository.read(cx),
8317            "tracked-dir/tracked-file1",
8318            None,
8319            false,
8320        );
8321        assert_entry_git_state(
8322            tree.read(cx),
8323            repository.read(cx),
8324            "tracked-dir/ancestor-ignored-file1",
8325            None,
8326            false,
8327        );
8328        assert_entry_git_state(
8329            tree.read(cx),
8330            repository.read(cx),
8331            "ignored-dir/ignored-file1",
8332            None,
8333            true,
8334        );
8335    });
8336
8337    fs.create_file(
8338        path!("/root/tree/tracked-dir/tracked-file2").as_ref(),
8339        Default::default(),
8340    )
8341    .await
8342    .unwrap();
8343    fs.set_index_for_repo(
8344        path!("/root/tree/.git").as_ref(),
8345        &[
8346            (".gitignore".into(), "ignored-dir\n".into()),
8347            ("tracked-dir/tracked-file1".into(), "".into()),
8348            ("tracked-dir/tracked-file2".into(), "".into()),
8349        ],
8350    );
8351    fs.create_file(
8352        path!("/root/tree/tracked-dir/ancestor-ignored-file2").as_ref(),
8353        Default::default(),
8354    )
8355    .await
8356    .unwrap();
8357    fs.create_file(
8358        path!("/root/tree/ignored-dir/ignored-file2").as_ref(),
8359        Default::default(),
8360    )
8361    .await
8362    .unwrap();
8363
8364    cx.executor().run_until_parked();
8365    cx.read(|cx| {
8366        assert_entry_git_state(
8367            tree.read(cx),
8368            repository.read(cx),
8369            "tracked-dir/tracked-file2",
8370            Some(StatusCode::Added),
8371            false,
8372        );
8373        assert_entry_git_state(
8374            tree.read(cx),
8375            repository.read(cx),
8376            "tracked-dir/ancestor-ignored-file2",
8377            None,
8378            false,
8379        );
8380        assert_entry_git_state(
8381            tree.read(cx),
8382            repository.read(cx),
8383            "ignored-dir/ignored-file2",
8384            None,
8385            true,
8386        );
8387        assert!(tree.read(cx).entry_for_path(".git").unwrap().is_ignored);
8388    });
8389}
8390
8391#[gpui::test]
8392async fn test_git_worktrees_and_submodules(cx: &mut gpui::TestAppContext) {
8393    init_test(cx);
8394
8395    let fs = FakeFs::new(cx.executor());
8396    fs.insert_tree(
8397        path!("/project"),
8398        json!({
8399            ".git": {
8400                "worktrees": {
8401                    "some-worktree": {
8402                        "commondir": "../..\n",
8403                        // For is_git_dir
8404                        "HEAD": "",
8405                        "config": ""
8406                    }
8407                },
8408                "modules": {
8409                    "subdir": {
8410                        "some-submodule": {
8411                            // For is_git_dir
8412                            "HEAD": "",
8413                            "config": "",
8414                        }
8415                    }
8416                }
8417            },
8418            "src": {
8419                "a.txt": "A",
8420            },
8421            "some-worktree": {
8422                ".git": "gitdir: ../.git/worktrees/some-worktree\n",
8423                "src": {
8424                    "b.txt": "B",
8425                }
8426            },
8427            "subdir": {
8428                "some-submodule": {
8429                    ".git": "gitdir: ../../.git/modules/subdir/some-submodule\n",
8430                    "c.txt": "C",
8431                }
8432            }
8433        }),
8434    )
8435    .await;
8436
8437    let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
8438    let scan_complete = project.update(cx, |project, cx| {
8439        project
8440            .worktrees(cx)
8441            .next()
8442            .unwrap()
8443            .read(cx)
8444            .as_local()
8445            .unwrap()
8446            .scan_complete()
8447    });
8448    scan_complete.await;
8449
8450    let mut repositories = project.update(cx, |project, cx| {
8451        project
8452            .repositories(cx)
8453            .values()
8454            .map(|repo| repo.read(cx).work_directory_abs_path.clone())
8455            .collect::<Vec<_>>()
8456    });
8457    repositories.sort();
8458    pretty_assertions::assert_eq!(
8459        repositories,
8460        [
8461            Path::new(path!("/project")).into(),
8462            Path::new(path!("/project/some-worktree")).into(),
8463            Path::new(path!("/project/subdir/some-submodule")).into(),
8464        ]
8465    );
8466
8467    // Generate a git-related event for the worktree and check that it's refreshed.
8468    fs.with_git_state(
8469        path!("/project/some-worktree/.git").as_ref(),
8470        true,
8471        |state| {
8472            state
8473                .head_contents
8474                .insert("src/b.txt".into(), "b".to_owned());
8475            state
8476                .index_contents
8477                .insert("src/b.txt".into(), "b".to_owned());
8478        },
8479    )
8480    .unwrap();
8481    cx.run_until_parked();
8482
8483    let buffer = project
8484        .update(cx, |project, cx| {
8485            project.open_local_buffer(path!("/project/some-worktree/src/b.txt"), cx)
8486        })
8487        .await
8488        .unwrap();
8489    let (worktree_repo, barrier) = project.update(cx, |project, cx| {
8490        let (repo, _) = project
8491            .git_store()
8492            .read(cx)
8493            .repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
8494            .unwrap();
8495        pretty_assertions::assert_eq!(
8496            repo.read(cx).work_directory_abs_path,
8497            Path::new(path!("/project/some-worktree")).into(),
8498        );
8499        let barrier = repo.update(cx, |repo, _| repo.barrier());
8500        (repo.clone(), barrier)
8501    });
8502    barrier.await.unwrap();
8503    worktree_repo.update(cx, |repo, _| {
8504        pretty_assertions::assert_eq!(
8505            repo.status_for_path(&"src/b.txt".into()).unwrap().status,
8506            StatusCode::Modified.worktree(),
8507        );
8508    });
8509
8510    // The same for the submodule.
8511    fs.with_git_state(
8512        path!("/project/subdir/some-submodule/.git").as_ref(),
8513        true,
8514        |state| {
8515            state.head_contents.insert("c.txt".into(), "c".to_owned());
8516            state.index_contents.insert("c.txt".into(), "c".to_owned());
8517        },
8518    )
8519    .unwrap();
8520    cx.run_until_parked();
8521
8522    let buffer = project
8523        .update(cx, |project, cx| {
8524            project.open_local_buffer(path!("/project/subdir/some-submodule/c.txt"), cx)
8525        })
8526        .await
8527        .unwrap();
8528    let (submodule_repo, barrier) = project.update(cx, |project, cx| {
8529        let (repo, _) = project
8530            .git_store()
8531            .read(cx)
8532            .repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
8533            .unwrap();
8534        pretty_assertions::assert_eq!(
8535            repo.read(cx).work_directory_abs_path,
8536            Path::new(path!("/project/subdir/some-submodule")).into(),
8537        );
8538        let barrier = repo.update(cx, |repo, _| repo.barrier());
8539        (repo.clone(), barrier)
8540    });
8541    barrier.await.unwrap();
8542    submodule_repo.update(cx, |repo, _| {
8543        pretty_assertions::assert_eq!(
8544            repo.status_for_path(&"c.txt".into()).unwrap().status,
8545            StatusCode::Modified.worktree(),
8546        );
8547    });
8548}
8549
8550#[gpui::test]
8551async fn test_repository_deduplication(cx: &mut gpui::TestAppContext) {
8552    init_test(cx);
8553    let fs = FakeFs::new(cx.background_executor.clone());
8554    fs.insert_tree(
8555        path!("/root"),
8556        json!({
8557            "project": {
8558                ".git": {},
8559                "child1": {
8560                    "a.txt": "A",
8561                },
8562                "child2": {
8563                    "b.txt": "B",
8564                }
8565            }
8566        }),
8567    )
8568    .await;
8569
8570    let project = Project::test(
8571        fs.clone(),
8572        [
8573            path!("/root/project/child1").as_ref(),
8574            path!("/root/project/child2").as_ref(),
8575        ],
8576        cx,
8577    )
8578    .await;
8579
8580    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
8581    tree.flush_fs_events(cx).await;
8582    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
8583        .await;
8584    cx.executor().run_until_parked();
8585
8586    let repos = project.read_with(cx, |project, cx| {
8587        project
8588            .repositories(cx)
8589            .values()
8590            .map(|repo| repo.read(cx).work_directory_abs_path.clone())
8591            .collect::<Vec<_>>()
8592    });
8593    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root/project")).into()]);
8594}
8595
8596async fn search(
8597    project: &Entity<Project>,
8598    query: SearchQuery,
8599    cx: &mut gpui::TestAppContext,
8600) -> Result<HashMap<String, Vec<Range<usize>>>> {
8601    let search_rx = project.update(cx, |project, cx| project.search(query, cx));
8602    let mut results = HashMap::default();
8603    while let Ok(search_result) = search_rx.recv().await {
8604        match search_result {
8605            SearchResult::Buffer { buffer, ranges } => {
8606                results.entry(buffer).or_insert(ranges);
8607            }
8608            SearchResult::LimitReached => {}
8609        }
8610    }
8611    Ok(results
8612        .into_iter()
8613        .map(|(buffer, ranges)| {
8614            buffer.update(cx, |buffer, cx| {
8615                let path = buffer
8616                    .file()
8617                    .unwrap()
8618                    .full_path(cx)
8619                    .to_string_lossy()
8620                    .to_string();
8621                let ranges = ranges
8622                    .into_iter()
8623                    .map(|range| range.to_offset(buffer))
8624                    .collect::<Vec<_>>();
8625                (path, ranges)
8626            })
8627        })
8628        .collect())
8629}
8630
8631pub fn init_test(cx: &mut gpui::TestAppContext) {
8632    zlog::init_test();
8633
8634    cx.update(|cx| {
8635        let settings_store = SettingsStore::test(cx);
8636        cx.set_global(settings_store);
8637        release_channel::init(SemanticVersion::default(), cx);
8638        language::init(cx);
8639        Project::init_settings(cx);
8640    });
8641}
8642
8643fn json_lang() -> Arc<Language> {
8644    Arc::new(Language::new(
8645        LanguageConfig {
8646            name: "JSON".into(),
8647            matcher: LanguageMatcher {
8648                path_suffixes: vec!["json".to_string()],
8649                ..Default::default()
8650            },
8651            ..Default::default()
8652        },
8653        None,
8654    ))
8655}
8656
8657fn js_lang() -> Arc<Language> {
8658    Arc::new(Language::new(
8659        LanguageConfig {
8660            name: "JavaScript".into(),
8661            matcher: LanguageMatcher {
8662                path_suffixes: vec!["js".to_string()],
8663                ..Default::default()
8664            },
8665            ..Default::default()
8666        },
8667        None,
8668    ))
8669}
8670
8671fn rust_lang() -> Arc<Language> {
8672    Arc::new(Language::new(
8673        LanguageConfig {
8674            name: "Rust".into(),
8675            matcher: LanguageMatcher {
8676                path_suffixes: vec!["rs".to_string()],
8677                ..Default::default()
8678            },
8679            ..Default::default()
8680        },
8681        Some(tree_sitter_rust::LANGUAGE.into()),
8682    ))
8683}
8684
8685fn typescript_lang() -> Arc<Language> {
8686    Arc::new(Language::new(
8687        LanguageConfig {
8688            name: "TypeScript".into(),
8689            matcher: LanguageMatcher {
8690                path_suffixes: vec!["ts".to_string()],
8691                ..Default::default()
8692            },
8693            ..Default::default()
8694        },
8695        Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8696    ))
8697}
8698
8699fn tsx_lang() -> Arc<Language> {
8700    Arc::new(Language::new(
8701        LanguageConfig {
8702            name: "tsx".into(),
8703            matcher: LanguageMatcher {
8704                path_suffixes: vec!["tsx".to_string()],
8705                ..Default::default()
8706            },
8707            ..Default::default()
8708        },
8709        Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8710    ))
8711}
8712
8713fn get_all_tasks(
8714    project: &Entity<Project>,
8715    task_contexts: &TaskContexts,
8716    cx: &mut App,
8717) -> Vec<(TaskSourceKind, ResolvedTask)> {
8718    let (mut old, new) = project.update(cx, |project, cx| {
8719        project
8720            .task_store
8721            .read(cx)
8722            .task_inventory()
8723            .unwrap()
8724            .read(cx)
8725            .used_and_current_resolved_tasks(task_contexts, cx)
8726    });
8727    old.extend(new);
8728    old
8729}
8730
8731#[track_caller]
8732fn assert_entry_git_state(
8733    tree: &Worktree,
8734    repository: &Repository,
8735    path: &str,
8736    index_status: Option<StatusCode>,
8737    is_ignored: bool,
8738) {
8739    assert_eq!(tree.abs_path(), repository.work_directory_abs_path);
8740    let entry = tree
8741        .entry_for_path(path)
8742        .unwrap_or_else(|| panic!("entry {path} not found"));
8743    let status = repository
8744        .status_for_path(&path.into())
8745        .map(|entry| entry.status);
8746    let expected = index_status.map(|index_status| {
8747        TrackedStatus {
8748            index_status,
8749            worktree_status: StatusCode::Unmodified,
8750        }
8751        .into()
8752    });
8753    assert_eq!(
8754        status, expected,
8755        "expected {path} to have git status: {expected:?}"
8756    );
8757    assert_eq!(
8758        entry.is_ignored, is_ignored,
8759        "expected {path} to have is_ignored: {is_ignored}"
8760    );
8761}
8762
8763#[track_caller]
8764fn git_init(path: &Path) -> git2::Repository {
8765    let mut init_opts = RepositoryInitOptions::new();
8766    init_opts.initial_head("main");
8767    git2::Repository::init_opts(path, &init_opts).expect("Failed to initialize git repository")
8768}
8769
8770#[track_caller]
8771fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
8772    let path = path.as_ref();
8773    let mut index = repo.index().expect("Failed to get index");
8774    index.add_path(path).expect("Failed to add file");
8775    index.write().expect("Failed to write index");
8776}
8777
8778#[track_caller]
8779fn git_remove_index(path: &Path, repo: &git2::Repository) {
8780    let mut index = repo.index().expect("Failed to get index");
8781    index.remove_path(path).expect("Failed to add file");
8782    index.write().expect("Failed to write index");
8783}
8784
8785#[track_caller]
8786fn git_commit(msg: &'static str, repo: &git2::Repository) {
8787    use git2::Signature;
8788
8789    let signature = Signature::now("test", "test@zed.dev").unwrap();
8790    let oid = repo.index().unwrap().write_tree().unwrap();
8791    let tree = repo.find_tree(oid).unwrap();
8792    if let Ok(head) = repo.head() {
8793        let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
8794
8795        let parent_commit = parent_obj.as_commit().unwrap();
8796
8797        repo.commit(
8798            Some("HEAD"),
8799            &signature,
8800            &signature,
8801            msg,
8802            &tree,
8803            &[parent_commit],
8804        )
8805        .expect("Failed to commit with parent");
8806    } else {
8807        repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
8808            .expect("Failed to commit");
8809    }
8810}
8811
8812#[cfg(any())]
8813#[track_caller]
8814fn git_cherry_pick(commit: &git2::Commit<'_>, repo: &git2::Repository) {
8815    repo.cherrypick(commit, None).expect("Failed to cherrypick");
8816}
8817
8818#[track_caller]
8819fn git_stash(repo: &mut git2::Repository) {
8820    use git2::Signature;
8821
8822    let signature = Signature::now("test", "test@zed.dev").unwrap();
8823    repo.stash_save(&signature, "N/A", None)
8824        .expect("Failed to stash");
8825}
8826
8827#[track_caller]
8828fn git_reset(offset: usize, repo: &git2::Repository) {
8829    let head = repo.head().expect("Couldn't get repo head");
8830    let object = head.peel(git2::ObjectType::Commit).unwrap();
8831    let commit = object.as_commit().unwrap();
8832    let new_head = commit
8833        .parents()
8834        .inspect(|parnet| {
8835            parnet.message();
8836        })
8837        .nth(offset)
8838        .expect("Not enough history");
8839    repo.reset(new_head.as_object(), git2::ResetType::Soft, None)
8840        .expect("Could not reset");
8841}
8842
8843#[cfg(any())]
8844#[track_caller]
8845fn git_branch(name: &str, repo: &git2::Repository) {
8846    let head = repo
8847        .head()
8848        .expect("Couldn't get repo head")
8849        .peel_to_commit()
8850        .expect("HEAD is not a commit");
8851    repo.branch(name, &head, false).expect("Failed to commit");
8852}
8853
8854#[cfg(any())]
8855#[track_caller]
8856fn git_checkout(name: &str, repo: &git2::Repository) {
8857    repo.set_head(name).expect("Failed to set head");
8858    repo.checkout_head(None).expect("Failed to check out head");
8859}
8860
8861#[cfg(any())]
8862#[track_caller]
8863fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
8864    repo.statuses(None)
8865        .unwrap()
8866        .iter()
8867        .map(|status| (status.path().unwrap().to_string(), status.status()))
8868        .collect()
8869}
8870
8871#[gpui::test]
8872async fn test_find_project_path_abs(
8873    background_executor: BackgroundExecutor,
8874    cx: &mut gpui::TestAppContext,
8875) {
8876    // find_project_path should work with absolute paths
8877    init_test(cx);
8878
8879    let fs = FakeFs::new(background_executor);
8880    fs.insert_tree(
8881        path!("/root"),
8882        json!({
8883            "project1": {
8884                "file1.txt": "content1",
8885                "subdir": {
8886                    "file2.txt": "content2"
8887                }
8888            },
8889            "project2": {
8890                "file3.txt": "content3"
8891            }
8892        }),
8893    )
8894    .await;
8895
8896    let project = Project::test(
8897        fs.clone(),
8898        [
8899            path!("/root/project1").as_ref(),
8900            path!("/root/project2").as_ref(),
8901        ],
8902        cx,
8903    )
8904    .await;
8905
8906    // Make sure the worktrees are fully initialized
8907    for worktree in project.read_with(cx, |project, cx| project.worktrees(cx).collect::<Vec<_>>()) {
8908        worktree
8909            .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
8910            .await;
8911    }
8912    cx.run_until_parked();
8913
8914    let (project1_abs_path, project1_id, project2_abs_path, project2_id) =
8915        project.read_with(cx, |project, cx| {
8916            let worktrees: Vec<_> = project.worktrees(cx).collect();
8917            let abs_path1 = worktrees[0].read(cx).abs_path().to_path_buf();
8918            let id1 = worktrees[0].read(cx).id();
8919            let abs_path2 = worktrees[1].read(cx).abs_path().to_path_buf();
8920            let id2 = worktrees[1].read(cx).id();
8921            (abs_path1, id1, abs_path2, id2)
8922        });
8923
8924    project.update(cx, |project, cx| {
8925        let abs_path = project1_abs_path.join("file1.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("file1.txt"));
8929
8930        let abs_path = project1_abs_path.join("subdir").join("file2.txt");
8931        let found_path = project.find_project_path(abs_path, cx).unwrap();
8932        assert_eq!(found_path.worktree_id, project1_id);
8933        assert_eq!(found_path.path.as_ref(), Path::new("subdir/file2.txt"));
8934
8935        let abs_path = project2_abs_path.join("file3.txt");
8936        let found_path = project.find_project_path(abs_path, cx).unwrap();
8937        assert_eq!(found_path.worktree_id, project2_id);
8938        assert_eq!(found_path.path.as_ref(), Path::new("file3.txt"));
8939
8940        let abs_path = project1_abs_path.join("nonexistent.txt");
8941        let found_path = project.find_project_path(abs_path, cx);
8942        assert!(
8943            found_path.is_some(),
8944            "Should find project path for nonexistent file in worktree"
8945        );
8946
8947        // Test with an absolute path outside any worktree
8948        let abs_path = Path::new("/some/other/path");
8949        let found_path = project.find_project_path(abs_path, cx);
8950        assert!(
8951            found_path.is_none(),
8952            "Should not find project path for path outside any worktree"
8953        );
8954    });
8955}