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: 0,
 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: 0,
 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: 0,
 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: 0,
 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        buffer
 850    });
 851}
 852
 853fn chunks_with_diagnostics<T: ToOffset>(
 854    buffer: &Buffer,
 855    range: Range<T>,
 856) -> Vec<(String, Option<DiagnosticSeverity>)> {
 857    let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
 858    for chunk in buffer.snapshot().highlighted_text_for_range(range) {
 859        if chunks
 860            .last()
 861            .map_or(false, |prev_chunk| prev_chunk.1 == chunk.diagnostic)
 862        {
 863            chunks.last_mut().unwrap().0.push_str(chunk.text);
 864        } else {
 865            chunks.push((chunk.text.to_string(), chunk.diagnostic));
 866        }
 867    }
 868    chunks
 869}
 870
 871#[test]
 872fn test_contiguous_ranges() {
 873    assert_eq!(
 874        contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12], 100).collect::<Vec<_>>(),
 875        &[1..4, 5..7, 9..13]
 876    );
 877
 878    // Respects the `max_len` parameter
 879    assert_eq!(
 880        contiguous_ranges([2, 3, 4, 5, 6, 7, 8, 9, 23, 24, 25, 26, 30, 31], 3).collect::<Vec<_>>(),
 881        &[2..5, 5..8, 8..10, 23..26, 26..27, 30..32],
 882    );
 883}
 884
 885impl Buffer {
 886    pub fn enclosing_bracket_point_ranges<T: ToOffset>(
 887        &self,
 888        range: Range<T>,
 889    ) -> Option<(Range<Point>, Range<Point>)> {
 890        self.enclosing_bracket_ranges(range).map(|(start, end)| {
 891            let point_start = start.start.to_point(self)..start.end.to_point(self);
 892            let point_end = end.start.to_point(self)..end.end.to_point(self);
 893            (point_start, point_end)
 894        })
 895    }
 896}
 897
 898fn rust_lang() -> Language {
 899    Language::new(
 900        LanguageConfig {
 901            name: "Rust".to_string(),
 902            path_suffixes: vec!["rs".to_string()],
 903            language_server: None,
 904            ..Default::default()
 905        },
 906        tree_sitter_rust::language(),
 907    )
 908    .with_indents_query(
 909        r#"
 910                (call_expression) @indent
 911                (field_expression) @indent
 912                (_ "(" ")" @end) @indent
 913                (_ "{" "}" @end) @indent
 914            "#,
 915    )
 916    .unwrap()
 917    .with_brackets_query(r#" ("{" @open "}" @close) "#)
 918    .unwrap()
 919}
 920
 921fn empty(point: Point) -> Range<Point> {
 922    point..point
 923}
 924
 925#[derive(Clone)]
 926struct FakeFile {
 927    abs_path: PathBuf,
 928}
 929
 930impl FakeFile {
 931    fn new(abs_path: impl Into<PathBuf>) -> Self {
 932        Self {
 933            abs_path: abs_path.into(),
 934        }
 935    }
 936}
 937
 938impl File for FakeFile {
 939    fn worktree_id(&self) -> usize {
 940        todo!()
 941    }
 942
 943    fn entry_id(&self) -> Option<usize> {
 944        todo!()
 945    }
 946
 947    fn mtime(&self) -> SystemTime {
 948        SystemTime::now()
 949    }
 950
 951    fn path(&self) -> &Arc<Path> {
 952        todo!()
 953    }
 954
 955    fn abs_path(&self) -> Option<PathBuf> {
 956        Some(self.abs_path.clone())
 957    }
 958
 959    fn full_path(&self) -> PathBuf {
 960        todo!()
 961    }
 962
 963    fn file_name(&self) -> Option<OsString> {
 964        todo!()
 965    }
 966
 967    fn is_deleted(&self) -> bool {
 968        todo!()
 969    }
 970
 971    fn save(
 972        &self,
 973        _: u64,
 974        _: Rope,
 975        _: clock::Global,
 976        _: &mut MutableAppContext,
 977    ) -> Task<Result<(clock::Global, SystemTime)>> {
 978        todo!()
 979    }
 980
 981    fn load_local(&self, _: &AppContext) -> Option<Task<Result<String>>> {
 982        todo!()
 983    }
 984
 985    fn buffer_updated(&self, _: u64, _: super::Operation, _: &mut MutableAppContext) {
 986        todo!()
 987    }
 988
 989    fn buffer_removed(&self, _: u64, _: &mut MutableAppContext) {
 990        todo!()
 991    }
 992
 993    fn boxed_clone(&self) -> Box<dyn File> {
 994        todo!()
 995    }
 996
 997    fn as_any(&self) -> &dyn Any {
 998        todo!()
 999    }
1000}