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                false,
4826                None
4827            )
4828            .unwrap(),
4829            cx
4830        )
4831        .await
4832        .unwrap(),
4833        HashMap::from_iter([
4834            (separator!("dir/two.rs").to_string(), vec![6..9]),
4835            (separator!("dir/three.rs").to_string(), vec![37..40])
4836        ])
4837    );
4838
4839    let buffer_4 = project
4840        .update(cx, |project, cx| {
4841            project.open_local_buffer(path!("/dir/four.rs"), cx)
4842        })
4843        .await
4844        .unwrap();
4845    buffer_4.update(cx, |buffer, cx| {
4846        let text = "two::TWO";
4847        buffer.edit([(20..28, text), (31..43, text)], None, cx);
4848    });
4849
4850    assert_eq!(
4851        search(
4852            &project,
4853            SearchQuery::text(
4854                "TWO",
4855                false,
4856                true,
4857                false,
4858                Default::default(),
4859                Default::default(),
4860                false,
4861                None,
4862            )
4863            .unwrap(),
4864            cx
4865        )
4866        .await
4867        .unwrap(),
4868        HashMap::from_iter([
4869            (separator!("dir/two.rs").to_string(), vec![6..9]),
4870            (separator!("dir/three.rs").to_string(), vec![37..40]),
4871            (separator!("dir/four.rs").to_string(), vec![25..28, 36..39])
4872        ])
4873    );
4874}
4875
4876#[gpui::test]
4877async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
4878    init_test(cx);
4879
4880    let search_query = "file";
4881
4882    let fs = FakeFs::new(cx.executor());
4883    fs.insert_tree(
4884        path!("/dir"),
4885        json!({
4886            "one.rs": r#"// Rust file one"#,
4887            "one.ts": r#"// TypeScript file one"#,
4888            "two.rs": r#"// Rust file two"#,
4889            "two.ts": r#"// TypeScript file two"#,
4890        }),
4891    )
4892    .await;
4893    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4894
4895    assert!(
4896        search(
4897            &project,
4898            SearchQuery::text(
4899                search_query,
4900                false,
4901                true,
4902                false,
4903                PathMatcher::new(&["*.odd".to_owned()]).unwrap(),
4904                Default::default(),
4905                false,
4906                None
4907            )
4908            .unwrap(),
4909            cx
4910        )
4911        .await
4912        .unwrap()
4913        .is_empty(),
4914        "If no inclusions match, no files should be returned"
4915    );
4916
4917    assert_eq!(
4918        search(
4919            &project,
4920            SearchQuery::text(
4921                search_query,
4922                false,
4923                true,
4924                false,
4925                PathMatcher::new(&["*.rs".to_owned()]).unwrap(),
4926                Default::default(),
4927                false,
4928                None
4929            )
4930            .unwrap(),
4931            cx
4932        )
4933        .await
4934        .unwrap(),
4935        HashMap::from_iter([
4936            (separator!("dir/one.rs").to_string(), vec![8..12]),
4937            (separator!("dir/two.rs").to_string(), vec![8..12]),
4938        ]),
4939        "Rust only search should give only Rust files"
4940    );
4941
4942    assert_eq!(
4943        search(
4944            &project,
4945            SearchQuery::text(
4946                search_query,
4947                false,
4948                true,
4949                false,
4950                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
4951                Default::default(),
4952                false,
4953                None,
4954            )
4955            .unwrap(),
4956            cx
4957        )
4958        .await
4959        .unwrap(),
4960        HashMap::from_iter([
4961            (separator!("dir/one.ts").to_string(), vec![14..18]),
4962            (separator!("dir/two.ts").to_string(), vec![14..18]),
4963        ]),
4964        "TypeScript only search should give only TypeScript files, even if other inclusions don't match anything"
4965    );
4966
4967    assert_eq!(
4968        search(
4969            &project,
4970            SearchQuery::text(
4971                search_query,
4972                false,
4973                true,
4974                false,
4975                PathMatcher::new(&["*.rs".to_owned(), "*.ts".to_owned(), "*.odd".to_owned()])
4976                    .unwrap(),
4977                Default::default(),
4978                false,
4979                None,
4980            )
4981            .unwrap(),
4982            cx
4983        )
4984        .await
4985        .unwrap(),
4986        HashMap::from_iter([
4987            (separator!("dir/two.ts").to_string(), vec![14..18]),
4988            (separator!("dir/one.rs").to_string(), vec![8..12]),
4989            (separator!("dir/one.ts").to_string(), vec![14..18]),
4990            (separator!("dir/two.rs").to_string(), vec![8..12]),
4991        ]),
4992        "Rust and typescript search should give both Rust and TypeScript files, even if other inclusions don't match anything"
4993    );
4994}
4995
4996#[gpui::test]
4997async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
4998    init_test(cx);
4999
5000    let search_query = "file";
5001
5002    let fs = FakeFs::new(cx.executor());
5003    fs.insert_tree(
5004        path!("/dir"),
5005        json!({
5006            "one.rs": r#"// Rust file one"#,
5007            "one.ts": r#"// TypeScript file one"#,
5008            "two.rs": r#"// Rust file two"#,
5009            "two.ts": r#"// TypeScript file two"#,
5010        }),
5011    )
5012    .await;
5013    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
5014
5015    assert_eq!(
5016        search(
5017            &project,
5018            SearchQuery::text(
5019                search_query,
5020                false,
5021                true,
5022                false,
5023                Default::default(),
5024                PathMatcher::new(&["*.odd".to_owned()]).unwrap(),
5025                false,
5026                None,
5027            )
5028            .unwrap(),
5029            cx
5030        )
5031        .await
5032        .unwrap(),
5033        HashMap::from_iter([
5034            (separator!("dir/one.rs").to_string(), vec![8..12]),
5035            (separator!("dir/one.ts").to_string(), vec![14..18]),
5036            (separator!("dir/two.rs").to_string(), vec![8..12]),
5037            (separator!("dir/two.ts").to_string(), vec![14..18]),
5038        ]),
5039        "If no exclusions match, all files should be returned"
5040    );
5041
5042    assert_eq!(
5043        search(
5044            &project,
5045            SearchQuery::text(
5046                search_query,
5047                false,
5048                true,
5049                false,
5050                Default::default(),
5051                PathMatcher::new(&["*.rs".to_owned()]).unwrap(),
5052                false,
5053                None,
5054            )
5055            .unwrap(),
5056            cx
5057        )
5058        .await
5059        .unwrap(),
5060        HashMap::from_iter([
5061            (separator!("dir/one.ts").to_string(), vec![14..18]),
5062            (separator!("dir/two.ts").to_string(), vec![14..18]),
5063        ]),
5064        "Rust exclusion search should give only TypeScript files"
5065    );
5066
5067    assert_eq!(
5068        search(
5069            &project,
5070            SearchQuery::text(
5071                search_query,
5072                false,
5073                true,
5074                false,
5075                Default::default(),
5076                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
5077                false,
5078                None,
5079            )
5080            .unwrap(),
5081            cx
5082        )
5083        .await
5084        .unwrap(),
5085        HashMap::from_iter([
5086            (separator!("dir/one.rs").to_string(), vec![8..12]),
5087            (separator!("dir/two.rs").to_string(), vec![8..12]),
5088        ]),
5089        "TypeScript exclusion search should give only Rust files, even if other exclusions don't match anything"
5090    );
5091
5092    assert!(
5093        search(
5094            &project,
5095            SearchQuery::text(
5096                search_query,
5097                false,
5098                true,
5099                false,
5100                Default::default(),
5101                PathMatcher::new(&["*.rs".to_owned(), "*.ts".to_owned(), "*.odd".to_owned()])
5102                    .unwrap(),
5103                false,
5104                None,
5105            )
5106            .unwrap(),
5107            cx
5108        )
5109        .await
5110        .unwrap()
5111        .is_empty(),
5112        "Rust and typescript exclusion should give no files, even if other exclusions don't match anything"
5113    );
5114}
5115
5116#[gpui::test]
5117async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContext) {
5118    init_test(cx);
5119
5120    let search_query = "file";
5121
5122    let fs = FakeFs::new(cx.executor());
5123    fs.insert_tree(
5124        path!("/dir"),
5125        json!({
5126            "one.rs": r#"// Rust file one"#,
5127            "one.ts": r#"// TypeScript file one"#,
5128            "two.rs": r#"// Rust file two"#,
5129            "two.ts": r#"// TypeScript file two"#,
5130        }),
5131    )
5132    .await;
5133    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
5134
5135    assert!(
5136        search(
5137            &project,
5138            SearchQuery::text(
5139                search_query,
5140                false,
5141                true,
5142                false,
5143                PathMatcher::new(&["*.odd".to_owned()]).unwrap(),
5144                PathMatcher::new(&["*.odd".to_owned()]).unwrap(),
5145                false,
5146                None,
5147            )
5148            .unwrap(),
5149            cx
5150        )
5151        .await
5152        .unwrap()
5153        .is_empty(),
5154        "If both no exclusions and inclusions match, exclusions should win and return nothing"
5155    );
5156
5157    assert!(
5158        search(
5159            &project,
5160            SearchQuery::text(
5161                search_query,
5162                false,
5163                true,
5164                false,
5165                PathMatcher::new(&["*.ts".to_owned()]).unwrap(),
5166                PathMatcher::new(&["*.ts".to_owned()]).unwrap(),
5167                false,
5168                None,
5169            )
5170            .unwrap(),
5171            cx
5172        )
5173        .await
5174        .unwrap()
5175        .is_empty(),
5176        "If both TypeScript exclusions and inclusions match, exclusions should win and return nothing files."
5177    );
5178
5179    assert!(
5180        search(
5181            &project,
5182            SearchQuery::text(
5183                search_query,
5184                false,
5185                true,
5186                false,
5187                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
5188                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
5189                false,
5190                None,
5191            )
5192            .unwrap(),
5193            cx
5194        )
5195        .await
5196        .unwrap()
5197        .is_empty(),
5198        "Non-matching inclusions and exclusions should not change that."
5199    );
5200
5201    assert_eq!(
5202        search(
5203            &project,
5204            SearchQuery::text(
5205                search_query,
5206                false,
5207                true,
5208                false,
5209                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
5210                PathMatcher::new(&["*.rs".to_owned(), "*.odd".to_owned()]).unwrap(),
5211                false,
5212                None,
5213            )
5214            .unwrap(),
5215            cx
5216        )
5217        .await
5218        .unwrap(),
5219        HashMap::from_iter([
5220            (separator!("dir/one.ts").to_string(), vec![14..18]),
5221            (separator!("dir/two.ts").to_string(), vec![14..18]),
5222        ]),
5223        "Non-intersecting TypeScript inclusions and Rust exclusions should return TypeScript files"
5224    );
5225}
5226
5227#[gpui::test]
5228async fn test_search_multiple_worktrees_with_inclusions(cx: &mut gpui::TestAppContext) {
5229    init_test(cx);
5230
5231    let fs = FakeFs::new(cx.executor());
5232    fs.insert_tree(
5233        path!("/worktree-a"),
5234        json!({
5235            "haystack.rs": r#"// NEEDLE"#,
5236            "haystack.ts": r#"// NEEDLE"#,
5237        }),
5238    )
5239    .await;
5240    fs.insert_tree(
5241        path!("/worktree-b"),
5242        json!({
5243            "haystack.rs": r#"// NEEDLE"#,
5244            "haystack.ts": r#"// NEEDLE"#,
5245        }),
5246    )
5247    .await;
5248
5249    let project = Project::test(
5250        fs.clone(),
5251        [path!("/worktree-a").as_ref(), path!("/worktree-b").as_ref()],
5252        cx,
5253    )
5254    .await;
5255
5256    assert_eq!(
5257        search(
5258            &project,
5259            SearchQuery::text(
5260                "NEEDLE",
5261                false,
5262                true,
5263                false,
5264                PathMatcher::new(&["worktree-a/*.rs".to_owned()]).unwrap(),
5265                Default::default(),
5266                true,
5267                None,
5268            )
5269            .unwrap(),
5270            cx
5271        )
5272        .await
5273        .unwrap(),
5274        HashMap::from_iter([(separator!("worktree-a/haystack.rs").to_string(), vec![3..9])]),
5275        "should only return results from included worktree"
5276    );
5277    assert_eq!(
5278        search(
5279            &project,
5280            SearchQuery::text(
5281                "NEEDLE",
5282                false,
5283                true,
5284                false,
5285                PathMatcher::new(&["worktree-b/*.rs".to_owned()]).unwrap(),
5286                Default::default(),
5287                true,
5288                None,
5289            )
5290            .unwrap(),
5291            cx
5292        )
5293        .await
5294        .unwrap(),
5295        HashMap::from_iter([(separator!("worktree-b/haystack.rs").to_string(), vec![3..9])]),
5296        "should only return results from included worktree"
5297    );
5298
5299    assert_eq!(
5300        search(
5301            &project,
5302            SearchQuery::text(
5303                "NEEDLE",
5304                false,
5305                true,
5306                false,
5307                PathMatcher::new(&["*.ts".to_owned()]).unwrap(),
5308                Default::default(),
5309                false,
5310                None,
5311            )
5312            .unwrap(),
5313            cx
5314        )
5315        .await
5316        .unwrap(),
5317        HashMap::from_iter([
5318            (separator!("worktree-a/haystack.ts").to_string(), vec![3..9]),
5319            (separator!("worktree-b/haystack.ts").to_string(), vec![3..9])
5320        ]),
5321        "should return results from both worktrees"
5322    );
5323}
5324
5325#[gpui::test]
5326async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) {
5327    init_test(cx);
5328
5329    let fs = FakeFs::new(cx.background_executor.clone());
5330    fs.insert_tree(
5331        path!("/dir"),
5332        json!({
5333            ".git": {},
5334            ".gitignore": "**/target\n/node_modules\n",
5335            "target": {
5336                "index.txt": "index_key:index_value"
5337            },
5338            "node_modules": {
5339                "eslint": {
5340                    "index.ts": "const eslint_key = 'eslint value'",
5341                    "package.json": r#"{ "some_key": "some value" }"#,
5342                },
5343                "prettier": {
5344                    "index.ts": "const prettier_key = 'prettier value'",
5345                    "package.json": r#"{ "other_key": "other value" }"#,
5346                },
5347            },
5348            "package.json": r#"{ "main_key": "main value" }"#,
5349        }),
5350    )
5351    .await;
5352    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
5353
5354    let query = "key";
5355    assert_eq!(
5356        search(
5357            &project,
5358            SearchQuery::text(
5359                query,
5360                false,
5361                false,
5362                false,
5363                Default::default(),
5364                Default::default(),
5365                false,
5366                None,
5367            )
5368            .unwrap(),
5369            cx
5370        )
5371        .await
5372        .unwrap(),
5373        HashMap::from_iter([(separator!("dir/package.json").to_string(), vec![8..11])]),
5374        "Only one non-ignored file should have the query"
5375    );
5376
5377    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
5378    assert_eq!(
5379        search(
5380            &project,
5381            SearchQuery::text(
5382                query,
5383                false,
5384                false,
5385                true,
5386                Default::default(),
5387                Default::default(),
5388                false,
5389                None,
5390            )
5391            .unwrap(),
5392            cx
5393        )
5394        .await
5395        .unwrap(),
5396        HashMap::from_iter([
5397            (separator!("dir/package.json").to_string(), vec![8..11]),
5398            (separator!("dir/target/index.txt").to_string(), vec![6..9]),
5399            (
5400                separator!("dir/node_modules/prettier/package.json").to_string(),
5401                vec![9..12]
5402            ),
5403            (
5404                separator!("dir/node_modules/prettier/index.ts").to_string(),
5405                vec![15..18]
5406            ),
5407            (
5408                separator!("dir/node_modules/eslint/index.ts").to_string(),
5409                vec![13..16]
5410            ),
5411            (
5412                separator!("dir/node_modules/eslint/package.json").to_string(),
5413                vec![8..11]
5414            ),
5415        ]),
5416        "Unrestricted search with ignored directories should find every file with the query"
5417    );
5418
5419    let files_to_include = PathMatcher::new(&["node_modules/prettier/**".to_owned()]).unwrap();
5420    let files_to_exclude = PathMatcher::new(&["*.ts".to_owned()]).unwrap();
5421    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
5422    assert_eq!(
5423        search(
5424            &project,
5425            SearchQuery::text(
5426                query,
5427                false,
5428                false,
5429                true,
5430                files_to_include,
5431                files_to_exclude,
5432                false,
5433                None,
5434            )
5435            .unwrap(),
5436            cx
5437        )
5438        .await
5439        .unwrap(),
5440        HashMap::from_iter([(
5441            separator!("dir/node_modules/prettier/package.json").to_string(),
5442            vec![9..12]
5443        )]),
5444        "With search including ignored prettier directory and excluding TS files, only one file should be found"
5445    );
5446}
5447
5448#[gpui::test]
5449async fn test_search_with_unicode(cx: &mut gpui::TestAppContext) {
5450    init_test(cx);
5451
5452    let fs = FakeFs::new(cx.executor());
5453    fs.insert_tree(
5454        path!("/dir"),
5455        json!({
5456            "one.rs": "// ПРИВЕТ? привет!",
5457            "two.rs": "// ПРИВЕТ.",
5458            "three.rs": "// привет",
5459        }),
5460    )
5461    .await;
5462    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
5463
5464    let unicode_case_sensitive_query = SearchQuery::text(
5465        "привет",
5466        false,
5467        true,
5468        false,
5469        Default::default(),
5470        Default::default(),
5471        false,
5472        None,
5473    );
5474    assert_matches!(unicode_case_sensitive_query, Ok(SearchQuery::Text { .. }));
5475    assert_eq!(
5476        search(&project, unicode_case_sensitive_query.unwrap(), cx)
5477            .await
5478            .unwrap(),
5479        HashMap::from_iter([
5480            (separator!("dir/one.rs").to_string(), vec![17..29]),
5481            (separator!("dir/three.rs").to_string(), vec![3..15]),
5482        ])
5483    );
5484
5485    let unicode_case_insensitive_query = SearchQuery::text(
5486        "привет",
5487        false,
5488        false,
5489        false,
5490        Default::default(),
5491        Default::default(),
5492        false,
5493        None,
5494    );
5495    assert_matches!(
5496        unicode_case_insensitive_query,
5497        Ok(SearchQuery::Regex { .. })
5498    );
5499    assert_eq!(
5500        search(&project, unicode_case_insensitive_query.unwrap(), cx)
5501            .await
5502            .unwrap(),
5503        HashMap::from_iter([
5504            (separator!("dir/one.rs").to_string(), vec![3..15, 17..29]),
5505            (separator!("dir/two.rs").to_string(), vec![3..15]),
5506            (separator!("dir/three.rs").to_string(), vec![3..15]),
5507        ])
5508    );
5509
5510    assert_eq!(
5511        search(
5512            &project,
5513            SearchQuery::text(
5514                "привет.",
5515                false,
5516                false,
5517                false,
5518                Default::default(),
5519                Default::default(),
5520                false,
5521                None,
5522            )
5523            .unwrap(),
5524            cx
5525        )
5526        .await
5527        .unwrap(),
5528        HashMap::from_iter([(separator!("dir/two.rs").to_string(), vec![3..16]),])
5529    );
5530}
5531
5532#[gpui::test]
5533async fn test_create_entry(cx: &mut gpui::TestAppContext) {
5534    init_test(cx);
5535
5536    let fs = FakeFs::new(cx.executor().clone());
5537    fs.insert_tree(
5538        "/one/two",
5539        json!({
5540            "three": {
5541                "a.txt": "",
5542                "four": {}
5543            },
5544            "c.rs": ""
5545        }),
5546    )
5547    .await;
5548
5549    let project = Project::test(fs.clone(), ["/one/two/three".as_ref()], cx).await;
5550    project
5551        .update(cx, |project, cx| {
5552            let id = project.worktrees(cx).next().unwrap().read(cx).id();
5553            project.create_entry((id, "b.."), true, cx)
5554        })
5555        .await
5556        .unwrap()
5557        .to_included()
5558        .unwrap();
5559
5560    // Can't create paths outside the project
5561    let result = project
5562        .update(cx, |project, cx| {
5563            let id = project.worktrees(cx).next().unwrap().read(cx).id();
5564            project.create_entry((id, "../../boop"), true, cx)
5565        })
5566        .await;
5567    assert!(result.is_err());
5568
5569    // Can't create paths with '..'
5570    let result = project
5571        .update(cx, |project, cx| {
5572            let id = project.worktrees(cx).next().unwrap().read(cx).id();
5573            project.create_entry((id, "four/../beep"), true, cx)
5574        })
5575        .await;
5576    assert!(result.is_err());
5577
5578    assert_eq!(
5579        fs.paths(true),
5580        vec![
5581            PathBuf::from(path!("/")),
5582            PathBuf::from(path!("/one")),
5583            PathBuf::from(path!("/one/two")),
5584            PathBuf::from(path!("/one/two/c.rs")),
5585            PathBuf::from(path!("/one/two/three")),
5586            PathBuf::from(path!("/one/two/three/a.txt")),
5587            PathBuf::from(path!("/one/two/three/b..")),
5588            PathBuf::from(path!("/one/two/three/four")),
5589        ]
5590    );
5591
5592    // And we cannot open buffers with '..'
5593    let result = project
5594        .update(cx, |project, cx| {
5595            let id = project.worktrees(cx).next().unwrap().read(cx).id();
5596            project.open_buffer((id, "../c.rs"), cx)
5597        })
5598        .await;
5599    assert!(result.is_err())
5600}
5601
5602#[gpui::test]
5603async fn test_multiple_language_server_hovers(cx: &mut gpui::TestAppContext) {
5604    init_test(cx);
5605
5606    let fs = FakeFs::new(cx.executor());
5607    fs.insert_tree(
5608        path!("/dir"),
5609        json!({
5610            "a.tsx": "a",
5611        }),
5612    )
5613    .await;
5614
5615    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
5616
5617    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
5618    language_registry.add(tsx_lang());
5619    let language_server_names = [
5620        "TypeScriptServer",
5621        "TailwindServer",
5622        "ESLintServer",
5623        "NoHoverCapabilitiesServer",
5624    ];
5625    let mut language_servers = [
5626        language_registry.register_fake_lsp(
5627            "tsx",
5628            FakeLspAdapter {
5629                name: language_server_names[0],
5630                capabilities: lsp::ServerCapabilities {
5631                    hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
5632                    ..lsp::ServerCapabilities::default()
5633                },
5634                ..FakeLspAdapter::default()
5635            },
5636        ),
5637        language_registry.register_fake_lsp(
5638            "tsx",
5639            FakeLspAdapter {
5640                name: language_server_names[1],
5641                capabilities: lsp::ServerCapabilities {
5642                    hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
5643                    ..lsp::ServerCapabilities::default()
5644                },
5645                ..FakeLspAdapter::default()
5646            },
5647        ),
5648        language_registry.register_fake_lsp(
5649            "tsx",
5650            FakeLspAdapter {
5651                name: language_server_names[2],
5652                capabilities: lsp::ServerCapabilities {
5653                    hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
5654                    ..lsp::ServerCapabilities::default()
5655                },
5656                ..FakeLspAdapter::default()
5657            },
5658        ),
5659        language_registry.register_fake_lsp(
5660            "tsx",
5661            FakeLspAdapter {
5662                name: language_server_names[3],
5663                capabilities: lsp::ServerCapabilities {
5664                    hover_provider: None,
5665                    ..lsp::ServerCapabilities::default()
5666                },
5667                ..FakeLspAdapter::default()
5668            },
5669        ),
5670    ];
5671
5672    let (buffer, _handle) = project
5673        .update(cx, |p, cx| {
5674            p.open_local_buffer_with_lsp(path!("/dir/a.tsx"), cx)
5675        })
5676        .await
5677        .unwrap();
5678    cx.executor().run_until_parked();
5679
5680    let mut servers_with_hover_requests = HashMap::default();
5681    for i in 0..language_server_names.len() {
5682        let new_server = language_servers[i].next().await.unwrap_or_else(|| {
5683            panic!(
5684                "Failed to get language server #{i} with name {}",
5685                &language_server_names[i]
5686            )
5687        });
5688        let new_server_name = new_server.server.name();
5689        assert!(
5690            !servers_with_hover_requests.contains_key(&new_server_name),
5691            "Unexpected: initialized server with the same name twice. Name: `{new_server_name}`"
5692        );
5693        match new_server_name.as_ref() {
5694            "TailwindServer" | "TypeScriptServer" => {
5695                servers_with_hover_requests.insert(
5696                    new_server_name.clone(),
5697                    new_server.set_request_handler::<lsp::request::HoverRequest, _, _>(
5698                        move |_, _| {
5699                            let name = new_server_name.clone();
5700                            async move {
5701                                Ok(Some(lsp::Hover {
5702                                    contents: lsp::HoverContents::Scalar(
5703                                        lsp::MarkedString::String(format!("{name} hover")),
5704                                    ),
5705                                    range: None,
5706                                }))
5707                            }
5708                        },
5709                    ),
5710                );
5711            }
5712            "ESLintServer" => {
5713                servers_with_hover_requests.insert(
5714                    new_server_name,
5715                    new_server.set_request_handler::<lsp::request::HoverRequest, _, _>(
5716                        |_, _| async move { Ok(None) },
5717                    ),
5718                );
5719            }
5720            "NoHoverCapabilitiesServer" => {
5721                let _never_handled = new_server
5722                    .set_request_handler::<lsp::request::HoverRequest, _, _>(|_, _| async move {
5723                        panic!(
5724                            "Should not call for hovers server with no corresponding capabilities"
5725                        )
5726                    });
5727            }
5728            unexpected => panic!("Unexpected server name: {unexpected}"),
5729        }
5730    }
5731
5732    let hover_task = project.update(cx, |project, cx| {
5733        project.hover(&buffer, Point::new(0, 0), cx)
5734    });
5735    let _: Vec<()> = futures::future::join_all(servers_with_hover_requests.into_values().map(
5736        |mut hover_request| async move {
5737            hover_request
5738                .next()
5739                .await
5740                .expect("All hover requests should have been triggered")
5741        },
5742    ))
5743    .await;
5744    assert_eq!(
5745        vec!["TailwindServer hover", "TypeScriptServer hover"],
5746        hover_task
5747            .await
5748            .into_iter()
5749            .map(|hover| hover.contents.iter().map(|block| &block.text).join("|"))
5750            .sorted()
5751            .collect::<Vec<_>>(),
5752        "Should receive hover responses from all related servers with hover capabilities"
5753    );
5754}
5755
5756#[gpui::test]
5757async fn test_hovers_with_empty_parts(cx: &mut gpui::TestAppContext) {
5758    init_test(cx);
5759
5760    let fs = FakeFs::new(cx.executor());
5761    fs.insert_tree(
5762        path!("/dir"),
5763        json!({
5764            "a.ts": "a",
5765        }),
5766    )
5767    .await;
5768
5769    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
5770
5771    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
5772    language_registry.add(typescript_lang());
5773    let mut fake_language_servers = language_registry.register_fake_lsp(
5774        "TypeScript",
5775        FakeLspAdapter {
5776            capabilities: lsp::ServerCapabilities {
5777                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
5778                ..lsp::ServerCapabilities::default()
5779            },
5780            ..FakeLspAdapter::default()
5781        },
5782    );
5783
5784    let (buffer, _handle) = project
5785        .update(cx, |p, cx| {
5786            p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
5787        })
5788        .await
5789        .unwrap();
5790    cx.executor().run_until_parked();
5791
5792    let fake_server = fake_language_servers
5793        .next()
5794        .await
5795        .expect("failed to get the language server");
5796
5797    let mut request_handled = fake_server.set_request_handler::<lsp::request::HoverRequest, _, _>(
5798        move |_, _| async move {
5799            Ok(Some(lsp::Hover {
5800                contents: lsp::HoverContents::Array(vec![
5801                    lsp::MarkedString::String("".to_string()),
5802                    lsp::MarkedString::String("      ".to_string()),
5803                    lsp::MarkedString::String("\n\n\n".to_string()),
5804                ]),
5805                range: None,
5806            }))
5807        },
5808    );
5809
5810    let hover_task = project.update(cx, |project, cx| {
5811        project.hover(&buffer, Point::new(0, 0), cx)
5812    });
5813    let () = request_handled
5814        .next()
5815        .await
5816        .expect("All hover requests should have been triggered");
5817    assert_eq!(
5818        Vec::<String>::new(),
5819        hover_task
5820            .await
5821            .into_iter()
5822            .map(|hover| hover.contents.iter().map(|block| &block.text).join("|"))
5823            .sorted()
5824            .collect::<Vec<_>>(),
5825        "Empty hover parts should be ignored"
5826    );
5827}
5828
5829#[gpui::test]
5830async fn test_code_actions_only_kinds(cx: &mut gpui::TestAppContext) {
5831    init_test(cx);
5832
5833    let fs = FakeFs::new(cx.executor());
5834    fs.insert_tree(
5835        path!("/dir"),
5836        json!({
5837            "a.ts": "a",
5838        }),
5839    )
5840    .await;
5841
5842    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
5843
5844    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
5845    language_registry.add(typescript_lang());
5846    let mut fake_language_servers = language_registry.register_fake_lsp(
5847        "TypeScript",
5848        FakeLspAdapter {
5849            capabilities: lsp::ServerCapabilities {
5850                code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
5851                ..lsp::ServerCapabilities::default()
5852            },
5853            ..FakeLspAdapter::default()
5854        },
5855    );
5856
5857    let (buffer, _handle) = project
5858        .update(cx, |p, cx| {
5859            p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
5860        })
5861        .await
5862        .unwrap();
5863    cx.executor().run_until_parked();
5864
5865    let fake_server = fake_language_servers
5866        .next()
5867        .await
5868        .expect("failed to get the language server");
5869
5870    let mut request_handled = fake_server
5871        .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |_, _| async move {
5872            Ok(Some(vec![
5873                lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
5874                    title: "organize imports".to_string(),
5875                    kind: Some(CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
5876                    ..lsp::CodeAction::default()
5877                }),
5878                lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
5879                    title: "fix code".to_string(),
5880                    kind: Some(CodeActionKind::SOURCE_FIX_ALL),
5881                    ..lsp::CodeAction::default()
5882                }),
5883            ]))
5884        });
5885
5886    let code_actions_task = project.update(cx, |project, cx| {
5887        project.code_actions(
5888            &buffer,
5889            0..buffer.read(cx).len(),
5890            Some(vec![CodeActionKind::SOURCE_ORGANIZE_IMPORTS]),
5891            cx,
5892        )
5893    });
5894
5895    let () = request_handled
5896        .next()
5897        .await
5898        .expect("The code action request should have been triggered");
5899
5900    let code_actions = code_actions_task.await.unwrap();
5901    assert_eq!(code_actions.len(), 1);
5902    assert_eq!(
5903        code_actions[0].lsp_action.action_kind(),
5904        Some(CodeActionKind::SOURCE_ORGANIZE_IMPORTS)
5905    );
5906}
5907
5908#[gpui::test]
5909async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) {
5910    init_test(cx);
5911
5912    let fs = FakeFs::new(cx.executor());
5913    fs.insert_tree(
5914        path!("/dir"),
5915        json!({
5916            "a.tsx": "a",
5917        }),
5918    )
5919    .await;
5920
5921    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
5922
5923    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
5924    language_registry.add(tsx_lang());
5925    let language_server_names = [
5926        "TypeScriptServer",
5927        "TailwindServer",
5928        "ESLintServer",
5929        "NoActionsCapabilitiesServer",
5930    ];
5931
5932    let mut language_server_rxs = [
5933        language_registry.register_fake_lsp(
5934            "tsx",
5935            FakeLspAdapter {
5936                name: language_server_names[0],
5937                capabilities: lsp::ServerCapabilities {
5938                    code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
5939                    ..lsp::ServerCapabilities::default()
5940                },
5941                ..FakeLspAdapter::default()
5942            },
5943        ),
5944        language_registry.register_fake_lsp(
5945            "tsx",
5946            FakeLspAdapter {
5947                name: language_server_names[1],
5948                capabilities: lsp::ServerCapabilities {
5949                    code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
5950                    ..lsp::ServerCapabilities::default()
5951                },
5952                ..FakeLspAdapter::default()
5953            },
5954        ),
5955        language_registry.register_fake_lsp(
5956            "tsx",
5957            FakeLspAdapter {
5958                name: language_server_names[2],
5959                capabilities: lsp::ServerCapabilities {
5960                    code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
5961                    ..lsp::ServerCapabilities::default()
5962                },
5963                ..FakeLspAdapter::default()
5964            },
5965        ),
5966        language_registry.register_fake_lsp(
5967            "tsx",
5968            FakeLspAdapter {
5969                name: language_server_names[3],
5970                capabilities: lsp::ServerCapabilities {
5971                    code_action_provider: None,
5972                    ..lsp::ServerCapabilities::default()
5973                },
5974                ..FakeLspAdapter::default()
5975            },
5976        ),
5977    ];
5978
5979    let (buffer, _handle) = project
5980        .update(cx, |p, cx| {
5981            p.open_local_buffer_with_lsp(path!("/dir/a.tsx"), cx)
5982        })
5983        .await
5984        .unwrap();
5985    cx.executor().run_until_parked();
5986
5987    let mut servers_with_actions_requests = HashMap::default();
5988    for i in 0..language_server_names.len() {
5989        let new_server = language_server_rxs[i].next().await.unwrap_or_else(|| {
5990            panic!(
5991                "Failed to get language server #{i} with name {}",
5992                &language_server_names[i]
5993            )
5994        });
5995        let new_server_name = new_server.server.name();
5996
5997        assert!(
5998            !servers_with_actions_requests.contains_key(&new_server_name),
5999            "Unexpected: initialized server with the same name twice. Name: `{new_server_name}`"
6000        );
6001        match new_server_name.0.as_ref() {
6002            "TailwindServer" | "TypeScriptServer" => {
6003                servers_with_actions_requests.insert(
6004                    new_server_name.clone(),
6005                    new_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
6006                        move |_, _| {
6007                            let name = new_server_name.clone();
6008                            async move {
6009                                Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
6010                                    lsp::CodeAction {
6011                                        title: format!("{name} code action"),
6012                                        ..lsp::CodeAction::default()
6013                                    },
6014                                )]))
6015                            }
6016                        },
6017                    ),
6018                );
6019            }
6020            "ESLintServer" => {
6021                servers_with_actions_requests.insert(
6022                    new_server_name,
6023                    new_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
6024                        |_, _| async move { Ok(None) },
6025                    ),
6026                );
6027            }
6028            "NoActionsCapabilitiesServer" => {
6029                let _never_handled = new_server
6030                    .set_request_handler::<lsp::request::CodeActionRequest, _, _>(|_, _| async move {
6031                        panic!(
6032                            "Should not call for code actions server with no corresponding capabilities"
6033                        )
6034                    });
6035            }
6036            unexpected => panic!("Unexpected server name: {unexpected}"),
6037        }
6038    }
6039
6040    let code_actions_task = project.update(cx, |project, cx| {
6041        project.code_actions(&buffer, 0..buffer.read(cx).len(), None, cx)
6042    });
6043
6044    // cx.run_until_parked();
6045    let _: Vec<()> = futures::future::join_all(servers_with_actions_requests.into_values().map(
6046        |mut code_actions_request| async move {
6047            code_actions_request
6048                .next()
6049                .await
6050                .expect("All code actions requests should have been triggered")
6051        },
6052    ))
6053    .await;
6054    assert_eq!(
6055        vec!["TailwindServer code action", "TypeScriptServer code action"],
6056        code_actions_task
6057            .await
6058            .unwrap()
6059            .into_iter()
6060            .map(|code_action| code_action.lsp_action.title().to_owned())
6061            .sorted()
6062            .collect::<Vec<_>>(),
6063        "Should receive code actions responses from all related servers with hover capabilities"
6064    );
6065}
6066
6067#[gpui::test]
6068async fn test_reordering_worktrees(cx: &mut gpui::TestAppContext) {
6069    init_test(cx);
6070
6071    let fs = FakeFs::new(cx.executor());
6072    fs.insert_tree(
6073        "/dir",
6074        json!({
6075            "a.rs": "let a = 1;",
6076            "b.rs": "let b = 2;",
6077            "c.rs": "let c = 2;",
6078        }),
6079    )
6080    .await;
6081
6082    let project = Project::test(
6083        fs,
6084        [
6085            "/dir/a.rs".as_ref(),
6086            "/dir/b.rs".as_ref(),
6087            "/dir/c.rs".as_ref(),
6088        ],
6089        cx,
6090    )
6091    .await;
6092
6093    // check the initial state and get the worktrees
6094    let (worktree_a, worktree_b, worktree_c) = project.update(cx, |project, cx| {
6095        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
6096        assert_eq!(worktrees.len(), 3);
6097
6098        let worktree_a = worktrees[0].read(cx);
6099        let worktree_b = worktrees[1].read(cx);
6100        let worktree_c = worktrees[2].read(cx);
6101
6102        // check they start in the right order
6103        assert_eq!(worktree_a.abs_path().to_str().unwrap(), "/dir/a.rs");
6104        assert_eq!(worktree_b.abs_path().to_str().unwrap(), "/dir/b.rs");
6105        assert_eq!(worktree_c.abs_path().to_str().unwrap(), "/dir/c.rs");
6106
6107        (
6108            worktrees[0].clone(),
6109            worktrees[1].clone(),
6110            worktrees[2].clone(),
6111        )
6112    });
6113
6114    // move first worktree to after the second
6115    // [a, b, c] -> [b, a, c]
6116    project
6117        .update(cx, |project, cx| {
6118            let first = worktree_a.read(cx);
6119            let second = worktree_b.read(cx);
6120            project.move_worktree(first.id(), second.id(), cx)
6121        })
6122        .expect("moving first after second");
6123
6124    // check the state after moving
6125    project.update(cx, |project, cx| {
6126        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
6127        assert_eq!(worktrees.len(), 3);
6128
6129        let first = worktrees[0].read(cx);
6130        let second = worktrees[1].read(cx);
6131        let third = worktrees[2].read(cx);
6132
6133        // check they are now in the right order
6134        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/b.rs");
6135        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/a.rs");
6136        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
6137    });
6138
6139    // move the second worktree to before the first
6140    // [b, a, c] -> [a, b, c]
6141    project
6142        .update(cx, |project, cx| {
6143            let second = worktree_a.read(cx);
6144            let first = worktree_b.read(cx);
6145            project.move_worktree(first.id(), second.id(), cx)
6146        })
6147        .expect("moving second before first");
6148
6149    // check the state after moving
6150    project.update(cx, |project, cx| {
6151        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
6152        assert_eq!(worktrees.len(), 3);
6153
6154        let first = worktrees[0].read(cx);
6155        let second = worktrees[1].read(cx);
6156        let third = worktrees[2].read(cx);
6157
6158        // check they are now in the right order
6159        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
6160        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/b.rs");
6161        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
6162    });
6163
6164    // move the second worktree to after the third
6165    // [a, b, c] -> [a, c, b]
6166    project
6167        .update(cx, |project, cx| {
6168            let second = worktree_b.read(cx);
6169            let third = worktree_c.read(cx);
6170            project.move_worktree(second.id(), third.id(), cx)
6171        })
6172        .expect("moving second after third");
6173
6174    // check the state after moving
6175    project.update(cx, |project, cx| {
6176        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
6177        assert_eq!(worktrees.len(), 3);
6178
6179        let first = worktrees[0].read(cx);
6180        let second = worktrees[1].read(cx);
6181        let third = worktrees[2].read(cx);
6182
6183        // check they are now in the right order
6184        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
6185        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/c.rs");
6186        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/b.rs");
6187    });
6188
6189    // move the third worktree to before the second
6190    // [a, c, b] -> [a, b, c]
6191    project
6192        .update(cx, |project, cx| {
6193            let third = worktree_c.read(cx);
6194            let second = worktree_b.read(cx);
6195            project.move_worktree(third.id(), second.id(), cx)
6196        })
6197        .expect("moving third before second");
6198
6199    // check the state after moving
6200    project.update(cx, |project, cx| {
6201        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
6202        assert_eq!(worktrees.len(), 3);
6203
6204        let first = worktrees[0].read(cx);
6205        let second = worktrees[1].read(cx);
6206        let third = worktrees[2].read(cx);
6207
6208        // check they are now in the right order
6209        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
6210        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/b.rs");
6211        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
6212    });
6213
6214    // move the first worktree to after the third
6215    // [a, b, c] -> [b, c, a]
6216    project
6217        .update(cx, |project, cx| {
6218            let first = worktree_a.read(cx);
6219            let third = worktree_c.read(cx);
6220            project.move_worktree(first.id(), third.id(), cx)
6221        })
6222        .expect("moving first after third");
6223
6224    // check the state after moving
6225    project.update(cx, |project, cx| {
6226        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
6227        assert_eq!(worktrees.len(), 3);
6228
6229        let first = worktrees[0].read(cx);
6230        let second = worktrees[1].read(cx);
6231        let third = worktrees[2].read(cx);
6232
6233        // check they are now in the right order
6234        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/b.rs");
6235        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/c.rs");
6236        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/a.rs");
6237    });
6238
6239    // move the third worktree to before the first
6240    // [b, c, a] -> [a, b, c]
6241    project
6242        .update(cx, |project, cx| {
6243            let third = worktree_a.read(cx);
6244            let first = worktree_b.read(cx);
6245            project.move_worktree(third.id(), first.id(), cx)
6246        })
6247        .expect("moving third before first");
6248
6249    // check the state after moving
6250    project.update(cx, |project, cx| {
6251        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
6252        assert_eq!(worktrees.len(), 3);
6253
6254        let first = worktrees[0].read(cx);
6255        let second = worktrees[1].read(cx);
6256        let third = worktrees[2].read(cx);
6257
6258        // check they are now in the right order
6259        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
6260        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/b.rs");
6261        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
6262    });
6263}
6264
6265#[gpui::test]
6266async fn test_unstaged_diff_for_buffer(cx: &mut gpui::TestAppContext) {
6267    init_test(cx);
6268
6269    let staged_contents = r#"
6270        fn main() {
6271            println!("hello world");
6272        }
6273    "#
6274    .unindent();
6275    let file_contents = r#"
6276        // print goodbye
6277        fn main() {
6278            println!("goodbye world");
6279        }
6280    "#
6281    .unindent();
6282
6283    let fs = FakeFs::new(cx.background_executor.clone());
6284    fs.insert_tree(
6285        "/dir",
6286        json!({
6287            ".git": {},
6288           "src": {
6289               "main.rs": file_contents,
6290           }
6291        }),
6292    )
6293    .await;
6294
6295    fs.set_index_for_repo(
6296        Path::new("/dir/.git"),
6297        &[("src/main.rs".into(), staged_contents)],
6298    );
6299
6300    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
6301
6302    let buffer = project
6303        .update(cx, |project, cx| {
6304            project.open_local_buffer("/dir/src/main.rs", cx)
6305        })
6306        .await
6307        .unwrap();
6308    let unstaged_diff = project
6309        .update(cx, |project, cx| {
6310            project.open_unstaged_diff(buffer.clone(), cx)
6311        })
6312        .await
6313        .unwrap();
6314
6315    cx.run_until_parked();
6316    unstaged_diff.update(cx, |unstaged_diff, cx| {
6317        let snapshot = buffer.read(cx).snapshot();
6318        assert_hunks(
6319            unstaged_diff.hunks(&snapshot, cx),
6320            &snapshot,
6321            &unstaged_diff.base_text_string().unwrap(),
6322            &[
6323                (0..1, "", "// print goodbye\n", DiffHunkStatus::added_none()),
6324                (
6325                    2..3,
6326                    "    println!(\"hello world\");\n",
6327                    "    println!(\"goodbye world\");\n",
6328                    DiffHunkStatus::modified_none(),
6329                ),
6330            ],
6331        );
6332    });
6333
6334    let staged_contents = r#"
6335        // print goodbye
6336        fn main() {
6337        }
6338    "#
6339    .unindent();
6340
6341    fs.set_index_for_repo(
6342        Path::new("/dir/.git"),
6343        &[("src/main.rs".into(), staged_contents)],
6344    );
6345
6346    cx.run_until_parked();
6347    unstaged_diff.update(cx, |unstaged_diff, cx| {
6348        let snapshot = buffer.read(cx).snapshot();
6349        assert_hunks(
6350            unstaged_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
6351            &snapshot,
6352            &unstaged_diff.base_text().text(),
6353            &[(
6354                2..3,
6355                "",
6356                "    println!(\"goodbye world\");\n",
6357                DiffHunkStatus::added_none(),
6358            )],
6359        );
6360    });
6361}
6362
6363#[gpui::test]
6364async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) {
6365    init_test(cx);
6366
6367    let committed_contents = r#"
6368        fn main() {
6369            println!("hello world");
6370        }
6371    "#
6372    .unindent();
6373    let staged_contents = r#"
6374        fn main() {
6375            println!("goodbye world");
6376        }
6377    "#
6378    .unindent();
6379    let file_contents = r#"
6380        // print goodbye
6381        fn main() {
6382            println!("goodbye world");
6383        }
6384    "#
6385    .unindent();
6386
6387    let fs = FakeFs::new(cx.background_executor.clone());
6388    fs.insert_tree(
6389        "/dir",
6390        json!({
6391            ".git": {},
6392           "src": {
6393               "modification.rs": file_contents,
6394           }
6395        }),
6396    )
6397    .await;
6398
6399    fs.set_head_for_repo(
6400        Path::new("/dir/.git"),
6401        &[
6402            ("src/modification.rs".into(), committed_contents),
6403            ("src/deletion.rs".into(), "// the-deleted-contents\n".into()),
6404        ],
6405    );
6406    fs.set_index_for_repo(
6407        Path::new("/dir/.git"),
6408        &[
6409            ("src/modification.rs".into(), staged_contents),
6410            ("src/deletion.rs".into(), "// the-deleted-contents\n".into()),
6411        ],
6412    );
6413
6414    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
6415    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
6416    let language = rust_lang();
6417    language_registry.add(language.clone());
6418
6419    let buffer_1 = project
6420        .update(cx, |project, cx| {
6421            project.open_local_buffer("/dir/src/modification.rs", cx)
6422        })
6423        .await
6424        .unwrap();
6425    let diff_1 = project
6426        .update(cx, |project, cx| {
6427            project.open_uncommitted_diff(buffer_1.clone(), cx)
6428        })
6429        .await
6430        .unwrap();
6431    diff_1.read_with(cx, |diff, _| {
6432        assert_eq!(diff.base_text().language().cloned(), Some(language))
6433    });
6434    cx.run_until_parked();
6435    diff_1.update(cx, |diff, cx| {
6436        let snapshot = buffer_1.read(cx).snapshot();
6437        assert_hunks(
6438            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
6439            &snapshot,
6440            &diff.base_text_string().unwrap(),
6441            &[
6442                (
6443                    0..1,
6444                    "",
6445                    "// print goodbye\n",
6446                    DiffHunkStatus::added(DiffHunkSecondaryStatus::HasSecondaryHunk),
6447                ),
6448                (
6449                    2..3,
6450                    "    println!(\"hello world\");\n",
6451                    "    println!(\"goodbye world\");\n",
6452                    DiffHunkStatus::modified_none(),
6453                ),
6454            ],
6455        );
6456    });
6457
6458    // Reset HEAD to a version that differs from both the buffer and the index.
6459    let committed_contents = r#"
6460        // print goodbye
6461        fn main() {
6462        }
6463    "#
6464    .unindent();
6465    fs.set_head_for_repo(
6466        Path::new("/dir/.git"),
6467        &[
6468            ("src/modification.rs".into(), committed_contents.clone()),
6469            ("src/deletion.rs".into(), "// the-deleted-contents\n".into()),
6470        ],
6471    );
6472
6473    // Buffer now has an unstaged hunk.
6474    cx.run_until_parked();
6475    diff_1.update(cx, |diff, cx| {
6476        let snapshot = buffer_1.read(cx).snapshot();
6477        assert_hunks(
6478            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
6479            &snapshot,
6480            &diff.base_text().text(),
6481            &[(
6482                2..3,
6483                "",
6484                "    println!(\"goodbye world\");\n",
6485                DiffHunkStatus::added_none(),
6486            )],
6487        );
6488    });
6489
6490    // Open a buffer for a file that's been deleted.
6491    let buffer_2 = project
6492        .update(cx, |project, cx| {
6493            project.open_local_buffer("/dir/src/deletion.rs", cx)
6494        })
6495        .await
6496        .unwrap();
6497    let diff_2 = project
6498        .update(cx, |project, cx| {
6499            project.open_uncommitted_diff(buffer_2.clone(), cx)
6500        })
6501        .await
6502        .unwrap();
6503    cx.run_until_parked();
6504    diff_2.update(cx, |diff, cx| {
6505        let snapshot = buffer_2.read(cx).snapshot();
6506        assert_hunks(
6507            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
6508            &snapshot,
6509            &diff.base_text_string().unwrap(),
6510            &[(
6511                0..0,
6512                "// the-deleted-contents\n",
6513                "",
6514                DiffHunkStatus::deleted(DiffHunkSecondaryStatus::HasSecondaryHunk),
6515            )],
6516        );
6517    });
6518
6519    // Stage the deletion of this file
6520    fs.set_index_for_repo(
6521        Path::new("/dir/.git"),
6522        &[("src/modification.rs".into(), committed_contents.clone())],
6523    );
6524    cx.run_until_parked();
6525    diff_2.update(cx, |diff, cx| {
6526        let snapshot = buffer_2.read(cx).snapshot();
6527        assert_hunks(
6528            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
6529            &snapshot,
6530            &diff.base_text_string().unwrap(),
6531            &[(
6532                0..0,
6533                "// the-deleted-contents\n",
6534                "",
6535                DiffHunkStatus::deleted(DiffHunkSecondaryStatus::NoSecondaryHunk),
6536            )],
6537        );
6538    });
6539}
6540
6541#[gpui::test]
6542async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
6543    use DiffHunkSecondaryStatus::*;
6544    init_test(cx);
6545
6546    let committed_contents = r#"
6547        zero
6548        one
6549        two
6550        three
6551        four
6552        five
6553    "#
6554    .unindent();
6555    let file_contents = r#"
6556        one
6557        TWO
6558        three
6559        FOUR
6560        five
6561    "#
6562    .unindent();
6563
6564    let fs = FakeFs::new(cx.background_executor.clone());
6565    fs.insert_tree(
6566        "/dir",
6567        json!({
6568            ".git": {},
6569            "file.txt": file_contents.clone()
6570        }),
6571    )
6572    .await;
6573
6574    fs.set_head_and_index_for_repo(
6575        "/dir/.git".as_ref(),
6576        &[("file.txt".into(), committed_contents.clone())],
6577    );
6578
6579    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
6580
6581    let buffer = project
6582        .update(cx, |project, cx| {
6583            project.open_local_buffer("/dir/file.txt", cx)
6584        })
6585        .await
6586        .unwrap();
6587    let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
6588    let uncommitted_diff = project
6589        .update(cx, |project, cx| {
6590            project.open_uncommitted_diff(buffer.clone(), cx)
6591        })
6592        .await
6593        .unwrap();
6594    let mut diff_events = cx.events(&uncommitted_diff);
6595
6596    // The hunks are initially unstaged.
6597    uncommitted_diff.read_with(cx, |diff, cx| {
6598        assert_hunks(
6599            diff.hunks(&snapshot, cx),
6600            &snapshot,
6601            &diff.base_text_string().unwrap(),
6602            &[
6603                (
6604                    0..0,
6605                    "zero\n",
6606                    "",
6607                    DiffHunkStatus::deleted(HasSecondaryHunk),
6608                ),
6609                (
6610                    1..2,
6611                    "two\n",
6612                    "TWO\n",
6613                    DiffHunkStatus::modified(HasSecondaryHunk),
6614                ),
6615                (
6616                    3..4,
6617                    "four\n",
6618                    "FOUR\n",
6619                    DiffHunkStatus::modified(HasSecondaryHunk),
6620                ),
6621            ],
6622        );
6623    });
6624
6625    // Stage a hunk. It appears as optimistically staged.
6626    uncommitted_diff.update(cx, |diff, cx| {
6627        let range =
6628            snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_before(Point::new(2, 0));
6629        let hunks = diff
6630            .hunks_intersecting_range(range, &snapshot, cx)
6631            .collect::<Vec<_>>();
6632        diff.stage_or_unstage_hunks(true, &hunks, &snapshot, true, cx);
6633
6634        assert_hunks(
6635            diff.hunks(&snapshot, cx),
6636            &snapshot,
6637            &diff.base_text_string().unwrap(),
6638            &[
6639                (
6640                    0..0,
6641                    "zero\n",
6642                    "",
6643                    DiffHunkStatus::deleted(HasSecondaryHunk),
6644                ),
6645                (
6646                    1..2,
6647                    "two\n",
6648                    "TWO\n",
6649                    DiffHunkStatus::modified(SecondaryHunkRemovalPending),
6650                ),
6651                (
6652                    3..4,
6653                    "four\n",
6654                    "FOUR\n",
6655                    DiffHunkStatus::modified(HasSecondaryHunk),
6656                ),
6657            ],
6658        );
6659    });
6660
6661    // The diff emits a change event for the range of the staged hunk.
6662    assert!(matches!(
6663        diff_events.next().await.unwrap(),
6664        BufferDiffEvent::HunksStagedOrUnstaged(_)
6665    ));
6666    let event = diff_events.next().await.unwrap();
6667    if let BufferDiffEvent::DiffChanged {
6668        changed_range: Some(changed_range),
6669    } = event
6670    {
6671        let changed_range = changed_range.to_point(&snapshot);
6672        assert_eq!(changed_range, Point::new(1, 0)..Point::new(2, 0));
6673    } else {
6674        panic!("Unexpected event {event:?}");
6675    }
6676
6677    // When the write to the index completes, it appears as staged.
6678    cx.run_until_parked();
6679    uncommitted_diff.update(cx, |diff, cx| {
6680        assert_hunks(
6681            diff.hunks(&snapshot, cx),
6682            &snapshot,
6683            &diff.base_text_string().unwrap(),
6684            &[
6685                (
6686                    0..0,
6687                    "zero\n",
6688                    "",
6689                    DiffHunkStatus::deleted(HasSecondaryHunk),
6690                ),
6691                (
6692                    1..2,
6693                    "two\n",
6694                    "TWO\n",
6695                    DiffHunkStatus::modified(NoSecondaryHunk),
6696                ),
6697                (
6698                    3..4,
6699                    "four\n",
6700                    "FOUR\n",
6701                    DiffHunkStatus::modified(HasSecondaryHunk),
6702                ),
6703            ],
6704        );
6705    });
6706
6707    // The diff emits a change event for the changed index text.
6708    let event = diff_events.next().await.unwrap();
6709    if let BufferDiffEvent::DiffChanged {
6710        changed_range: Some(changed_range),
6711    } = event
6712    {
6713        let changed_range = changed_range.to_point(&snapshot);
6714        assert_eq!(changed_range, Point::new(0, 0)..Point::new(4, 0));
6715    } else {
6716        panic!("Unexpected event {event:?}");
6717    }
6718
6719    // Simulate a problem writing to the git index.
6720    fs.set_error_message_for_index_write(
6721        "/dir/.git".as_ref(),
6722        Some("failed to write git index".into()),
6723    );
6724
6725    // Stage another hunk.
6726    uncommitted_diff.update(cx, |diff, cx| {
6727        let range =
6728            snapshot.anchor_before(Point::new(3, 0))..snapshot.anchor_before(Point::new(4, 0));
6729        let hunks = diff
6730            .hunks_intersecting_range(range, &snapshot, cx)
6731            .collect::<Vec<_>>();
6732        diff.stage_or_unstage_hunks(true, &hunks, &snapshot, true, cx);
6733
6734        assert_hunks(
6735            diff.hunks(&snapshot, cx),
6736            &snapshot,
6737            &diff.base_text_string().unwrap(),
6738            &[
6739                (
6740                    0..0,
6741                    "zero\n",
6742                    "",
6743                    DiffHunkStatus::deleted(HasSecondaryHunk),
6744                ),
6745                (
6746                    1..2,
6747                    "two\n",
6748                    "TWO\n",
6749                    DiffHunkStatus::modified(NoSecondaryHunk),
6750                ),
6751                (
6752                    3..4,
6753                    "four\n",
6754                    "FOUR\n",
6755                    DiffHunkStatus::modified(SecondaryHunkRemovalPending),
6756                ),
6757            ],
6758        );
6759    });
6760    assert!(matches!(
6761        diff_events.next().await.unwrap(),
6762        BufferDiffEvent::HunksStagedOrUnstaged(_)
6763    ));
6764    let event = diff_events.next().await.unwrap();
6765    if let BufferDiffEvent::DiffChanged {
6766        changed_range: Some(changed_range),
6767    } = event
6768    {
6769        let changed_range = changed_range.to_point(&snapshot);
6770        assert_eq!(changed_range, Point::new(3, 0)..Point::new(4, 0));
6771    } else {
6772        panic!("Unexpected event {event:?}");
6773    }
6774
6775    // When the write fails, the hunk returns to being unstaged.
6776    cx.run_until_parked();
6777    uncommitted_diff.update(cx, |diff, cx| {
6778        assert_hunks(
6779            diff.hunks(&snapshot, cx),
6780            &snapshot,
6781            &diff.base_text_string().unwrap(),
6782            &[
6783                (
6784                    0..0,
6785                    "zero\n",
6786                    "",
6787                    DiffHunkStatus::deleted(HasSecondaryHunk),
6788                ),
6789                (
6790                    1..2,
6791                    "two\n",
6792                    "TWO\n",
6793                    DiffHunkStatus::modified(NoSecondaryHunk),
6794                ),
6795                (
6796                    3..4,
6797                    "four\n",
6798                    "FOUR\n",
6799                    DiffHunkStatus::modified(HasSecondaryHunk),
6800                ),
6801            ],
6802        );
6803    });
6804
6805    let event = diff_events.next().await.unwrap();
6806    if let BufferDiffEvent::DiffChanged {
6807        changed_range: Some(changed_range),
6808    } = event
6809    {
6810        let changed_range = changed_range.to_point(&snapshot);
6811        assert_eq!(changed_range, Point::new(0, 0)..Point::new(5, 0));
6812    } else {
6813        panic!("Unexpected event {event:?}");
6814    }
6815
6816    // Allow writing to the git index to succeed again.
6817    fs.set_error_message_for_index_write("/dir/.git".as_ref(), None);
6818
6819    // Stage two hunks with separate operations.
6820    uncommitted_diff.update(cx, |diff, cx| {
6821        let hunks = diff.hunks(&snapshot, cx).collect::<Vec<_>>();
6822        diff.stage_or_unstage_hunks(true, &hunks[0..1], &snapshot, true, cx);
6823        diff.stage_or_unstage_hunks(true, &hunks[2..3], &snapshot, true, cx);
6824    });
6825
6826    // Both staged hunks appear as pending.
6827    uncommitted_diff.update(cx, |diff, cx| {
6828        assert_hunks(
6829            diff.hunks(&snapshot, cx),
6830            &snapshot,
6831            &diff.base_text_string().unwrap(),
6832            &[
6833                (
6834                    0..0,
6835                    "zero\n",
6836                    "",
6837                    DiffHunkStatus::deleted(SecondaryHunkRemovalPending),
6838                ),
6839                (
6840                    1..2,
6841                    "two\n",
6842                    "TWO\n",
6843                    DiffHunkStatus::modified(NoSecondaryHunk),
6844                ),
6845                (
6846                    3..4,
6847                    "four\n",
6848                    "FOUR\n",
6849                    DiffHunkStatus::modified(SecondaryHunkRemovalPending),
6850                ),
6851            ],
6852        );
6853    });
6854
6855    // Both staging operations take effect.
6856    cx.run_until_parked();
6857    uncommitted_diff.update(cx, |diff, cx| {
6858        assert_hunks(
6859            diff.hunks(&snapshot, cx),
6860            &snapshot,
6861            &diff.base_text_string().unwrap(),
6862            &[
6863                (0..0, "zero\n", "", DiffHunkStatus::deleted(NoSecondaryHunk)),
6864                (
6865                    1..2,
6866                    "two\n",
6867                    "TWO\n",
6868                    DiffHunkStatus::modified(NoSecondaryHunk),
6869                ),
6870                (
6871                    3..4,
6872                    "four\n",
6873                    "FOUR\n",
6874                    DiffHunkStatus::modified(NoSecondaryHunk),
6875                ),
6876            ],
6877        );
6878    });
6879}
6880
6881#[gpui::test(seeds(340, 472))]
6882async fn test_staging_hunks_with_delayed_fs_event(cx: &mut gpui::TestAppContext) {
6883    use DiffHunkSecondaryStatus::*;
6884    init_test(cx);
6885
6886    let committed_contents = r#"
6887        zero
6888        one
6889        two
6890        three
6891        four
6892        five
6893    "#
6894    .unindent();
6895    let file_contents = r#"
6896        one
6897        TWO
6898        three
6899        FOUR
6900        five
6901    "#
6902    .unindent();
6903
6904    let fs = FakeFs::new(cx.background_executor.clone());
6905    fs.insert_tree(
6906        "/dir",
6907        json!({
6908            ".git": {},
6909            "file.txt": file_contents.clone()
6910        }),
6911    )
6912    .await;
6913
6914    fs.set_head_for_repo(
6915        "/dir/.git".as_ref(),
6916        &[("file.txt".into(), committed_contents.clone())],
6917    );
6918    fs.set_index_for_repo(
6919        "/dir/.git".as_ref(),
6920        &[("file.txt".into(), committed_contents.clone())],
6921    );
6922
6923    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
6924
6925    let buffer = project
6926        .update(cx, |project, cx| {
6927            project.open_local_buffer("/dir/file.txt", cx)
6928        })
6929        .await
6930        .unwrap();
6931    let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
6932    let uncommitted_diff = project
6933        .update(cx, |project, cx| {
6934            project.open_uncommitted_diff(buffer.clone(), cx)
6935        })
6936        .await
6937        .unwrap();
6938
6939    // The hunks are initially unstaged.
6940    uncommitted_diff.read_with(cx, |diff, cx| {
6941        assert_hunks(
6942            diff.hunks(&snapshot, cx),
6943            &snapshot,
6944            &diff.base_text_string().unwrap(),
6945            &[
6946                (
6947                    0..0,
6948                    "zero\n",
6949                    "",
6950                    DiffHunkStatus::deleted(HasSecondaryHunk),
6951                ),
6952                (
6953                    1..2,
6954                    "two\n",
6955                    "TWO\n",
6956                    DiffHunkStatus::modified(HasSecondaryHunk),
6957                ),
6958                (
6959                    3..4,
6960                    "four\n",
6961                    "FOUR\n",
6962                    DiffHunkStatus::modified(HasSecondaryHunk),
6963                ),
6964            ],
6965        );
6966    });
6967
6968    // Pause IO events
6969    fs.pause_events();
6970
6971    // Stage the first hunk.
6972    uncommitted_diff.update(cx, |diff, cx| {
6973        let hunk = diff.hunks(&snapshot, cx).next().unwrap();
6974        diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx);
6975        assert_hunks(
6976            diff.hunks(&snapshot, cx),
6977            &snapshot,
6978            &diff.base_text_string().unwrap(),
6979            &[
6980                (
6981                    0..0,
6982                    "zero\n",
6983                    "",
6984                    DiffHunkStatus::deleted(SecondaryHunkRemovalPending),
6985                ),
6986                (
6987                    1..2,
6988                    "two\n",
6989                    "TWO\n",
6990                    DiffHunkStatus::modified(HasSecondaryHunk),
6991                ),
6992                (
6993                    3..4,
6994                    "four\n",
6995                    "FOUR\n",
6996                    DiffHunkStatus::modified(HasSecondaryHunk),
6997                ),
6998            ],
6999        );
7000    });
7001
7002    // Stage the second hunk *before* receiving the FS event for the first hunk.
7003    cx.run_until_parked();
7004    uncommitted_diff.update(cx, |diff, cx| {
7005        let hunk = diff.hunks(&snapshot, cx).nth(1).unwrap();
7006        diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx);
7007        assert_hunks(
7008            diff.hunks(&snapshot, cx),
7009            &snapshot,
7010            &diff.base_text_string().unwrap(),
7011            &[
7012                (
7013                    0..0,
7014                    "zero\n",
7015                    "",
7016                    DiffHunkStatus::deleted(SecondaryHunkRemovalPending),
7017                ),
7018                (
7019                    1..2,
7020                    "two\n",
7021                    "TWO\n",
7022                    DiffHunkStatus::modified(SecondaryHunkRemovalPending),
7023                ),
7024                (
7025                    3..4,
7026                    "four\n",
7027                    "FOUR\n",
7028                    DiffHunkStatus::modified(HasSecondaryHunk),
7029                ),
7030            ],
7031        );
7032    });
7033
7034    // Process the FS event for staging the first hunk (second event is still pending).
7035    fs.flush_events(1);
7036    cx.run_until_parked();
7037
7038    // Stage the third hunk before receiving the second FS event.
7039    uncommitted_diff.update(cx, |diff, cx| {
7040        let hunk = diff.hunks(&snapshot, cx).nth(2).unwrap();
7041        diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx);
7042    });
7043
7044    // Wait for all remaining IO.
7045    cx.run_until_parked();
7046    fs.flush_events(fs.buffered_event_count());
7047
7048    // Now all hunks are staged.
7049    cx.run_until_parked();
7050    uncommitted_diff.update(cx, |diff, cx| {
7051        assert_hunks(
7052            diff.hunks(&snapshot, cx),
7053            &snapshot,
7054            &diff.base_text_string().unwrap(),
7055            &[
7056                (0..0, "zero\n", "", DiffHunkStatus::deleted(NoSecondaryHunk)),
7057                (
7058                    1..2,
7059                    "two\n",
7060                    "TWO\n",
7061                    DiffHunkStatus::modified(NoSecondaryHunk),
7062                ),
7063                (
7064                    3..4,
7065                    "four\n",
7066                    "FOUR\n",
7067                    DiffHunkStatus::modified(NoSecondaryHunk),
7068                ),
7069            ],
7070        );
7071    });
7072}
7073
7074#[gpui::test(iterations = 25)]
7075async fn test_staging_random_hunks(
7076    mut rng: StdRng,
7077    executor: BackgroundExecutor,
7078    cx: &mut gpui::TestAppContext,
7079) {
7080    let operations = env::var("OPERATIONS")
7081        .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
7082        .unwrap_or(20);
7083
7084    // Try to induce races between diff recalculation and index writes.
7085    if rng.gen_bool(0.5) {
7086        executor.deprioritize(*CALCULATE_DIFF_TASK);
7087    }
7088
7089    use DiffHunkSecondaryStatus::*;
7090    init_test(cx);
7091
7092    let committed_text = (0..30).map(|i| format!("line {i}\n")).collect::<String>();
7093    let index_text = committed_text.clone();
7094    let buffer_text = (0..30)
7095        .map(|i| match i % 5 {
7096            0 => format!("line {i} (modified)\n"),
7097            _ => format!("line {i}\n"),
7098        })
7099        .collect::<String>();
7100
7101    let fs = FakeFs::new(cx.background_executor.clone());
7102    fs.insert_tree(
7103        path!("/dir"),
7104        json!({
7105            ".git": {},
7106            "file.txt": buffer_text.clone()
7107        }),
7108    )
7109    .await;
7110    fs.set_head_for_repo(
7111        path!("/dir/.git").as_ref(),
7112        &[("file.txt".into(), committed_text.clone())],
7113    );
7114    fs.set_index_for_repo(
7115        path!("/dir/.git").as_ref(),
7116        &[("file.txt".into(), index_text.clone())],
7117    );
7118    let repo = fs.open_repo(path!("/dir/.git").as_ref()).unwrap();
7119
7120    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
7121    let buffer = project
7122        .update(cx, |project, cx| {
7123            project.open_local_buffer(path!("/dir/file.txt"), cx)
7124        })
7125        .await
7126        .unwrap();
7127    let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
7128    let uncommitted_diff = project
7129        .update(cx, |project, cx| {
7130            project.open_uncommitted_diff(buffer.clone(), cx)
7131        })
7132        .await
7133        .unwrap();
7134
7135    let mut hunks =
7136        uncommitted_diff.update(cx, |diff, cx| diff.hunks(&snapshot, cx).collect::<Vec<_>>());
7137    assert_eq!(hunks.len(), 6);
7138
7139    for _i in 0..operations {
7140        let hunk_ix = rng.gen_range(0..hunks.len());
7141        let hunk = &mut hunks[hunk_ix];
7142        let row = hunk.range.start.row;
7143
7144        if hunk.status().has_secondary_hunk() {
7145            log::info!("staging hunk at {row}");
7146            uncommitted_diff.update(cx, |diff, cx| {
7147                diff.stage_or_unstage_hunks(true, &[hunk.clone()], &snapshot, true, cx);
7148            });
7149            hunk.secondary_status = SecondaryHunkRemovalPending;
7150        } else {
7151            log::info!("unstaging hunk at {row}");
7152            uncommitted_diff.update(cx, |diff, cx| {
7153                diff.stage_or_unstage_hunks(false, &[hunk.clone()], &snapshot, true, cx);
7154            });
7155            hunk.secondary_status = SecondaryHunkAdditionPending;
7156        }
7157
7158        for _ in 0..rng.gen_range(0..10) {
7159            log::info!("yielding");
7160            cx.executor().simulate_random_delay().await;
7161        }
7162    }
7163
7164    cx.executor().run_until_parked();
7165
7166    for hunk in &mut hunks {
7167        if hunk.secondary_status == SecondaryHunkRemovalPending {
7168            hunk.secondary_status = NoSecondaryHunk;
7169        } else if hunk.secondary_status == SecondaryHunkAdditionPending {
7170            hunk.secondary_status = HasSecondaryHunk;
7171        }
7172    }
7173
7174    log::info!(
7175        "index text:\n{}",
7176        repo.load_index_text("file.txt".into()).await.unwrap()
7177    );
7178
7179    uncommitted_diff.update(cx, |diff, cx| {
7180        let expected_hunks = hunks
7181            .iter()
7182            .map(|hunk| (hunk.range.start.row, hunk.secondary_status))
7183            .collect::<Vec<_>>();
7184        let actual_hunks = diff
7185            .hunks(&snapshot, cx)
7186            .map(|hunk| (hunk.range.start.row, hunk.secondary_status))
7187            .collect::<Vec<_>>();
7188        assert_eq!(actual_hunks, expected_hunks);
7189    });
7190}
7191
7192#[gpui::test]
7193async fn test_single_file_diffs(cx: &mut gpui::TestAppContext) {
7194    init_test(cx);
7195
7196    let committed_contents = r#"
7197        fn main() {
7198            println!("hello from HEAD");
7199        }
7200    "#
7201    .unindent();
7202    let file_contents = r#"
7203        fn main() {
7204            println!("hello from the working copy");
7205        }
7206    "#
7207    .unindent();
7208
7209    let fs = FakeFs::new(cx.background_executor.clone());
7210    fs.insert_tree(
7211        "/dir",
7212        json!({
7213            ".git": {},
7214           "src": {
7215               "main.rs": file_contents,
7216           }
7217        }),
7218    )
7219    .await;
7220
7221    fs.set_head_for_repo(
7222        Path::new("/dir/.git"),
7223        &[("src/main.rs".into(), committed_contents.clone())],
7224    );
7225    fs.set_index_for_repo(
7226        Path::new("/dir/.git"),
7227        &[("src/main.rs".into(), committed_contents.clone())],
7228    );
7229
7230    let project = Project::test(fs.clone(), ["/dir/src/main.rs".as_ref()], cx).await;
7231
7232    let buffer = project
7233        .update(cx, |project, cx| {
7234            project.open_local_buffer("/dir/src/main.rs", cx)
7235        })
7236        .await
7237        .unwrap();
7238    let uncommitted_diff = project
7239        .update(cx, |project, cx| {
7240            project.open_uncommitted_diff(buffer.clone(), cx)
7241        })
7242        .await
7243        .unwrap();
7244
7245    cx.run_until_parked();
7246    uncommitted_diff.update(cx, |uncommitted_diff, cx| {
7247        let snapshot = buffer.read(cx).snapshot();
7248        assert_hunks(
7249            uncommitted_diff.hunks(&snapshot, cx),
7250            &snapshot,
7251            &uncommitted_diff.base_text_string().unwrap(),
7252            &[(
7253                1..2,
7254                "    println!(\"hello from HEAD\");\n",
7255                "    println!(\"hello from the working copy\");\n",
7256                DiffHunkStatus {
7257                    kind: DiffHunkStatusKind::Modified,
7258                    secondary: DiffHunkSecondaryStatus::HasSecondaryHunk,
7259                },
7260            )],
7261        );
7262    });
7263}
7264
7265#[gpui::test]
7266async fn test_repository_and_path_for_project_path(
7267    background_executor: BackgroundExecutor,
7268    cx: &mut gpui::TestAppContext,
7269) {
7270    init_test(cx);
7271    let fs = FakeFs::new(background_executor);
7272    fs.insert_tree(
7273        path!("/root"),
7274        json!({
7275            "c.txt": "",
7276            "dir1": {
7277                ".git": {},
7278                "deps": {
7279                    "dep1": {
7280                        ".git": {},
7281                        "src": {
7282                            "a.txt": ""
7283                        }
7284                    }
7285                },
7286                "src": {
7287                    "b.txt": ""
7288                }
7289            },
7290        }),
7291    )
7292    .await;
7293
7294    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
7295    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7296    let tree_id = tree.read_with(cx, |tree, _| tree.id());
7297    tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
7298        .await;
7299    cx.run_until_parked();
7300
7301    project.read_with(cx, |project, cx| {
7302        let git_store = project.git_store().read(cx);
7303        let pairs = [
7304            ("c.txt", None),
7305            ("dir1/src/b.txt", Some((path!("/root/dir1"), "src/b.txt"))),
7306            (
7307                "dir1/deps/dep1/src/a.txt",
7308                Some((path!("/root/dir1/deps/dep1"), "src/a.txt")),
7309            ),
7310        ];
7311        let expected = pairs
7312            .iter()
7313            .map(|(path, result)| {
7314                (
7315                    path,
7316                    result.map(|(repo, repo_path)| {
7317                        (Path::new(repo).into(), RepoPath::from(repo_path))
7318                    }),
7319                )
7320            })
7321            .collect::<Vec<_>>();
7322        let actual = pairs
7323            .iter()
7324            .map(|(path, _)| {
7325                let project_path = (tree_id, Path::new(path)).into();
7326                let result = maybe!({
7327                    let (repo, repo_path) =
7328                        git_store.repository_and_path_for_project_path(&project_path, cx)?;
7329                    Some((repo.read(cx).work_directory_abs_path.clone(), repo_path))
7330                });
7331                (path, result)
7332            })
7333            .collect::<Vec<_>>();
7334        pretty_assertions::assert_eq!(expected, actual);
7335    });
7336
7337    fs.remove_dir(path!("/root/dir1/.git").as_ref(), RemoveOptions::default())
7338        .await
7339        .unwrap();
7340    cx.run_until_parked();
7341
7342    project.read_with(cx, |project, cx| {
7343        let git_store = project.git_store().read(cx);
7344        assert_eq!(
7345            git_store.repository_and_path_for_project_path(
7346                &(tree_id, Path::new("dir1/src/b.txt")).into(),
7347                cx
7348            ),
7349            None
7350        );
7351    });
7352}
7353
7354#[gpui::test]
7355async fn test_home_dir_as_git_repository(cx: &mut gpui::TestAppContext) {
7356    init_test(cx);
7357    let fs = FakeFs::new(cx.background_executor.clone());
7358    fs.insert_tree(
7359        path!("/root"),
7360        json!({
7361            "home": {
7362                ".git": {},
7363                "project": {
7364                    "a.txt": "A"
7365                },
7366            },
7367        }),
7368    )
7369    .await;
7370    fs.set_home_dir(Path::new(path!("/root/home")).to_owned());
7371
7372    let project = Project::test(fs.clone(), [path!("/root/home/project").as_ref()], cx).await;
7373    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7374    let tree_id = tree.read_with(cx, |tree, _| tree.id());
7375    tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
7376        .await;
7377    tree.flush_fs_events(cx).await;
7378
7379    project.read_with(cx, |project, cx| {
7380        let containing = project
7381            .git_store()
7382            .read(cx)
7383            .repository_and_path_for_project_path(&(tree_id, "a.txt").into(), cx);
7384        assert!(containing.is_none());
7385    });
7386
7387    let project = Project::test(fs.clone(), [path!("/root/home").as_ref()], cx).await;
7388    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7389    let tree_id = tree.read_with(cx, |tree, _| tree.id());
7390    tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
7391        .await;
7392    tree.flush_fs_events(cx).await;
7393
7394    project.read_with(cx, |project, cx| {
7395        let containing = project
7396            .git_store()
7397            .read(cx)
7398            .repository_and_path_for_project_path(&(tree_id, "project/a.txt").into(), cx);
7399        assert_eq!(
7400            containing
7401                .unwrap()
7402                .0
7403                .read(cx)
7404                .work_directory_abs_path
7405                .as_ref(),
7406            Path::new(path!("/root/home"))
7407        );
7408    });
7409}
7410
7411#[gpui::test]
7412async fn test_git_repository_status(cx: &mut gpui::TestAppContext) {
7413    init_test(cx);
7414    cx.executor().allow_parking();
7415
7416    let root = TempTree::new(json!({
7417        "project": {
7418            "a.txt": "a",    // Modified
7419            "b.txt": "bb",   // Added
7420            "c.txt": "ccc",  // Unchanged
7421            "d.txt": "dddd", // Deleted
7422        },
7423    }));
7424
7425    // Set up git repository before creating the project.
7426    let work_dir = root.path().join("project");
7427    let repo = git_init(work_dir.as_path());
7428    git_add("a.txt", &repo);
7429    git_add("c.txt", &repo);
7430    git_add("d.txt", &repo);
7431    git_commit("Initial commit", &repo);
7432    std::fs::remove_file(work_dir.join("d.txt")).unwrap();
7433    std::fs::write(work_dir.join("a.txt"), "aa").unwrap();
7434
7435    let project = Project::test(
7436        Arc::new(RealFs::new(None, cx.executor())),
7437        [root.path()],
7438        cx,
7439    )
7440    .await;
7441
7442    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7443    tree.flush_fs_events(cx).await;
7444    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7445        .await;
7446    cx.executor().run_until_parked();
7447
7448    let repository = project.read_with(cx, |project, cx| {
7449        project.repositories(cx).values().next().unwrap().clone()
7450    });
7451
7452    // Check that the right git state is observed on startup
7453    repository.read_with(cx, |repository, _| {
7454        let entries = repository.cached_status().collect::<Vec<_>>();
7455        assert_eq!(
7456            entries,
7457            [
7458                StatusEntry {
7459                    repo_path: "a.txt".into(),
7460                    status: StatusCode::Modified.worktree(),
7461                },
7462                StatusEntry {
7463                    repo_path: "b.txt".into(),
7464                    status: FileStatus::Untracked,
7465                },
7466                StatusEntry {
7467                    repo_path: "d.txt".into(),
7468                    status: StatusCode::Deleted.worktree(),
7469                },
7470            ]
7471        );
7472    });
7473
7474    std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
7475
7476    tree.flush_fs_events(cx).await;
7477    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7478        .await;
7479    cx.executor().run_until_parked();
7480
7481    repository.read_with(cx, |repository, _| {
7482        let entries = repository.cached_status().collect::<Vec<_>>();
7483        assert_eq!(
7484            entries,
7485            [
7486                StatusEntry {
7487                    repo_path: "a.txt".into(),
7488                    status: StatusCode::Modified.worktree(),
7489                },
7490                StatusEntry {
7491                    repo_path: "b.txt".into(),
7492                    status: FileStatus::Untracked,
7493                },
7494                StatusEntry {
7495                    repo_path: "c.txt".into(),
7496                    status: StatusCode::Modified.worktree(),
7497                },
7498                StatusEntry {
7499                    repo_path: "d.txt".into(),
7500                    status: StatusCode::Deleted.worktree(),
7501                },
7502            ]
7503        );
7504    });
7505
7506    git_add("a.txt", &repo);
7507    git_add("c.txt", &repo);
7508    git_remove_index(Path::new("d.txt"), &repo);
7509    git_commit("Another commit", &repo);
7510    tree.flush_fs_events(cx).await;
7511    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7512        .await;
7513    cx.executor().run_until_parked();
7514
7515    std::fs::remove_file(work_dir.join("a.txt")).unwrap();
7516    std::fs::remove_file(work_dir.join("b.txt")).unwrap();
7517    tree.flush_fs_events(cx).await;
7518    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7519        .await;
7520    cx.executor().run_until_parked();
7521
7522    repository.read_with(cx, |repository, _cx| {
7523        let entries = repository.cached_status().collect::<Vec<_>>();
7524
7525        // Deleting an untracked entry, b.txt, should leave no status
7526        // a.txt was tracked, and so should have a status
7527        assert_eq!(
7528            entries,
7529            [StatusEntry {
7530                repo_path: "a.txt".into(),
7531                status: StatusCode::Deleted.worktree(),
7532            }]
7533        );
7534    });
7535}
7536
7537#[gpui::test]
7538async fn test_git_status_postprocessing(cx: &mut gpui::TestAppContext) {
7539    init_test(cx);
7540    cx.executor().allow_parking();
7541
7542    let root = TempTree::new(json!({
7543        "project": {
7544            "sub": {},
7545            "a.txt": "",
7546        },
7547    }));
7548
7549    let work_dir = root.path().join("project");
7550    let repo = git_init(work_dir.as_path());
7551    // a.txt exists in HEAD and the working copy but is deleted in the index.
7552    git_add("a.txt", &repo);
7553    git_commit("Initial commit", &repo);
7554    git_remove_index("a.txt".as_ref(), &repo);
7555    // `sub` is a nested git repository.
7556    let _sub = git_init(&work_dir.join("sub"));
7557
7558    let project = Project::test(
7559        Arc::new(RealFs::new(None, cx.executor())),
7560        [root.path()],
7561        cx,
7562    )
7563    .await;
7564
7565    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7566    tree.flush_fs_events(cx).await;
7567    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7568        .await;
7569    cx.executor().run_until_parked();
7570
7571    let repository = project.read_with(cx, |project, cx| {
7572        project
7573            .repositories(cx)
7574            .values()
7575            .find(|repo| repo.read(cx).work_directory_abs_path.ends_with("project"))
7576            .unwrap()
7577            .clone()
7578    });
7579
7580    repository.read_with(cx, |repository, _cx| {
7581        let entries = repository.cached_status().collect::<Vec<_>>();
7582
7583        // `sub` doesn't appear in our computed statuses.
7584        // a.txt appears with a combined `DA` status.
7585        assert_eq!(
7586            entries,
7587            [StatusEntry {
7588                repo_path: "a.txt".into(),
7589                status: TrackedStatus {
7590                    index_status: StatusCode::Deleted,
7591                    worktree_status: StatusCode::Added
7592                }
7593                .into(),
7594            }]
7595        )
7596    });
7597}
7598
7599#[gpui::test]
7600async fn test_repository_subfolder_git_status(
7601    executor: gpui::BackgroundExecutor,
7602    cx: &mut gpui::TestAppContext,
7603) {
7604    init_test(cx);
7605
7606    let fs = FakeFs::new(executor);
7607    fs.insert_tree(
7608        path!("/root"),
7609        json!({
7610            "my-repo": {
7611                ".git": {},
7612                "a.txt": "a",
7613                "sub-folder-1": {
7614                    "sub-folder-2": {
7615                        "c.txt": "cc",
7616                        "d": {
7617                            "e.txt": "eee"
7618                        }
7619                    },
7620                }
7621            },
7622        }),
7623    )
7624    .await;
7625
7626    const C_TXT: &str = "sub-folder-1/sub-folder-2/c.txt";
7627    const E_TXT: &str = "sub-folder-1/sub-folder-2/d/e.txt";
7628
7629    fs.set_status_for_repo(
7630        path!("/root/my-repo/.git").as_ref(),
7631        &[(E_TXT.as_ref(), FileStatus::Untracked)],
7632    );
7633
7634    let project = Project::test(
7635        fs.clone(),
7636        [path!("/root/my-repo/sub-folder-1/sub-folder-2").as_ref()],
7637        cx,
7638    )
7639    .await;
7640
7641    project
7642        .update(cx, |project, cx| project.git_scans_complete(cx))
7643        .await;
7644    cx.run_until_parked();
7645
7646    let repository = project.read_with(cx, |project, cx| {
7647        project.repositories(cx).values().next().unwrap().clone()
7648    });
7649
7650    // Ensure that the git status is loaded correctly
7651    repository.read_with(cx, |repository, _cx| {
7652        assert_eq!(
7653            repository.work_directory_abs_path,
7654            Path::new(path!("/root/my-repo")).into()
7655        );
7656
7657        assert_eq!(repository.status_for_path(&C_TXT.into()), None);
7658        assert_eq!(
7659            repository.status_for_path(&E_TXT.into()).unwrap().status,
7660            FileStatus::Untracked
7661        );
7662    });
7663
7664    fs.set_status_for_repo(path!("/root/my-repo/.git").as_ref(), &[]);
7665    project
7666        .update(cx, |project, cx| project.git_scans_complete(cx))
7667        .await;
7668    cx.run_until_parked();
7669
7670    repository.read_with(cx, |repository, _cx| {
7671        assert_eq!(repository.status_for_path(&C_TXT.into()), None);
7672        assert_eq!(repository.status_for_path(&E_TXT.into()), None);
7673    });
7674}
7675
7676// TODO: this test is flaky (especially on Windows but at least sometimes on all platforms).
7677#[cfg(any())]
7678#[gpui::test]
7679async fn test_conflicted_cherry_pick(cx: &mut gpui::TestAppContext) {
7680    init_test(cx);
7681    cx.executor().allow_parking();
7682
7683    let root = TempTree::new(json!({
7684        "project": {
7685            "a.txt": "a",
7686        },
7687    }));
7688    let root_path = root.path();
7689
7690    let repo = git_init(&root_path.join("project"));
7691    git_add("a.txt", &repo);
7692    git_commit("init", &repo);
7693
7694    let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [root_path], cx).await;
7695
7696    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7697    tree.flush_fs_events(cx).await;
7698    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7699        .await;
7700    cx.executor().run_until_parked();
7701
7702    let repository = project.read_with(cx, |project, cx| {
7703        project.repositories(cx).values().next().unwrap().clone()
7704    });
7705
7706    git_branch("other-branch", &repo);
7707    git_checkout("refs/heads/other-branch", &repo);
7708    std::fs::write(root_path.join("project/a.txt"), "A").unwrap();
7709    git_add("a.txt", &repo);
7710    git_commit("capitalize", &repo);
7711    let commit = repo
7712        .head()
7713        .expect("Failed to get HEAD")
7714        .peel_to_commit()
7715        .expect("HEAD is not a commit");
7716    git_checkout("refs/heads/main", &repo);
7717    std::fs::write(root_path.join("project/a.txt"), "b").unwrap();
7718    git_add("a.txt", &repo);
7719    git_commit("improve letter", &repo);
7720    git_cherry_pick(&commit, &repo);
7721    std::fs::read_to_string(root_path.join("project/.git/CHERRY_PICK_HEAD"))
7722        .expect("No CHERRY_PICK_HEAD");
7723    pretty_assertions::assert_eq!(
7724        git_status(&repo),
7725        collections::HashMap::from_iter([("a.txt".to_owned(), git2::Status::CONFLICTED)])
7726    );
7727    tree.flush_fs_events(cx).await;
7728    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7729        .await;
7730    cx.executor().run_until_parked();
7731    let conflicts = repository.update(cx, |repository, _| {
7732        repository
7733            .merge_conflicts
7734            .iter()
7735            .cloned()
7736            .collect::<Vec<_>>()
7737    });
7738    pretty_assertions::assert_eq!(conflicts, [RepoPath::from("a.txt")]);
7739
7740    git_add("a.txt", &repo);
7741    // Attempt to manually simulate what `git cherry-pick --continue` would do.
7742    git_commit("whatevs", &repo);
7743    std::fs::remove_file(root.path().join("project/.git/CHERRY_PICK_HEAD"))
7744        .expect("Failed to remove CHERRY_PICK_HEAD");
7745    pretty_assertions::assert_eq!(git_status(&repo), collections::HashMap::default());
7746    tree.flush_fs_events(cx).await;
7747    let conflicts = repository.update(cx, |repository, _| {
7748        repository
7749            .merge_conflicts
7750            .iter()
7751            .cloned()
7752            .collect::<Vec<_>>()
7753    });
7754    pretty_assertions::assert_eq!(conflicts, []);
7755}
7756
7757#[gpui::test]
7758async fn test_update_gitignore(cx: &mut gpui::TestAppContext) {
7759    init_test(cx);
7760    let fs = FakeFs::new(cx.background_executor.clone());
7761    fs.insert_tree(
7762        path!("/root"),
7763        json!({
7764            ".git": {},
7765            ".gitignore": "*.txt\n",
7766            "a.xml": "<a></a>",
7767            "b.txt": "Some text"
7768        }),
7769    )
7770    .await;
7771
7772    fs.set_head_and_index_for_repo(
7773        path!("/root/.git").as_ref(),
7774        &[
7775            (".gitignore".into(), "*.txt\n".into()),
7776            ("a.xml".into(), "<a></a>".into()),
7777        ],
7778    );
7779
7780    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
7781
7782    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7783    tree.flush_fs_events(cx).await;
7784    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7785        .await;
7786    cx.executor().run_until_parked();
7787
7788    let repository = project.read_with(cx, |project, cx| {
7789        project.repositories(cx).values().next().unwrap().clone()
7790    });
7791
7792    // One file is unmodified, the other is ignored.
7793    cx.read(|cx| {
7794        assert_entry_git_state(tree.read(cx), repository.read(cx), "a.xml", None, false);
7795        assert_entry_git_state(tree.read(cx), repository.read(cx), "b.txt", None, true);
7796    });
7797
7798    // Change the gitignore, and stage the newly non-ignored file.
7799    fs.atomic_write(path!("/root/.gitignore").into(), "*.xml\n".into())
7800        .await
7801        .unwrap();
7802    fs.set_index_for_repo(
7803        Path::new(path!("/root/.git")),
7804        &[
7805            (".gitignore".into(), "*.txt\n".into()),
7806            ("a.xml".into(), "<a></a>".into()),
7807            ("b.txt".into(), "Some text".into()),
7808        ],
7809    );
7810
7811    cx.executor().run_until_parked();
7812    cx.read(|cx| {
7813        assert_entry_git_state(tree.read(cx), repository.read(cx), "a.xml", None, true);
7814        assert_entry_git_state(
7815            tree.read(cx),
7816            repository.read(cx),
7817            "b.txt",
7818            Some(StatusCode::Added),
7819            false,
7820        );
7821    });
7822}
7823
7824// NOTE:
7825// This test always fails on Windows, because on Windows, unlike on Unix, you can't rename
7826// a directory which some program has already open.
7827// This is a limitation of the Windows.
7828// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
7829#[gpui::test]
7830#[cfg_attr(target_os = "windows", ignore)]
7831async fn test_rename_work_directory(cx: &mut gpui::TestAppContext) {
7832    init_test(cx);
7833    cx.executor().allow_parking();
7834    let root = TempTree::new(json!({
7835        "projects": {
7836            "project1": {
7837                "a": "",
7838                "b": "",
7839            }
7840        },
7841
7842    }));
7843    let root_path = root.path();
7844
7845    let repo = git_init(&root_path.join("projects/project1"));
7846    git_add("a", &repo);
7847    git_commit("init", &repo);
7848    std::fs::write(root_path.join("projects/project1/a"), "aa").unwrap();
7849
7850    let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [root_path], cx).await;
7851
7852    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7853    tree.flush_fs_events(cx).await;
7854    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7855        .await;
7856    cx.executor().run_until_parked();
7857
7858    let repository = project.read_with(cx, |project, cx| {
7859        project.repositories(cx).values().next().unwrap().clone()
7860    });
7861
7862    repository.read_with(cx, |repository, _| {
7863        assert_eq!(
7864            repository.work_directory_abs_path.as_ref(),
7865            root_path.join("projects/project1").as_path()
7866        );
7867        assert_eq!(
7868            repository
7869                .status_for_path(&"a".into())
7870                .map(|entry| entry.status),
7871            Some(StatusCode::Modified.worktree()),
7872        );
7873        assert_eq!(
7874            repository
7875                .status_for_path(&"b".into())
7876                .map(|entry| entry.status),
7877            Some(FileStatus::Untracked),
7878        );
7879    });
7880
7881    std::fs::rename(
7882        root_path.join("projects/project1"),
7883        root_path.join("projects/project2"),
7884    )
7885    .unwrap();
7886    tree.flush_fs_events(cx).await;
7887
7888    repository.read_with(cx, |repository, _| {
7889        assert_eq!(
7890            repository.work_directory_abs_path.as_ref(),
7891            root_path.join("projects/project2").as_path()
7892        );
7893        assert_eq!(
7894            repository.status_for_path(&"a".into()).unwrap().status,
7895            StatusCode::Modified.worktree(),
7896        );
7897        assert_eq!(
7898            repository.status_for_path(&"b".into()).unwrap().status,
7899            FileStatus::Untracked,
7900        );
7901    });
7902}
7903
7904// NOTE: This test always fails on Windows, because on Windows, unlike on Unix,
7905// you can't rename a directory which some program has already open. This is a
7906// limitation of the Windows. See:
7907// https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
7908#[gpui::test]
7909#[cfg_attr(target_os = "windows", ignore)]
7910async fn test_file_status(cx: &mut gpui::TestAppContext) {
7911    init_test(cx);
7912    cx.executor().allow_parking();
7913    const IGNORE_RULE: &str = "**/target";
7914
7915    let root = TempTree::new(json!({
7916        "project": {
7917            "a.txt": "a",
7918            "b.txt": "bb",
7919            "c": {
7920                "d": {
7921                    "e.txt": "eee"
7922                }
7923            },
7924            "f.txt": "ffff",
7925            "target": {
7926                "build_file": "???"
7927            },
7928            ".gitignore": IGNORE_RULE
7929        },
7930
7931    }));
7932    let root_path = root.path();
7933
7934    const A_TXT: &str = "a.txt";
7935    const B_TXT: &str = "b.txt";
7936    const E_TXT: &str = "c/d/e.txt";
7937    const F_TXT: &str = "f.txt";
7938    const DOTGITIGNORE: &str = ".gitignore";
7939    const BUILD_FILE: &str = "target/build_file";
7940
7941    // Set up git repository before creating the worktree.
7942    let work_dir = root.path().join("project");
7943    let mut repo = git_init(work_dir.as_path());
7944    repo.add_ignore_rule(IGNORE_RULE).unwrap();
7945    git_add(A_TXT, &repo);
7946    git_add(E_TXT, &repo);
7947    git_add(DOTGITIGNORE, &repo);
7948    git_commit("Initial commit", &repo);
7949
7950    let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [root_path], cx).await;
7951
7952    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7953    tree.flush_fs_events(cx).await;
7954    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7955        .await;
7956    cx.executor().run_until_parked();
7957
7958    let repository = project.read_with(cx, |project, cx| {
7959        project.repositories(cx).values().next().unwrap().clone()
7960    });
7961
7962    // Check that the right git state is observed on startup
7963    repository.read_with(cx, |repository, _cx| {
7964        assert_eq!(
7965            repository.work_directory_abs_path.as_ref(),
7966            root_path.join("project").as_path()
7967        );
7968
7969        assert_eq!(
7970            repository.status_for_path(&B_TXT.into()).unwrap().status,
7971            FileStatus::Untracked,
7972        );
7973        assert_eq!(
7974            repository.status_for_path(&F_TXT.into()).unwrap().status,
7975            FileStatus::Untracked,
7976        );
7977    });
7978
7979    // Modify a file in the working copy.
7980    std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
7981    tree.flush_fs_events(cx).await;
7982    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7983        .await;
7984    cx.executor().run_until_parked();
7985
7986    // The worktree detects that the file's git status has changed.
7987    repository.read_with(cx, |repository, _| {
7988        assert_eq!(
7989            repository.status_for_path(&A_TXT.into()).unwrap().status,
7990            StatusCode::Modified.worktree(),
7991        );
7992    });
7993
7994    // Create a commit in the git repository.
7995    git_add(A_TXT, &repo);
7996    git_add(B_TXT, &repo);
7997    git_commit("Committing modified and added", &repo);
7998    tree.flush_fs_events(cx).await;
7999    cx.executor().run_until_parked();
8000
8001    // The worktree detects that the files' git status have changed.
8002    repository.read_with(cx, |repository, _cx| {
8003        assert_eq!(
8004            repository.status_for_path(&F_TXT.into()).unwrap().status,
8005            FileStatus::Untracked,
8006        );
8007        assert_eq!(repository.status_for_path(&B_TXT.into()), None);
8008        assert_eq!(repository.status_for_path(&A_TXT.into()), None);
8009    });
8010
8011    // Modify files in the working copy and perform git operations on other files.
8012    git_reset(0, &repo);
8013    git_remove_index(Path::new(B_TXT), &repo);
8014    git_stash(&mut repo);
8015    std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
8016    std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
8017    tree.flush_fs_events(cx).await;
8018    cx.executor().run_until_parked();
8019
8020    // Check that more complex repo changes are tracked
8021    repository.read_with(cx, |repository, _cx| {
8022        assert_eq!(repository.status_for_path(&A_TXT.into()), None);
8023        assert_eq!(
8024            repository.status_for_path(&B_TXT.into()).unwrap().status,
8025            FileStatus::Untracked,
8026        );
8027        assert_eq!(
8028            repository.status_for_path(&E_TXT.into()).unwrap().status,
8029            StatusCode::Modified.worktree(),
8030        );
8031    });
8032
8033    std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
8034    std::fs::remove_dir_all(work_dir.join("c")).unwrap();
8035    std::fs::write(
8036        work_dir.join(DOTGITIGNORE),
8037        [IGNORE_RULE, "f.txt"].join("\n"),
8038    )
8039    .unwrap();
8040
8041    git_add(Path::new(DOTGITIGNORE), &repo);
8042    git_commit("Committing modified git ignore", &repo);
8043
8044    tree.flush_fs_events(cx).await;
8045    cx.executor().run_until_parked();
8046
8047    let mut renamed_dir_name = "first_directory/second_directory";
8048    const RENAMED_FILE: &str = "rf.txt";
8049
8050    std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
8051    std::fs::write(
8052        work_dir.join(renamed_dir_name).join(RENAMED_FILE),
8053        "new-contents",
8054    )
8055    .unwrap();
8056
8057    tree.flush_fs_events(cx).await;
8058    cx.executor().run_until_parked();
8059
8060    repository.read_with(cx, |repository, _cx| {
8061        assert_eq!(
8062            repository
8063                .status_for_path(&Path::new(renamed_dir_name).join(RENAMED_FILE).into())
8064                .unwrap()
8065                .status,
8066            FileStatus::Untracked,
8067        );
8068    });
8069
8070    renamed_dir_name = "new_first_directory/second_directory";
8071
8072    std::fs::rename(
8073        work_dir.join("first_directory"),
8074        work_dir.join("new_first_directory"),
8075    )
8076    .unwrap();
8077
8078    tree.flush_fs_events(cx).await;
8079    cx.executor().run_until_parked();
8080
8081    repository.read_with(cx, |repository, _cx| {
8082        assert_eq!(
8083            repository
8084                .status_for_path(&Path::new(renamed_dir_name).join(RENAMED_FILE).into())
8085                .unwrap()
8086                .status,
8087            FileStatus::Untracked,
8088        );
8089    });
8090}
8091
8092#[gpui::test]
8093async fn test_repos_in_invisible_worktrees(
8094    executor: BackgroundExecutor,
8095    cx: &mut gpui::TestAppContext,
8096) {
8097    init_test(cx);
8098    let fs = FakeFs::new(executor);
8099    fs.insert_tree(
8100        path!("/root"),
8101        json!({
8102            "dir1": {
8103                ".git": {},
8104                "dep1": {
8105                    ".git": {},
8106                    "src": {
8107                        "a.txt": "",
8108                    },
8109                },
8110                "b.txt": "",
8111            },
8112        }),
8113    )
8114    .await;
8115
8116    let project = Project::test(fs.clone(), [path!("/root/dir1/dep1").as_ref()], cx).await;
8117    let visible_worktree =
8118        project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
8119    visible_worktree
8120        .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
8121        .await;
8122
8123    let repos = project.read_with(cx, |project, cx| {
8124        project
8125            .repositories(cx)
8126            .values()
8127            .map(|repo| repo.read(cx).work_directory_abs_path.clone())
8128            .collect::<Vec<_>>()
8129    });
8130    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root/dir1/dep1")).into()]);
8131
8132    let (invisible_worktree, _) = project
8133        .update(cx, |project, cx| {
8134            project.worktree_store.update(cx, |worktree_store, cx| {
8135                worktree_store.find_or_create_worktree(path!("/root/dir1/b.txt"), false, cx)
8136            })
8137        })
8138        .await
8139        .expect("failed to create worktree");
8140    invisible_worktree
8141        .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
8142        .await;
8143
8144    let repos = project.read_with(cx, |project, cx| {
8145        project
8146            .repositories(cx)
8147            .values()
8148            .map(|repo| repo.read(cx).work_directory_abs_path.clone())
8149            .collect::<Vec<_>>()
8150    });
8151    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root/dir1/dep1")).into()]);
8152}
8153
8154#[gpui::test(iterations = 10)]
8155async fn test_rescan_with_gitignore(cx: &mut gpui::TestAppContext) {
8156    init_test(cx);
8157    cx.update(|cx| {
8158        cx.update_global::<SettingsStore, _>(|store, cx| {
8159            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
8160                project_settings.file_scan_exclusions = Some(Vec::new());
8161            });
8162        });
8163    });
8164    let fs = FakeFs::new(cx.background_executor.clone());
8165    fs.insert_tree(
8166        path!("/root"),
8167        json!({
8168            ".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
8169            "tree": {
8170                ".git": {},
8171                ".gitignore": "ignored-dir\n",
8172                "tracked-dir": {
8173                    "tracked-file1": "",
8174                    "ancestor-ignored-file1": "",
8175                },
8176                "ignored-dir": {
8177                    "ignored-file1": ""
8178                }
8179            }
8180        }),
8181    )
8182    .await;
8183    fs.set_head_and_index_for_repo(
8184        path!("/root/tree/.git").as_ref(),
8185        &[
8186            (".gitignore".into(), "ignored-dir\n".into()),
8187            ("tracked-dir/tracked-file1".into(), "".into()),
8188        ],
8189    );
8190
8191    let project = Project::test(fs.clone(), [path!("/root/tree").as_ref()], cx).await;
8192
8193    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
8194    tree.flush_fs_events(cx).await;
8195    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
8196        .await;
8197    cx.executor().run_until_parked();
8198
8199    let repository = project.read_with(cx, |project, cx| {
8200        project.repositories(cx).values().next().unwrap().clone()
8201    });
8202
8203    tree.read_with(cx, |tree, _| {
8204        tree.as_local()
8205            .unwrap()
8206            .manually_refresh_entries_for_paths(vec![Path::new("ignored-dir").into()])
8207    })
8208    .recv()
8209    .await;
8210
8211    cx.read(|cx| {
8212        assert_entry_git_state(
8213            tree.read(cx),
8214            repository.read(cx),
8215            "tracked-dir/tracked-file1",
8216            None,
8217            false,
8218        );
8219        assert_entry_git_state(
8220            tree.read(cx),
8221            repository.read(cx),
8222            "tracked-dir/ancestor-ignored-file1",
8223            None,
8224            false,
8225        );
8226        assert_entry_git_state(
8227            tree.read(cx),
8228            repository.read(cx),
8229            "ignored-dir/ignored-file1",
8230            None,
8231            true,
8232        );
8233    });
8234
8235    fs.create_file(
8236        path!("/root/tree/tracked-dir/tracked-file2").as_ref(),
8237        Default::default(),
8238    )
8239    .await
8240    .unwrap();
8241    fs.set_index_for_repo(
8242        path!("/root/tree/.git").as_ref(),
8243        &[
8244            (".gitignore".into(), "ignored-dir\n".into()),
8245            ("tracked-dir/tracked-file1".into(), "".into()),
8246            ("tracked-dir/tracked-file2".into(), "".into()),
8247        ],
8248    );
8249    fs.create_file(
8250        path!("/root/tree/tracked-dir/ancestor-ignored-file2").as_ref(),
8251        Default::default(),
8252    )
8253    .await
8254    .unwrap();
8255    fs.create_file(
8256        path!("/root/tree/ignored-dir/ignored-file2").as_ref(),
8257        Default::default(),
8258    )
8259    .await
8260    .unwrap();
8261
8262    cx.executor().run_until_parked();
8263    cx.read(|cx| {
8264        assert_entry_git_state(
8265            tree.read(cx),
8266            repository.read(cx),
8267            "tracked-dir/tracked-file2",
8268            Some(StatusCode::Added),
8269            false,
8270        );
8271        assert_entry_git_state(
8272            tree.read(cx),
8273            repository.read(cx),
8274            "tracked-dir/ancestor-ignored-file2",
8275            None,
8276            false,
8277        );
8278        assert_entry_git_state(
8279            tree.read(cx),
8280            repository.read(cx),
8281            "ignored-dir/ignored-file2",
8282            None,
8283            true,
8284        );
8285        assert!(tree.read(cx).entry_for_path(".git").unwrap().is_ignored);
8286    });
8287}
8288
8289#[gpui::test]
8290async fn test_git_worktrees_and_submodules(cx: &mut gpui::TestAppContext) {
8291    init_test(cx);
8292
8293    let fs = FakeFs::new(cx.executor());
8294    fs.insert_tree(
8295        path!("/project"),
8296        json!({
8297            ".git": {
8298                "worktrees": {
8299                    "some-worktree": {
8300                        "commondir": "../..\n"
8301                    }
8302                },
8303                "modules": {
8304                    "subdir": {
8305                        "some-submodule": {
8306                            // For is_git_dir
8307                            "HEAD": "",
8308                            "config": "",
8309                        }
8310                    }
8311                }
8312            },
8313            "src": {
8314                "a.txt": "A",
8315            },
8316            "some-worktree": {
8317                ".git": "gitdir: ../.git/worktrees/some-worktree\n",
8318                "src": {
8319                    "b.txt": "B",
8320                }
8321            },
8322            "subdir": {
8323                "some-submodule": {
8324                    ".git": "gitdir: ../../.git/modules/subdir/some-submodule\n",
8325                    "c.txt": "C",
8326                }
8327            }
8328        }),
8329    )
8330    .await;
8331
8332    let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
8333    let scan_complete = project.update(cx, |project, cx| {
8334        project
8335            .worktrees(cx)
8336            .next()
8337            .unwrap()
8338            .read(cx)
8339            .as_local()
8340            .unwrap()
8341            .scan_complete()
8342    });
8343    scan_complete.await;
8344
8345    let mut repositories = project.update(cx, |project, cx| {
8346        project
8347            .repositories(cx)
8348            .values()
8349            .map(|repo| repo.read(cx).work_directory_abs_path.clone())
8350            .collect::<Vec<_>>()
8351    });
8352    repositories.sort();
8353    pretty_assertions::assert_eq!(
8354        repositories,
8355        [
8356            Path::new(path!("/project")).into(),
8357            Path::new(path!("/project/some-worktree")).into(),
8358            Path::new(path!("/project/subdir/some-submodule")).into(),
8359        ]
8360    );
8361
8362    // Generate a git-related event for the worktree and check that it's refreshed.
8363    fs.with_git_state(
8364        path!("/project/some-worktree/.git").as_ref(),
8365        true,
8366        |state| {
8367            state
8368                .head_contents
8369                .insert("src/b.txt".into(), "b".to_owned());
8370            state
8371                .index_contents
8372                .insert("src/b.txt".into(), "b".to_owned());
8373        },
8374    )
8375    .unwrap();
8376    cx.run_until_parked();
8377
8378    let buffer = project
8379        .update(cx, |project, cx| {
8380            project.open_local_buffer(path!("/project/some-worktree/src/b.txt"), cx)
8381        })
8382        .await
8383        .unwrap();
8384    let (worktree_repo, barrier) = project.update(cx, |project, cx| {
8385        let (repo, _) = project
8386            .git_store()
8387            .read(cx)
8388            .repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
8389            .unwrap();
8390        pretty_assertions::assert_eq!(
8391            repo.read(cx).work_directory_abs_path,
8392            Path::new(path!("/project/some-worktree")).into(),
8393        );
8394        let barrier = repo.update(cx, |repo, _| repo.barrier());
8395        (repo.clone(), barrier)
8396    });
8397    barrier.await.unwrap();
8398    worktree_repo.update(cx, |repo, _| {
8399        pretty_assertions::assert_eq!(
8400            repo.status_for_path(&"src/b.txt".into()).unwrap().status,
8401            StatusCode::Modified.worktree(),
8402        );
8403    });
8404
8405    // The same for the submodule.
8406    fs.with_git_state(
8407        path!("/project/subdir/some-submodule/.git").as_ref(),
8408        true,
8409        |state| {
8410            state.head_contents.insert("c.txt".into(), "c".to_owned());
8411            state.index_contents.insert("c.txt".into(), "c".to_owned());
8412        },
8413    )
8414    .unwrap();
8415    cx.run_until_parked();
8416
8417    let buffer = project
8418        .update(cx, |project, cx| {
8419            project.open_local_buffer(path!("/project/subdir/some-submodule/c.txt"), cx)
8420        })
8421        .await
8422        .unwrap();
8423    let (submodule_repo, barrier) = project.update(cx, |project, cx| {
8424        let (repo, _) = project
8425            .git_store()
8426            .read(cx)
8427            .repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
8428            .unwrap();
8429        pretty_assertions::assert_eq!(
8430            repo.read(cx).work_directory_abs_path,
8431            Path::new(path!("/project/subdir/some-submodule")).into(),
8432        );
8433        let barrier = repo.update(cx, |repo, _| repo.barrier());
8434        (repo.clone(), barrier)
8435    });
8436    barrier.await.unwrap();
8437    submodule_repo.update(cx, |repo, _| {
8438        pretty_assertions::assert_eq!(
8439            repo.status_for_path(&"c.txt".into()).unwrap().status,
8440            StatusCode::Modified.worktree(),
8441        );
8442    });
8443}
8444
8445#[gpui::test]
8446async fn test_repository_deduplication(cx: &mut gpui::TestAppContext) {
8447    init_test(cx);
8448    let fs = FakeFs::new(cx.background_executor.clone());
8449    fs.insert_tree(
8450        path!("/root"),
8451        json!({
8452            "project": {
8453                ".git": {},
8454                "child1": {
8455                    "a.txt": "A",
8456                },
8457                "child2": {
8458                    "b.txt": "B",
8459                }
8460            }
8461        }),
8462    )
8463    .await;
8464
8465    let project = Project::test(
8466        fs.clone(),
8467        [
8468            path!("/root/project/child1").as_ref(),
8469            path!("/root/project/child2").as_ref(),
8470        ],
8471        cx,
8472    )
8473    .await;
8474
8475    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
8476    tree.flush_fs_events(cx).await;
8477    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
8478        .await;
8479    cx.executor().run_until_parked();
8480
8481    let repos = project.read_with(cx, |project, cx| {
8482        project
8483            .repositories(cx)
8484            .values()
8485            .map(|repo| repo.read(cx).work_directory_abs_path.clone())
8486            .collect::<Vec<_>>()
8487    });
8488    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root/project")).into()]);
8489}
8490
8491async fn search(
8492    project: &Entity<Project>,
8493    query: SearchQuery,
8494    cx: &mut gpui::TestAppContext,
8495) -> Result<HashMap<String, Vec<Range<usize>>>> {
8496    let search_rx = project.update(cx, |project, cx| project.search(query, cx));
8497    let mut results = HashMap::default();
8498    while let Ok(search_result) = search_rx.recv().await {
8499        match search_result {
8500            SearchResult::Buffer { buffer, ranges } => {
8501                results.entry(buffer).or_insert(ranges);
8502            }
8503            SearchResult::LimitReached => {}
8504        }
8505    }
8506    Ok(results
8507        .into_iter()
8508        .map(|(buffer, ranges)| {
8509            buffer.update(cx, |buffer, cx| {
8510                let path = buffer
8511                    .file()
8512                    .unwrap()
8513                    .full_path(cx)
8514                    .to_string_lossy()
8515                    .to_string();
8516                let ranges = ranges
8517                    .into_iter()
8518                    .map(|range| range.to_offset(buffer))
8519                    .collect::<Vec<_>>();
8520                (path, ranges)
8521            })
8522        })
8523        .collect())
8524}
8525
8526pub fn init_test(cx: &mut gpui::TestAppContext) {
8527    if std::env::var("RUST_LOG").is_ok() {
8528        env_logger::try_init().ok();
8529    }
8530
8531    cx.update(|cx| {
8532        let settings_store = SettingsStore::test(cx);
8533        cx.set_global(settings_store);
8534        release_channel::init(SemanticVersion::default(), cx);
8535        language::init(cx);
8536        Project::init_settings(cx);
8537    });
8538}
8539
8540fn json_lang() -> Arc<Language> {
8541    Arc::new(Language::new(
8542        LanguageConfig {
8543            name: "JSON".into(),
8544            matcher: LanguageMatcher {
8545                path_suffixes: vec!["json".to_string()],
8546                ..Default::default()
8547            },
8548            ..Default::default()
8549        },
8550        None,
8551    ))
8552}
8553
8554fn js_lang() -> Arc<Language> {
8555    Arc::new(Language::new(
8556        LanguageConfig {
8557            name: "JavaScript".into(),
8558            matcher: LanguageMatcher {
8559                path_suffixes: vec!["js".to_string()],
8560                ..Default::default()
8561            },
8562            ..Default::default()
8563        },
8564        None,
8565    ))
8566}
8567
8568fn rust_lang() -> Arc<Language> {
8569    Arc::new(Language::new(
8570        LanguageConfig {
8571            name: "Rust".into(),
8572            matcher: LanguageMatcher {
8573                path_suffixes: vec!["rs".to_string()],
8574                ..Default::default()
8575            },
8576            ..Default::default()
8577        },
8578        Some(tree_sitter_rust::LANGUAGE.into()),
8579    ))
8580}
8581
8582fn typescript_lang() -> Arc<Language> {
8583    Arc::new(Language::new(
8584        LanguageConfig {
8585            name: "TypeScript".into(),
8586            matcher: LanguageMatcher {
8587                path_suffixes: vec!["ts".to_string()],
8588                ..Default::default()
8589            },
8590            ..Default::default()
8591        },
8592        Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8593    ))
8594}
8595
8596fn tsx_lang() -> Arc<Language> {
8597    Arc::new(Language::new(
8598        LanguageConfig {
8599            name: "tsx".into(),
8600            matcher: LanguageMatcher {
8601                path_suffixes: vec!["tsx".to_string()],
8602                ..Default::default()
8603            },
8604            ..Default::default()
8605        },
8606        Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8607    ))
8608}
8609
8610fn get_all_tasks(
8611    project: &Entity<Project>,
8612    task_contexts: &TaskContexts,
8613    cx: &mut App,
8614) -> Vec<(TaskSourceKind, ResolvedTask)> {
8615    let (mut old, new) = project.update(cx, |project, cx| {
8616        project
8617            .task_store
8618            .read(cx)
8619            .task_inventory()
8620            .unwrap()
8621            .read(cx)
8622            .used_and_current_resolved_tasks(task_contexts, cx)
8623    });
8624    old.extend(new);
8625    old
8626}
8627
8628#[track_caller]
8629fn assert_entry_git_state(
8630    tree: &Worktree,
8631    repository: &Repository,
8632    path: &str,
8633    index_status: Option<StatusCode>,
8634    is_ignored: bool,
8635) {
8636    assert_eq!(tree.abs_path(), repository.work_directory_abs_path);
8637    let entry = tree
8638        .entry_for_path(path)
8639        .unwrap_or_else(|| panic!("entry {path} not found"));
8640    let status = repository
8641        .status_for_path(&path.into())
8642        .map(|entry| entry.status);
8643    let expected = index_status.map(|index_status| {
8644        TrackedStatus {
8645            index_status,
8646            worktree_status: StatusCode::Unmodified,
8647        }
8648        .into()
8649    });
8650    assert_eq!(
8651        status, expected,
8652        "expected {path} to have git status: {expected:?}"
8653    );
8654    assert_eq!(
8655        entry.is_ignored, is_ignored,
8656        "expected {path} to have is_ignored: {is_ignored}"
8657    );
8658}
8659
8660#[track_caller]
8661fn git_init(path: &Path) -> git2::Repository {
8662    let mut init_opts = RepositoryInitOptions::new();
8663    init_opts.initial_head("main");
8664    git2::Repository::init_opts(path, &init_opts).expect("Failed to initialize git repository")
8665}
8666
8667#[track_caller]
8668fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
8669    let path = path.as_ref();
8670    let mut index = repo.index().expect("Failed to get index");
8671    index.add_path(path).expect("Failed to add file");
8672    index.write().expect("Failed to write index");
8673}
8674
8675#[track_caller]
8676fn git_remove_index(path: &Path, repo: &git2::Repository) {
8677    let mut index = repo.index().expect("Failed to get index");
8678    index.remove_path(path).expect("Failed to add file");
8679    index.write().expect("Failed to write index");
8680}
8681
8682#[track_caller]
8683fn git_commit(msg: &'static str, repo: &git2::Repository) {
8684    use git2::Signature;
8685
8686    let signature = Signature::now("test", "test@zed.dev").unwrap();
8687    let oid = repo.index().unwrap().write_tree().unwrap();
8688    let tree = repo.find_tree(oid).unwrap();
8689    if let Ok(head) = repo.head() {
8690        let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
8691
8692        let parent_commit = parent_obj.as_commit().unwrap();
8693
8694        repo.commit(
8695            Some("HEAD"),
8696            &signature,
8697            &signature,
8698            msg,
8699            &tree,
8700            &[parent_commit],
8701        )
8702        .expect("Failed to commit with parent");
8703    } else {
8704        repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
8705            .expect("Failed to commit");
8706    }
8707}
8708
8709#[cfg(any())]
8710#[track_caller]
8711fn git_cherry_pick(commit: &git2::Commit<'_>, repo: &git2::Repository) {
8712    repo.cherrypick(commit, None).expect("Failed to cherrypick");
8713}
8714
8715#[track_caller]
8716fn git_stash(repo: &mut git2::Repository) {
8717    use git2::Signature;
8718
8719    let signature = Signature::now("test", "test@zed.dev").unwrap();
8720    repo.stash_save(&signature, "N/A", None)
8721        .expect("Failed to stash");
8722}
8723
8724#[track_caller]
8725fn git_reset(offset: usize, repo: &git2::Repository) {
8726    let head = repo.head().expect("Couldn't get repo head");
8727    let object = head.peel(git2::ObjectType::Commit).unwrap();
8728    let commit = object.as_commit().unwrap();
8729    let new_head = commit
8730        .parents()
8731        .inspect(|parnet| {
8732            parnet.message();
8733        })
8734        .nth(offset)
8735        .expect("Not enough history");
8736    repo.reset(new_head.as_object(), git2::ResetType::Soft, None)
8737        .expect("Could not reset");
8738}
8739
8740#[cfg(any())]
8741#[track_caller]
8742fn git_branch(name: &str, repo: &git2::Repository) {
8743    let head = repo
8744        .head()
8745        .expect("Couldn't get repo head")
8746        .peel_to_commit()
8747        .expect("HEAD is not a commit");
8748    repo.branch(name, &head, false).expect("Failed to commit");
8749}
8750
8751#[cfg(any())]
8752#[track_caller]
8753fn git_checkout(name: &str, repo: &git2::Repository) {
8754    repo.set_head(name).expect("Failed to set head");
8755    repo.checkout_head(None).expect("Failed to check out head");
8756}
8757
8758#[cfg(any())]
8759#[track_caller]
8760fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
8761    repo.statuses(None)
8762        .unwrap()
8763        .iter()
8764        .map(|status| (status.path().unwrap().to_string(), status.status()))
8765        .collect()
8766}