project_tests.rs

   1use crate::{search::PathMatcher, Event, *};
   2use fs2::FakeFs;
   3use futures::{future, StreamExt};
   4use gpui2::AppContext;
   5use language2::{
   6    language_settings::{AllLanguageSettings, LanguageSettingsContent},
   7    tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig,
   8    LineEnding, OffsetRangeExt, Point, ToPoint,
   9};
  10use lsp2::Url;
  11use parking_lot::Mutex;
  12use pretty_assertions::assert_eq;
  13use serde_json::json;
  14use std::{os, task::Poll};
  15use unindent::Unindent as _;
  16use util::{assert_set_eq, test::temp_tree};
  17
  18#[gpui2::test]
  19async fn test_block_via_channel(cx: &mut gpui2::TestAppContext) {
  20    cx.executor().allow_parking();
  21
  22    let (tx, mut rx) = futures::channel::mpsc::unbounded();
  23    let _thread = std::thread::spawn(move || {
  24        std::fs::metadata("/Users").unwrap();
  25        std::thread::sleep(Duration::from_millis(1000));
  26        tx.unbounded_send(1).unwrap();
  27    });
  28    rx.next().await.unwrap();
  29}
  30
  31#[gpui2::test]
  32async fn test_block_via_smol(cx: &mut gpui2::TestAppContext) {
  33    cx.executor().allow_parking();
  34
  35    let io_task = smol::unblock(move || {
  36        println!("sleeping on thread {:?}", std::thread::current().id());
  37        std::thread::sleep(Duration::from_millis(10));
  38        1
  39    });
  40
  41    let task = cx.foreground_executor().spawn(async move {
  42        io_task.await;
  43    });
  44
  45    task.await;
  46}
  47
  48#[gpui2::test]
  49async fn test_symlinks(cx: &mut gpui2::TestAppContext) {
  50    init_test(cx);
  51    cx.executor().allow_parking();
  52
  53    let dir = temp_tree(json!({
  54        "root": {
  55            "apple": "",
  56            "banana": {
  57                "carrot": {
  58                    "date": "",
  59                    "endive": "",
  60                }
  61            },
  62            "fennel": {
  63                "grape": "",
  64            }
  65        }
  66    }));
  67
  68    let root_link_path = dir.path().join("root_link");
  69    os::unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap();
  70    os::unix::fs::symlink(
  71        &dir.path().join("root/fennel"),
  72        &dir.path().join("root/finnochio"),
  73    )
  74    .unwrap();
  75
  76    let project = Project::test(Arc::new(RealFs), [root_link_path.as_ref()], cx).await;
  77
  78    project.update(cx, |project, cx| {
  79        let tree = project.worktrees().next().unwrap().read(cx);
  80        assert_eq!(tree.file_count(), 5);
  81        assert_eq!(
  82            tree.inode_for_path("fennel/grape"),
  83            tree.inode_for_path("finnochio/grape")
  84        );
  85    });
  86}
  87
  88#[gpui2::test]
  89async fn test_managing_project_specific_settings(cx: &mut gpui2::TestAppContext) {
  90    init_test(cx);
  91
  92    let fs = FakeFs::new(cx.executor().clone());
  93    fs.insert_tree(
  94        "/the-root",
  95        json!({
  96            ".zed": {
  97                "settings.json": r#"{ "tab_size": 8 }"#
  98            },
  99            "a": {
 100                "a.rs": "fn a() {\n    A\n}"
 101            },
 102            "b": {
 103                ".zed": {
 104                    "settings.json": r#"{ "tab_size": 2 }"#
 105                },
 106                "b.rs": "fn b() {\n  B\n}"
 107            }
 108        }),
 109    )
 110    .await;
 111
 112    let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
 113    let worktree = project.update(cx, |project, _| project.worktrees().next().unwrap());
 114
 115    cx.executor().run_until_parked();
 116    cx.update(|cx| {
 117        let tree = worktree.read(cx);
 118
 119        let settings_a = language_settings(
 120            None,
 121            Some(
 122                &(File::for_entry(
 123                    tree.entry_for_path("a/a.rs").unwrap().clone(),
 124                    worktree.clone(),
 125                ) as _),
 126            ),
 127            cx,
 128        );
 129        let settings_b = language_settings(
 130            None,
 131            Some(
 132                &(File::for_entry(
 133                    tree.entry_for_path("b/b.rs").unwrap().clone(),
 134                    worktree.clone(),
 135                ) as _),
 136            ),
 137            cx,
 138        );
 139
 140        assert_eq!(settings_a.tab_size.get(), 8);
 141        assert_eq!(settings_b.tab_size.get(), 2);
 142    });
 143}
 144
 145#[gpui2::test]
 146async fn test_managing_language_servers(cx: &mut gpui2::TestAppContext) {
 147    init_test(cx);
 148
 149    let mut rust_language = Language::new(
 150        LanguageConfig {
 151            name: "Rust".into(),
 152            path_suffixes: vec!["rs".to_string()],
 153            ..Default::default()
 154        },
 155        Some(tree_sitter_rust::language()),
 156    );
 157    let mut json_language = Language::new(
 158        LanguageConfig {
 159            name: "JSON".into(),
 160            path_suffixes: vec!["json".to_string()],
 161            ..Default::default()
 162        },
 163        None,
 164    );
 165    let mut fake_rust_servers = rust_language
 166        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
 167            name: "the-rust-language-server",
 168            capabilities: lsp2::ServerCapabilities {
 169                completion_provider: Some(lsp2::CompletionOptions {
 170                    trigger_characters: Some(vec![".".to_string(), "::".to_string()]),
 171                    ..Default::default()
 172                }),
 173                ..Default::default()
 174            },
 175            ..Default::default()
 176        }))
 177        .await;
 178    let mut fake_json_servers = json_language
 179        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
 180            name: "the-json-language-server",
 181            capabilities: lsp2::ServerCapabilities {
 182                completion_provider: Some(lsp2::CompletionOptions {
 183                    trigger_characters: Some(vec![":".to_string()]),
 184                    ..Default::default()
 185                }),
 186                ..Default::default()
 187            },
 188            ..Default::default()
 189        }))
 190        .await;
 191
 192    let fs = FakeFs::new(cx.executor().clone());
 193    fs.insert_tree(
 194        "/the-root",
 195        json!({
 196            "test.rs": "const A: i32 = 1;",
 197            "test2.rs": "",
 198            "Cargo.toml": "a = 1",
 199            "package.json": "{\"a\": 1}",
 200        }),
 201    )
 202    .await;
 203
 204    let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
 205
 206    // Open a buffer without an associated language server.
 207    let toml_buffer = project
 208        .update(cx, |project, cx| {
 209            project.open_local_buffer("/the-root/Cargo.toml", cx)
 210        })
 211        .await
 212        .unwrap();
 213
 214    // Open a buffer with an associated language server before the language for it has been loaded.
 215    let rust_buffer = project
 216        .update(cx, |project, cx| {
 217            project.open_local_buffer("/the-root/test.rs", cx)
 218        })
 219        .await
 220        .unwrap();
 221    rust_buffer.update(cx, |buffer, _| {
 222        assert_eq!(buffer.language().map(|l| l.name()), None);
 223    });
 224
 225    // Now we add the languages to the project, and ensure they get assigned to all
 226    // the relevant open buffers.
 227    project.update(cx, |project, _| {
 228        project.languages.add(Arc::new(json_language));
 229        project.languages.add(Arc::new(rust_language));
 230    });
 231    cx.executor().run_until_parked();
 232    rust_buffer.update(cx, |buffer, _| {
 233        assert_eq!(buffer.language().map(|l| l.name()), Some("Rust".into()));
 234    });
 235
 236    // A server is started up, and it is notified about Rust files.
 237    let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
 238    assert_eq!(
 239        fake_rust_server
 240            .receive_notification::<lsp2::notification::DidOpenTextDocument>()
 241            .await
 242            .text_document,
 243        lsp2::TextDocumentItem {
 244            uri: lsp2::Url::from_file_path("/the-root/test.rs").unwrap(),
 245            version: 0,
 246            text: "const A: i32 = 1;".to_string(),
 247            language_id: Default::default()
 248        }
 249    );
 250
 251    // The buffer is configured based on the language server's capabilities.
 252    rust_buffer.update(cx, |buffer, _| {
 253        assert_eq!(
 254            buffer.completion_triggers(),
 255            &[".".to_string(), "::".to_string()]
 256        );
 257    });
 258    toml_buffer.update(cx, |buffer, _| {
 259        assert!(buffer.completion_triggers().is_empty());
 260    });
 261
 262    // Edit a buffer. The changes are reported to the language server.
 263    rust_buffer.update(cx, |buffer, cx| buffer.edit([(16..16, "2")], None, cx));
 264    assert_eq!(
 265        fake_rust_server
 266            .receive_notification::<lsp2::notification::DidChangeTextDocument>()
 267            .await
 268            .text_document,
 269        lsp2::VersionedTextDocumentIdentifier::new(
 270            lsp2::Url::from_file_path("/the-root/test.rs").unwrap(),
 271            1
 272        )
 273    );
 274
 275    // Open a third buffer with a different associated language server.
 276    let json_buffer = project
 277        .update(cx, |project, cx| {
 278            project.open_local_buffer("/the-root/package.json", cx)
 279        })
 280        .await
 281        .unwrap();
 282
 283    // A json language server is started up and is only notified about the json buffer.
 284    let mut fake_json_server = fake_json_servers.next().await.unwrap();
 285    assert_eq!(
 286        fake_json_server
 287            .receive_notification::<lsp2::notification::DidOpenTextDocument>()
 288            .await
 289            .text_document,
 290        lsp2::TextDocumentItem {
 291            uri: lsp2::Url::from_file_path("/the-root/package.json").unwrap(),
 292            version: 0,
 293            text: "{\"a\": 1}".to_string(),
 294            language_id: Default::default()
 295        }
 296    );
 297
 298    // This buffer is configured based on the second language server's
 299    // capabilities.
 300    json_buffer.update(cx, |buffer, _| {
 301        assert_eq!(buffer.completion_triggers(), &[":".to_string()]);
 302    });
 303
 304    // When opening another buffer whose language server is already running,
 305    // it is also configured based on the existing language server's capabilities.
 306    let rust_buffer2 = project
 307        .update(cx, |project, cx| {
 308            project.open_local_buffer("/the-root/test2.rs", cx)
 309        })
 310        .await
 311        .unwrap();
 312    rust_buffer2.update(cx, |buffer, _| {
 313        assert_eq!(
 314            buffer.completion_triggers(),
 315            &[".".to_string(), "::".to_string()]
 316        );
 317    });
 318
 319    // Changes are reported only to servers matching the buffer's language.
 320    toml_buffer.update(cx, |buffer, cx| buffer.edit([(5..5, "23")], None, cx));
 321    rust_buffer2.update(cx, |buffer, cx| {
 322        buffer.edit([(0..0, "let x = 1;")], None, cx)
 323    });
 324    assert_eq!(
 325        fake_rust_server
 326            .receive_notification::<lsp2::notification::DidChangeTextDocument>()
 327            .await
 328            .text_document,
 329        lsp2::VersionedTextDocumentIdentifier::new(
 330            lsp2::Url::from_file_path("/the-root/test2.rs").unwrap(),
 331            1
 332        )
 333    );
 334
 335    // Save notifications are reported to all servers.
 336    project
 337        .update(cx, |project, cx| project.save_buffer(toml_buffer, cx))
 338        .await
 339        .unwrap();
 340    assert_eq!(
 341        fake_rust_server
 342            .receive_notification::<lsp2::notification::DidSaveTextDocument>()
 343            .await
 344            .text_document,
 345        lsp2::TextDocumentIdentifier::new(
 346            lsp2::Url::from_file_path("/the-root/Cargo.toml").unwrap()
 347        )
 348    );
 349    assert_eq!(
 350        fake_json_server
 351            .receive_notification::<lsp2::notification::DidSaveTextDocument>()
 352            .await
 353            .text_document,
 354        lsp2::TextDocumentIdentifier::new(
 355            lsp2::Url::from_file_path("/the-root/Cargo.toml").unwrap()
 356        )
 357    );
 358
 359    // Renames are reported only to servers matching the buffer's language.
 360    fs.rename(
 361        Path::new("/the-root/test2.rs"),
 362        Path::new("/the-root/test3.rs"),
 363        Default::default(),
 364    )
 365    .await
 366    .unwrap();
 367    assert_eq!(
 368        fake_rust_server
 369            .receive_notification::<lsp2::notification::DidCloseTextDocument>()
 370            .await
 371            .text_document,
 372        lsp2::TextDocumentIdentifier::new(lsp2::Url::from_file_path("/the-root/test2.rs").unwrap()),
 373    );
 374    assert_eq!(
 375        fake_rust_server
 376            .receive_notification::<lsp2::notification::DidOpenTextDocument>()
 377            .await
 378            .text_document,
 379        lsp2::TextDocumentItem {
 380            uri: lsp2::Url::from_file_path("/the-root/test3.rs").unwrap(),
 381            version: 0,
 382            text: rust_buffer2.update(cx, |buffer, _| buffer.text()),
 383            language_id: Default::default()
 384        },
 385    );
 386
 387    rust_buffer2.update(cx, |buffer, cx| {
 388        buffer.update_diagnostics(
 389            LanguageServerId(0),
 390            DiagnosticSet::from_sorted_entries(
 391                vec![DiagnosticEntry {
 392                    diagnostic: Default::default(),
 393                    range: Anchor::MIN..Anchor::MAX,
 394                }],
 395                &buffer.snapshot(),
 396            ),
 397            cx,
 398        );
 399        assert_eq!(
 400            buffer
 401                .snapshot()
 402                .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
 403                .count(),
 404            1
 405        );
 406    });
 407
 408    // When the rename changes the extension of the file, the buffer gets closed on the old
 409    // language server and gets opened on the new one.
 410    fs.rename(
 411        Path::new("/the-root/test3.rs"),
 412        Path::new("/the-root/test3.json"),
 413        Default::default(),
 414    )
 415    .await
 416    .unwrap();
 417    assert_eq!(
 418        fake_rust_server
 419            .receive_notification::<lsp2::notification::DidCloseTextDocument>()
 420            .await
 421            .text_document,
 422        lsp2::TextDocumentIdentifier::new(lsp2::Url::from_file_path("/the-root/test3.rs").unwrap(),),
 423    );
 424    assert_eq!(
 425        fake_json_server
 426            .receive_notification::<lsp2::notification::DidOpenTextDocument>()
 427            .await
 428            .text_document,
 429        lsp2::TextDocumentItem {
 430            uri: lsp2::Url::from_file_path("/the-root/test3.json").unwrap(),
 431            version: 0,
 432            text: rust_buffer2.update(cx, |buffer, _| buffer.text()),
 433            language_id: Default::default()
 434        },
 435    );
 436
 437    // We clear the diagnostics, since the language has changed.
 438    rust_buffer2.update(cx, |buffer, _| {
 439        assert_eq!(
 440            buffer
 441                .snapshot()
 442                .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
 443                .count(),
 444            0
 445        );
 446    });
 447
 448    // The renamed file's version resets after changing language server.
 449    rust_buffer2.update(cx, |buffer, cx| buffer.edit([(0..0, "// ")], None, cx));
 450    assert_eq!(
 451        fake_json_server
 452            .receive_notification::<lsp2::notification::DidChangeTextDocument>()
 453            .await
 454            .text_document,
 455        lsp2::VersionedTextDocumentIdentifier::new(
 456            lsp2::Url::from_file_path("/the-root/test3.json").unwrap(),
 457            1
 458        )
 459    );
 460
 461    // Restart language servers
 462    project.update(cx, |project, cx| {
 463        project.restart_language_servers_for_buffers(
 464            vec![rust_buffer.clone(), json_buffer.clone()],
 465            cx,
 466        );
 467    });
 468
 469    let mut rust_shutdown_requests = fake_rust_server
 470        .handle_request::<lsp2::request::Shutdown, _, _>(|_, _| future::ready(Ok(())));
 471    let mut json_shutdown_requests = fake_json_server
 472        .handle_request::<lsp2::request::Shutdown, _, _>(|_, _| future::ready(Ok(())));
 473    futures::join!(rust_shutdown_requests.next(), json_shutdown_requests.next());
 474
 475    let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
 476    let mut fake_json_server = fake_json_servers.next().await.unwrap();
 477
 478    // Ensure rust document is reopened in new rust language server
 479    assert_eq!(
 480        fake_rust_server
 481            .receive_notification::<lsp2::notification::DidOpenTextDocument>()
 482            .await
 483            .text_document,
 484        lsp2::TextDocumentItem {
 485            uri: lsp2::Url::from_file_path("/the-root/test.rs").unwrap(),
 486            version: 0,
 487            text: rust_buffer.update(cx, |buffer, _| buffer.text()),
 488            language_id: Default::default()
 489        }
 490    );
 491
 492    // Ensure json documents are reopened in new json language server
 493    assert_set_eq!(
 494        [
 495            fake_json_server
 496                .receive_notification::<lsp2::notification::DidOpenTextDocument>()
 497                .await
 498                .text_document,
 499            fake_json_server
 500                .receive_notification::<lsp2::notification::DidOpenTextDocument>()
 501                .await
 502                .text_document,
 503        ],
 504        [
 505            lsp2::TextDocumentItem {
 506                uri: lsp2::Url::from_file_path("/the-root/package.json").unwrap(),
 507                version: 0,
 508                text: json_buffer.update(cx, |buffer, _| buffer.text()),
 509                language_id: Default::default()
 510            },
 511            lsp2::TextDocumentItem {
 512                uri: lsp2::Url::from_file_path("/the-root/test3.json").unwrap(),
 513                version: 0,
 514                text: rust_buffer2.update(cx, |buffer, _| buffer.text()),
 515                language_id: Default::default()
 516            }
 517        ]
 518    );
 519
 520    // Close notifications are reported only to servers matching the buffer's language.
 521    cx.update(|_| drop(json_buffer));
 522    let close_message = lsp2::DidCloseTextDocumentParams {
 523        text_document: lsp2::TextDocumentIdentifier::new(
 524            lsp2::Url::from_file_path("/the-root/package.json").unwrap(),
 525        ),
 526    };
 527    assert_eq!(
 528        fake_json_server
 529            .receive_notification::<lsp2::notification::DidCloseTextDocument>()
 530            .await,
 531        close_message,
 532    );
 533}
 534
 535#[gpui2::test]
 536async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui2::TestAppContext) {
 537    init_test(cx);
 538
 539    let mut language = Language::new(
 540        LanguageConfig {
 541            name: "Rust".into(),
 542            path_suffixes: vec!["rs".to_string()],
 543            ..Default::default()
 544        },
 545        Some(tree_sitter_rust::language()),
 546    );
 547    let mut fake_servers = language
 548        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
 549            name: "the-language-server",
 550            ..Default::default()
 551        }))
 552        .await;
 553
 554    let fs = FakeFs::new(cx.executor().clone());
 555    fs.insert_tree(
 556        "/the-root",
 557        json!({
 558            ".gitignore": "target\n",
 559            "src": {
 560                "a.rs": "",
 561                "b.rs": "",
 562            },
 563            "target": {
 564                "x": {
 565                    "out": {
 566                        "x.rs": ""
 567                    }
 568                },
 569                "y": {
 570                    "out": {
 571                        "y.rs": "",
 572                    }
 573                },
 574                "z": {
 575                    "out": {
 576                        "z.rs": ""
 577                    }
 578                }
 579            }
 580        }),
 581    )
 582    .await;
 583
 584    let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
 585    project.update(cx, |project, _| {
 586        project.languages.add(Arc::new(language));
 587    });
 588    cx.executor().run_until_parked();
 589
 590    // Start the language server by opening a buffer with a compatible file extension.
 591    let _buffer = project
 592        .update(cx, |project, cx| {
 593            project.open_local_buffer("/the-root/src/a.rs", cx)
 594        })
 595        .await
 596        .unwrap();
 597
 598    // Initially, we don't load ignored files because the language server has not explicitly asked us to watch them.
 599    project.update(cx, |project, cx| {
 600        let worktree = project.worktrees().next().unwrap();
 601        assert_eq!(
 602            worktree
 603                .read(cx)
 604                .snapshot()
 605                .entries(true)
 606                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
 607                .collect::<Vec<_>>(),
 608            &[
 609                (Path::new(""), false),
 610                (Path::new(".gitignore"), false),
 611                (Path::new("src"), false),
 612                (Path::new("src/a.rs"), false),
 613                (Path::new("src/b.rs"), false),
 614                (Path::new("target"), true),
 615            ]
 616        );
 617    });
 618
 619    let prev_read_dir_count = fs.read_dir_call_count();
 620
 621    // Keep track of the FS events reported to the language server.
 622    let fake_server = fake_servers.next().await.unwrap();
 623    let file_changes = Arc::new(Mutex::new(Vec::new()));
 624    fake_server
 625        .request::<lsp2::request::RegisterCapability>(lsp2::RegistrationParams {
 626            registrations: vec![lsp2::Registration {
 627                id: Default::default(),
 628                method: "workspace/didChangeWatchedFiles".to_string(),
 629                register_options: serde_json::to_value(
 630                    lsp2::DidChangeWatchedFilesRegistrationOptions {
 631                        watchers: vec![
 632                            lsp2::FileSystemWatcher {
 633                                glob_pattern: lsp2::GlobPattern::String(
 634                                    "/the-root/Cargo.toml".to_string(),
 635                                ),
 636                                kind: None,
 637                            },
 638                            lsp2::FileSystemWatcher {
 639                                glob_pattern: lsp2::GlobPattern::String(
 640                                    "/the-root/src/*.{rs,c}".to_string(),
 641                                ),
 642                                kind: None,
 643                            },
 644                            lsp2::FileSystemWatcher {
 645                                glob_pattern: lsp2::GlobPattern::String(
 646                                    "/the-root/target/y/**/*.rs".to_string(),
 647                                ),
 648                                kind: None,
 649                            },
 650                        ],
 651                    },
 652                )
 653                .ok(),
 654            }],
 655        })
 656        .await
 657        .unwrap();
 658    fake_server.handle_notification::<lsp2::notification::DidChangeWatchedFiles, _>({
 659        let file_changes = file_changes.clone();
 660        move |params, _| {
 661            let mut file_changes = file_changes.lock();
 662            file_changes.extend(params.changes);
 663            file_changes.sort_by(|a, b| a.uri.cmp(&b.uri));
 664        }
 665    });
 666
 667    cx.executor().run_until_parked();
 668    assert_eq!(mem::take(&mut *file_changes.lock()), &[]);
 669    assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 4);
 670
 671    // Now the language server has asked us to watch an ignored directory path,
 672    // so we recursively load it.
 673    project.update(cx, |project, cx| {
 674        let worktree = project.worktrees().next().unwrap();
 675        assert_eq!(
 676            worktree
 677                .read(cx)
 678                .snapshot()
 679                .entries(true)
 680                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
 681                .collect::<Vec<_>>(),
 682            &[
 683                (Path::new(""), false),
 684                (Path::new(".gitignore"), false),
 685                (Path::new("src"), false),
 686                (Path::new("src/a.rs"), false),
 687                (Path::new("src/b.rs"), false),
 688                (Path::new("target"), true),
 689                (Path::new("target/x"), true),
 690                (Path::new("target/y"), true),
 691                (Path::new("target/y/out"), true),
 692                (Path::new("target/y/out/y.rs"), true),
 693                (Path::new("target/z"), true),
 694            ]
 695        );
 696    });
 697
 698    // Perform some file system mutations, two of which match the watched patterns,
 699    // and one of which does not.
 700    fs.create_file("/the-root/src/c.rs".as_ref(), Default::default())
 701        .await
 702        .unwrap();
 703    fs.create_file("/the-root/src/d.txt".as_ref(), Default::default())
 704        .await
 705        .unwrap();
 706    fs.remove_file("/the-root/src/b.rs".as_ref(), Default::default())
 707        .await
 708        .unwrap();
 709    fs.create_file("/the-root/target/x/out/x2.rs".as_ref(), Default::default())
 710        .await
 711        .unwrap();
 712    fs.create_file("/the-root/target/y/out/y2.rs".as_ref(), Default::default())
 713        .await
 714        .unwrap();
 715
 716    // The language server receives events for the FS mutations that match its watch patterns.
 717    cx.executor().run_until_parked();
 718    assert_eq!(
 719        &*file_changes.lock(),
 720        &[
 721            lsp2::FileEvent {
 722                uri: lsp2::Url::from_file_path("/the-root/src/b.rs").unwrap(),
 723                typ: lsp2::FileChangeType::DELETED,
 724            },
 725            lsp2::FileEvent {
 726                uri: lsp2::Url::from_file_path("/the-root/src/c.rs").unwrap(),
 727                typ: lsp2::FileChangeType::CREATED,
 728            },
 729            lsp2::FileEvent {
 730                uri: lsp2::Url::from_file_path("/the-root/target/y/out/y2.rs").unwrap(),
 731                typ: lsp2::FileChangeType::CREATED,
 732            },
 733        ]
 734    );
 735}
 736
 737#[gpui2::test]
 738async fn test_single_file_worktrees_diagnostics(cx: &mut gpui2::TestAppContext) {
 739    init_test(cx);
 740
 741    let fs = FakeFs::new(cx.executor().clone());
 742    fs.insert_tree(
 743        "/dir",
 744        json!({
 745            "a.rs": "let a = 1;",
 746            "b.rs": "let b = 2;"
 747        }),
 748    )
 749    .await;
 750
 751    let project = Project::test(fs, ["/dir/a.rs".as_ref(), "/dir/b.rs".as_ref()], cx).await;
 752
 753    let buffer_a = project
 754        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
 755        .await
 756        .unwrap();
 757    let buffer_b = project
 758        .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
 759        .await
 760        .unwrap();
 761
 762    project.update(cx, |project, cx| {
 763        project
 764            .update_diagnostics(
 765                LanguageServerId(0),
 766                lsp2::PublishDiagnosticsParams {
 767                    uri: Url::from_file_path("/dir/a.rs").unwrap(),
 768                    version: None,
 769                    diagnostics: vec![lsp2::Diagnostic {
 770                        range: lsp2::Range::new(
 771                            lsp2::Position::new(0, 4),
 772                            lsp2::Position::new(0, 5),
 773                        ),
 774                        severity: Some(lsp2::DiagnosticSeverity::ERROR),
 775                        message: "error 1".to_string(),
 776                        ..Default::default()
 777                    }],
 778                },
 779                &[],
 780                cx,
 781            )
 782            .unwrap();
 783        project
 784            .update_diagnostics(
 785                LanguageServerId(0),
 786                lsp2::PublishDiagnosticsParams {
 787                    uri: Url::from_file_path("/dir/b.rs").unwrap(),
 788                    version: None,
 789                    diagnostics: vec![lsp2::Diagnostic {
 790                        range: lsp2::Range::new(
 791                            lsp2::Position::new(0, 4),
 792                            lsp2::Position::new(0, 5),
 793                        ),
 794                        severity: Some(lsp2::DiagnosticSeverity::WARNING),
 795                        message: "error 2".to_string(),
 796                        ..Default::default()
 797                    }],
 798                },
 799                &[],
 800                cx,
 801            )
 802            .unwrap();
 803    });
 804
 805    buffer_a.update(cx, |buffer, _| {
 806        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
 807        assert_eq!(
 808            chunks
 809                .iter()
 810                .map(|(s, d)| (s.as_str(), *d))
 811                .collect::<Vec<_>>(),
 812            &[
 813                ("let ", None),
 814                ("a", Some(DiagnosticSeverity::ERROR)),
 815                (" = 1;", None),
 816            ]
 817        );
 818    });
 819    buffer_b.update(cx, |buffer, _| {
 820        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
 821        assert_eq!(
 822            chunks
 823                .iter()
 824                .map(|(s, d)| (s.as_str(), *d))
 825                .collect::<Vec<_>>(),
 826            &[
 827                ("let ", None),
 828                ("b", Some(DiagnosticSeverity::WARNING)),
 829                (" = 2;", None),
 830            ]
 831        );
 832    });
 833}
 834
 835#[gpui2::test]
 836async fn test_hidden_worktrees_diagnostics(cx: &mut gpui2::TestAppContext) {
 837    init_test(cx);
 838
 839    let fs = FakeFs::new(cx.executor().clone());
 840    fs.insert_tree(
 841        "/root",
 842        json!({
 843            "dir": {
 844                "a.rs": "let a = 1;",
 845            },
 846            "other.rs": "let b = c;"
 847        }),
 848    )
 849    .await;
 850
 851    let project = Project::test(fs, ["/root/dir".as_ref()], cx).await;
 852
 853    let (worktree, _) = project
 854        .update(cx, |project, cx| {
 855            project.find_or_create_local_worktree("/root/other.rs", false, cx)
 856        })
 857        .await
 858        .unwrap();
 859    let worktree_id = worktree.update(cx, |tree, _| tree.id());
 860
 861    project.update(cx, |project, cx| {
 862        project
 863            .update_diagnostics(
 864                LanguageServerId(0),
 865                lsp2::PublishDiagnosticsParams {
 866                    uri: Url::from_file_path("/root/other.rs").unwrap(),
 867                    version: None,
 868                    diagnostics: vec![lsp2::Diagnostic {
 869                        range: lsp2::Range::new(
 870                            lsp2::Position::new(0, 8),
 871                            lsp2::Position::new(0, 9),
 872                        ),
 873                        severity: Some(lsp2::DiagnosticSeverity::ERROR),
 874                        message: "unknown variable 'c'".to_string(),
 875                        ..Default::default()
 876                    }],
 877                },
 878                &[],
 879                cx,
 880            )
 881            .unwrap();
 882    });
 883
 884    let buffer = project
 885        .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx))
 886        .await
 887        .unwrap();
 888    buffer.update(cx, |buffer, _| {
 889        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
 890        assert_eq!(
 891            chunks
 892                .iter()
 893                .map(|(s, d)| (s.as_str(), *d))
 894                .collect::<Vec<_>>(),
 895            &[
 896                ("let b = ", None),
 897                ("c", Some(DiagnosticSeverity::ERROR)),
 898                (";", None),
 899            ]
 900        );
 901    });
 902
 903    project.update(cx, |project, cx| {
 904        assert_eq!(project.diagnostic_summaries(cx).next(), None);
 905        assert_eq!(project.diagnostic_summary(cx).error_count, 0);
 906    });
 907}
 908
 909#[gpui2::test]
 910async fn test_disk_based_diagnostics_progress(cx: &mut gpui2::TestAppContext) {
 911    init_test(cx);
 912
 913    let progress_token = "the-progress-token";
 914    let mut language = Language::new(
 915        LanguageConfig {
 916            name: "Rust".into(),
 917            path_suffixes: vec!["rs".to_string()],
 918            ..Default::default()
 919        },
 920        Some(tree_sitter_rust::language()),
 921    );
 922    let mut fake_servers = language
 923        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
 924            disk_based_diagnostics_progress_token: Some(progress_token.into()),
 925            disk_based_diagnostics_sources: vec!["disk".into()],
 926            ..Default::default()
 927        }))
 928        .await;
 929
 930    let fs = FakeFs::new(cx.executor().clone());
 931    fs.insert_tree(
 932        "/dir",
 933        json!({
 934            "a.rs": "fn a() { A }",
 935            "b.rs": "const y: i32 = 1",
 936        }),
 937    )
 938    .await;
 939
 940    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
 941    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
 942    let worktree_id = project.update(cx, |p, cx| p.worktrees().next().unwrap().read(cx).id());
 943
 944    // Cause worktree to start the fake language server
 945    let _buffer = project
 946        .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
 947        .await
 948        .unwrap();
 949
 950    let mut events = cx.events(&project);
 951
 952    let fake_server = fake_servers.next().await.unwrap();
 953    assert_eq!(
 954        events.next().await.unwrap(),
 955        Event::LanguageServerAdded(LanguageServerId(0)),
 956    );
 957
 958    fake_server
 959        .start_progress(format!("{}/0", progress_token))
 960        .await;
 961    assert_eq!(
 962        events.next().await.unwrap(),
 963        Event::DiskBasedDiagnosticsStarted {
 964            language_server_id: LanguageServerId(0),
 965        }
 966    );
 967
 968    fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
 969        uri: Url::from_file_path("/dir/a.rs").unwrap(),
 970        version: None,
 971        diagnostics: vec![lsp2::Diagnostic {
 972            range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)),
 973            severity: Some(lsp2::DiagnosticSeverity::ERROR),
 974            message: "undefined variable 'A'".to_string(),
 975            ..Default::default()
 976        }],
 977    });
 978    assert_eq!(
 979        events.next().await.unwrap(),
 980        Event::DiagnosticsUpdated {
 981            language_server_id: LanguageServerId(0),
 982            path: (worktree_id, Path::new("a.rs")).into()
 983        }
 984    );
 985
 986    fake_server.end_progress(format!("{}/0", progress_token));
 987    assert_eq!(
 988        events.next().await.unwrap(),
 989        Event::DiskBasedDiagnosticsFinished {
 990            language_server_id: LanguageServerId(0)
 991        }
 992    );
 993
 994    let buffer = project
 995        .update(cx, |p, cx| p.open_local_buffer("/dir/a.rs", cx))
 996        .await
 997        .unwrap();
 998
 999    buffer.update(cx, |buffer, _| {
1000        let snapshot = buffer.snapshot();
1001        let diagnostics = snapshot
1002            .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
1003            .collect::<Vec<_>>();
1004        assert_eq!(
1005            diagnostics,
1006            &[DiagnosticEntry {
1007                range: Point::new(0, 9)..Point::new(0, 10),
1008                diagnostic: Diagnostic {
1009                    severity: lsp2::DiagnosticSeverity::ERROR,
1010                    message: "undefined variable 'A'".to_string(),
1011                    group_id: 0,
1012                    is_primary: true,
1013                    ..Default::default()
1014                }
1015            }]
1016        )
1017    });
1018
1019    // Ensure publishing empty diagnostics twice only results in one update event.
1020    fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
1021        uri: Url::from_file_path("/dir/a.rs").unwrap(),
1022        version: None,
1023        diagnostics: Default::default(),
1024    });
1025    assert_eq!(
1026        events.next().await.unwrap(),
1027        Event::DiagnosticsUpdated {
1028            language_server_id: LanguageServerId(0),
1029            path: (worktree_id, Path::new("a.rs")).into()
1030        }
1031    );
1032
1033    fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
1034        uri: Url::from_file_path("/dir/a.rs").unwrap(),
1035        version: None,
1036        diagnostics: Default::default(),
1037    });
1038    cx.executor().run_until_parked();
1039    assert_eq!(futures::poll!(events.next()), Poll::Pending);
1040}
1041
1042#[gpui2::test]
1043async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui2::TestAppContext) {
1044    init_test(cx);
1045
1046    let progress_token = "the-progress-token";
1047    let mut language = Language::new(
1048        LanguageConfig {
1049            path_suffixes: vec!["rs".to_string()],
1050            ..Default::default()
1051        },
1052        None,
1053    );
1054    let mut fake_servers = language
1055        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1056            disk_based_diagnostics_sources: vec!["disk".into()],
1057            disk_based_diagnostics_progress_token: Some(progress_token.into()),
1058            ..Default::default()
1059        }))
1060        .await;
1061
1062    let fs = FakeFs::new(cx.executor().clone());
1063    fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
1064
1065    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1066    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
1067
1068    let buffer = project
1069        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1070        .await
1071        .unwrap();
1072
1073    // Simulate diagnostics starting to update.
1074    let fake_server = fake_servers.next().await.unwrap();
1075    fake_server.start_progress(progress_token).await;
1076
1077    // Restart the server before the diagnostics finish updating.
1078    project.update(cx, |project, cx| {
1079        project.restart_language_servers_for_buffers([buffer], cx);
1080    });
1081    let mut events = cx.events(&project);
1082
1083    // Simulate the newly started server sending more diagnostics.
1084    let fake_server = fake_servers.next().await.unwrap();
1085    assert_eq!(
1086        events.next().await.unwrap(),
1087        Event::LanguageServerAdded(LanguageServerId(1))
1088    );
1089    fake_server.start_progress(progress_token).await;
1090    assert_eq!(
1091        events.next().await.unwrap(),
1092        Event::DiskBasedDiagnosticsStarted {
1093            language_server_id: LanguageServerId(1)
1094        }
1095    );
1096    project.update(cx, |project, _| {
1097        assert_eq!(
1098            project
1099                .language_servers_running_disk_based_diagnostics()
1100                .collect::<Vec<_>>(),
1101            [LanguageServerId(1)]
1102        );
1103    });
1104
1105    // All diagnostics are considered done, despite the old server's diagnostic
1106    // task never completing.
1107    fake_server.end_progress(progress_token);
1108    assert_eq!(
1109        events.next().await.unwrap(),
1110        Event::DiskBasedDiagnosticsFinished {
1111            language_server_id: LanguageServerId(1)
1112        }
1113    );
1114    project.update(cx, |project, _| {
1115        assert_eq!(
1116            project
1117                .language_servers_running_disk_based_diagnostics()
1118                .collect::<Vec<_>>(),
1119            [LanguageServerId(0); 0]
1120        );
1121    });
1122}
1123
1124#[gpui2::test]
1125async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui2::TestAppContext) {
1126    init_test(cx);
1127
1128    let mut language = Language::new(
1129        LanguageConfig {
1130            path_suffixes: vec!["rs".to_string()],
1131            ..Default::default()
1132        },
1133        None,
1134    );
1135    let mut fake_servers = language
1136        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1137            ..Default::default()
1138        }))
1139        .await;
1140
1141    let fs = FakeFs::new(cx.executor().clone());
1142    fs.insert_tree("/dir", json!({ "a.rs": "x" })).await;
1143
1144    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1145    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
1146
1147    let buffer = project
1148        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1149        .await
1150        .unwrap();
1151
1152    // Publish diagnostics
1153    let fake_server = fake_servers.next().await.unwrap();
1154    fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
1155        uri: Url::from_file_path("/dir/a.rs").unwrap(),
1156        version: None,
1157        diagnostics: vec![lsp2::Diagnostic {
1158            range: lsp2::Range::new(lsp2::Position::new(0, 0), lsp2::Position::new(0, 0)),
1159            severity: Some(lsp2::DiagnosticSeverity::ERROR),
1160            message: "the message".to_string(),
1161            ..Default::default()
1162        }],
1163    });
1164
1165    cx.executor().run_until_parked();
1166    buffer.update(cx, |buffer, _| {
1167        assert_eq!(
1168            buffer
1169                .snapshot()
1170                .diagnostics_in_range::<_, usize>(0..1, false)
1171                .map(|entry| entry.diagnostic.message.clone())
1172                .collect::<Vec<_>>(),
1173            ["the message".to_string()]
1174        );
1175    });
1176    project.update(cx, |project, cx| {
1177        assert_eq!(
1178            project.diagnostic_summary(cx),
1179            DiagnosticSummary {
1180                error_count: 1,
1181                warning_count: 0,
1182            }
1183        );
1184    });
1185
1186    project.update(cx, |project, cx| {
1187        project.restart_language_servers_for_buffers([buffer.clone()], cx);
1188    });
1189
1190    // The diagnostics are cleared.
1191    cx.executor().run_until_parked();
1192    buffer.update(cx, |buffer, _| {
1193        assert_eq!(
1194            buffer
1195                .snapshot()
1196                .diagnostics_in_range::<_, usize>(0..1, false)
1197                .map(|entry| entry.diagnostic.message.clone())
1198                .collect::<Vec<_>>(),
1199            Vec::<String>::new(),
1200        );
1201    });
1202    project.update(cx, |project, cx| {
1203        assert_eq!(
1204            project.diagnostic_summary(cx),
1205            DiagnosticSummary {
1206                error_count: 0,
1207                warning_count: 0,
1208            }
1209        );
1210    });
1211}
1212
1213#[gpui2::test]
1214async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui2::TestAppContext) {
1215    init_test(cx);
1216
1217    let mut language = Language::new(
1218        LanguageConfig {
1219            path_suffixes: vec!["rs".to_string()],
1220            ..Default::default()
1221        },
1222        None,
1223    );
1224    let mut fake_servers = language
1225        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1226            name: "the-lsp",
1227            ..Default::default()
1228        }))
1229        .await;
1230
1231    let fs = FakeFs::new(cx.executor().clone());
1232    fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
1233
1234    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1235    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
1236
1237    let buffer = project
1238        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1239        .await
1240        .unwrap();
1241
1242    // Before restarting the server, report diagnostics with an unknown buffer version.
1243    let fake_server = fake_servers.next().await.unwrap();
1244    fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
1245        uri: lsp2::Url::from_file_path("/dir/a.rs").unwrap(),
1246        version: Some(10000),
1247        diagnostics: Vec::new(),
1248    });
1249    cx.executor().run_until_parked();
1250
1251    project.update(cx, |project, cx| {
1252        project.restart_language_servers_for_buffers([buffer.clone()], cx);
1253    });
1254    let mut fake_server = fake_servers.next().await.unwrap();
1255    let notification = fake_server
1256        .receive_notification::<lsp2::notification::DidOpenTextDocument>()
1257        .await
1258        .text_document;
1259    assert_eq!(notification.version, 0);
1260}
1261
1262#[gpui2::test]
1263async fn test_toggling_enable_language_server(cx: &mut gpui2::TestAppContext) {
1264    init_test(cx);
1265
1266    let mut rust = Language::new(
1267        LanguageConfig {
1268            name: Arc::from("Rust"),
1269            path_suffixes: vec!["rs".to_string()],
1270            ..Default::default()
1271        },
1272        None,
1273    );
1274    let mut fake_rust_servers = rust
1275        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1276            name: "rust-lsp",
1277            ..Default::default()
1278        }))
1279        .await;
1280    let mut js = Language::new(
1281        LanguageConfig {
1282            name: Arc::from("JavaScript"),
1283            path_suffixes: vec!["js".to_string()],
1284            ..Default::default()
1285        },
1286        None,
1287    );
1288    let mut fake_js_servers = js
1289        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1290            name: "js-lsp",
1291            ..Default::default()
1292        }))
1293        .await;
1294
1295    let fs = FakeFs::new(cx.executor().clone());
1296    fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" }))
1297        .await;
1298
1299    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1300    project.update(cx, |project, _| {
1301        project.languages.add(Arc::new(rust));
1302        project.languages.add(Arc::new(js));
1303    });
1304
1305    let _rs_buffer = project
1306        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1307        .await
1308        .unwrap();
1309    let _js_buffer = project
1310        .update(cx, |project, cx| project.open_local_buffer("/dir/b.js", cx))
1311        .await
1312        .unwrap();
1313
1314    let mut fake_rust_server_1 = fake_rust_servers.next().await.unwrap();
1315    assert_eq!(
1316        fake_rust_server_1
1317            .receive_notification::<lsp2::notification::DidOpenTextDocument>()
1318            .await
1319            .text_document
1320            .uri
1321            .as_str(),
1322        "file:///dir/a.rs"
1323    );
1324
1325    let mut fake_js_server = fake_js_servers.next().await.unwrap();
1326    assert_eq!(
1327        fake_js_server
1328            .receive_notification::<lsp2::notification::DidOpenTextDocument>()
1329            .await
1330            .text_document
1331            .uri
1332            .as_str(),
1333        "file:///dir/b.js"
1334    );
1335
1336    // Disable Rust language server, ensuring only that server gets stopped.
1337    cx.update(|cx| {
1338        cx.update_global(|settings: &mut SettingsStore, cx| {
1339            settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1340                settings.languages.insert(
1341                    Arc::from("Rust"),
1342                    LanguageSettingsContent {
1343                        enable_language_server: Some(false),
1344                        ..Default::default()
1345                    },
1346                );
1347            });
1348        })
1349    });
1350    fake_rust_server_1
1351        .receive_notification::<lsp2::notification::Exit>()
1352        .await;
1353
1354    // Enable Rust and disable JavaScript language servers, ensuring that the
1355    // former gets started again and that the latter stops.
1356    cx.update(|cx| {
1357        cx.update_global(|settings: &mut SettingsStore, cx| {
1358            settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1359                settings.languages.insert(
1360                    Arc::from("Rust"),
1361                    LanguageSettingsContent {
1362                        enable_language_server: Some(true),
1363                        ..Default::default()
1364                    },
1365                );
1366                settings.languages.insert(
1367                    Arc::from("JavaScript"),
1368                    LanguageSettingsContent {
1369                        enable_language_server: Some(false),
1370                        ..Default::default()
1371                    },
1372                );
1373            });
1374        })
1375    });
1376    let mut fake_rust_server_2 = fake_rust_servers.next().await.unwrap();
1377    assert_eq!(
1378        fake_rust_server_2
1379            .receive_notification::<lsp2::notification::DidOpenTextDocument>()
1380            .await
1381            .text_document
1382            .uri
1383            .as_str(),
1384        "file:///dir/a.rs"
1385    );
1386    fake_js_server
1387        .receive_notification::<lsp2::notification::Exit>()
1388        .await;
1389}
1390
1391#[gpui2::test(iterations = 3)]
1392async fn test_transforming_diagnostics(cx: &mut gpui2::TestAppContext) {
1393    init_test(cx);
1394
1395    let mut language = Language::new(
1396        LanguageConfig {
1397            name: "Rust".into(),
1398            path_suffixes: vec!["rs".to_string()],
1399            ..Default::default()
1400        },
1401        Some(tree_sitter_rust::language()),
1402    );
1403    let mut fake_servers = language
1404        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1405            disk_based_diagnostics_sources: vec!["disk".into()],
1406            ..Default::default()
1407        }))
1408        .await;
1409
1410    let text = "
1411        fn a() { A }
1412        fn b() { BB }
1413        fn c() { CCC }
1414    "
1415    .unindent();
1416
1417    let fs = FakeFs::new(cx.executor().clone());
1418    fs.insert_tree("/dir", json!({ "a.rs": text })).await;
1419
1420    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1421    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
1422
1423    let buffer = project
1424        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1425        .await
1426        .unwrap();
1427
1428    let mut fake_server = fake_servers.next().await.unwrap();
1429    let open_notification = fake_server
1430        .receive_notification::<lsp2::notification::DidOpenTextDocument>()
1431        .await;
1432
1433    // Edit the buffer, moving the content down
1434    buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "\n\n")], None, cx));
1435    let change_notification_1 = fake_server
1436        .receive_notification::<lsp2::notification::DidChangeTextDocument>()
1437        .await;
1438    assert!(change_notification_1.text_document.version > open_notification.text_document.version);
1439
1440    // Report some diagnostics for the initial version of the buffer
1441    fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
1442        uri: lsp2::Url::from_file_path("/dir/a.rs").unwrap(),
1443        version: Some(open_notification.text_document.version),
1444        diagnostics: vec![
1445            lsp2::Diagnostic {
1446                range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)),
1447                severity: Some(DiagnosticSeverity::ERROR),
1448                message: "undefined variable 'A'".to_string(),
1449                source: Some("disk".to_string()),
1450                ..Default::default()
1451            },
1452            lsp2::Diagnostic {
1453                range: lsp2::Range::new(lsp2::Position::new(1, 9), lsp2::Position::new(1, 11)),
1454                severity: Some(DiagnosticSeverity::ERROR),
1455                message: "undefined variable 'BB'".to_string(),
1456                source: Some("disk".to_string()),
1457                ..Default::default()
1458            },
1459            lsp2::Diagnostic {
1460                range: lsp2::Range::new(lsp2::Position::new(2, 9), lsp2::Position::new(2, 12)),
1461                severity: Some(DiagnosticSeverity::ERROR),
1462                source: Some("disk".to_string()),
1463                message: "undefined variable 'CCC'".to_string(),
1464                ..Default::default()
1465            },
1466        ],
1467    });
1468
1469    // The diagnostics have moved down since they were created.
1470    cx.executor().run_until_parked();
1471    buffer.update(cx, |buffer, _| {
1472        assert_eq!(
1473            buffer
1474                .snapshot()
1475                .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0), false)
1476                .collect::<Vec<_>>(),
1477            &[
1478                DiagnosticEntry {
1479                    range: Point::new(3, 9)..Point::new(3, 11),
1480                    diagnostic: Diagnostic {
1481                        source: Some("disk".into()),
1482                        severity: DiagnosticSeverity::ERROR,
1483                        message: "undefined variable 'BB'".to_string(),
1484                        is_disk_based: true,
1485                        group_id: 1,
1486                        is_primary: true,
1487                        ..Default::default()
1488                    },
1489                },
1490                DiagnosticEntry {
1491                    range: Point::new(4, 9)..Point::new(4, 12),
1492                    diagnostic: Diagnostic {
1493                        source: Some("disk".into()),
1494                        severity: DiagnosticSeverity::ERROR,
1495                        message: "undefined variable 'CCC'".to_string(),
1496                        is_disk_based: true,
1497                        group_id: 2,
1498                        is_primary: true,
1499                        ..Default::default()
1500                    }
1501                }
1502            ]
1503        );
1504        assert_eq!(
1505            chunks_with_diagnostics(buffer, 0..buffer.len()),
1506            [
1507                ("\n\nfn a() { ".to_string(), None),
1508                ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
1509                (" }\nfn b() { ".to_string(), None),
1510                ("BB".to_string(), Some(DiagnosticSeverity::ERROR)),
1511                (" }\nfn c() { ".to_string(), None),
1512                ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)),
1513                (" }\n".to_string(), None),
1514            ]
1515        );
1516        assert_eq!(
1517            chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)),
1518            [
1519                ("B".to_string(), Some(DiagnosticSeverity::ERROR)),
1520                (" }\nfn c() { ".to_string(), None),
1521                ("CC".to_string(), Some(DiagnosticSeverity::ERROR)),
1522            ]
1523        );
1524    });
1525
1526    // Ensure overlapping diagnostics are highlighted correctly.
1527    fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
1528        uri: lsp2::Url::from_file_path("/dir/a.rs").unwrap(),
1529        version: Some(open_notification.text_document.version),
1530        diagnostics: vec![
1531            lsp2::Diagnostic {
1532                range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)),
1533                severity: Some(DiagnosticSeverity::ERROR),
1534                message: "undefined variable 'A'".to_string(),
1535                source: Some("disk".to_string()),
1536                ..Default::default()
1537            },
1538            lsp2::Diagnostic {
1539                range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 12)),
1540                severity: Some(DiagnosticSeverity::WARNING),
1541                message: "unreachable statement".to_string(),
1542                source: Some("disk".to_string()),
1543                ..Default::default()
1544            },
1545        ],
1546    });
1547
1548    cx.executor().run_until_parked();
1549    buffer.update(cx, |buffer, _| {
1550        assert_eq!(
1551            buffer
1552                .snapshot()
1553                .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0), false)
1554                .collect::<Vec<_>>(),
1555            &[
1556                DiagnosticEntry {
1557                    range: Point::new(2, 9)..Point::new(2, 12),
1558                    diagnostic: Diagnostic {
1559                        source: Some("disk".into()),
1560                        severity: DiagnosticSeverity::WARNING,
1561                        message: "unreachable statement".to_string(),
1562                        is_disk_based: true,
1563                        group_id: 4,
1564                        is_primary: true,
1565                        ..Default::default()
1566                    }
1567                },
1568                DiagnosticEntry {
1569                    range: Point::new(2, 9)..Point::new(2, 10),
1570                    diagnostic: Diagnostic {
1571                        source: Some("disk".into()),
1572                        severity: DiagnosticSeverity::ERROR,
1573                        message: "undefined variable 'A'".to_string(),
1574                        is_disk_based: true,
1575                        group_id: 3,
1576                        is_primary: true,
1577                        ..Default::default()
1578                    },
1579                }
1580            ]
1581        );
1582        assert_eq!(
1583            chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)),
1584            [
1585                ("fn a() { ".to_string(), None),
1586                ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
1587                (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
1588                ("\n".to_string(), None),
1589            ]
1590        );
1591        assert_eq!(
1592            chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)),
1593            [
1594                (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
1595                ("\n".to_string(), None),
1596            ]
1597        );
1598    });
1599
1600    // Keep editing the buffer and ensure disk-based diagnostics get translated according to the
1601    // changes since the last save.
1602    buffer.update(cx, |buffer, cx| {
1603        buffer.edit([(Point::new(2, 0)..Point::new(2, 0), "    ")], None, cx);
1604        buffer.edit(
1605            [(Point::new(2, 8)..Point::new(2, 10), "(x: usize)")],
1606            None,
1607            cx,
1608        );
1609        buffer.edit([(Point::new(3, 10)..Point::new(3, 10), "xxx")], None, cx);
1610    });
1611    let change_notification_2 = fake_server
1612        .receive_notification::<lsp2::notification::DidChangeTextDocument>()
1613        .await;
1614    assert!(
1615        change_notification_2.text_document.version > change_notification_1.text_document.version
1616    );
1617
1618    // Handle out-of-order diagnostics
1619    fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
1620        uri: lsp2::Url::from_file_path("/dir/a.rs").unwrap(),
1621        version: Some(change_notification_2.text_document.version),
1622        diagnostics: vec![
1623            lsp2::Diagnostic {
1624                range: lsp2::Range::new(lsp2::Position::new(1, 9), lsp2::Position::new(1, 11)),
1625                severity: Some(DiagnosticSeverity::ERROR),
1626                message: "undefined variable 'BB'".to_string(),
1627                source: Some("disk".to_string()),
1628                ..Default::default()
1629            },
1630            lsp2::Diagnostic {
1631                range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)),
1632                severity: Some(DiagnosticSeverity::WARNING),
1633                message: "undefined variable 'A'".to_string(),
1634                source: Some("disk".to_string()),
1635                ..Default::default()
1636            },
1637        ],
1638    });
1639
1640    cx.executor().run_until_parked();
1641    buffer.update(cx, |buffer, _| {
1642        assert_eq!(
1643            buffer
1644                .snapshot()
1645                .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
1646                .collect::<Vec<_>>(),
1647            &[
1648                DiagnosticEntry {
1649                    range: Point::new(2, 21)..Point::new(2, 22),
1650                    diagnostic: Diagnostic {
1651                        source: Some("disk".into()),
1652                        severity: DiagnosticSeverity::WARNING,
1653                        message: "undefined variable 'A'".to_string(),
1654                        is_disk_based: true,
1655                        group_id: 6,
1656                        is_primary: true,
1657                        ..Default::default()
1658                    }
1659                },
1660                DiagnosticEntry {
1661                    range: Point::new(3, 9)..Point::new(3, 14),
1662                    diagnostic: Diagnostic {
1663                        source: Some("disk".into()),
1664                        severity: DiagnosticSeverity::ERROR,
1665                        message: "undefined variable 'BB'".to_string(),
1666                        is_disk_based: true,
1667                        group_id: 5,
1668                        is_primary: true,
1669                        ..Default::default()
1670                    },
1671                }
1672            ]
1673        );
1674    });
1675}
1676
1677#[gpui2::test]
1678async fn test_empty_diagnostic_ranges(cx: &mut gpui2::TestAppContext) {
1679    init_test(cx);
1680
1681    let text = concat!(
1682        "let one = ;\n", //
1683        "let two = \n",
1684        "let three = 3;\n",
1685    );
1686
1687    let fs = FakeFs::new(cx.executor().clone());
1688    fs.insert_tree("/dir", json!({ "a.rs": text })).await;
1689
1690    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1691    let buffer = project
1692        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1693        .await
1694        .unwrap();
1695
1696    project.update(cx, |project, cx| {
1697        project
1698            .update_buffer_diagnostics(
1699                &buffer,
1700                LanguageServerId(0),
1701                None,
1702                vec![
1703                    DiagnosticEntry {
1704                        range: Unclipped(PointUtf16::new(0, 10))..Unclipped(PointUtf16::new(0, 10)),
1705                        diagnostic: Diagnostic {
1706                            severity: DiagnosticSeverity::ERROR,
1707                            message: "syntax error 1".to_string(),
1708                            ..Default::default()
1709                        },
1710                    },
1711                    DiagnosticEntry {
1712                        range: Unclipped(PointUtf16::new(1, 10))..Unclipped(PointUtf16::new(1, 10)),
1713                        diagnostic: Diagnostic {
1714                            severity: DiagnosticSeverity::ERROR,
1715                            message: "syntax error 2".to_string(),
1716                            ..Default::default()
1717                        },
1718                    },
1719                ],
1720                cx,
1721            )
1722            .unwrap();
1723    });
1724
1725    // An empty range is extended forward to include the following character.
1726    // At the end of a line, an empty range is extended backward to include
1727    // the preceding character.
1728    buffer.update(cx, |buffer, _| {
1729        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
1730        assert_eq!(
1731            chunks
1732                .iter()
1733                .map(|(s, d)| (s.as_str(), *d))
1734                .collect::<Vec<_>>(),
1735            &[
1736                ("let one = ", None),
1737                (";", Some(DiagnosticSeverity::ERROR)),
1738                ("\nlet two =", None),
1739                (" ", Some(DiagnosticSeverity::ERROR)),
1740                ("\nlet three = 3;\n", None)
1741            ]
1742        );
1743    });
1744}
1745
1746#[gpui2::test]
1747async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui2::TestAppContext) {
1748    init_test(cx);
1749
1750    let fs = FakeFs::new(cx.executor().clone());
1751    fs.insert_tree("/dir", json!({ "a.rs": "one two three" }))
1752        .await;
1753
1754    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1755
1756    project.update(cx, |project, cx| {
1757        project
1758            .update_diagnostic_entries(
1759                LanguageServerId(0),
1760                Path::new("/dir/a.rs").to_owned(),
1761                None,
1762                vec![DiagnosticEntry {
1763                    range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
1764                    diagnostic: Diagnostic {
1765                        severity: DiagnosticSeverity::ERROR,
1766                        is_primary: true,
1767                        message: "syntax error a1".to_string(),
1768                        ..Default::default()
1769                    },
1770                }],
1771                cx,
1772            )
1773            .unwrap();
1774        project
1775            .update_diagnostic_entries(
1776                LanguageServerId(1),
1777                Path::new("/dir/a.rs").to_owned(),
1778                None,
1779                vec![DiagnosticEntry {
1780                    range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
1781                    diagnostic: Diagnostic {
1782                        severity: DiagnosticSeverity::ERROR,
1783                        is_primary: true,
1784                        message: "syntax error b1".to_string(),
1785                        ..Default::default()
1786                    },
1787                }],
1788                cx,
1789            )
1790            .unwrap();
1791
1792        assert_eq!(
1793            project.diagnostic_summary(cx),
1794            DiagnosticSummary {
1795                error_count: 2,
1796                warning_count: 0,
1797            }
1798        );
1799    });
1800}
1801
1802#[gpui2::test]
1803async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui2::TestAppContext) {
1804    init_test(cx);
1805
1806    let mut language = Language::new(
1807        LanguageConfig {
1808            name: "Rust".into(),
1809            path_suffixes: vec!["rs".to_string()],
1810            ..Default::default()
1811        },
1812        Some(tree_sitter_rust::language()),
1813    );
1814    let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
1815
1816    let text = "
1817        fn a() {
1818            f1();
1819        }
1820        fn b() {
1821            f2();
1822        }
1823        fn c() {
1824            f3();
1825        }
1826    "
1827    .unindent();
1828
1829    let fs = FakeFs::new(cx.executor().clone());
1830    fs.insert_tree(
1831        "/dir",
1832        json!({
1833            "a.rs": text.clone(),
1834        }),
1835    )
1836    .await;
1837
1838    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1839    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
1840    let buffer = project
1841        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1842        .await
1843        .unwrap();
1844
1845    let mut fake_server = fake_servers.next().await.unwrap();
1846    let lsp_document_version = fake_server
1847        .receive_notification::<lsp2::notification::DidOpenTextDocument>()
1848        .await
1849        .text_document
1850        .version;
1851
1852    // Simulate editing the buffer after the language server computes some edits.
1853    buffer.update(cx, |buffer, cx| {
1854        buffer.edit(
1855            [(
1856                Point::new(0, 0)..Point::new(0, 0),
1857                "// above first function\n",
1858            )],
1859            None,
1860            cx,
1861        );
1862        buffer.edit(
1863            [(
1864                Point::new(2, 0)..Point::new(2, 0),
1865                "    // inside first function\n",
1866            )],
1867            None,
1868            cx,
1869        );
1870        buffer.edit(
1871            [(
1872                Point::new(6, 4)..Point::new(6, 4),
1873                "// inside second function ",
1874            )],
1875            None,
1876            cx,
1877        );
1878
1879        assert_eq!(
1880            buffer.text(),
1881            "
1882                // above first function
1883                fn a() {
1884                    // inside first function
1885                    f1();
1886                }
1887                fn b() {
1888                    // inside second function f2();
1889                }
1890                fn c() {
1891                    f3();
1892                }
1893            "
1894            .unindent()
1895        );
1896    });
1897
1898    let edits = project
1899        .update(cx, |project, cx| {
1900            project.edits_from_lsp(
1901                &buffer,
1902                vec![
1903                    // replace body of first function
1904                    lsp2::TextEdit {
1905                        range: lsp2::Range::new(
1906                            lsp2::Position::new(0, 0),
1907                            lsp2::Position::new(3, 0),
1908                        ),
1909                        new_text: "
1910                            fn a() {
1911                                f10();
1912                            }
1913                            "
1914                        .unindent(),
1915                    },
1916                    // edit inside second function
1917                    lsp2::TextEdit {
1918                        range: lsp2::Range::new(
1919                            lsp2::Position::new(4, 6),
1920                            lsp2::Position::new(4, 6),
1921                        ),
1922                        new_text: "00".into(),
1923                    },
1924                    // edit inside third function via two distinct edits
1925                    lsp2::TextEdit {
1926                        range: lsp2::Range::new(
1927                            lsp2::Position::new(7, 5),
1928                            lsp2::Position::new(7, 5),
1929                        ),
1930                        new_text: "4000".into(),
1931                    },
1932                    lsp2::TextEdit {
1933                        range: lsp2::Range::new(
1934                            lsp2::Position::new(7, 5),
1935                            lsp2::Position::new(7, 6),
1936                        ),
1937                        new_text: "".into(),
1938                    },
1939                ],
1940                LanguageServerId(0),
1941                Some(lsp_document_version),
1942                cx,
1943            )
1944        })
1945        .await
1946        .unwrap();
1947
1948    buffer.update(cx, |buffer, cx| {
1949        for (range, new_text) in edits {
1950            buffer.edit([(range, new_text)], None, cx);
1951        }
1952        assert_eq!(
1953            buffer.text(),
1954            "
1955                // above first function
1956                fn a() {
1957                    // inside first function
1958                    f10();
1959                }
1960                fn b() {
1961                    // inside second function f200();
1962                }
1963                fn c() {
1964                    f4000();
1965                }
1966                "
1967            .unindent()
1968        );
1969    });
1970}
1971
1972#[gpui2::test]
1973async fn test_edits_from_lsp2_with_edits_on_adjacent_lines(cx: &mut gpui2::TestAppContext) {
1974    init_test(cx);
1975
1976    let text = "
1977        use a::b;
1978        use a::c;
1979
1980        fn f() {
1981            b();
1982            c();
1983        }
1984    "
1985    .unindent();
1986
1987    let fs = FakeFs::new(cx.executor().clone());
1988    fs.insert_tree(
1989        "/dir",
1990        json!({
1991            "a.rs": text.clone(),
1992        }),
1993    )
1994    .await;
1995
1996    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1997    let buffer = project
1998        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1999        .await
2000        .unwrap();
2001
2002    // Simulate the language server sending us a small edit in the form of a very large diff.
2003    // Rust-analyzer does this when performing a merge-imports code action.
2004    let edits = project
2005        .update(cx, |project, cx| {
2006            project.edits_from_lsp(
2007                &buffer,
2008                [
2009                    // Replace the first use statement without editing the semicolon.
2010                    lsp2::TextEdit {
2011                        range: lsp2::Range::new(
2012                            lsp2::Position::new(0, 4),
2013                            lsp2::Position::new(0, 8),
2014                        ),
2015                        new_text: "a::{b, c}".into(),
2016                    },
2017                    // Reinsert the remainder of the file between the semicolon and the final
2018                    // newline of the file.
2019                    lsp2::TextEdit {
2020                        range: lsp2::Range::new(
2021                            lsp2::Position::new(0, 9),
2022                            lsp2::Position::new(0, 9),
2023                        ),
2024                        new_text: "\n\n".into(),
2025                    },
2026                    lsp2::TextEdit {
2027                        range: lsp2::Range::new(
2028                            lsp2::Position::new(0, 9),
2029                            lsp2::Position::new(0, 9),
2030                        ),
2031                        new_text: "
2032                            fn f() {
2033                                b();
2034                                c();
2035                            }"
2036                        .unindent(),
2037                    },
2038                    // Delete everything after the first newline of the file.
2039                    lsp2::TextEdit {
2040                        range: lsp2::Range::new(
2041                            lsp2::Position::new(1, 0),
2042                            lsp2::Position::new(7, 0),
2043                        ),
2044                        new_text: "".into(),
2045                    },
2046                ],
2047                LanguageServerId(0),
2048                None,
2049                cx,
2050            )
2051        })
2052        .await
2053        .unwrap();
2054
2055    buffer.update(cx, |buffer, cx| {
2056        let edits = edits
2057            .into_iter()
2058            .map(|(range, text)| {
2059                (
2060                    range.start.to_point(buffer)..range.end.to_point(buffer),
2061                    text,
2062                )
2063            })
2064            .collect::<Vec<_>>();
2065
2066        assert_eq!(
2067            edits,
2068            [
2069                (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
2070                (Point::new(1, 0)..Point::new(2, 0), "".into())
2071            ]
2072        );
2073
2074        for (range, new_text) in edits {
2075            buffer.edit([(range, new_text)], None, cx);
2076        }
2077        assert_eq!(
2078            buffer.text(),
2079            "
2080                use a::{b, c};
2081
2082                fn f() {
2083                    b();
2084                    c();
2085                }
2086            "
2087            .unindent()
2088        );
2089    });
2090}
2091
2092#[gpui2::test]
2093async fn test_invalid_edits_from_lsp2(cx: &mut gpui2::TestAppContext) {
2094    init_test(cx);
2095
2096    let text = "
2097        use a::b;
2098        use a::c;
2099
2100        fn f() {
2101            b();
2102            c();
2103        }
2104    "
2105    .unindent();
2106
2107    let fs = FakeFs::new(cx.executor().clone());
2108    fs.insert_tree(
2109        "/dir",
2110        json!({
2111            "a.rs": text.clone(),
2112        }),
2113    )
2114    .await;
2115
2116    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2117    let buffer = project
2118        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
2119        .await
2120        .unwrap();
2121
2122    // Simulate the language server sending us edits in a non-ordered fashion,
2123    // with ranges sometimes being inverted or pointing to invalid locations.
2124    let edits = project
2125        .update(cx, |project, cx| {
2126            project.edits_from_lsp(
2127                &buffer,
2128                [
2129                    lsp2::TextEdit {
2130                        range: lsp2::Range::new(
2131                            lsp2::Position::new(0, 9),
2132                            lsp2::Position::new(0, 9),
2133                        ),
2134                        new_text: "\n\n".into(),
2135                    },
2136                    lsp2::TextEdit {
2137                        range: lsp2::Range::new(
2138                            lsp2::Position::new(0, 8),
2139                            lsp2::Position::new(0, 4),
2140                        ),
2141                        new_text: "a::{b, c}".into(),
2142                    },
2143                    lsp2::TextEdit {
2144                        range: lsp2::Range::new(
2145                            lsp2::Position::new(1, 0),
2146                            lsp2::Position::new(99, 0),
2147                        ),
2148                        new_text: "".into(),
2149                    },
2150                    lsp2::TextEdit {
2151                        range: lsp2::Range::new(
2152                            lsp2::Position::new(0, 9),
2153                            lsp2::Position::new(0, 9),
2154                        ),
2155                        new_text: "
2156                            fn f() {
2157                                b();
2158                                c();
2159                            }"
2160                        .unindent(),
2161                    },
2162                ],
2163                LanguageServerId(0),
2164                None,
2165                cx,
2166            )
2167        })
2168        .await
2169        .unwrap();
2170
2171    buffer.update(cx, |buffer, cx| {
2172        let edits = edits
2173            .into_iter()
2174            .map(|(range, text)| {
2175                (
2176                    range.start.to_point(buffer)..range.end.to_point(buffer),
2177                    text,
2178                )
2179            })
2180            .collect::<Vec<_>>();
2181
2182        assert_eq!(
2183            edits,
2184            [
2185                (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
2186                (Point::new(1, 0)..Point::new(2, 0), "".into())
2187            ]
2188        );
2189
2190        for (range, new_text) in edits {
2191            buffer.edit([(range, new_text)], None, cx);
2192        }
2193        assert_eq!(
2194            buffer.text(),
2195            "
2196                use a::{b, c};
2197
2198                fn f() {
2199                    b();
2200                    c();
2201                }
2202            "
2203            .unindent()
2204        );
2205    });
2206}
2207
2208fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
2209    buffer: &Buffer,
2210    range: Range<T>,
2211) -> Vec<(String, Option<DiagnosticSeverity>)> {
2212    let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
2213    for chunk in buffer.snapshot().chunks(range, true) {
2214        if chunks.last().map_or(false, |prev_chunk| {
2215            prev_chunk.1 == chunk.diagnostic_severity
2216        }) {
2217            chunks.last_mut().unwrap().0.push_str(chunk.text);
2218        } else {
2219            chunks.push((chunk.text.to_string(), chunk.diagnostic_severity));
2220        }
2221    }
2222    chunks
2223}
2224
2225#[gpui2::test(iterations = 10)]
2226async fn test_definition(cx: &mut gpui2::TestAppContext) {
2227    init_test(cx);
2228
2229    let mut language = Language::new(
2230        LanguageConfig {
2231            name: "Rust".into(),
2232            path_suffixes: vec!["rs".to_string()],
2233            ..Default::default()
2234        },
2235        Some(tree_sitter_rust::language()),
2236    );
2237    let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
2238
2239    let fs = FakeFs::new(cx.executor().clone());
2240    fs.insert_tree(
2241        "/dir",
2242        json!({
2243            "a.rs": "const fn a() { A }",
2244            "b.rs": "const y: i32 = crate::a()",
2245        }),
2246    )
2247    .await;
2248
2249    let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await;
2250    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
2251
2252    let buffer = project
2253        .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
2254        .await
2255        .unwrap();
2256
2257    let fake_server = fake_servers.next().await.unwrap();
2258    fake_server.handle_request::<lsp2::request::GotoDefinition, _, _>(|params, _| async move {
2259        let params = params.text_document_position_params;
2260        assert_eq!(
2261            params.text_document.uri.to_file_path().unwrap(),
2262            Path::new("/dir/b.rs"),
2263        );
2264        assert_eq!(params.position, lsp2::Position::new(0, 22));
2265
2266        Ok(Some(lsp2::GotoDefinitionResponse::Scalar(
2267            lsp2::Location::new(
2268                lsp2::Url::from_file_path("/dir/a.rs").unwrap(),
2269                lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)),
2270            ),
2271        )))
2272    });
2273
2274    let mut definitions = project
2275        .update(cx, |project, cx| project.definition(&buffer, 22, cx))
2276        .await
2277        .unwrap();
2278
2279    // Assert no new language server started
2280    cx.executor().run_until_parked();
2281    assert!(fake_servers.try_next().is_err());
2282
2283    assert_eq!(definitions.len(), 1);
2284    let definition = definitions.pop().unwrap();
2285    cx.update(|cx| {
2286        let target_buffer = definition.target.buffer.read(cx);
2287        assert_eq!(
2288            target_buffer
2289                .file()
2290                .unwrap()
2291                .as_local()
2292                .unwrap()
2293                .abs_path(cx),
2294            Path::new("/dir/a.rs"),
2295        );
2296        assert_eq!(definition.target.range.to_offset(target_buffer), 9..10);
2297        assert_eq!(
2298            list_worktrees(&project, cx),
2299            [("/dir/b.rs".as_ref(), true), ("/dir/a.rs".as_ref(), false)]
2300        );
2301
2302        drop(definition);
2303    });
2304    cx.update(|cx| {
2305        assert_eq!(list_worktrees(&project, cx), [("/dir/b.rs".as_ref(), true)]);
2306    });
2307
2308    fn list_worktrees<'a>(
2309        project: &'a Model<Project>,
2310        cx: &'a AppContext,
2311    ) -> Vec<(&'a Path, bool)> {
2312        project
2313            .read(cx)
2314            .worktrees()
2315            .map(|worktree| {
2316                let worktree = worktree.read(cx);
2317                (
2318                    worktree.as_local().unwrap().abs_path().as_ref(),
2319                    worktree.is_visible(),
2320                )
2321            })
2322            .collect::<Vec<_>>()
2323    }
2324}
2325
2326#[gpui2::test]
2327async fn test_completions_without_edit_ranges(cx: &mut gpui2::TestAppContext) {
2328    init_test(cx);
2329
2330    let mut language = Language::new(
2331        LanguageConfig {
2332            name: "TypeScript".into(),
2333            path_suffixes: vec!["ts".to_string()],
2334            ..Default::default()
2335        },
2336        Some(tree_sitter_typescript::language_typescript()),
2337    );
2338    let mut fake_language_servers = language
2339        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2340            capabilities: lsp2::ServerCapabilities {
2341                completion_provider: Some(lsp2::CompletionOptions {
2342                    trigger_characters: Some(vec![":".to_string()]),
2343                    ..Default::default()
2344                }),
2345                ..Default::default()
2346            },
2347            ..Default::default()
2348        }))
2349        .await;
2350
2351    let fs = FakeFs::new(cx.executor().clone());
2352    fs.insert_tree(
2353        "/dir",
2354        json!({
2355            "a.ts": "",
2356        }),
2357    )
2358    .await;
2359
2360    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2361    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
2362    let buffer = project
2363        .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
2364        .await
2365        .unwrap();
2366
2367    let fake_server = fake_language_servers.next().await.unwrap();
2368
2369    let text = "let a = b.fqn";
2370    buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
2371    let completions = project.update(cx, |project, cx| {
2372        project.completions(&buffer, text.len(), cx)
2373    });
2374
2375    fake_server
2376        .handle_request::<lsp2::request::Completion, _, _>(|_, _| async move {
2377            Ok(Some(lsp2::CompletionResponse::Array(vec![
2378                lsp2::CompletionItem {
2379                    label: "fullyQualifiedName?".into(),
2380                    insert_text: Some("fullyQualifiedName".into()),
2381                    ..Default::default()
2382                },
2383            ])))
2384        })
2385        .next()
2386        .await;
2387    let completions = completions.await.unwrap();
2388    let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
2389    assert_eq!(completions.len(), 1);
2390    assert_eq!(completions[0].new_text, "fullyQualifiedName");
2391    assert_eq!(
2392        completions[0].old_range.to_offset(&snapshot),
2393        text.len() - 3..text.len()
2394    );
2395
2396    let text = "let a = \"atoms/cmp\"";
2397    buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
2398    let completions = project.update(cx, |project, cx| {
2399        project.completions(&buffer, text.len() - 1, cx)
2400    });
2401
2402    fake_server
2403        .handle_request::<lsp2::request::Completion, _, _>(|_, _| async move {
2404            Ok(Some(lsp2::CompletionResponse::Array(vec![
2405                lsp2::CompletionItem {
2406                    label: "component".into(),
2407                    ..Default::default()
2408                },
2409            ])))
2410        })
2411        .next()
2412        .await;
2413    let completions = completions.await.unwrap();
2414    let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
2415    assert_eq!(completions.len(), 1);
2416    assert_eq!(completions[0].new_text, "component");
2417    assert_eq!(
2418        completions[0].old_range.to_offset(&snapshot),
2419        text.len() - 4..text.len() - 1
2420    );
2421}
2422
2423#[gpui2::test]
2424async fn test_completions_with_carriage_returns(cx: &mut gpui2::TestAppContext) {
2425    init_test(cx);
2426
2427    let mut language = Language::new(
2428        LanguageConfig {
2429            name: "TypeScript".into(),
2430            path_suffixes: vec!["ts".to_string()],
2431            ..Default::default()
2432        },
2433        Some(tree_sitter_typescript::language_typescript()),
2434    );
2435    let mut fake_language_servers = language
2436        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2437            capabilities: lsp2::ServerCapabilities {
2438                completion_provider: Some(lsp2::CompletionOptions {
2439                    trigger_characters: Some(vec![":".to_string()]),
2440                    ..Default::default()
2441                }),
2442                ..Default::default()
2443            },
2444            ..Default::default()
2445        }))
2446        .await;
2447
2448    let fs = FakeFs::new(cx.executor().clone());
2449    fs.insert_tree(
2450        "/dir",
2451        json!({
2452            "a.ts": "",
2453        }),
2454    )
2455    .await;
2456
2457    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2458    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
2459    let buffer = project
2460        .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
2461        .await
2462        .unwrap();
2463
2464    let fake_server = fake_language_servers.next().await.unwrap();
2465
2466    let text = "let a = b.fqn";
2467    buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
2468    let completions = project.update(cx, |project, cx| {
2469        project.completions(&buffer, text.len(), cx)
2470    });
2471
2472    fake_server
2473        .handle_request::<lsp2::request::Completion, _, _>(|_, _| async move {
2474            Ok(Some(lsp2::CompletionResponse::Array(vec![
2475                lsp2::CompletionItem {
2476                    label: "fullyQualifiedName?".into(),
2477                    insert_text: Some("fully\rQualified\r\nName".into()),
2478                    ..Default::default()
2479                },
2480            ])))
2481        })
2482        .next()
2483        .await;
2484    let completions = completions.await.unwrap();
2485    assert_eq!(completions.len(), 1);
2486    assert_eq!(completions[0].new_text, "fully\nQualified\nName");
2487}
2488
2489#[gpui2::test(iterations = 10)]
2490async fn test_apply_code_actions_with_commands(cx: &mut gpui2::TestAppContext) {
2491    init_test(cx);
2492
2493    let mut language = Language::new(
2494        LanguageConfig {
2495            name: "TypeScript".into(),
2496            path_suffixes: vec!["ts".to_string()],
2497            ..Default::default()
2498        },
2499        None,
2500    );
2501    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2502
2503    let fs = FakeFs::new(cx.executor().clone());
2504    fs.insert_tree(
2505        "/dir",
2506        json!({
2507            "a.ts": "a",
2508        }),
2509    )
2510    .await;
2511
2512    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2513    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
2514    let buffer = project
2515        .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
2516        .await
2517        .unwrap();
2518
2519    let fake_server = fake_language_servers.next().await.unwrap();
2520
2521    // Language server returns code actions that contain commands, and not edits.
2522    let actions = project.update(cx, |project, cx| project.code_actions(&buffer, 0..0, cx));
2523    fake_server
2524        .handle_request::<lsp2::request::CodeActionRequest, _, _>(|_, _| async move {
2525            Ok(Some(vec![
2526                lsp2::CodeActionOrCommand::CodeAction(lsp2::CodeAction {
2527                    title: "The code action".into(),
2528                    command: Some(lsp2::Command {
2529                        title: "The command".into(),
2530                        command: "_the/command".into(),
2531                        arguments: Some(vec![json!("the-argument")]),
2532                    }),
2533                    ..Default::default()
2534                }),
2535                lsp2::CodeActionOrCommand::CodeAction(lsp2::CodeAction {
2536                    title: "two".into(),
2537                    ..Default::default()
2538                }),
2539            ]))
2540        })
2541        .next()
2542        .await;
2543
2544    let action = actions.await.unwrap()[0].clone();
2545    let apply = project.update(cx, |project, cx| {
2546        project.apply_code_action(buffer.clone(), action, true, cx)
2547    });
2548
2549    // Resolving the code action does not populate its edits. In absence of
2550    // edits, we must execute the given command.
2551    fake_server.handle_request::<lsp2::request::CodeActionResolveRequest, _, _>(
2552        |action, _| async move { Ok(action) },
2553    );
2554
2555    // While executing the command, the language server sends the editor
2556    // a `workspaceEdit` request.
2557    fake_server
2558        .handle_request::<lsp2::request::ExecuteCommand, _, _>({
2559            let fake = fake_server.clone();
2560            move |params, _| {
2561                assert_eq!(params.command, "_the/command");
2562                let fake = fake.clone();
2563                async move {
2564                    fake.server
2565                        .request::<lsp2::request::ApplyWorkspaceEdit>(
2566                            lsp2::ApplyWorkspaceEditParams {
2567                                label: None,
2568                                edit: lsp2::WorkspaceEdit {
2569                                    changes: Some(
2570                                        [(
2571                                            lsp2::Url::from_file_path("/dir/a.ts").unwrap(),
2572                                            vec![lsp2::TextEdit {
2573                                                range: lsp2::Range::new(
2574                                                    lsp2::Position::new(0, 0),
2575                                                    lsp2::Position::new(0, 0),
2576                                                ),
2577                                                new_text: "X".into(),
2578                                            }],
2579                                        )]
2580                                        .into_iter()
2581                                        .collect(),
2582                                    ),
2583                                    ..Default::default()
2584                                },
2585                            },
2586                        )
2587                        .await
2588                        .unwrap();
2589                    Ok(Some(json!(null)))
2590                }
2591            }
2592        })
2593        .next()
2594        .await;
2595
2596    // Applying the code action returns a project transaction containing the edits
2597    // sent by the language server in its `workspaceEdit` request.
2598    let transaction = apply.await.unwrap();
2599    assert!(transaction.0.contains_key(&buffer));
2600    buffer.update(cx, |buffer, cx| {
2601        assert_eq!(buffer.text(), "Xa");
2602        buffer.undo(cx);
2603        assert_eq!(buffer.text(), "a");
2604    });
2605}
2606
2607#[gpui2::test(iterations = 10)]
2608async fn test_save_file(cx: &mut gpui2::TestAppContext) {
2609    init_test(cx);
2610
2611    let fs = FakeFs::new(cx.executor().clone());
2612    fs.insert_tree(
2613        "/dir",
2614        json!({
2615            "file1": "the old contents",
2616        }),
2617    )
2618    .await;
2619
2620    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2621    let buffer = project
2622        .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2623        .await
2624        .unwrap();
2625    buffer.update(cx, |buffer, cx| {
2626        assert_eq!(buffer.text(), "the old contents");
2627        buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
2628    });
2629
2630    project
2631        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2632        .await
2633        .unwrap();
2634
2635    let new_text = fs.load(Path::new("/dir/file1")).await.unwrap();
2636    assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text()));
2637}
2638
2639#[gpui2::test]
2640async fn test_save_in_single_file_worktree(cx: &mut gpui2::TestAppContext) {
2641    init_test(cx);
2642
2643    let fs = FakeFs::new(cx.executor().clone());
2644    fs.insert_tree(
2645        "/dir",
2646        json!({
2647            "file1": "the old contents",
2648        }),
2649    )
2650    .await;
2651
2652    let project = Project::test(fs.clone(), ["/dir/file1".as_ref()], cx).await;
2653    let buffer = project
2654        .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2655        .await
2656        .unwrap();
2657    buffer.update(cx, |buffer, cx| {
2658        buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
2659    });
2660
2661    project
2662        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2663        .await
2664        .unwrap();
2665
2666    let new_text = fs.load(Path::new("/dir/file1")).await.unwrap();
2667    assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text()));
2668}
2669
2670#[gpui2::test]
2671async fn test_save_as(cx: &mut gpui2::TestAppContext) {
2672    init_test(cx);
2673
2674    let fs = FakeFs::new(cx.executor().clone());
2675    fs.insert_tree("/dir", json!({})).await;
2676
2677    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2678
2679    let languages = project.update(cx, |project, _| project.languages().clone());
2680    languages.register(
2681        "/some/path",
2682        LanguageConfig {
2683            name: "Rust".into(),
2684            path_suffixes: vec!["rs".into()],
2685            ..Default::default()
2686        },
2687        tree_sitter_rust::language(),
2688        vec![],
2689        |_| Default::default(),
2690    );
2691
2692    let buffer = project.update(cx, |project, cx| {
2693        project.create_buffer("", None, cx).unwrap()
2694    });
2695    buffer.update(cx, |buffer, cx| {
2696        buffer.edit([(0..0, "abc")], None, cx);
2697        assert!(buffer.is_dirty());
2698        assert!(!buffer.has_conflict());
2699        assert_eq!(buffer.language().unwrap().name().as_ref(), "Plain Text");
2700    });
2701    project
2702        .update(cx, |project, cx| {
2703            project.save_buffer_as(buffer.clone(), "/dir/file1.rs".into(), cx)
2704        })
2705        .await
2706        .unwrap();
2707    assert_eq!(fs.load(Path::new("/dir/file1.rs")).await.unwrap(), "abc");
2708
2709    cx.executor().run_until_parked();
2710    buffer.update(cx, |buffer, cx| {
2711        assert_eq!(
2712            buffer.file().unwrap().full_path(cx),
2713            Path::new("dir/file1.rs")
2714        );
2715        assert!(!buffer.is_dirty());
2716        assert!(!buffer.has_conflict());
2717        assert_eq!(buffer.language().unwrap().name().as_ref(), "Rust");
2718    });
2719
2720    let opened_buffer = project
2721        .update(cx, |project, cx| {
2722            project.open_local_buffer("/dir/file1.rs", cx)
2723        })
2724        .await
2725        .unwrap();
2726    assert_eq!(opened_buffer, buffer);
2727}
2728
2729#[gpui2::test(retries = 5)]
2730async fn test_rescan_and_remote_updates(cx: &mut gpui2::TestAppContext) {
2731    init_test(cx);
2732    cx.executor().allow_parking();
2733
2734    let dir = temp_tree(json!({
2735        "a": {
2736            "file1": "",
2737            "file2": "",
2738            "file3": "",
2739        },
2740        "b": {
2741            "c": {
2742                "file4": "",
2743                "file5": "",
2744            }
2745        }
2746    }));
2747
2748    let project = Project::test(Arc::new(RealFs), [dir.path()], cx).await;
2749    let rpc = project.update(cx, |p, _| p.client.clone());
2750
2751    let buffer_for_path = |path: &'static str, cx: &mut gpui2::TestAppContext| {
2752        let buffer = project.update(cx, |p, cx| p.open_local_buffer(dir.path().join(path), cx));
2753        async move { buffer.await.unwrap() }
2754    };
2755    let id_for_path = |path: &'static str, cx: &mut gpui2::TestAppContext| {
2756        project.update(cx, |project, cx| {
2757            let tree = project.worktrees().next().unwrap();
2758            tree.read(cx)
2759                .entry_for_path(path)
2760                .unwrap_or_else(|| panic!("no entry for path {}", path))
2761                .id
2762        })
2763    };
2764
2765    let buffer2 = buffer_for_path("a/file2", cx).await;
2766    let buffer3 = buffer_for_path("a/file3", cx).await;
2767    let buffer4 = buffer_for_path("b/c/file4", cx).await;
2768    let buffer5 = buffer_for_path("b/c/file5", cx).await;
2769
2770    let file2_id = id_for_path("a/file2", cx);
2771    let file3_id = id_for_path("a/file3", cx);
2772    let file4_id = id_for_path("b/c/file4", cx);
2773
2774    // Create a remote copy of this worktree.
2775    let tree = project.update(cx, |project, _| project.worktrees().next().unwrap());
2776
2777    let metadata = tree.update(cx, |tree, _| tree.as_local().unwrap().metadata_proto());
2778
2779    let updates = Arc::new(Mutex::new(Vec::new()));
2780    tree.update(cx, |tree, cx| {
2781        let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, {
2782            let updates = updates.clone();
2783            move |update| {
2784                updates.lock().push(update);
2785                async { true }
2786            }
2787        });
2788    });
2789
2790    let remote = cx.update(|cx| Worktree::remote(1, 1, metadata, rpc.clone(), cx));
2791
2792    cx.executor().run_until_parked();
2793
2794    cx.update(|cx| {
2795        assert!(!buffer2.read(cx).is_dirty());
2796        assert!(!buffer3.read(cx).is_dirty());
2797        assert!(!buffer4.read(cx).is_dirty());
2798        assert!(!buffer5.read(cx).is_dirty());
2799    });
2800
2801    // Rename and delete files and directories.
2802    tree.flush_fs_events(cx).await;
2803    std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap();
2804    std::fs::remove_file(dir.path().join("b/c/file5")).unwrap();
2805    std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap();
2806    std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap();
2807    tree.flush_fs_events(cx).await;
2808
2809    let expected_paths = vec![
2810        "a",
2811        "a/file1",
2812        "a/file2.new",
2813        "b",
2814        "d",
2815        "d/file3",
2816        "d/file4",
2817    ];
2818
2819    cx.update(|app| {
2820        assert_eq!(
2821            tree.read(app)
2822                .paths()
2823                .map(|p| p.to_str().unwrap())
2824                .collect::<Vec<_>>(),
2825            expected_paths
2826        );
2827    });
2828
2829    assert_eq!(id_for_path("a/file2.new", cx), file2_id);
2830    assert_eq!(id_for_path("d/file3", cx), file3_id);
2831    assert_eq!(id_for_path("d/file4", cx), file4_id);
2832
2833    cx.update(|cx| {
2834        assert_eq!(
2835            buffer2.read(cx).file().unwrap().path().as_ref(),
2836            Path::new("a/file2.new")
2837        );
2838        assert_eq!(
2839            buffer3.read(cx).file().unwrap().path().as_ref(),
2840            Path::new("d/file3")
2841        );
2842        assert_eq!(
2843            buffer4.read(cx).file().unwrap().path().as_ref(),
2844            Path::new("d/file4")
2845        );
2846        assert_eq!(
2847            buffer5.read(cx).file().unwrap().path().as_ref(),
2848            Path::new("b/c/file5")
2849        );
2850
2851        assert!(!buffer2.read(cx).file().unwrap().is_deleted());
2852        assert!(!buffer3.read(cx).file().unwrap().is_deleted());
2853        assert!(!buffer4.read(cx).file().unwrap().is_deleted());
2854        assert!(buffer5.read(cx).file().unwrap().is_deleted());
2855    });
2856
2857    // Update the remote worktree. Check that it becomes consistent with the
2858    // local worktree.
2859    cx.executor().run_until_parked();
2860
2861    remote.update(cx, |remote, _| {
2862        for update in updates.lock().drain(..) {
2863            remote.as_remote_mut().unwrap().update_from_remote(update);
2864        }
2865    });
2866    cx.executor().run_until_parked();
2867    remote.update(cx, |remote, _| {
2868        assert_eq!(
2869            remote
2870                .paths()
2871                .map(|p| p.to_str().unwrap())
2872                .collect::<Vec<_>>(),
2873            expected_paths
2874        );
2875    });
2876}
2877
2878#[gpui2::test(iterations = 10)]
2879async fn test_buffer_identity_across_renames(cx: &mut gpui2::TestAppContext) {
2880    init_test(cx);
2881
2882    let fs = FakeFs::new(cx.executor().clone());
2883    fs.insert_tree(
2884        "/dir",
2885        json!({
2886            "a": {
2887                "file1": "",
2888            }
2889        }),
2890    )
2891    .await;
2892
2893    let project = Project::test(fs, [Path::new("/dir")], cx).await;
2894    let tree = project.update(cx, |project, _| project.worktrees().next().unwrap());
2895    let tree_id = tree.update(cx, |tree, _| tree.id());
2896
2897    let id_for_path = |path: &'static str, cx: &mut gpui2::TestAppContext| {
2898        project.update(cx, |project, cx| {
2899            let tree = project.worktrees().next().unwrap();
2900            tree.read(cx)
2901                .entry_for_path(path)
2902                .unwrap_or_else(|| panic!("no entry for path {}", path))
2903                .id
2904        })
2905    };
2906
2907    let dir_id = id_for_path("a", cx);
2908    let file_id = id_for_path("a/file1", cx);
2909    let buffer = project
2910        .update(cx, |p, cx| p.open_buffer((tree_id, "a/file1"), cx))
2911        .await
2912        .unwrap();
2913    buffer.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
2914
2915    project
2916        .update(cx, |project, cx| {
2917            project.rename_entry(dir_id, Path::new("b"), cx)
2918        })
2919        .unwrap()
2920        .await
2921        .unwrap();
2922    cx.executor().run_until_parked();
2923
2924    assert_eq!(id_for_path("b", cx), dir_id);
2925    assert_eq!(id_for_path("b/file1", cx), file_id);
2926    buffer.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
2927}
2928
2929#[gpui2::test]
2930async fn test_buffer_deduping(cx: &mut gpui2::TestAppContext) {
2931    init_test(cx);
2932
2933    let fs = FakeFs::new(cx.executor().clone());
2934    fs.insert_tree(
2935        "/dir",
2936        json!({
2937            "a.txt": "a-contents",
2938            "b.txt": "b-contents",
2939        }),
2940    )
2941    .await;
2942
2943    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2944
2945    // Spawn multiple tasks to open paths, repeating some paths.
2946    let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| {
2947        (
2948            p.open_local_buffer("/dir/a.txt", cx),
2949            p.open_local_buffer("/dir/b.txt", cx),
2950            p.open_local_buffer("/dir/a.txt", cx),
2951        )
2952    });
2953
2954    let buffer_a_1 = buffer_a_1.await.unwrap();
2955    let buffer_a_2 = buffer_a_2.await.unwrap();
2956    let buffer_b = buffer_b.await.unwrap();
2957    assert_eq!(buffer_a_1.update(cx, |b, _| b.text()), "a-contents");
2958    assert_eq!(buffer_b.update(cx, |b, _| b.text()), "b-contents");
2959
2960    // There is only one buffer per path.
2961    let buffer_a_id = buffer_a_1.entity_id();
2962    assert_eq!(buffer_a_2.entity_id(), buffer_a_id);
2963
2964    // Open the same path again while it is still open.
2965    drop(buffer_a_1);
2966    let buffer_a_3 = project
2967        .update(cx, |p, cx| p.open_local_buffer("/dir/a.txt", cx))
2968        .await
2969        .unwrap();
2970
2971    // There's still only one buffer per path.
2972    assert_eq!(buffer_a_3.entity_id(), buffer_a_id);
2973}
2974
2975#[gpui2::test]
2976async fn test_buffer_is_dirty(cx: &mut gpui2::TestAppContext) {
2977    init_test(cx);
2978
2979    let fs = FakeFs::new(cx.executor().clone());
2980    fs.insert_tree(
2981        "/dir",
2982        json!({
2983            "file1": "abc",
2984            "file2": "def",
2985            "file3": "ghi",
2986        }),
2987    )
2988    .await;
2989
2990    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2991
2992    let buffer1 = project
2993        .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2994        .await
2995        .unwrap();
2996    let events = Arc::new(Mutex::new(Vec::new()));
2997
2998    // initially, the buffer isn't dirty.
2999    buffer1.update(cx, |buffer, cx| {
3000        cx.subscribe(&buffer1, {
3001            let events = events.clone();
3002            move |_, _, event, _| match event {
3003                BufferEvent::Operation(_) => {}
3004                _ => events.lock().push(event.clone()),
3005            }
3006        })
3007        .detach();
3008
3009        assert!(!buffer.is_dirty());
3010        assert!(events.lock().is_empty());
3011
3012        buffer.edit([(1..2, "")], None, cx);
3013    });
3014
3015    // after the first edit, the buffer is dirty, and emits a dirtied event.
3016    buffer1.update(cx, |buffer, cx| {
3017        assert!(buffer.text() == "ac");
3018        assert!(buffer.is_dirty());
3019        assert_eq!(
3020            *events.lock(),
3021            &[language2::Event::Edited, language2::Event::DirtyChanged]
3022        );
3023        events.lock().clear();
3024        buffer.did_save(
3025            buffer.version(),
3026            buffer.as_rope().fingerprint(),
3027            buffer.file().unwrap().mtime(),
3028            cx,
3029        );
3030    });
3031
3032    // after saving, the buffer is not dirty, and emits a saved event.
3033    buffer1.update(cx, |buffer, cx| {
3034        assert!(!buffer.is_dirty());
3035        assert_eq!(*events.lock(), &[language2::Event::Saved]);
3036        events.lock().clear();
3037
3038        buffer.edit([(1..1, "B")], None, cx);
3039        buffer.edit([(2..2, "D")], None, cx);
3040    });
3041
3042    // after editing again, the buffer is dirty, and emits another dirty event.
3043    buffer1.update(cx, |buffer, cx| {
3044        assert!(buffer.text() == "aBDc");
3045        assert!(buffer.is_dirty());
3046        assert_eq!(
3047            *events.lock(),
3048            &[
3049                language2::Event::Edited,
3050                language2::Event::DirtyChanged,
3051                language2::Event::Edited,
3052            ],
3053        );
3054        events.lock().clear();
3055
3056        // After restoring the buffer to its previously-saved state,
3057        // the buffer is not considered dirty anymore.
3058        buffer.edit([(1..3, "")], None, cx);
3059        assert!(buffer.text() == "ac");
3060        assert!(!buffer.is_dirty());
3061    });
3062
3063    assert_eq!(
3064        *events.lock(),
3065        &[language2::Event::Edited, language2::Event::DirtyChanged]
3066    );
3067
3068    // When a file is deleted, the buffer is considered dirty.
3069    let events = Arc::new(Mutex::new(Vec::new()));
3070    let buffer2 = project
3071        .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx))
3072        .await
3073        .unwrap();
3074    buffer2.update(cx, |_, cx| {
3075        cx.subscribe(&buffer2, {
3076            let events = events.clone();
3077            move |_, _, event, _| events.lock().push(event.clone())
3078        })
3079        .detach();
3080    });
3081
3082    fs.remove_file("/dir/file2".as_ref(), Default::default())
3083        .await
3084        .unwrap();
3085    cx.executor().run_until_parked();
3086    buffer2.update(cx, |buffer, _| assert!(buffer.is_dirty()));
3087    assert_eq!(
3088        *events.lock(),
3089        &[
3090            language2::Event::DirtyChanged,
3091            language2::Event::FileHandleChanged
3092        ]
3093    );
3094
3095    // When a file is already dirty when deleted, we don't emit a Dirtied event.
3096    let events = Arc::new(Mutex::new(Vec::new()));
3097    let buffer3 = project
3098        .update(cx, |p, cx| p.open_local_buffer("/dir/file3", cx))
3099        .await
3100        .unwrap();
3101    buffer3.update(cx, |_, cx| {
3102        cx.subscribe(&buffer3, {
3103            let events = events.clone();
3104            move |_, _, event, _| events.lock().push(event.clone())
3105        })
3106        .detach();
3107    });
3108
3109    buffer3.update(cx, |buffer, cx| {
3110        buffer.edit([(0..0, "x")], None, cx);
3111    });
3112    events.lock().clear();
3113    fs.remove_file("/dir/file3".as_ref(), Default::default())
3114        .await
3115        .unwrap();
3116    cx.executor().run_until_parked();
3117    assert_eq!(*events.lock(), &[language2::Event::FileHandleChanged]);
3118    cx.update(|cx| assert!(buffer3.read(cx).is_dirty()));
3119}
3120
3121#[gpui2::test]
3122async fn test_buffer_file_changes_on_disk(cx: &mut gpui2::TestAppContext) {
3123    init_test(cx);
3124
3125    let initial_contents = "aaa\nbbbbb\nc\n";
3126    let fs = FakeFs::new(cx.executor().clone());
3127    fs.insert_tree(
3128        "/dir",
3129        json!({
3130            "the-file": initial_contents,
3131        }),
3132    )
3133    .await;
3134    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3135    let buffer = project
3136        .update(cx, |p, cx| p.open_local_buffer("/dir/the-file", cx))
3137        .await
3138        .unwrap();
3139
3140    let anchors = (0..3)
3141        .map(|row| buffer.update(cx, |b, _| b.anchor_before(Point::new(row, 1))))
3142        .collect::<Vec<_>>();
3143
3144    // Change the file on disk, adding two new lines of text, and removing
3145    // one line.
3146    buffer.update(cx, |buffer, _| {
3147        assert!(!buffer.is_dirty());
3148        assert!(!buffer.has_conflict());
3149    });
3150    let new_contents = "AAAA\naaa\nBB\nbbbbb\n";
3151    fs.save(
3152        "/dir/the-file".as_ref(),
3153        &new_contents.into(),
3154        LineEnding::Unix,
3155    )
3156    .await
3157    .unwrap();
3158
3159    // Because the buffer was not modified, it is reloaded from disk. Its
3160    // contents are edited according to the diff between the old and new
3161    // file contents.
3162    cx.executor().run_until_parked();
3163    buffer.update(cx, |buffer, _| {
3164        assert_eq!(buffer.text(), new_contents);
3165        assert!(!buffer.is_dirty());
3166        assert!(!buffer.has_conflict());
3167
3168        let anchor_positions = anchors
3169            .iter()
3170            .map(|anchor| anchor.to_point(&*buffer))
3171            .collect::<Vec<_>>();
3172        assert_eq!(
3173            anchor_positions,
3174            [Point::new(1, 1), Point::new(3, 1), Point::new(3, 5)]
3175        );
3176    });
3177
3178    // Modify the buffer
3179    buffer.update(cx, |buffer, cx| {
3180        buffer.edit([(0..0, " ")], None, cx);
3181        assert!(buffer.is_dirty());
3182        assert!(!buffer.has_conflict());
3183    });
3184
3185    // Change the file on disk again, adding blank lines to the beginning.
3186    fs.save(
3187        "/dir/the-file".as_ref(),
3188        &"\n\n\nAAAA\naaa\nBB\nbbbbb\n".into(),
3189        LineEnding::Unix,
3190    )
3191    .await
3192    .unwrap();
3193
3194    // Because the buffer is modified, it doesn't reload from disk, but is
3195    // marked as having a conflict.
3196    cx.executor().run_until_parked();
3197    buffer.update(cx, |buffer, _| {
3198        assert!(buffer.has_conflict());
3199    });
3200}
3201
3202#[gpui2::test]
3203async fn test_buffer_line_endings(cx: &mut gpui2::TestAppContext) {
3204    init_test(cx);
3205
3206    let fs = FakeFs::new(cx.executor().clone());
3207    fs.insert_tree(
3208        "/dir",
3209        json!({
3210            "file1": "a\nb\nc\n",
3211            "file2": "one\r\ntwo\r\nthree\r\n",
3212        }),
3213    )
3214    .await;
3215
3216    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3217    let buffer1 = project
3218        .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
3219        .await
3220        .unwrap();
3221    let buffer2 = project
3222        .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx))
3223        .await
3224        .unwrap();
3225
3226    buffer1.update(cx, |buffer, _| {
3227        assert_eq!(buffer.text(), "a\nb\nc\n");
3228        assert_eq!(buffer.line_ending(), LineEnding::Unix);
3229    });
3230    buffer2.update(cx, |buffer, _| {
3231        assert_eq!(buffer.text(), "one\ntwo\nthree\n");
3232        assert_eq!(buffer.line_ending(), LineEnding::Windows);
3233    });
3234
3235    // Change a file's line endings on disk from unix to windows. The buffer's
3236    // state updates correctly.
3237    fs.save(
3238        "/dir/file1".as_ref(),
3239        &"aaa\nb\nc\n".into(),
3240        LineEnding::Windows,
3241    )
3242    .await
3243    .unwrap();
3244    cx.executor().run_until_parked();
3245    buffer1.update(cx, |buffer, _| {
3246        assert_eq!(buffer.text(), "aaa\nb\nc\n");
3247        assert_eq!(buffer.line_ending(), LineEnding::Windows);
3248    });
3249
3250    // Save a file with windows line endings. The file is written correctly.
3251    buffer2.update(cx, |buffer, cx| {
3252        buffer.set_text("one\ntwo\nthree\nfour\n", cx);
3253    });
3254    project
3255        .update(cx, |project, cx| project.save_buffer(buffer2, cx))
3256        .await
3257        .unwrap();
3258    assert_eq!(
3259        fs.load("/dir/file2".as_ref()).await.unwrap(),
3260        "one\r\ntwo\r\nthree\r\nfour\r\n",
3261    );
3262}
3263
3264#[gpui2::test]
3265async fn test_grouped_diagnostics(cx: &mut gpui2::TestAppContext) {
3266    init_test(cx);
3267
3268    let fs = FakeFs::new(cx.executor().clone());
3269    fs.insert_tree(
3270        "/the-dir",
3271        json!({
3272            "a.rs": "
3273                fn foo(mut v: Vec<usize>) {
3274                    for x in &v {
3275                        v.push(1);
3276                    }
3277                }
3278            "
3279            .unindent(),
3280        }),
3281    )
3282    .await;
3283
3284    let project = Project::test(fs.clone(), ["/the-dir".as_ref()], cx).await;
3285    let buffer = project
3286        .update(cx, |p, cx| p.open_local_buffer("/the-dir/a.rs", cx))
3287        .await
3288        .unwrap();
3289
3290    let buffer_uri = Url::from_file_path("/the-dir/a.rs").unwrap();
3291    let message = lsp2::PublishDiagnosticsParams {
3292        uri: buffer_uri.clone(),
3293        diagnostics: vec![
3294            lsp2::Diagnostic {
3295                range: lsp2::Range::new(lsp2::Position::new(1, 8), lsp2::Position::new(1, 9)),
3296                severity: Some(DiagnosticSeverity::WARNING),
3297                message: "error 1".to_string(),
3298                related_information: Some(vec![lsp2::DiagnosticRelatedInformation {
3299                    location: lsp2::Location {
3300                        uri: buffer_uri.clone(),
3301                        range: lsp2::Range::new(
3302                            lsp2::Position::new(1, 8),
3303                            lsp2::Position::new(1, 9),
3304                        ),
3305                    },
3306                    message: "error 1 hint 1".to_string(),
3307                }]),
3308                ..Default::default()
3309            },
3310            lsp2::Diagnostic {
3311                range: lsp2::Range::new(lsp2::Position::new(1, 8), lsp2::Position::new(1, 9)),
3312                severity: Some(DiagnosticSeverity::HINT),
3313                message: "error 1 hint 1".to_string(),
3314                related_information: Some(vec![lsp2::DiagnosticRelatedInformation {
3315                    location: lsp2::Location {
3316                        uri: buffer_uri.clone(),
3317                        range: lsp2::Range::new(
3318                            lsp2::Position::new(1, 8),
3319                            lsp2::Position::new(1, 9),
3320                        ),
3321                    },
3322                    message: "original diagnostic".to_string(),
3323                }]),
3324                ..Default::default()
3325            },
3326            lsp2::Diagnostic {
3327                range: lsp2::Range::new(lsp2::Position::new(2, 8), lsp2::Position::new(2, 17)),
3328                severity: Some(DiagnosticSeverity::ERROR),
3329                message: "error 2".to_string(),
3330                related_information: Some(vec![
3331                    lsp2::DiagnosticRelatedInformation {
3332                        location: lsp2::Location {
3333                            uri: buffer_uri.clone(),
3334                            range: lsp2::Range::new(
3335                                lsp2::Position::new(1, 13),
3336                                lsp2::Position::new(1, 15),
3337                            ),
3338                        },
3339                        message: "error 2 hint 1".to_string(),
3340                    },
3341                    lsp2::DiagnosticRelatedInformation {
3342                        location: lsp2::Location {
3343                            uri: buffer_uri.clone(),
3344                            range: lsp2::Range::new(
3345                                lsp2::Position::new(1, 13),
3346                                lsp2::Position::new(1, 15),
3347                            ),
3348                        },
3349                        message: "error 2 hint 2".to_string(),
3350                    },
3351                ]),
3352                ..Default::default()
3353            },
3354            lsp2::Diagnostic {
3355                range: lsp2::Range::new(lsp2::Position::new(1, 13), lsp2::Position::new(1, 15)),
3356                severity: Some(DiagnosticSeverity::HINT),
3357                message: "error 2 hint 1".to_string(),
3358                related_information: Some(vec![lsp2::DiagnosticRelatedInformation {
3359                    location: lsp2::Location {
3360                        uri: buffer_uri.clone(),
3361                        range: lsp2::Range::new(
3362                            lsp2::Position::new(2, 8),
3363                            lsp2::Position::new(2, 17),
3364                        ),
3365                    },
3366                    message: "original diagnostic".to_string(),
3367                }]),
3368                ..Default::default()
3369            },
3370            lsp2::Diagnostic {
3371                range: lsp2::Range::new(lsp2::Position::new(1, 13), lsp2::Position::new(1, 15)),
3372                severity: Some(DiagnosticSeverity::HINT),
3373                message: "error 2 hint 2".to_string(),
3374                related_information: Some(vec![lsp2::DiagnosticRelatedInformation {
3375                    location: lsp2::Location {
3376                        uri: buffer_uri,
3377                        range: lsp2::Range::new(
3378                            lsp2::Position::new(2, 8),
3379                            lsp2::Position::new(2, 17),
3380                        ),
3381                    },
3382                    message: "original diagnostic".to_string(),
3383                }]),
3384                ..Default::default()
3385            },
3386        ],
3387        version: None,
3388    };
3389
3390    project
3391        .update(cx, |p, cx| {
3392            p.update_diagnostics(LanguageServerId(0), message, &[], cx)
3393        })
3394        .unwrap();
3395    let buffer = buffer.update(cx, |buffer, _| buffer.snapshot());
3396
3397    assert_eq!(
3398        buffer
3399            .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
3400            .collect::<Vec<_>>(),
3401        &[
3402            DiagnosticEntry {
3403                range: Point::new(1, 8)..Point::new(1, 9),
3404                diagnostic: Diagnostic {
3405                    severity: DiagnosticSeverity::WARNING,
3406                    message: "error 1".to_string(),
3407                    group_id: 1,
3408                    is_primary: true,
3409                    ..Default::default()
3410                }
3411            },
3412            DiagnosticEntry {
3413                range: Point::new(1, 8)..Point::new(1, 9),
3414                diagnostic: Diagnostic {
3415                    severity: DiagnosticSeverity::HINT,
3416                    message: "error 1 hint 1".to_string(),
3417                    group_id: 1,
3418                    is_primary: false,
3419                    ..Default::default()
3420                }
3421            },
3422            DiagnosticEntry {
3423                range: Point::new(1, 13)..Point::new(1, 15),
3424                diagnostic: Diagnostic {
3425                    severity: DiagnosticSeverity::HINT,
3426                    message: "error 2 hint 1".to_string(),
3427                    group_id: 0,
3428                    is_primary: false,
3429                    ..Default::default()
3430                }
3431            },
3432            DiagnosticEntry {
3433                range: Point::new(1, 13)..Point::new(1, 15),
3434                diagnostic: Diagnostic {
3435                    severity: DiagnosticSeverity::HINT,
3436                    message: "error 2 hint 2".to_string(),
3437                    group_id: 0,
3438                    is_primary: false,
3439                    ..Default::default()
3440                }
3441            },
3442            DiagnosticEntry {
3443                range: Point::new(2, 8)..Point::new(2, 17),
3444                diagnostic: Diagnostic {
3445                    severity: DiagnosticSeverity::ERROR,
3446                    message: "error 2".to_string(),
3447                    group_id: 0,
3448                    is_primary: true,
3449                    ..Default::default()
3450                }
3451            }
3452        ]
3453    );
3454
3455    assert_eq!(
3456        buffer.diagnostic_group::<Point>(0).collect::<Vec<_>>(),
3457        &[
3458            DiagnosticEntry {
3459                range: Point::new(1, 13)..Point::new(1, 15),
3460                diagnostic: Diagnostic {
3461                    severity: DiagnosticSeverity::HINT,
3462                    message: "error 2 hint 1".to_string(),
3463                    group_id: 0,
3464                    is_primary: false,
3465                    ..Default::default()
3466                }
3467            },
3468            DiagnosticEntry {
3469                range: Point::new(1, 13)..Point::new(1, 15),
3470                diagnostic: Diagnostic {
3471                    severity: DiagnosticSeverity::HINT,
3472                    message: "error 2 hint 2".to_string(),
3473                    group_id: 0,
3474                    is_primary: false,
3475                    ..Default::default()
3476                }
3477            },
3478            DiagnosticEntry {
3479                range: Point::new(2, 8)..Point::new(2, 17),
3480                diagnostic: Diagnostic {
3481                    severity: DiagnosticSeverity::ERROR,
3482                    message: "error 2".to_string(),
3483                    group_id: 0,
3484                    is_primary: true,
3485                    ..Default::default()
3486                }
3487            }
3488        ]
3489    );
3490
3491    assert_eq!(
3492        buffer.diagnostic_group::<Point>(1).collect::<Vec<_>>(),
3493        &[
3494            DiagnosticEntry {
3495                range: Point::new(1, 8)..Point::new(1, 9),
3496                diagnostic: Diagnostic {
3497                    severity: DiagnosticSeverity::WARNING,
3498                    message: "error 1".to_string(),
3499                    group_id: 1,
3500                    is_primary: true,
3501                    ..Default::default()
3502                }
3503            },
3504            DiagnosticEntry {
3505                range: Point::new(1, 8)..Point::new(1, 9),
3506                diagnostic: Diagnostic {
3507                    severity: DiagnosticSeverity::HINT,
3508                    message: "error 1 hint 1".to_string(),
3509                    group_id: 1,
3510                    is_primary: false,
3511                    ..Default::default()
3512                }
3513            },
3514        ]
3515    );
3516}
3517
3518#[gpui2::test]
3519async fn test_rename(cx: &mut gpui2::TestAppContext) {
3520    init_test(cx);
3521
3522    let mut language = Language::new(
3523        LanguageConfig {
3524            name: "Rust".into(),
3525            path_suffixes: vec!["rs".to_string()],
3526            ..Default::default()
3527        },
3528        Some(tree_sitter_rust::language()),
3529    );
3530    let mut fake_servers = language
3531        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3532            capabilities: lsp2::ServerCapabilities {
3533                rename_provider: Some(lsp2::OneOf::Right(lsp2::RenameOptions {
3534                    prepare_provider: Some(true),
3535                    work_done_progress_options: Default::default(),
3536                })),
3537                ..Default::default()
3538            },
3539            ..Default::default()
3540        }))
3541        .await;
3542
3543    let fs = FakeFs::new(cx.executor().clone());
3544    fs.insert_tree(
3545        "/dir",
3546        json!({
3547            "one.rs": "const ONE: usize = 1;",
3548            "two.rs": "const TWO: usize = one::ONE + one::ONE;"
3549        }),
3550    )
3551    .await;
3552
3553    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3554    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
3555    let buffer = project
3556        .update(cx, |project, cx| {
3557            project.open_local_buffer("/dir/one.rs", cx)
3558        })
3559        .await
3560        .unwrap();
3561
3562    let fake_server = fake_servers.next().await.unwrap();
3563
3564    let response = project.update(cx, |project, cx| {
3565        project.prepare_rename(buffer.clone(), 7, cx)
3566    });
3567    fake_server
3568        .handle_request::<lsp2::request::PrepareRenameRequest, _, _>(|params, _| async move {
3569            assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
3570            assert_eq!(params.position, lsp2::Position::new(0, 7));
3571            Ok(Some(lsp2::PrepareRenameResponse::Range(lsp2::Range::new(
3572                lsp2::Position::new(0, 6),
3573                lsp2::Position::new(0, 9),
3574            ))))
3575        })
3576        .next()
3577        .await
3578        .unwrap();
3579    let range = response.await.unwrap().unwrap();
3580    let range = buffer.update(cx, |buffer, _| range.to_offset(buffer));
3581    assert_eq!(range, 6..9);
3582
3583    let response = project.update(cx, |project, cx| {
3584        project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx)
3585    });
3586    fake_server
3587        .handle_request::<lsp2::request::Rename, _, _>(|params, _| async move {
3588            assert_eq!(
3589                params.text_document_position.text_document.uri.as_str(),
3590                "file:///dir/one.rs"
3591            );
3592            assert_eq!(
3593                params.text_document_position.position,
3594                lsp2::Position::new(0, 7)
3595            );
3596            assert_eq!(params.new_name, "THREE");
3597            Ok(Some(lsp2::WorkspaceEdit {
3598                changes: Some(
3599                    [
3600                        (
3601                            lsp2::Url::from_file_path("/dir/one.rs").unwrap(),
3602                            vec![lsp2::TextEdit::new(
3603                                lsp2::Range::new(
3604                                    lsp2::Position::new(0, 6),
3605                                    lsp2::Position::new(0, 9),
3606                                ),
3607                                "THREE".to_string(),
3608                            )],
3609                        ),
3610                        (
3611                            lsp2::Url::from_file_path("/dir/two.rs").unwrap(),
3612                            vec![
3613                                lsp2::TextEdit::new(
3614                                    lsp2::Range::new(
3615                                        lsp2::Position::new(0, 24),
3616                                        lsp2::Position::new(0, 27),
3617                                    ),
3618                                    "THREE".to_string(),
3619                                ),
3620                                lsp2::TextEdit::new(
3621                                    lsp2::Range::new(
3622                                        lsp2::Position::new(0, 35),
3623                                        lsp2::Position::new(0, 38),
3624                                    ),
3625                                    "THREE".to_string(),
3626                                ),
3627                            ],
3628                        ),
3629                    ]
3630                    .into_iter()
3631                    .collect(),
3632                ),
3633                ..Default::default()
3634            }))
3635        })
3636        .next()
3637        .await
3638        .unwrap();
3639    let mut transaction = response.await.unwrap().0;
3640    assert_eq!(transaction.len(), 2);
3641    assert_eq!(
3642        transaction
3643            .remove_entry(&buffer)
3644            .unwrap()
3645            .0
3646            .update(cx, |buffer, _| buffer.text()),
3647        "const THREE: usize = 1;"
3648    );
3649    assert_eq!(
3650        transaction
3651            .into_keys()
3652            .next()
3653            .unwrap()
3654            .update(cx, |buffer, _| buffer.text()),
3655        "const TWO: usize = one::THREE + one::THREE;"
3656    );
3657}
3658
3659#[gpui2::test]
3660async fn test_search(cx: &mut gpui2::TestAppContext) {
3661    init_test(cx);
3662
3663    let fs = FakeFs::new(cx.executor().clone());
3664    fs.insert_tree(
3665        "/dir",
3666        json!({
3667            "one.rs": "const ONE: usize = 1;",
3668            "two.rs": "const TWO: usize = one::ONE + one::ONE;",
3669            "three.rs": "const THREE: usize = one::ONE + two::TWO;",
3670            "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
3671        }),
3672    )
3673    .await;
3674    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3675    assert_eq!(
3676        search(
3677            &project,
3678            SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(),
3679            cx
3680        )
3681        .await
3682        .unwrap(),
3683        HashMap::from_iter([
3684            ("two.rs".to_string(), vec![6..9]),
3685            ("three.rs".to_string(), vec![37..40])
3686        ])
3687    );
3688
3689    let buffer_4 = project
3690        .update(cx, |project, cx| {
3691            project.open_local_buffer("/dir/four.rs", cx)
3692        })
3693        .await
3694        .unwrap();
3695    buffer_4.update(cx, |buffer, cx| {
3696        let text = "two::TWO";
3697        buffer.edit([(20..28, text), (31..43, text)], None, cx);
3698    });
3699
3700    assert_eq!(
3701        search(
3702            &project,
3703            SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(),
3704            cx
3705        )
3706        .await
3707        .unwrap(),
3708        HashMap::from_iter([
3709            ("two.rs".to_string(), vec![6..9]),
3710            ("three.rs".to_string(), vec![37..40]),
3711            ("four.rs".to_string(), vec![25..28, 36..39])
3712        ])
3713    );
3714}
3715
3716#[gpui2::test]
3717async fn test_search_with_inclusions(cx: &mut gpui2::TestAppContext) {
3718    init_test(cx);
3719
3720    let search_query = "file";
3721
3722    let fs = FakeFs::new(cx.executor().clone());
3723    fs.insert_tree(
3724        "/dir",
3725        json!({
3726            "one.rs": r#"// Rust file one"#,
3727            "one.ts": r#"// TypeScript file one"#,
3728            "two.rs": r#"// Rust file two"#,
3729            "two.ts": r#"// TypeScript file two"#,
3730        }),
3731    )
3732    .await;
3733    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3734
3735    assert!(
3736        search(
3737            &project,
3738            SearchQuery::text(
3739                search_query,
3740                false,
3741                true,
3742                vec![PathMatcher::new("*.odd").unwrap()],
3743                Vec::new()
3744            )
3745            .unwrap(),
3746            cx
3747        )
3748        .await
3749        .unwrap()
3750        .is_empty(),
3751        "If no inclusions match, no files should be returned"
3752    );
3753
3754    assert_eq!(
3755        search(
3756            &project,
3757            SearchQuery::text(
3758                search_query,
3759                false,
3760                true,
3761                vec![PathMatcher::new("*.rs").unwrap()],
3762                Vec::new()
3763            )
3764            .unwrap(),
3765            cx
3766        )
3767        .await
3768        .unwrap(),
3769        HashMap::from_iter([
3770            ("one.rs".to_string(), vec![8..12]),
3771            ("two.rs".to_string(), vec![8..12]),
3772        ]),
3773        "Rust only search should give only Rust files"
3774    );
3775
3776    assert_eq!(
3777        search(
3778            &project,
3779            SearchQuery::text(
3780                search_query,
3781                false,
3782                true,
3783                vec![
3784                    PathMatcher::new("*.ts").unwrap(),
3785                    PathMatcher::new("*.odd").unwrap(),
3786                ],
3787                Vec::new()
3788            ).unwrap(),
3789            cx
3790        )
3791        .await
3792        .unwrap(),
3793        HashMap::from_iter([
3794            ("one.ts".to_string(), vec![14..18]),
3795            ("two.ts".to_string(), vec![14..18]),
3796        ]),
3797        "TypeScript only search should give only TypeScript files, even if other inclusions don't match anything"
3798    );
3799
3800    assert_eq!(
3801        search(
3802            &project,
3803            SearchQuery::text(
3804                search_query,
3805                false,
3806                true,
3807                vec![
3808                    PathMatcher::new("*.rs").unwrap(),
3809                    PathMatcher::new("*.ts").unwrap(),
3810                    PathMatcher::new("*.odd").unwrap(),
3811                ],
3812                Vec::new()
3813            ).unwrap(),
3814            cx
3815        )
3816        .await
3817        .unwrap(),
3818        HashMap::from_iter([
3819            ("one.rs".to_string(), vec![8..12]),
3820            ("one.ts".to_string(), vec![14..18]),
3821            ("two.rs".to_string(), vec![8..12]),
3822            ("two.ts".to_string(), vec![14..18]),
3823        ]),
3824        "Rust and typescript search should give both Rust and TypeScript files, even if other inclusions don't match anything"
3825    );
3826}
3827
3828#[gpui2::test]
3829async fn test_search_with_exclusions(cx: &mut gpui2::TestAppContext) {
3830    init_test(cx);
3831
3832    let search_query = "file";
3833
3834    let fs = FakeFs::new(cx.executor().clone());
3835    fs.insert_tree(
3836        "/dir",
3837        json!({
3838            "one.rs": r#"// Rust file one"#,
3839            "one.ts": r#"// TypeScript file one"#,
3840            "two.rs": r#"// Rust file two"#,
3841            "two.ts": r#"// TypeScript file two"#,
3842        }),
3843    )
3844    .await;
3845    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3846
3847    assert_eq!(
3848        search(
3849            &project,
3850            SearchQuery::text(
3851                search_query,
3852                false,
3853                true,
3854                Vec::new(),
3855                vec![PathMatcher::new("*.odd").unwrap()],
3856            )
3857            .unwrap(),
3858            cx
3859        )
3860        .await
3861        .unwrap(),
3862        HashMap::from_iter([
3863            ("one.rs".to_string(), vec![8..12]),
3864            ("one.ts".to_string(), vec![14..18]),
3865            ("two.rs".to_string(), vec![8..12]),
3866            ("two.ts".to_string(), vec![14..18]),
3867        ]),
3868        "If no exclusions match, all files should be returned"
3869    );
3870
3871    assert_eq!(
3872        search(
3873            &project,
3874            SearchQuery::text(
3875                search_query,
3876                false,
3877                true,
3878                Vec::new(),
3879                vec![PathMatcher::new("*.rs").unwrap()],
3880            )
3881            .unwrap(),
3882            cx
3883        )
3884        .await
3885        .unwrap(),
3886        HashMap::from_iter([
3887            ("one.ts".to_string(), vec![14..18]),
3888            ("two.ts".to_string(), vec![14..18]),
3889        ]),
3890        "Rust exclusion search should give only TypeScript files"
3891    );
3892
3893    assert_eq!(
3894        search(
3895            &project,
3896            SearchQuery::text(
3897                search_query,
3898                false,
3899                true,
3900                Vec::new(),
3901                vec![
3902                    PathMatcher::new("*.ts").unwrap(),
3903                    PathMatcher::new("*.odd").unwrap(),
3904                ],
3905            ).unwrap(),
3906            cx
3907        )
3908        .await
3909        .unwrap(),
3910        HashMap::from_iter([
3911            ("one.rs".to_string(), vec![8..12]),
3912            ("two.rs".to_string(), vec![8..12]),
3913        ]),
3914        "TypeScript exclusion search should give only Rust files, even if other exclusions don't match anything"
3915    );
3916
3917    assert!(
3918        search(
3919            &project,
3920            SearchQuery::text(
3921                search_query,
3922                false,
3923                true,
3924                Vec::new(),
3925                vec![
3926                    PathMatcher::new("*.rs").unwrap(),
3927                    PathMatcher::new("*.ts").unwrap(),
3928                    PathMatcher::new("*.odd").unwrap(),
3929                ],
3930            ).unwrap(),
3931            cx
3932        )
3933        .await
3934        .unwrap().is_empty(),
3935        "Rust and typescript exclusion should give no files, even if other exclusions don't match anything"
3936    );
3937}
3938
3939#[gpui2::test]
3940async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui2::TestAppContext) {
3941    init_test(cx);
3942
3943    let search_query = "file";
3944
3945    let fs = FakeFs::new(cx.executor().clone());
3946    fs.insert_tree(
3947        "/dir",
3948        json!({
3949            "one.rs": r#"// Rust file one"#,
3950            "one.ts": r#"// TypeScript file one"#,
3951            "two.rs": r#"// Rust file two"#,
3952            "two.ts": r#"// TypeScript file two"#,
3953        }),
3954    )
3955    .await;
3956    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3957
3958    assert!(
3959        search(
3960            &project,
3961            SearchQuery::text(
3962                search_query,
3963                false,
3964                true,
3965                vec![PathMatcher::new("*.odd").unwrap()],
3966                vec![PathMatcher::new("*.odd").unwrap()],
3967            )
3968            .unwrap(),
3969            cx
3970        )
3971        .await
3972        .unwrap()
3973        .is_empty(),
3974        "If both no exclusions and inclusions match, exclusions should win and return nothing"
3975    );
3976
3977    assert!(
3978        search(
3979            &project,
3980            SearchQuery::text(
3981                search_query,
3982                false,
3983                true,
3984                vec![PathMatcher::new("*.ts").unwrap()],
3985                vec![PathMatcher::new("*.ts").unwrap()],
3986            ).unwrap(),
3987            cx
3988        )
3989        .await
3990        .unwrap()
3991        .is_empty(),
3992        "If both TypeScript exclusions and inclusions match, exclusions should win and return nothing files."
3993    );
3994
3995    assert!(
3996        search(
3997            &project,
3998            SearchQuery::text(
3999                search_query,
4000                false,
4001                true,
4002                vec![
4003                    PathMatcher::new("*.ts").unwrap(),
4004                    PathMatcher::new("*.odd").unwrap()
4005                ],
4006                vec![
4007                    PathMatcher::new("*.ts").unwrap(),
4008                    PathMatcher::new("*.odd").unwrap()
4009                ],
4010            )
4011            .unwrap(),
4012            cx
4013        )
4014        .await
4015        .unwrap()
4016        .is_empty(),
4017        "Non-matching inclusions and exclusions should not change that."
4018    );
4019
4020    assert_eq!(
4021        search(
4022            &project,
4023            SearchQuery::text(
4024                search_query,
4025                false,
4026                true,
4027                vec![
4028                    PathMatcher::new("*.ts").unwrap(),
4029                    PathMatcher::new("*.odd").unwrap()
4030                ],
4031                vec![
4032                    PathMatcher::new("*.rs").unwrap(),
4033                    PathMatcher::new("*.odd").unwrap()
4034                ],
4035            )
4036            .unwrap(),
4037            cx
4038        )
4039        .await
4040        .unwrap(),
4041        HashMap::from_iter([
4042            ("one.ts".to_string(), vec![14..18]),
4043            ("two.ts".to_string(), vec![14..18]),
4044        ]),
4045        "Non-intersecting TypeScript inclusions and Rust exclusions should return TypeScript files"
4046    );
4047}
4048
4049#[test]
4050fn test_glob_literal_prefix() {
4051    assert_eq!(glob_literal_prefix("**/*.js"), "");
4052    assert_eq!(glob_literal_prefix("node_modules/**/*.js"), "node_modules");
4053    assert_eq!(glob_literal_prefix("foo/{bar,baz}.js"), "foo");
4054    assert_eq!(glob_literal_prefix("foo/bar/baz.js"), "foo/bar/baz.js");
4055}
4056
4057async fn search(
4058    project: &Model<Project>,
4059    query: SearchQuery,
4060    cx: &mut gpui2::TestAppContext,
4061) -> Result<HashMap<String, Vec<Range<usize>>>> {
4062    let mut search_rx = project.update(cx, |project, cx| project.search(query, cx));
4063    let mut result = HashMap::default();
4064    while let Some((buffer, range)) = search_rx.next().await {
4065        result.entry(buffer).or_insert(range);
4066    }
4067    Ok(result
4068        .into_iter()
4069        .map(|(buffer, ranges)| {
4070            buffer.update(cx, |buffer, _| {
4071                let path = buffer.file().unwrap().path().to_string_lossy().to_string();
4072                let ranges = ranges
4073                    .into_iter()
4074                    .map(|range| range.to_offset(buffer))
4075                    .collect::<Vec<_>>();
4076                (path, ranges)
4077            })
4078        })
4079        .collect())
4080}
4081
4082fn init_test(cx: &mut gpui2::TestAppContext) {
4083    if std::env::var("RUST_LOG").is_ok() {
4084        env_logger::init();
4085    }
4086
4087    cx.update(|cx| {
4088        let settings_store = SettingsStore::test(cx);
4089        cx.set_global(settings_store);
4090        language2::init(cx);
4091        Project::init_settings(cx);
4092    });
4093}