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