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();
  96        buffer.end_transaction(cx);
  97
  98        // A transaction containing two edits emits one edited event.
  99        now += Duration::from_secs(1);
 100        buffer.start_transaction_at(now);
 101        buffer.edit(Some(5..5), "u", cx);
 102        buffer.edit(Some(6..6), "w", cx);
 103        buffer.end_transaction_at(now, cx);
 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();
 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(cx);
 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// We need another approach to managing selections with auto-indent
 337
 338// #[gpui::test]
 339// fn test_autoindent_moves_selections(cx: &mut MutableAppContext) {
 340//     cx.add_model(|cx| {
 341//         let text = "fn a() {}";
 342
 343//         let mut buffer =
 344//             Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
 345
 346//         let selection_set_id = buffer.add_selection_set::<usize>(&[], cx);
 347//         buffer.start_transaction();
 348//         buffer.edit_with_autoindent([5..5, 9..9], "\n\n", cx);
 349//         buffer
 350//             .update_selection_set(
 351//                 selection_set_id,
 352//                 &[
 353//                     Selection {
 354//                         id: 0,
 355//                         start: Point::new(1, 0),
 356//                         end: Point::new(1, 0),
 357//                         reversed: false,
 358//                         goal: SelectionGoal::None,
 359//                     },
 360//                     Selection {
 361//                         id: 1,
 362//                         start: Point::new(4, 0),
 363//                         end: Point::new(4, 0),
 364//                         reversed: false,
 365//                         goal: SelectionGoal::None,
 366//                     },
 367//                 ],
 368//                 cx,
 369//             )
 370//             .unwrap();
 371//         assert_eq!(buffer.text(), "fn a(\n\n) {}\n\n");
 372
 373//         // TODO! Come up with a different approach to moving selections now that we don't manage selection sets in the buffer
 374
 375//         // Ending the transaction runs the auto-indent. The selection
 376//         // at the start of the auto-indented row is pushed to the right.
 377//         buffer.end_transaction(cx);
 378//         assert_eq!(buffer.text(), "fn a(\n    \n) {}\n\n");
 379//         let selection_ranges = buffer
 380//             .selection_set(selection_set_id)
 381//             .unwrap()
 382//             .selections::<Point>(&buffer)
 383//             .map(|selection| selection.start.to_point(&buffer)..selection.end.to_point(&buffer))
 384//             .collect::<Vec<_>>();
 385
 386//         assert_eq!(selection_ranges[0], empty(Point::new(1, 4)));
 387//         assert_eq!(selection_ranges[1], empty(Point::new(4, 0)));
 388
 389//         buffer
 390//     });
 391// }
 392#[gpui::test]
 393fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut MutableAppContext) {
 394    cx.add_model(|cx| {
 395        let text = "
 396            fn a() {
 397            c;
 398            d;
 399            }
 400        "
 401        .unindent();
 402
 403        let mut buffer =
 404            Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
 405
 406        // Lines 2 and 3 don't match the indentation suggestion. When editing these lines,
 407        // their indentation is not adjusted.
 408        buffer.edit_with_autoindent([empty(Point::new(1, 1)), empty(Point::new(2, 1))], "()", cx);
 409        assert_eq!(
 410            buffer.text(),
 411            "
 412            fn a() {
 413            c();
 414            d();
 415            }
 416            "
 417            .unindent()
 418        );
 419
 420        // When appending new content after these lines, the indentation is based on the
 421        // preceding lines' actual indentation.
 422        buffer.edit_with_autoindent(
 423            [empty(Point::new(1, 1)), empty(Point::new(2, 1))],
 424            "\n.f\n.g",
 425            cx,
 426        );
 427        assert_eq!(
 428            buffer.text(),
 429            "
 430            fn a() {
 431            c
 432                .f
 433                .g();
 434            d
 435                .f
 436                .g();
 437            }
 438            "
 439            .unindent()
 440        );
 441        buffer
 442    });
 443}
 444
 445#[gpui::test]
 446fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppContext) {
 447    cx.add_model(|cx| {
 448        let text = "
 449            fn a() {}
 450        "
 451        .unindent();
 452
 453        let mut buffer =
 454            Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
 455
 456        buffer.edit_with_autoindent([5..5], "\nb", cx);
 457        assert_eq!(
 458            buffer.text(),
 459            "
 460                fn a(
 461                    b) {}
 462            "
 463            .unindent()
 464        );
 465
 466        // The indentation suggestion changed because `@end` node (a close paren)
 467        // is now at the beginning of the line.
 468        buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 5)], "", cx);
 469        assert_eq!(
 470            buffer.text(),
 471            "
 472                fn a(
 473                ) {}
 474            "
 475            .unindent()
 476        );
 477
 478        buffer
 479    });
 480}
 481
 482#[gpui::test]
 483async fn test_diagnostics(mut cx: gpui::TestAppContext) {
 484    let (language_server, mut fake) = lsp::LanguageServer::fake(cx.background()).await;
 485    let mut rust_lang = rust_lang();
 486    rust_lang.config.language_server = Some(LanguageServerConfig {
 487        disk_based_diagnostic_sources: HashSet::from_iter(["disk".to_string()]),
 488        ..Default::default()
 489    });
 490
 491    let text = "
 492        fn a() { A }
 493        fn b() { BB }
 494        fn c() { CCC }
 495    "
 496    .unindent();
 497
 498    let buffer = cx.add_model(|cx| {
 499        Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang)), Some(language_server), cx)
 500    });
 501
 502    let open_notification = fake
 503        .receive_notification::<lsp::notification::DidOpenTextDocument>()
 504        .await;
 505
 506    // Edit the buffer, moving the content down
 507    buffer.update(&mut cx, |buffer, cx| buffer.edit([0..0], "\n\n", cx));
 508    let change_notification_1 = fake
 509        .receive_notification::<lsp::notification::DidChangeTextDocument>()
 510        .await;
 511    assert!(change_notification_1.text_document.version > open_notification.text_document.version);
 512
 513    buffer.update(&mut cx, |buffer, cx| {
 514        // Receive diagnostics for an earlier version of the buffer.
 515        buffer
 516            .update_diagnostics(
 517                Some(open_notification.text_document.version),
 518                vec![
 519                    lsp::Diagnostic {
 520                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
 521                        severity: Some(lsp::DiagnosticSeverity::ERROR),
 522                        message: "undefined variable 'A'".to_string(),
 523                        ..Default::default()
 524                    },
 525                    lsp::Diagnostic {
 526                        range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
 527                        severity: Some(lsp::DiagnosticSeverity::ERROR),
 528                        message: "undefined variable 'BB'".to_string(),
 529                        ..Default::default()
 530                    },
 531                    lsp::Diagnostic {
 532                        range: lsp::Range::new(lsp::Position::new(2, 9), lsp::Position::new(2, 12)),
 533                        severity: Some(lsp::DiagnosticSeverity::ERROR),
 534                        message: "undefined variable 'CCC'".to_string(),
 535                        ..Default::default()
 536                    },
 537                ],
 538                cx,
 539            )
 540            .unwrap();
 541
 542        // The diagnostics have moved down since they were created.
 543        assert_eq!(
 544            buffer
 545                .snapshot()
 546                .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0))
 547                .collect::<Vec<_>>(),
 548            &[
 549                DiagnosticEntry {
 550                    range: Point::new(3, 9)..Point::new(3, 11),
 551                    diagnostic: Diagnostic {
 552                        severity: DiagnosticSeverity::ERROR,
 553                        message: "undefined variable 'BB'".to_string(),
 554                        group_id: 1,
 555                        is_primary: true,
 556                    },
 557                },
 558                DiagnosticEntry {
 559                    range: Point::new(4, 9)..Point::new(4, 12),
 560                    diagnostic: Diagnostic {
 561                        severity: DiagnosticSeverity::ERROR,
 562                        message: "undefined variable 'CCC'".to_string(),
 563                        group_id: 2,
 564                        is_primary: true,
 565                    }
 566                }
 567            ]
 568        );
 569        assert_eq!(
 570            chunks_with_diagnostics(buffer, 0..buffer.len()),
 571            [
 572                ("\n\nfn a() { ".to_string(), None),
 573                ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
 574                (" }\nfn b() { ".to_string(), None),
 575                ("BB".to_string(), Some(DiagnosticSeverity::ERROR)),
 576                (" }\nfn c() { ".to_string(), None),
 577                ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)),
 578                (" }\n".to_string(), None),
 579            ]
 580        );
 581        assert_eq!(
 582            chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)),
 583            [
 584                ("B".to_string(), Some(DiagnosticSeverity::ERROR)),
 585                (" }\nfn c() { ".to_string(), None),
 586                ("CC".to_string(), Some(DiagnosticSeverity::ERROR)),
 587            ]
 588        );
 589
 590        // Ensure overlapping diagnostics are highlighted correctly.
 591        buffer
 592            .update_diagnostics(
 593                Some(open_notification.text_document.version),
 594                vec![
 595                    lsp::Diagnostic {
 596                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
 597                        severity: Some(lsp::DiagnosticSeverity::ERROR),
 598                        message: "undefined variable 'A'".to_string(),
 599                        ..Default::default()
 600                    },
 601                    lsp::Diagnostic {
 602                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 12)),
 603                        severity: Some(lsp::DiagnosticSeverity::WARNING),
 604                        message: "unreachable statement".to_string(),
 605                        ..Default::default()
 606                    },
 607                ],
 608                cx,
 609            )
 610            .unwrap();
 611        assert_eq!(
 612            buffer
 613                .snapshot()
 614                .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0))
 615                .collect::<Vec<_>>(),
 616            &[
 617                DiagnosticEntry {
 618                    range: Point::new(2, 9)..Point::new(2, 12),
 619                    diagnostic: Diagnostic {
 620                        severity: DiagnosticSeverity::WARNING,
 621                        message: "unreachable statement".to_string(),
 622                        group_id: 1,
 623                        is_primary: true,
 624                    }
 625                },
 626                DiagnosticEntry {
 627                    range: Point::new(2, 9)..Point::new(2, 10),
 628                    diagnostic: Diagnostic {
 629                        severity: DiagnosticSeverity::ERROR,
 630                        message: "undefined variable 'A'".to_string(),
 631                        group_id: 0,
 632                        is_primary: true,
 633                    },
 634                }
 635            ]
 636        );
 637        assert_eq!(
 638            chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)),
 639            [
 640                ("fn a() { ".to_string(), None),
 641                ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
 642                (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
 643                ("\n".to_string(), None),
 644            ]
 645        );
 646        assert_eq!(
 647            chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)),
 648            [
 649                (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
 650                ("\n".to_string(), None),
 651            ]
 652        );
 653    });
 654
 655    // Keep editing the buffer and ensure disk-based diagnostics get translated according to the
 656    // changes since the last save.
 657    buffer.update(&mut cx, |buffer, cx| {
 658        buffer.edit(Some(Point::new(2, 0)..Point::new(2, 0)), "    ", cx);
 659        buffer.edit(Some(Point::new(2, 8)..Point::new(2, 10)), "(x: usize)", cx);
 660    });
 661    let change_notification_2 = fake
 662        .receive_notification::<lsp::notification::DidChangeTextDocument>()
 663        .await;
 664    assert!(
 665        change_notification_2.text_document.version > change_notification_1.text_document.version
 666    );
 667
 668    buffer.update(&mut cx, |buffer, cx| {
 669        buffer
 670            .update_diagnostics(
 671                Some(change_notification_2.text_document.version),
 672                vec![
 673                    lsp::Diagnostic {
 674                        range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
 675                        severity: Some(lsp::DiagnosticSeverity::ERROR),
 676                        message: "undefined variable 'BB'".to_string(),
 677                        source: Some("disk".to_string()),
 678                        ..Default::default()
 679                    },
 680                    lsp::Diagnostic {
 681                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
 682                        severity: Some(lsp::DiagnosticSeverity::ERROR),
 683                        message: "undefined variable 'A'".to_string(),
 684                        source: Some("disk".to_string()),
 685                        ..Default::default()
 686                    },
 687                ],
 688                cx,
 689            )
 690            .unwrap();
 691        assert_eq!(
 692            buffer
 693                .snapshot()
 694                .diagnostics_in_range::<_, Point>(0..buffer.len())
 695                .collect::<Vec<_>>(),
 696            &[
 697                DiagnosticEntry {
 698                    range: Point::new(2, 21)..Point::new(2, 22),
 699                    diagnostic: Diagnostic {
 700                        severity: DiagnosticSeverity::ERROR,
 701                        message: "undefined variable 'A'".to_string(),
 702                        group_id: 0,
 703                        is_primary: true,
 704                    }
 705                },
 706                DiagnosticEntry {
 707                    range: Point::new(3, 9)..Point::new(3, 11),
 708                    diagnostic: Diagnostic {
 709                        severity: DiagnosticSeverity::ERROR,
 710                        message: "undefined variable 'BB'".to_string(),
 711                        group_id: 1,
 712                        is_primary: true,
 713                    },
 714                }
 715            ]
 716        );
 717    });
 718}
 719
 720#[gpui::test]
 721async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
 722    cx.add_model(|cx| {
 723        let text = concat!(
 724            "let one = ;\n", //
 725            "let two = \n",
 726            "let three = 3;\n",
 727        );
 728
 729        let mut buffer = Buffer::new(0, text, cx);
 730        buffer.set_language(Some(Arc::new(rust_lang())), None, cx);
 731        buffer
 732            .update_diagnostics(
 733                None,
 734                vec![
 735                    lsp::Diagnostic {
 736                        range: lsp::Range::new(
 737                            lsp::Position::new(0, 10),
 738                            lsp::Position::new(0, 10),
 739                        ),
 740                        severity: Some(lsp::DiagnosticSeverity::ERROR),
 741                        message: "syntax error 1".to_string(),
 742                        ..Default::default()
 743                    },
 744                    lsp::Diagnostic {
 745                        range: lsp::Range::new(
 746                            lsp::Position::new(1, 10),
 747                            lsp::Position::new(1, 10),
 748                        ),
 749                        severity: Some(lsp::DiagnosticSeverity::ERROR),
 750                        message: "syntax error 2".to_string(),
 751                        ..Default::default()
 752                    },
 753                ],
 754                cx,
 755            )
 756            .unwrap();
 757
 758        // An empty range is extended forward to include the following character.
 759        // At the end of a line, an empty range is extended backward to include
 760        // the preceding character.
 761        let chunks = chunks_with_diagnostics(&buffer, 0..buffer.len());
 762        assert_eq!(
 763            chunks
 764                .iter()
 765                .map(|(s, d)| (s.as_str(), *d))
 766                .collect::<Vec<_>>(),
 767            &[
 768                ("let one = ", None),
 769                (";", Some(lsp::DiagnosticSeverity::ERROR)),
 770                ("\nlet two =", None),
 771                (" ", Some(lsp::DiagnosticSeverity::ERROR)),
 772                ("\nlet three = 3;\n", None)
 773            ]
 774        );
 775        buffer
 776    });
 777}
 778
 779#[gpui::test]
 780async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) {
 781    cx.add_model(|cx| {
 782        let text = "
 783            fn foo(mut v: Vec<usize>) {
 784                for x in &v {
 785                    v.push(1);
 786                }
 787            }
 788        "
 789        .unindent();
 790
 791        let file = FakeFile::new("/example.rs");
 792        let mut buffer = Buffer::from_file(0, text, Box::new(file.clone()), cx);
 793        buffer.set_language(Some(Arc::new(rust_lang())), None, cx);
 794        let diagnostics = vec![
 795            lsp::Diagnostic {
 796                range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
 797                severity: Some(DiagnosticSeverity::WARNING),
 798                message: "error 1".to_string(),
 799                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
 800                    location: lsp::Location {
 801                        uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
 802                        range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
 803                    },
 804                    message: "error 1 hint 1".to_string(),
 805                }]),
 806                ..Default::default()
 807            },
 808            lsp::Diagnostic {
 809                range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
 810                severity: Some(DiagnosticSeverity::HINT),
 811                message: "error 1 hint 1".to_string(),
 812                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
 813                    location: lsp::Location {
 814                        uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
 815                        range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
 816                    },
 817                    message: "original diagnostic".to_string(),
 818                }]),
 819                ..Default::default()
 820            },
 821            lsp::Diagnostic {
 822                range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
 823                severity: Some(DiagnosticSeverity::ERROR),
 824                message: "error 2".to_string(),
 825                related_information: Some(vec![
 826                    lsp::DiagnosticRelatedInformation {
 827                        location: lsp::Location {
 828                            uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
 829                            range: lsp::Range::new(
 830                                lsp::Position::new(1, 13),
 831                                lsp::Position::new(1, 15),
 832                            ),
 833                        },
 834                        message: "error 2 hint 1".to_string(),
 835                    },
 836                    lsp::DiagnosticRelatedInformation {
 837                        location: lsp::Location {
 838                            uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
 839                            range: lsp::Range::new(
 840                                lsp::Position::new(1, 13),
 841                                lsp::Position::new(1, 15),
 842                            ),
 843                        },
 844                        message: "error 2 hint 2".to_string(),
 845                    },
 846                ]),
 847                ..Default::default()
 848            },
 849            lsp::Diagnostic {
 850                range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
 851                severity: Some(DiagnosticSeverity::HINT),
 852                message: "error 2 hint 1".to_string(),
 853                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
 854                    location: lsp::Location {
 855                        uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
 856                        range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
 857                    },
 858                    message: "original diagnostic".to_string(),
 859                }]),
 860                ..Default::default()
 861            },
 862            lsp::Diagnostic {
 863                range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
 864                severity: Some(DiagnosticSeverity::HINT),
 865                message: "error 2 hint 2".to_string(),
 866                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
 867                    location: lsp::Location {
 868                        uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
 869                        range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
 870                    },
 871                    message: "original diagnostic".to_string(),
 872                }]),
 873                ..Default::default()
 874            },
 875        ];
 876        buffer.update_diagnostics(None, diagnostics, cx).unwrap();
 877        assert_eq!(
 878            buffer
 879                .snapshot()
 880                .diagnostics_in_range::<_, Point>(0..buffer.len())
 881                .collect::<Vec<_>>(),
 882            &[
 883                DiagnosticEntry {
 884                    range: Point::new(1, 8)..Point::new(1, 9),
 885                    diagnostic: Diagnostic {
 886                        severity: DiagnosticSeverity::WARNING,
 887                        message: "error 1".to_string(),
 888                        group_id: 0,
 889                        is_primary: true,
 890                    }
 891                },
 892                DiagnosticEntry {
 893                    range: Point::new(1, 8)..Point::new(1, 9),
 894                    diagnostic: Diagnostic {
 895                        severity: DiagnosticSeverity::HINT,
 896                        message: "error 1 hint 1".to_string(),
 897                        group_id: 0,
 898                        is_primary: false,
 899                    }
 900                },
 901                DiagnosticEntry {
 902                    range: Point::new(1, 13)..Point::new(1, 15),
 903                    diagnostic: Diagnostic {
 904                        severity: DiagnosticSeverity::HINT,
 905                        message: "error 2 hint 1".to_string(),
 906                        group_id: 1,
 907                        is_primary: false,
 908                    }
 909                },
 910                DiagnosticEntry {
 911                    range: Point::new(1, 13)..Point::new(1, 15),
 912                    diagnostic: Diagnostic {
 913                        severity: DiagnosticSeverity::HINT,
 914                        message: "error 2 hint 2".to_string(),
 915                        group_id: 1,
 916                        is_primary: false,
 917                    }
 918                },
 919                DiagnosticEntry {
 920                    range: Point::new(2, 8)..Point::new(2, 17),
 921                    diagnostic: Diagnostic {
 922                        severity: DiagnosticSeverity::ERROR,
 923                        message: "error 2".to_string(),
 924                        group_id: 1,
 925                        is_primary: true,
 926                    }
 927                }
 928            ]
 929        );
 930
 931        assert_eq!(
 932            buffer
 933                .snapshot()
 934                .diagnostic_group::<Point>(0)
 935                .collect::<Vec<_>>(),
 936            &[
 937                DiagnosticEntry {
 938                    range: Point::new(1, 8)..Point::new(1, 9),
 939                    diagnostic: Diagnostic {
 940                        severity: DiagnosticSeverity::WARNING,
 941                        message: "error 1".to_string(),
 942                        group_id: 0,
 943                        is_primary: true,
 944                    }
 945                },
 946                DiagnosticEntry {
 947                    range: Point::new(1, 8)..Point::new(1, 9),
 948                    diagnostic: Diagnostic {
 949                        severity: DiagnosticSeverity::HINT,
 950                        message: "error 1 hint 1".to_string(),
 951                        group_id: 0,
 952                        is_primary: false,
 953                    }
 954                },
 955            ]
 956        );
 957        assert_eq!(
 958            buffer
 959                .snapshot()
 960                .diagnostic_group::<Point>(1)
 961                .collect::<Vec<_>>(),
 962            &[
 963                DiagnosticEntry {
 964                    range: Point::new(1, 13)..Point::new(1, 15),
 965                    diagnostic: Diagnostic {
 966                        severity: DiagnosticSeverity::HINT,
 967                        message: "error 2 hint 1".to_string(),
 968                        group_id: 1,
 969                        is_primary: false,
 970                    }
 971                },
 972                DiagnosticEntry {
 973                    range: Point::new(1, 13)..Point::new(1, 15),
 974                    diagnostic: Diagnostic {
 975                        severity: DiagnosticSeverity::HINT,
 976                        message: "error 2 hint 2".to_string(),
 977                        group_id: 1,
 978                        is_primary: false,
 979                    }
 980                },
 981                DiagnosticEntry {
 982                    range: Point::new(2, 8)..Point::new(2, 17),
 983                    diagnostic: Diagnostic {
 984                        severity: DiagnosticSeverity::ERROR,
 985                        message: "error 2".to_string(),
 986                        group_id: 1,
 987                        is_primary: true,
 988                    }
 989                }
 990            ]
 991        );
 992
 993        buffer
 994    });
 995}
 996
 997fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
 998    buffer: &Buffer,
 999    range: Range<T>,
