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                    },
 488                ),
 489                (
 490                    Point::new(4, 9)..Point::new(4, 12),
 491                    &Diagnostic {
 492                        severity: DiagnosticSeverity::ERROR,
 493                        message: "undefined variable 'CCC'".to_string(),
 494                        group_id: 2,
 495                    }
 496                )
 497            ]
 498        );
 499        assert_eq!(
 500            chunks_with_diagnostics(buffer, 0..buffer.len()),
 501            [
 502                ("\n\nfn a() { ".to_string(), None),
 503                ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
 504                (" }\nfn b() { ".to_string(), None),
 505                ("BB".to_string(), Some(DiagnosticSeverity::ERROR)),
 506                (" }\nfn c() { ".to_string(), None),
 507                ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)),
 508                (" }\n".to_string(), None),
 509            ]
 510        );
 511        assert_eq!(
 512            chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)),
 513            [
 514                ("B".to_string(), Some(DiagnosticSeverity::ERROR)),
 515                (" }\nfn c() { ".to_string(), None),
 516                ("CC".to_string(), Some(DiagnosticSeverity::ERROR)),
 517            ]
 518        );
 519
 520        // Ensure overlapping diagnostics are highlighted correctly.
 521        buffer
 522            .update_diagnostics(
 523                Some(open_notification.text_document.version),
 524                vec![
 525                    lsp::Diagnostic {
 526                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
 527                        severity: Some(lsp::DiagnosticSeverity::ERROR),
 528                        message: "undefined variable 'A'".to_string(),
 529                        ..Default::default()
 530                    },
 531                    lsp::Diagnostic {
 532                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 12)),
 533                        severity: Some(lsp::DiagnosticSeverity::WARNING),
 534                        message: "unreachable statement".to_string(),
 535                        ..Default::default()
 536                    },
 537                ],
 538                cx,
 539            )
 540            .unwrap();
 541        assert_eq!(
 542            buffer
 543                .diagnostics_in_range(Point::new(2, 0)..Point::new(3, 0))
 544                .collect::<Vec<_>>(),
 545            &[
 546                (
 547                    Point::new(2, 9)..Point::new(2, 12),
 548                    &Diagnostic {
 549                        severity: DiagnosticSeverity::WARNING,
 550                        message: "unreachable statement".to_string(),
 551                        group_id: 1,
 552                    }
 553                ),
 554                (
 555                    Point::new(2, 9)..Point::new(2, 10),
 556                    &Diagnostic {
 557                        severity: DiagnosticSeverity::ERROR,
 558                        message: "undefined variable 'A'".to_string(),
 559                        group_id: 0,
 560                    },
 561                )
 562            ]
 563        );
 564        assert_eq!(
 565            chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)),
 566            [
 567                ("fn a() { ".to_string(), None),
 568                ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
 569                (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
 570                ("\n".to_string(), None),
 571            ]
 572        );
 573        assert_eq!(
 574            chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)),
 575            [
 576                (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
 577                ("\n".to_string(), None),
 578            ]
 579        );
 580    });
 581
 582    // Keep editing the buffer and ensure disk-based diagnostics get translated according to the
 583    // changes since the last save.
 584    buffer.update(&mut cx, |buffer, cx| {
 585        buffer.edit(Some(Point::new(2, 0)..Point::new(2, 0)), "    ", cx);
 586        buffer.edit(Some(Point::new(2, 8)..Point::new(2, 10)), "(x: usize)", cx);
 587    });
 588    let change_notification_2 = fake
 589        .receive_notification::<lsp::notification::DidChangeTextDocument>()
 590        .await;
 591    assert!(
 592        change_notification_2.text_document.version > change_notification_1.text_document.version
 593    );
 594
 595    buffer.update(&mut cx, |buffer, cx| {
 596        buffer
 597            .update_diagnostics(
 598                Some(change_notification_2.text_document.version),
 599                vec![
 600                    lsp::Diagnostic {
 601                        range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
 602                        severity: Some(lsp::DiagnosticSeverity::ERROR),
 603                        message: "undefined variable 'BB'".to_string(),
 604                        source: Some("disk".to_string()),
 605                        ..Default::default()
 606                    },
 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                        source: Some("disk".to_string()),
 612                        ..Default::default()
 613                    },
 614                ],
 615                cx,
 616            )
 617            .unwrap();
 618        assert_eq!(
 619            buffer
 620                .diagnostics_in_range(0..buffer.len())
 621                .collect::<Vec<_>>(),
 622            &[
 623                (
 624                    Point::new(2, 21)..Point::new(2, 22),
 625                    &Diagnostic {
 626                        severity: DiagnosticSeverity::ERROR,
 627                        message: "undefined variable 'A'".to_string(),
 628                        group_id: 0,
 629                    }
 630                ),
 631                (
 632                    Point::new(3, 9)..Point::new(3, 11),
 633                    &Diagnostic {
 634                        severity: DiagnosticSeverity::ERROR,
 635                        message: "undefined variable 'BB'".to_string(),
 636                        group_id: 1,
 637                    },
 638                )
 639            ]
 640        );
 641    });
 642}
 643
 644#[gpui::test]
 645async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
 646    cx.add_model(|cx| {
 647        let text = concat!(
 648            "let one = ;\n", //
 649            "let two = \n",
 650            "let three = 3;\n",
 651        );
 652
 653        let mut buffer = Buffer::new(0, text, cx);
 654        buffer.set_language(Some(Arc::new(rust_lang())), None, cx);
 655        buffer
 656            .update_diagnostics(
 657                None,
 658                vec![
 659                    lsp::Diagnostic {
 660                        range: lsp::Range::new(
 661                            lsp::Position::new(0, 10),
 662                            lsp::Position::new(0, 10),
 663                        ),
 664                        severity: Some(lsp::DiagnosticSeverity::ERROR),
 665                        message: "syntax error 1".to_string(),
 666                        ..Default::default()
 667                    },
 668                    lsp::Diagnostic {
 669                        range: lsp::Range::new(
 670                            lsp::Position::new(1, 10),
 671                            lsp::Position::new(1, 10),
 672                        ),
 673                        severity: Some(lsp::DiagnosticSeverity::ERROR),
 674                        message: "syntax error 2".to_string(),
 675                        ..Default::default()
 676                    },
 677                ],
 678                cx,
 679            )
 680            .unwrap();
 681
 682        // An empty range is extended forward to include the following character.
 683        // At the end of a line, an empty range is extended backward to include
 684        // the preceding character.
 685        let chunks = chunks_with_diagnostics(&buffer, 0..buffer.len());
 686        assert_eq!(
 687            chunks
 688                .iter()
 689                .map(|(s, d)| (s.as_str(), *d))
 690                .collect::<Vec<_>>(),
 691            &[
 692                ("let one = ", None),
 693                (";", Some(lsp::DiagnosticSeverity::ERROR)),
 694                ("\nlet two =", None),
 695                (" ", Some(lsp::DiagnosticSeverity::ERROR)),
 696                ("\nlet three = 3;\n", None)
 697            ]
 698        );
 699        buffer
 700    });
 701}
 702
 703#[gpui::test]
 704async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) {
 705    cx.add_model(|cx| {
 706        let text = "
 707            fn foo(mut v: Vec<usize>) {
 708                for x in &v {
 709                    v.push(1);
 710                }
 711            }
 712        "
 713        .unindent();
 714
 715        let file = FakeFile::new("/example.rs");
 716        let mut buffer = Buffer::from_file(0, text, Box::new(file.clone()), cx);
 717        buffer.set_language(Some(Arc::new(rust_lang())), None, cx);
 718        let diagnostics = vec![
 719            lsp::Diagnostic {
 720                range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
 721                severity: Some(DiagnosticSeverity::WARNING),
 722                message: "error 1".to_string(),
 723                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
 724                    location: lsp::Location {
 725                        uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
 726                        range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
 727                    },
 728                    message: "error 1 hint 1".to_string(),
 729                }]),
 730                ..Default::default()
 731            },
 732            lsp::Diagnostic {
 733                range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
 734                severity: Some(DiagnosticSeverity::HINT),
 735                message: "error 1 hint 1".to_string(),
 736                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
 737                    location: lsp::Location {
 738                        uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
 739                        range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
 740                    },
 741                    message: "original diagnostic".to_string(),
 742                }]),
 743                ..Default::default()
 744            },
 745            lsp::Diagnostic {
 746                range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
 747                severity: Some(DiagnosticSeverity::ERROR),
 748                message: "error 2".to_string(),
 749                related_information: Some(vec![
 750                    lsp::DiagnosticRelatedInformation {
 751                        location: lsp::Location {
 752                            uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
 753                            range: lsp::Range::new(
 754                                lsp::Position::new(1, 13),
 755                                lsp::Position::new(1, 15),
 756                            ),
 757                        },
 758                        message: "error 2 hint 1".to_string(),
 759                    },
 760                    lsp::DiagnosticRelatedInformation {
 761                        location: lsp::Location {
 762                            uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
 763                            range: lsp::Range::new(
 764                                lsp::Position::new(1, 13),
 765                                lsp::Position::new(1, 15),
 766                            ),
 767                        },
 768                        message: "error 2 hint 2".to_string(),
 769                    },
 770                ]),
 771                ..Default::default()
 772            },
 773            lsp::Diagnostic {
 774                range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
 775                severity: Some(DiagnosticSeverity::HINT),
 776                message: "error 2 hint 1".to_string(),
 777                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
 778                    location: lsp::Location {
 779                        uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
 780                        range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
 781                    },
 782                    message: "original diagnostic".to_string(),
 783                }]),
 784                ..Default::default()
 785            },
 786            lsp::Diagnostic {
 787                range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
 788                severity: Some(DiagnosticSeverity::HINT),
 789                message: "error 2 hint 2".to_string(),
 790                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
 791                    location: lsp::Location {
 792                        uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
 793                        range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
 794                    },
 795                    message: "original diagnostic".to_string(),
 796                }]),
 797                ..Default::default()
 798            },
 799        ];
 800        buffer.update_diagnostics(None, diagnostics, cx).unwrap();
 801        assert_eq!(
 802            buffer
 803                .diagnostics_in_range::<_, Point>(0..buffer.len())
 804                .collect::<Vec<_>>(),
 805            &[
 806                (
 807                    Point::new(1, 8)..Point::new(1, 9),
 808                    &Diagnostic {
 809                        severity: DiagnosticSeverity::WARNING,
 810                        message: "error 1".to_string(),
 811                        group_id: 0
 812                    }
 813                ),
 814                (
 815                    Point::new(1, 8)..Point::new(1, 9),
 816                    &Diagnostic {
 817                        severity: DiagnosticSeverity::HINT,
 818                        message: "error 1 hint 1".to_string(),
 819                        group_id: 0
 820                    }
 821                ),
 822                (
 823                    Point::new(1, 13)..Point::new(1, 15),
 824                    &Diagnostic {
 825                        severity: DiagnosticSeverity::HINT,
 826                        message: "error 2 hint 1".to_string(),
 827                        group_id: 1
 828                    }
 829                ),
 830                (
 831                    Point::new(1, 13)..Point::new(1, 15),
 832                    &Diagnostic {
 833                        severity: DiagnosticSeverity::HINT,
 834                        message: "error 2 hint 2".to_string(),
 835                        group_id: 1
 836                    }
 837                ),
 838                (
 839                    Point::new(2, 8)..Point::new(2, 17),
 840                    &Diagnostic {
 841                        severity: DiagnosticSeverity::ERROR,
 842                        message: "error 2".to_string(),
 843                        group_id: 1
 844                    }
 845                )
 846            ]
 847        );
 848
 849        assert_eq!(
 850            buffer.diagnostic_group(0).collect::<Vec<_>>(),
 851            &[
 852                (
 853                    Point::new(1, 8)..Point::new(1, 9),
 854                    &Diagnostic {
 855                        severity: DiagnosticSeverity::WARNING,
 856                        message: "error 1".to_string(),
 857                        group_id: 0
 858                    }
 859                ),
 860                (
 861                    Point::new(1, 8)..Point::new(1, 9),
 862                    &Diagnostic {
 863                        severity: DiagnosticSeverity::HINT,
 864                        message: "error 1 hint 1".to_string(),
 865                        group_id: 0
 866                    }
 867                ),
 868            ]
 869        );
 870        assert_eq!(
 871            buffer.diagnostic_group(1).collect::<Vec<_>>(),
 872            &[
 873                (
 874                    Point::new(1, 13)..Point::new(1, 15),
 875                    &Diagnostic {
 876                        severity: DiagnosticSeverity::HINT,
 877                        message: "error 2 hint 1".to_string(),
 878                        group_id: 1
 879                    }
 880                ),
 881                (
 882                    Point::new(1, 13)..Point::new(1, 15),
 883                    &Diagnostic {
 884                        severity: DiagnosticSeverity::HINT,
 885                        message: "error 2 hint 2".to_string(),
 886                        group_id: 1
 887                    }
 888                ),
 889                (
 890                    Point::new(2, 8)..Point::new(2, 17),
 891                    &Diagnostic {
 892                        severity: DiagnosticSeverity::ERROR,
 893                        message: "error 2".to_string(),
 894                        group_id: 1
 895                    }
 896                )
 897            ]
 898        );
 899
 900        buffer
 901    });
 902}
 903
 904fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
 905    buffer: &Buffer,
 906    range: Range<T>,
 907) -> Vec<(String, Option<DiagnosticSeverity>)> {
 908    let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
 909    for chunk in buffer.snapshot().chunks(range, true) {
 910        if chunks
 911            .last()
 912            .map_or(false, |prev_chunk| prev_chunk.1 == chunk.diagnostic)
 913        {
 914            chunks.last_mut().unwrap().0.push_str(chunk.text);
 915        } else {
 916            chunks.push((chunk.text.to_string(), chunk.diagnostic));
 917        }
 918    }
 919    chunks
 920}
 921
 922#[test]
 923fn test_contiguous_ranges() {
 924    assert_eq!(
 925        contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12], 100).collect::<Vec<_>>(),
 926        &[1..4, 5..7, 9..13]
 927    );
 928
 929    // Respects the `max_len` parameter
 930    assert_eq!(
 931        contiguous_ranges([2, 3, 4, 5, 6, 7, 8, 9, 23, 24, 25, 26, 30, 31], 3).collect::<Vec<_>>(),
 932        &[2..5, 5..8, 8..10, 23..26, 26..27, 30..32],
 933    );
 934}
 935
 936impl Buffer {
 937    pub fn enclosing_bracket_point_ranges<T: ToOffset>(
 938        &self,
 939        range: Range<T>,
 940    ) -> Option<(Range<Point>, Range<Point>)> {
 941        self.enclosing_bracket_ranges(range).map(|(start, end)| {
 942            let point_start = start.start.to_point(self)..start.end.to_point(self);
 943            let point_end = end.start.to_point(self)..end.end.to_point(self);
 944            (point_start, point_end)
 945        })
 946    }
 947}
 948
 949fn rust_lang() -> Language {
 950    Language::new(
 951        LanguageConfig {
 952            name: "Rust".to_string(),
 953            path_suffixes: vec!["rs".to_string()],
 954            language_server: None,
 955            ..Default::default()
 956        },
 957        tree_sitter_rust::language(),
 958    )
 959    .with_indents_query(
 960        r#"
 961                (call_expression) @indent
 962                (field_expression) @indent
 963                (_ "(" ")" @end) @indent
 964                (_ "{" "}" @end) @indent
 965            "#,
 966    )
 967    .unwrap()
 968    .with_brackets_query(r#" ("{" @open "}" @close) "#)
 969    .unwrap()
 970}
 971
 972fn empty(point: Point) -> Range<Point> {
 973    point..point
 974}
 975
 976#[derive(Clone)]
 977struct FakeFile {
 978    abs_path: PathBuf,
 979}
 980
 981impl FakeFile {
 982    fn new(abs_path: impl Into<PathBuf>) -> Self {
 983        Self {
 984            abs_path: abs_path.into(),
 985        }
 986    }
 987}
 988
 989impl File for FakeFile {
 990    fn worktree_id(&self) -> usize {
 991        todo!()
 992    }
 993
 994    fn entry_id(&self) -> Option<usize> {
 995        todo!()
 996    }
 997
 998    fn mtime(&self) -> SystemTime {
 999        SystemTime::now()
1000    }
1001
1002    fn path(&self) -> &Arc<Path> {
1003        todo!()
1004    }
1005
1006    fn abs_path(&self) -> Option<PathBuf> {
1007        Some(self.abs_path.clone())
1008    }
1009
1010    fn full_path(&self) -> PathBuf {
1011        todo!()
1012    }
1013
1014    fn file_name(&self) -> Option<OsString> {
1015        todo!()
1016    }
1017
1018    fn is_deleted(&self) -> bool {
1019        todo!()
1020    }
1021
1022    fn save(
1023        &self,
1024        _: u64,
1025        _: Rope,
1026        _: clock::Global,
1027        _: &mut MutableAppContext,
1028    ) -> Task<Result<(clock::Global, SystemTime)>> {
1029        todo!()
1030    }
1031
1032    fn load_local(&self, _: &AppContext) -> Option<Task<Result<String>>> {
1033        todo!()
1034    }
1035
1036    fn buffer_updated(&self, _: u64, _: super::Operation, _: &mut MutableAppContext) {
1037        todo!()
1038    }
1039
1040    fn buffer_removed(&self, _: u64, _: &mut MutableAppContext) {
1041        todo!()
1042    }
1043
1044    fn boxed_clone(&self) -> Box<dyn File> {
1045        todo!()
1046    }
1047
1048    fn as_any(&self) -> &dyn Any {
1049        todo!()
1050    }
1051}