tests.rs

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