tests.rs

   1use super::*;
   2use gpui::{ModelHandle, MutableAppContext};
   3use std::{
   4    cell::RefCell,
   5    iter::FromIterator,
   6    ops::Range,
   7    rc::Rc,
   8    time::{Duration, Instant},
   9};
  10use unindent::Unindent as _;
  11
  12#[cfg(test)]
  13#[ctor::ctor]
  14fn init_logger() {
  15    // std::env::set_var("RUST_LOG", "info");
  16    env_logger::init();
  17}
  18
  19#[test]
  20fn test_select_language() {
  21    let registry = LanguageRegistry {
  22        languages: vec![
  23            Arc::new(Language::new(
  24                LanguageConfig {
  25                    name: "Rust".to_string(),
  26                    path_suffixes: vec!["rs".to_string()],
  27                    ..Default::default()
  28                },
  29                Some(tree_sitter_rust::language()),
  30            )),
  31            Arc::new(Language::new(
  32                LanguageConfig {
  33                    name: "Make".to_string(),
  34                    path_suffixes: vec!["Makefile".to_string(), "mk".to_string()],
  35                    ..Default::default()
  36                },
  37                Some(tree_sitter_rust::language()),
  38            )),
  39        ],
  40    };
  41
  42    // matching file extension
  43    assert_eq!(
  44        registry.select_language("zed/lib.rs").map(|l| l.name()),
  45        Some("Rust")
  46    );
  47    assert_eq!(
  48        registry.select_language("zed/lib.mk").map(|l| l.name()),
  49        Some("Make")
  50    );
  51
  52    // matching filename
  53    assert_eq!(
  54        registry.select_language("zed/Makefile").map(|l| l.name()),
  55        Some("Make")
  56    );
  57
  58    // matching suffix that is not the full file extension or filename
  59    assert_eq!(registry.select_language("zed/cars").map(|l| l.name()), None);
  60    assert_eq!(
  61        registry.select_language("zed/a.cars").map(|l| l.name()),
  62        None
  63    );
  64    assert_eq!(registry.select_language("zed/sumk").map(|l| l.name()), None);
  65}
  66
  67#[gpui::test]
  68fn test_edit_events(cx: &mut gpui::MutableAppContext) {
  69    let mut now = Instant::now();
  70    let buffer_1_events = Rc::new(RefCell::new(Vec::new()));
  71    let buffer_2_events = Rc::new(RefCell::new(Vec::new()));
  72
  73    let buffer1 = cx.add_model(|cx| Buffer::new(0, "abcdef", cx));
  74    let buffer2 = cx.add_model(|cx| Buffer::new(1, "abcdef", cx));
  75    let buffer_ops = buffer1.update(cx, |buffer, cx| {
  76        let buffer_1_events = buffer_1_events.clone();
  77        cx.subscribe(&buffer1, move |_, _, event, _| {
  78            buffer_1_events.borrow_mut().push(event.clone())
  79        })
  80        .detach();
  81        let buffer_2_events = buffer_2_events.clone();
  82        cx.subscribe(&buffer2, move |_, _, event, _| {
  83            buffer_2_events.borrow_mut().push(event.clone())
  84        })
  85        .detach();
  86
  87        // An edit emits an edited event, followed by a dirtied event,
  88        // since the buffer was previously in a clean state.
  89        buffer.edit(Some(2..4), "XYZ", cx);
  90
  91        // An empty transaction does not emit any events.
  92        buffer.start_transaction();
  93        buffer.end_transaction(cx);
  94
  95        // A transaction containing two edits emits one edited event.
  96        now += Duration::from_secs(1);
  97        buffer.start_transaction_at(now);
  98        buffer.edit(Some(5..5), "u", cx);
  99        buffer.edit(Some(6..6), "w", cx);
 100        buffer.end_transaction_at(now, cx);
 101
 102        // Undoing a transaction emits one edited event.
 103        buffer.undo(cx);
 104
 105        buffer.operations.clone()
 106    });
 107
 108    // Incorporating a set of remote ops emits a single edited event,
 109    // followed by a dirtied event.
 110    buffer2.update(cx, |buffer, cx| {
 111        buffer.apply_ops(buffer_ops, cx).unwrap();
 112    });
 113
 114    let buffer_1_events = buffer_1_events.borrow();
 115    assert_eq!(
 116        *buffer_1_events,
 117        vec![Event::Edited, Event::Dirtied, Event::Edited, Event::Edited]
 118    );
 119
 120    let buffer_2_events = buffer_2_events.borrow();
 121    assert_eq!(*buffer_2_events, vec![Event::Edited, Event::Dirtied]);
 122}
 123
 124#[gpui::test]
 125async fn test_apply_diff(mut cx: gpui::TestAppContext) {
 126    let text = "a\nbb\nccc\ndddd\neeeee\nffffff\n";
 127    let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
 128
 129    let text = "a\nccc\ndddd\nffffff\n";
 130    let diff = buffer.read_with(&cx, |b, cx| b.diff(text.into(), cx)).await;
 131    buffer.update(&mut cx, |b, cx| b.apply_diff(diff, cx));
 132    cx.read(|cx| assert_eq!(buffer.read(cx).text(), text));
 133
 134    let text = "a\n1\n\nccc\ndd2dd\nffffff\n";
 135    let diff = buffer.read_with(&cx, |b, cx| b.diff(text.into(), cx)).await;
 136    buffer.update(&mut cx, |b, cx| b.apply_diff(diff, cx));
 137    cx.read(|cx| assert_eq!(buffer.read(cx).text(), text));
 138}
 139
 140#[gpui::test]
 141async fn test_reparse(mut cx: gpui::TestAppContext) {
 142    let text = "fn a() {}";
 143    let buffer = cx.add_model(|cx| {
 144        Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx)
 145    });
 146
 147    // Wait for the initial text to parse
 148    buffer
 149        .condition(&cx, |buffer, _| !buffer.is_parsing())
 150        .await;
 151    assert_eq!(
 152        get_tree_sexp(&buffer, &cx),
 153        concat!(
 154            "(source_file (function_item name: (identifier) ",
 155            "parameters: (parameters) ",
 156            "body: (block)))"
 157        )
 158    );
 159
 160    buffer.update(&mut cx, |buffer, _| {
 161        buffer.set_sync_parse_timeout(Duration::ZERO)
 162    });
 163
 164    // Perform some edits (add parameter and variable reference)
 165    // Parsing doesn't begin until the transaction is complete
 166    buffer.update(&mut cx, |buf, cx| {
 167        buf.start_transaction();
 168
 169        let offset = buf.text().find(")").unwrap();
 170        buf.edit(vec![offset..offset], "b: C", cx);
 171        assert!(!buf.is_parsing());
 172
 173        let offset = buf.text().find("}").unwrap();
 174        buf.edit(vec![offset..offset], " d; ", cx);
 175        assert!(!buf.is_parsing());
 176
 177        buf.end_transaction(cx);
 178        assert_eq!(buf.text(), "fn a(b: C) { d; }");
 179        assert!(buf.is_parsing());
 180    });
 181    buffer
 182        .condition(&cx, |buffer, _| !buffer.is_parsing())
 183        .await;
 184    assert_eq!(
 185        get_tree_sexp(&buffer, &cx),
 186        concat!(
 187            "(source_file (function_item name: (identifier) ",
 188            "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
 189            "body: (block (identifier))))"
 190        )
 191    );
 192
 193    // Perform a series of edits without waiting for the current parse to complete:
 194    // * turn identifier into a field expression
 195    // * turn field expression into a method call
 196    // * add a turbofish to the method call
 197    buffer.update(&mut cx, |buf, cx| {
 198        let offset = buf.text().find(";").unwrap();
 199        buf.edit(vec![offset..offset], ".e", cx);
 200        assert_eq!(buf.text(), "fn a(b: C) { d.e; }");
 201        assert!(buf.is_parsing());
 202    });
 203    buffer.update(&mut cx, |buf, cx| {
 204        let offset = buf.text().find(";").unwrap();
 205        buf.edit(vec![offset..offset], "(f)", cx);
 206        assert_eq!(buf.text(), "fn a(b: C) { d.e(f); }");
 207        assert!(buf.is_parsing());
 208    });
 209    buffer.update(&mut cx, |buf, cx| {
 210        let offset = buf.text().find("(f)").unwrap();
 211        buf.edit(vec![offset..offset], "::<G>", cx);
 212        assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }");
 213        assert!(buf.is_parsing());
 214    });
 215    buffer
 216        .condition(&cx, |buffer, _| !buffer.is_parsing())
 217        .await;
 218    assert_eq!(
 219        get_tree_sexp(&buffer, &cx),
 220        concat!(
 221            "(source_file (function_item name: (identifier) ",
 222            "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
 223            "body: (block (call_expression ",
 224            "function: (generic_function ",
 225            "function: (field_expression value: (identifier) field: (field_identifier)) ",
 226            "type_arguments: (type_arguments (type_identifier))) ",
 227            "arguments: (arguments (identifier))))))",
 228        )
 229    );
 230
 231    buffer.update(&mut cx, |buf, cx| {
 232        buf.undo(cx);
 233        assert_eq!(buf.text(), "fn a() {}");
 234        assert!(buf.is_parsing());
 235    });
 236    buffer
 237        .condition(&cx, |buffer, _| !buffer.is_parsing())
 238        .await;
 239    assert_eq!(
 240        get_tree_sexp(&buffer, &cx),
 241        concat!(
 242            "(source_file (function_item name: (identifier) ",
 243            "parameters: (parameters) ",
 244            "body: (block)))"
 245        )
 246    );
 247
 248    buffer.update(&mut cx, |buf, cx| {
 249        buf.redo(cx);
 250        assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }");
 251        assert!(buf.is_parsing());
 252    });
 253    buffer
 254        .condition(&cx, |buffer, _| !buffer.is_parsing())
 255        .await;
 256    assert_eq!(
 257        get_tree_sexp(&buffer, &cx),
 258        concat!(
 259            "(source_file (function_item name: (identifier) ",
 260            "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
 261            "body: (block (call_expression ",
 262            "function: (generic_function ",
 263            "function: (field_expression value: (identifier) field: (field_identifier)) ",
 264            "type_arguments: (type_arguments (type_identifier))) ",
 265            "arguments: (arguments (identifier))))))",
 266        )
 267    );
 268
 269    fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> String {
 270        buffer.read_with(cx, |buffer, _| {
 271            buffer.syntax_tree().unwrap().root_node().to_sexp()
 272        })
 273    }
 274}
 275
 276#[gpui::test]
 277fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
 278    let buffer = cx.add_model(|cx| {
 279        let text = "
 280            mod x {
 281                mod y {
 282
 283                }
 284            }
 285        "
 286        .unindent();
 287        Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx)
 288    });
 289    let buffer = buffer.read(cx);
 290    assert_eq!(
 291        buffer.enclosing_bracket_point_ranges(Point::new(1, 6)..Point::new(1, 6)),
 292        Some((
 293            Point::new(0, 6)..Point::new(0, 7),
 294            Point::new(4, 0)..Point::new(4, 1)
 295        ))
 296    );
 297    assert_eq!(
 298        buffer.enclosing_bracket_point_ranges(Point::new(1, 10)..Point::new(1, 10)),
 299        Some((
 300            Point::new(1, 10)..Point::new(1, 11),
 301            Point::new(3, 4)..Point::new(3, 5)
 302        ))
 303    );
 304    assert_eq!(
 305        buffer.enclosing_bracket_point_ranges(Point::new(3, 5)..Point::new(3, 5)),
 306        Some((
 307            Point::new(1, 10)..Point::new(1, 11),
 308            Point::new(3, 4)..Point::new(3, 5)
 309        ))
 310    );
 311}
 312
 313#[gpui::test]
 314fn test_edit_with_autoindent(cx: &mut MutableAppContext) {
 315    cx.add_model(|cx| {
 316        let text = "fn a() {}";
 317        let mut buffer =
 318            Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
 319
 320        buffer.edit_with_autoindent([8..8], "\n\n", cx);
 321        assert_eq!(buffer.text(), "fn a() {\n    \n}");
 322
 323        buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 4)], "b()\n", cx);
 324        assert_eq!(buffer.text(), "fn a() {\n    b()\n    \n}");
 325
 326        buffer.edit_with_autoindent([Point::new(2, 4)..Point::new(2, 4)], ".c", cx);
 327        assert_eq!(buffer.text(), "fn a() {\n    b()\n        .c\n}");
 328
 329        buffer
 330    });
 331}
 332
 333// We need another approach to managing selections with auto-indent
 334
 335// #[gpui::test]
 336// fn test_autoindent_moves_selections(cx: &mut MutableAppContext) {
 337//     cx.add_model(|cx| {
 338//         let text = "fn a() {}";
 339
 340//         let mut buffer =
 341//             Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
 342
 343//         let selection_set_id = buffer.add_selection_set::<usize>(&[], cx);
 344//         buffer.start_transaction();
 345//         buffer.edit_with_autoindent([5..5, 9..9], "\n\n", cx);
 346//         buffer
 347//             .update_selection_set(
 348//                 selection_set_id,
 349//                 &[
 350//                     Selection {
 351//                         id: 0,
 352//                         start: Point::new(1, 0),
 353//                         end: Point::new(1, 0),
 354//                         reversed: false,
 355//                         goal: SelectionGoal::None,
 356//                     },
 357//                     Selection {
 358//                         id: 1,
 359//                         start: Point::new(4, 0),
 360//                         end: Point::new(4, 0),
 361//                         reversed: false,
 362//                         goal: SelectionGoal::None,
 363//                     },
 364//                 ],
 365//                 cx,
 366//             )
 367//             .unwrap();
 368//         assert_eq!(buffer.text(), "fn a(\n\n) {}\n\n");
 369
 370//         // TODO! Come up with a different approach to moving selections now that we don't manage selection sets in the buffer
 371
 372//         // Ending the transaction runs the auto-indent. The selection
 373//         // at the start of the auto-indented row is pushed to the right.
 374//         buffer.end_transaction(cx);
 375//         assert_eq!(buffer.text(), "fn a(\n    \n) {}\n\n");
 376//         let selection_ranges = buffer
 377//             .selection_set(selection_set_id)
 378//             .unwrap()
 379//             .selections::<Point>(&buffer)
 380//             .map(|selection| selection.start.to_point(&buffer)..selection.end.to_point(&buffer))
 381//             .collect::<Vec<_>>();
 382
 383//         assert_eq!(selection_ranges[0], empty(Point::new(1, 4)));
 384//         assert_eq!(selection_ranges[1], empty(Point::new(4, 0)));
 385
 386//         buffer
 387//     });
 388// }
 389
 390#[gpui::test]
 391fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut MutableAppContext) {
 392    cx.add_model(|cx| {
 393        let text = "
 394            fn a() {
 395            c;
 396            d;
 397            }
 398        "
 399        .unindent();
 400
 401        let mut buffer =
 402            Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
 403
 404        // Lines 2 and 3 don't match the indentation suggestion. When editing these lines,
 405        // their indentation is not adjusted.
 406        buffer.edit_with_autoindent([empty(Point::new(1, 1)), empty(Point::new(2, 1))], "()", cx);
 407        assert_eq!(
 408            buffer.text(),
 409            "
 410            fn a() {
 411            c();
 412            d();
 413            }
 414            "
 415            .unindent()
 416        );
 417
 418        // When appending new content after these lines, the indentation is based on the
 419        // preceding lines' actual indentation.
 420        buffer.edit_with_autoindent(
 421            [empty(Point::new(1, 1)), empty(Point::new(2, 1))],
 422            "\n.f\n.g",
 423            cx,
 424        );
 425        assert_eq!(
 426            buffer.text(),
 427            "
 428            fn a() {
 429            c
 430                .f
 431                .g();
 432            d
 433                .f
 434                .g();
 435            }
 436            "
 437            .unindent()
 438        );
 439        buffer
 440    });
 441}
 442
 443#[gpui::test]
 444fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppContext) {
 445    cx.add_model(|cx| {
 446        let text = "
 447            fn a() {}
 448        "
 449        .unindent();
 450
 451        let mut buffer =
 452            Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
 453
 454        buffer.edit_with_autoindent([5..5], "\nb", cx);
 455        assert_eq!(
 456            buffer.text(),
 457            "
 458                fn a(
 459                    b) {}
 460            "
 461            .unindent()
 462        );
 463
 464        // The indentation suggestion changed because `@end` node (a close paren)
 465        // is now at the beginning of the line.
 466        buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 5)], "", cx);
 467        assert_eq!(
 468            buffer.text(),
 469            "
 470                fn a(
 471                ) {}
 472            "
 473            .unindent()
 474        );
 475
 476        buffer
 477    });
 478}
 479
 480#[gpui::test]
 481async fn test_diagnostics(mut cx: gpui::TestAppContext) {
 482    let (language_server, mut fake) = lsp::LanguageServer::fake(cx.background()).await;
 483    let mut rust_lang = rust_lang();
 484    rust_lang.config.language_server = Some(LanguageServerConfig {
 485        disk_based_diagnostic_sources: HashSet::from_iter(["disk".to_string()]),
 486        ..Default::default()
 487    });
 488
 489    let text = "
 490        fn a() { A }
 491        fn b() { BB }
 492        fn c() { CCC }
 493    "
 494    .unindent();
 495
 496    let buffer = cx.add_model(|cx| {
 497        Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang)), Some(language_server), cx)
 498    });
 499
 500    let open_notification = fake
 501        .receive_notification::<lsp::notification::DidOpenTextDocument>()
 502        .await;
 503
 504    // Edit the buffer, moving the content down
 505    buffer.update(&mut cx, |buffer, cx| buffer.edit([0..0], "\n\n", cx));
 506    let change_notification_1 = fake
 507        .receive_notification::<lsp::notification::DidChangeTextDocument>()
 508        .await;
 509    assert!(change_notification_1.text_document.version > open_notification.text_document.version);
 510
 511    buffer.update(&mut cx, |buffer, cx| {
 512        // Receive diagnostics for an earlier version of the buffer.
 513        buffer
 514            .update_diagnostics(
 515                "lsp".into(),
 516                Some(open_notification.text_document.version),
 517                vec![
 518                    DiagnosticEntry {
 519                        range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10),
 520                        diagnostic: Diagnostic {
 521                            severity: DiagnosticSeverity::ERROR,
 522                            message: "undefined variable 'A'".to_string(),
 523                            is_disk_based: true,
 524                            group_id: 0,
 525                            is_primary: true,
 526                            ..Default::default()
 527                        },
 528                    },
 529                    DiagnosticEntry {
 530                        range: PointUtf16::new(1, 9)..PointUtf16::new(1, 11),
 531                        diagnostic: Diagnostic {
 532                            severity: DiagnosticSeverity::ERROR,
 533                            message: "undefined variable 'BB'".to_string(),
 534                            is_disk_based: true,
 535                            group_id: 1,
 536                            is_primary: true,
 537                            ..Default::default()
 538                        },
 539                    },
 540                    DiagnosticEntry {
 541                        range: PointUtf16::new(2, 9)..PointUtf16::new(2, 12),
 542                        diagnostic: Diagnostic {
 543                            severity: DiagnosticSeverity::ERROR,
 544                            is_disk_based: true,
 545                            message: "undefined variable 'CCC'".to_string(),
 546                            group_id: 2,
 547                            is_primary: true,
 548                            ..Default::default()
 549                        },
 550                    },
 551                ],
 552                cx,
 553            )
 554            .unwrap();
 555
 556        // The diagnostics have moved down since they were created.
 557        assert_eq!(
 558            buffer
 559                .snapshot()
 560                .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0))
 561                .collect::<Vec<_>>(),
 562            &[
 563                (
 564                    "lsp",
 565                    DiagnosticEntry {
 566                        range: Point::new(3, 9)..Point::new(3, 11),
 567                        diagnostic: Diagnostic {
 568                            severity: DiagnosticSeverity::ERROR,
 569                            message: "undefined variable 'BB'".to_string(),
 570                            is_disk_based: true,
 571                            group_id: 1,
 572                            is_primary: true,
 573                            ..Default::default()
 574                        },
 575                    }
 576                ),
 577                (
 578                    "lsp",
 579                    DiagnosticEntry {
 580                        range: Point::new(4, 9)..Point::new(4, 12),
 581                        diagnostic: Diagnostic {
 582                            severity: DiagnosticSeverity::ERROR,
 583                            message: "undefined variable 'CCC'".to_string(),
 584                            is_disk_based: true,
 585                            group_id: 2,
 586                            is_primary: true,
 587                            ..Default::default()
 588                        }
 589                    }
 590                )
 591            ]
 592        );
 593        assert_eq!(
 594            chunks_with_diagnostics(buffer, 0..buffer.len()),
 595            [
 596                ("\n\nfn a() { ".to_string(), None),
 597                ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
 598                (" }\nfn b() { ".to_string(), None),
 599                ("BB".to_string(), Some(DiagnosticSeverity::ERROR)),
 600                (" }\nfn c() { ".to_string(), None),
 601                ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)),
 602                (" }\n".to_string(), None),
 603            ]
 604        );
 605        assert_eq!(
 606            chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)),
 607            [
 608                ("B".to_string(), Some(DiagnosticSeverity::ERROR)),
 609                (" }\nfn c() { ".to_string(), None),
 610                ("CC".to_string(), Some(DiagnosticSeverity::ERROR)),
 611            ]
 612        );
 613
 614        // Ensure overlapping diagnostics are highlighted correctly.
 615        buffer
 616            .update_diagnostics(
 617                "lsp".into(),
 618                Some(open_notification.text_document.version),
 619                vec![
 620                    DiagnosticEntry {
 621                        range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10),
 622                        diagnostic: Diagnostic {
 623                            severity: DiagnosticSeverity::ERROR,
 624                            message: "undefined variable 'A'".to_string(),
 625                            is_disk_based: true,
 626                            group_id: 0,
 627                            is_primary: true,
 628                            ..Default::default()
 629                        },
 630                    },
 631                    DiagnosticEntry {
 632                        range: PointUtf16::new(0, 9)..PointUtf16::new(0, 12),
 633                        diagnostic: Diagnostic {
 634                            severity: DiagnosticSeverity::WARNING,
 635                            message: "unreachable statement".to_string(),
 636                            group_id: 1,
 637                            is_primary: true,
 638                            ..Default::default()
 639                        },
 640                    },
 641                ],
 642                cx,
 643            )
 644            .unwrap();
 645        assert_eq!(
 646            buffer
 647                .snapshot()
 648                .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0))
 649                .collect::<Vec<_>>(),
 650            &[
 651                (
 652                    "lsp",
 653                    DiagnosticEntry {
 654                        range: Point::new(2, 9)..Point::new(2, 12),
 655                        diagnostic: Diagnostic {
 656                            severity: DiagnosticSeverity::WARNING,
 657                            message: "unreachable statement".to_string(),
 658                            group_id: 3,
 659                            is_primary: true,
 660                            ..Default::default()
 661                        }
 662                    }
 663                ),
 664                (
 665                    "lsp",
 666                    DiagnosticEntry {
 667                        range: Point::new(2, 9)..Point::new(2, 10),
 668                        diagnostic: Diagnostic {
 669                            severity: DiagnosticSeverity::ERROR,
 670                            message: "undefined variable 'A'".to_string(),
 671                            is_disk_based: true,
 672                            group_id: 0,
 673                            is_primary: true,
 674                            ..Default::default()
 675                        },
 676                    }
 677                )
 678            ]
 679        );
 680        assert_eq!(
 681            chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)),
 682            [
 683                ("fn a() { ".to_string(), None),
 684                ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
 685                (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
 686                ("\n".to_string(), None),
 687            ]
 688        );
 689        assert_eq!(
 690            chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)),
 691            [
 692                (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
 693                ("\n".to_string(), None),
 694            ]
 695        );
 696    });
 697
 698    // Keep editing the buffer and ensure disk-based diagnostics get translated according to the
 699    // changes since the last save.
 700    buffer.update(&mut cx, |buffer, cx| {
 701        buffer.edit(Some(Point::new(2, 0)..Point::new(2, 0)), "    ", cx);
 702        buffer.edit(Some(Point::new(2, 8)..Point::new(2, 10)), "(x: usize)", cx);
 703    });
 704    let change_notification_2 = fake
 705        .receive_notification::<lsp::notification::DidChangeTextDocument>()
 706        .await;
 707    assert!(
 708        change_notification_2.text_document.version > change_notification_1.text_document.version
 709    );
 710
 711    buffer.update(&mut cx, |buffer, cx| {
 712        buffer
 713            .update_diagnostics(
 714                "lsp".into(),
 715                Some(change_notification_2.text_document.version),
 716                vec![
 717                    DiagnosticEntry {
 718                        range: PointUtf16::new(1, 9)..PointUtf16::new(1, 11),
 719                        diagnostic: Diagnostic {
 720                            severity: DiagnosticSeverity::ERROR,
 721                            message: "undefined variable 'BB'".to_string(),
 722                            is_disk_based: true,
 723                            group_id: 1,
 724                            is_primary: true,
 725                            ..Default::default()
 726                        },
 727                    },
 728                    DiagnosticEntry {
 729                        range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10),
 730                        diagnostic: Diagnostic {
 731                            severity: DiagnosticSeverity::ERROR,
 732                            message: "undefined variable 'A'".to_string(),
 733                            is_disk_based: true,
 734                            group_id: 0,
 735                            is_primary: true,
 736                            ..Default::default()
 737                        },
 738                    },
 739                ],
 740                cx,
 741            )
 742            .unwrap();
 743        assert_eq!(
 744            buffer
 745                .snapshot()
 746                .diagnostics_in_range::<_, Point>(0..buffer.len())
 747                .collect::<Vec<_>>(),
 748            &[
 749                (
 750                    "lsp",
 751                    DiagnosticEntry {
 752                        range: Point::new(2, 21)..Point::new(2, 22),
 753                        diagnostic: Diagnostic {
 754                            severity: DiagnosticSeverity::ERROR,
 755                            message: "undefined variable 'A'".to_string(),
 756                            is_disk_based: true,
 757                            group_id: 0,
 758                            is_primary: true,
 759                            ..Default::default()
 760                        }
 761                    }
 762                ),
 763                (
 764                    "lsp",
 765                    DiagnosticEntry {
 766                        range: Point::new(3, 9)..Point::new(3, 11),
 767                        diagnostic: Diagnostic {
 768                            severity: DiagnosticSeverity::ERROR,
 769                            message: "undefined variable 'BB'".to_string(),
 770                            is_disk_based: true,
 771                            group_id: 4,
 772                            is_primary: true,
 773                            ..Default::default()
 774                        },
 775                    }
 776                )
 777            ]
 778        );
 779    });
 780}
 781
 782#[gpui::test]
 783async fn test_preserving_old_group_ids_and_disk_based_diagnostics(mut cx: gpui::TestAppContext) {
 784    let buffer = cx.add_model(|cx| {
 785        let text = "
 786            use a::*;
 787            const b: i32 = c::;
 788            const c: i32 = d;
 789            const e: i32 = f +;
 790        "
 791        .unindent();
 792
 793        let mut rust_lang = rust_lang();
 794        rust_lang.config.language_server = Some(LanguageServerConfig {
 795            disk_based_diagnostic_sources: HashSet::from_iter(["disk".to_string()]),
 796            ..Default::default()
 797        });
 798
 799        let mut buffer = Buffer::new(0, text, cx);
 800        buffer.set_language(Some(Arc::new(rust_lang)), None, cx);
 801        buffer
 802    });
 803
 804    // Initially, there are three errors. The second one is disk-based.
 805    let diagnostics = vec![
 806        DiagnosticEntry {
 807            range: PointUtf16::new(1, 16)..PointUtf16::new(1, 18),
 808            diagnostic: Diagnostic {
 809                severity: DiagnosticSeverity::ERROR,
 810                message: "syntax error 1".to_string(),
 811                group_id: 0,
 812                is_primary: true,
 813                is_valid: true,
 814                ..Default::default()
 815            },
 816        },
 817        DiagnosticEntry {
 818            range: PointUtf16::new(2, 15)..PointUtf16::new(2, 16),
 819            diagnostic: Diagnostic {
 820                severity: DiagnosticSeverity::ERROR,
 821                message: "cannot find value `d` in this scope".to_string(),
 822                is_disk_based: true,
 823                group_id: 1,
 824                is_primary: true,
 825                is_valid: true,
 826                ..Default::default()
 827            },
 828        },
 829        DiagnosticEntry {
 830            range: PointUtf16::new(3, 17)..PointUtf16::new(3, 18),
 831            diagnostic: Diagnostic {
 832                severity: DiagnosticSeverity::ERROR,
 833                message: "syntax error 2".to_string(),
 834                group_id: 2,
 835                is_primary: true,
 836                is_valid: true,
 837                ..Default::default()
 838            },
 839        },
 840    ];
 841    buffer.update(&mut cx, |buffer, cx| {
 842        buffer
 843            .update_diagnostics("lsp".into(), None, diagnostics.clone(), cx)
 844            .unwrap();
 845        assert_eq!(
 846            buffer
 847                .snapshot()
 848                .diagnostics_in_range::<_, PointUtf16>(PointUtf16::new(0, 0)..PointUtf16::new(4, 0))
 849                .collect::<Vec<_>>(),
 850            diagnostics
 851                .iter()
 852                .map(|entry| ("lsp", entry.clone()))
 853                .collect::<Vec<_>>(),
 854        );
 855    });
 856
 857    // The diagnostics are updated. The disk-based diagnostic is omitted, and one
 858    // other diagnostic has changed its message.
 859    let mut new_diagnostics = vec![diagnostics[0].clone(), diagnostics[2].clone()];
 860    new_diagnostics[0].diagnostic.message = "another syntax error".to_string();
 861
 862    buffer.update(&mut cx, |buffer, cx| {
 863        buffer
 864            .update_diagnostics("lsp".into(), None, new_diagnostics.clone(), cx)
 865            .unwrap();
 866        assert_eq!(
 867            buffer
 868                .snapshot()
 869                .diagnostics_in_range::<_, PointUtf16>(PointUtf16::new(0, 0)..PointUtf16::new(4, 0))
 870                .collect::<Vec<_>>(),
 871            &[
 872                // The changed diagnostic is given a new group id.
 873                (
 874                    "lsp",
 875                    DiagnosticEntry {
 876                        range: new_diagnostics[0].range.clone(),
 877                        diagnostic: Diagnostic {
 878                            group_id: 3,
 879                            ..new_diagnostics[0].diagnostic.clone()
 880                        },
 881                    }
 882                ),
 883                // The old disk-based diagnostic is marked as invalid, but keeps
 884                // its original group id.
 885                (
 886                    "lsp",
 887                    DiagnosticEntry {
 888                        range: diagnostics[1].range.clone(),
 889                        diagnostic: Diagnostic {
 890                            is_valid: false,
 891                            ..diagnostics[1].diagnostic.clone()
 892                        },
 893                    }
 894                ),
 895                // The unchanged diagnostic keeps its original group id
 896                ("lsp", new_diagnostics[1].clone()),
 897            ],
 898        );
 899    });
 900}
 901
 902#[gpui::test]
 903async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
 904    cx.add_model(|cx| {
 905        let text = concat!(
 906            "let one = ;\n", //
 907            "let two = \n",
 908            "let three = 3;\n",
 909        );
 910
 911        let mut buffer = Buffer::new(0, text, cx);
 912        buffer.set_language(Some(Arc::new(rust_lang())), None, cx);
 913        buffer
 914            .update_diagnostics(
 915                "lsp".into(),
 916                None,
 917                vec![
 918                    DiagnosticEntry {
 919                        range: PointUtf16::new(0, 10)..PointUtf16::new(0, 10),
 920                        diagnostic: Diagnostic {
 921                            severity: DiagnosticSeverity::ERROR,
 922                            message: "syntax error 1".to_string(),
 923                            ..Default::default()
 924                        },
 925                    },
 926                    DiagnosticEntry {
 927                        range: PointUtf16::new(1, 10)..PointUtf16::new(1, 10),
 928                        diagnostic: Diagnostic {
 929                            severity: DiagnosticSeverity::ERROR,
 930                            message: "syntax error 2".to_string(),
 931                            ..Default::default()
 932                        },
 933                    },
 934                ],
 935                cx,
 936            )
 937            .unwrap();
 938
 939        // An empty range is extended forward to include the following character.
 940        // At the end of a line, an empty range is extended backward to include
 941        // the preceding character.
 942        let chunks = chunks_with_diagnostics(&buffer, 0..buffer.len());
 943        assert_eq!(
 944            chunks
 945                .iter()
 946                .map(|(s, d)| (s.as_str(), *d))
 947                .collect::<Vec<_>>(),
 948            &[
 949                ("let one = ", None),
 950                (";", Some(DiagnosticSeverity::ERROR)),
 951                ("\nlet two =", None),
 952                (" ", Some(DiagnosticSeverity::ERROR)),
 953                ("\nlet three = 3;\n", None)
 954            ]
 955        );
 956        buffer
 957    });
 958}
 959
 960fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
 961    buffer: &Buffer,
 962    range: Range<T>,
 963) -> Vec<(String, Option<DiagnosticSeverity>)> {
 964    let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
 965    for chunk in buffer.snapshot().chunks(range, Some(&Default::default())) {
 966        if chunks
 967            .last()
 968            .map_or(false, |prev_chunk| prev_chunk.1 == chunk.diagnostic)
 969        {
 970            chunks.last_mut().unwrap().0.push_str(chunk.text);
 971        } else {
 972            chunks.push((chunk.text.to_string(), chunk.diagnostic));
 973        }
 974    }
 975    chunks
 976}
 977
 978#[test]
 979fn test_contiguous_ranges() {
 980    assert_eq!(
 981        contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12].into_iter(), 100).collect::<Vec<_>>(),
 982        &[1..4, 5..7, 9..13]
 983    );
 984
 985    // Respects the `max_len` parameter
 986    assert_eq!(
 987        contiguous_ranges(
 988            [2, 3, 4, 5, 6, 7, 8, 9, 23, 24, 25, 26, 30, 31].into_iter(),
 989            3
 990        )
 991        .collect::<Vec<_>>(),
 992        &[2..5, 5..8, 8..10, 23..26, 26..27, 30..32],
 993    );
 994}
 995
 996impl Buffer {
 997    pub fn enclosing_bracket_point_ranges<T: ToOffset>(
 998        &self,
 999        range: Range<T>,
1000    ) -> Option<(Range<Point>, Range<Point>)> {
1001        self.snapshot()
1002            .enclosing_bracket_ranges(range)
1003            .map(|(start, end)| {
1004                let point_start = start.start.to_point(self)..start.end.to_point(self);
1005                let point_end = end.start.to_point(self)..end.end.to_point(self);
1006                (point_start, point_end)
1007            })
1008    }
1009}
1010
1011fn rust_lang() -> Language {
1012    Language::new(
1013        LanguageConfig {
1014            name: "Rust".to_string(),
1015            path_suffixes: vec!["rs".to_string()],
1016            language_server: None,
1017            ..Default::default()
1018        },
1019        Some(tree_sitter_rust::language()),
1020    )
1021    .with_indents_query(
1022        r#"
1023                (call_expression) @indent
1024                (field_expression) @indent
1025                (_ "(" ")" @end) @indent
1026                (_ "{" "}" @end) @indent
1027            "#,
1028    )
1029    .unwrap()
1030    .with_brackets_query(r#" ("{" @open "}" @close) "#)
1031    .unwrap()
1032}
1033
1034fn empty(point: Point) -> Range<Point> {
1035    point..point
1036}