tests.rs

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