1000) -> Vec<(String, Option<DiagnosticSeverity>)> {
1001    let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
1002    for chunk in buffer.snapshot().chunks(range, Some(&Default::default())) {
1003        if chunks
1004            .last()
1005            .map_or(false, |prev_chunk| prev_chunk.1 == chunk.diagnostic)
1006        {
1007            chunks.last_mut().unwrap().0.push_str(chunk.text);
1008        } else {
1009            chunks.push((chunk.text.to_string(), chunk.diagnostic));
1010        }
1011    }
1012    chunks
1013}
1014
1015#[test]
1016fn test_contiguous_ranges() {
1017    assert_eq!(
1018        contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12].into_iter(), 100).collect::<Vec<_>>(),
1019        &[1..4, 5..7, 9..13]
1020    );
1021
1022    // Respects the `max_len` parameter
1023    assert_eq!(
1024        contiguous_ranges(
1025            [2, 3, 4, 5, 6, 7, 8, 9, 23, 24, 25, 26, 30, 31].into_iter(),
1026            3
1027        )
1028        .collect::<Vec<_>>(),
1029        &[2..5, 5..8, 8..10, 23..26, 26..27, 30..32],
1030    );
1031}
1032
1033impl Buffer {
1034    pub fn enclosing_bracket_point_ranges<T: ToOffset>(
1035        &self,
1036        range: Range<T>,
1037    ) -> Option<(Range<Point>, Range<Point>)> {
1038        self.snapshot()
1039            .enclosing_bracket_ranges(range)
1040            .map(|(start, end)| {
1041                let point_start = start.start.to_point(self)..start.end.to_point(self);
1042                let point_end = end.start.to_point(self)..end.end.to_point(self);
1043                (point_start, point_end)
1044            })
1045    }
1046}
1047
1048fn rust_lang() -> Language {
1049    Language::new(
1050        LanguageConfig {
1051            name: "Rust".to_string(),
1052            path_suffixes: vec!["rs".to_string()],
1053            language_server: None,
1054            ..Default::default()
1055        },
1056        Some(tree_sitter_rust::language()),
1057    )
1058    .with_indents_query(
1059        r#"
1060                (call_expression) @indent
1061                (field_expression) @indent
1062                (_ "(" ")" @end) @indent
1063                (_ "{" "}" @end) @indent
1064            "#,
1065    )
1066    .unwrap()
1067    .with_brackets_query(r#" ("{" @open "}" @close) "#)
1068    .unwrap()
1069}
1070
1071fn empty(point: Point) -> Range<Point> {
1072    point..point
1073}
1074
1075#[derive(Clone)]
1076struct FakeFile {
1077    abs_path: PathBuf,
1078}
1079
1080impl FakeFile {
1081    fn new(abs_path: impl Into<PathBuf>) -> Self {
1082        Self {
1083            abs_path: abs_path.into(),
1084        }
1085    }
1086}
1087
1088impl File for FakeFile {
1089    fn worktree_id(&self) -> usize {
1090        todo!()
1091    }
1092
1093    fn entry_id(&self) -> Option<usize> {
1094        todo!()
1095    }
1096
1097    fn mtime(&self) -> SystemTime {
1098        SystemTime::now()
1099    }
1100
1101    fn path(&self) -> &Arc<Path> {
1102        todo!()
1103    }
1104
1105    fn abs_path(&self) -> Option<PathBuf> {
1106        Some(self.abs_path.clone())
1107    }
1108
1109    fn full_path(&self) -> PathBuf {
1110        todo!()
1111    }
1112
1113    fn file_name(&self) -> Option<OsString> {
1114        todo!()
1115    }
1116
1117    fn is_deleted(&self) -> bool {
1118        todo!()
1119    }
1120
1121    fn save(
1122        &self,
1123        _: u64,
1124        _: Rope,
1125        _: clock::Global,
1126        _: &mut MutableAppContext,
1127    ) -> Task<Result<(clock::Global, SystemTime)>> {
1128        todo!()
1129    }
1130
1131    fn load_local(&self, _: &AppContext) -> Option<Task<Result<String>>> {
1132        todo!()
1133    }
1134
1135    fn buffer_updated(&self, _: u64, _: super::Operation, _: &mut MutableAppContext) {
1136        todo!()
1137    }
1138
1139    fn buffer_removed(&self, _: u64, _: &mut MutableAppContext) {
1140        todo!()
1141    }
1142
1143    fn boxed_clone(&self) -> Box<dyn File> {
1144        todo!()
1145    }
1146
1147    fn as_any(&self) -> &dyn Any {
1148        todo!()
1149    }
1150}