tests.rs

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