tests.rs

  1use super::*;
  2use gpui::{ModelHandle, MutableAppContext};
  3use std::{
  4    cell::RefCell,
  5    iter::FromIterator,
  6    ops::Range,
  7    rc::Rc,
  8    time::{Duration, Instant},
  9};
 10use unindent::Unindent as _;
 11
 12#[cfg(test)]
 13#[ctor::ctor]
 14fn init_logger() {
 15    // std::env::set_var("RUST_LOG", "info");
 16    env_logger::init();
 17}
 18
 19#[test]
 20fn test_select_language() {
 21    let registry = LanguageRegistry {
 22        languages: vec![
 23            Arc::new(Language::new(
 24                LanguageConfig {
 25                    name: "Rust".to_string(),
 26                    path_suffixes: vec!["rs".to_string()],
 27                    ..Default::default()
 28                },
 29                Some(tree_sitter_rust::language()),
 30            )),
 31            Arc::new(Language::new(
 32                LanguageConfig {
 33                    name: "Make".to_string(),
 34                    path_suffixes: vec!["Makefile".to_string(), "mk".to_string()],
 35                    ..Default::default()
 36                },
 37                Some(tree_sitter_rust::language()),
 38            )),
 39        ],
 40    };
 41
 42    // matching file extension
 43    assert_eq!(
 44        registry.select_language("zed/lib.rs").map(|l| l.name()),
 45        Some("Rust")
 46    );
 47    assert_eq!(
 48        registry.select_language("zed/lib.mk").map(|l| l.name()),
 49        Some("Make")
 50    );
 51
 52    // matching filename
 53    assert_eq!(
 54        registry.select_language("zed/Makefile").map(|l| l.name()),
 55        Some("Make")
 56    );
 57
 58    // matching suffix that is not the full file extension or filename
 59    assert_eq!(registry.select_language("zed/cars").map(|l| l.name()), None);
 60    assert_eq!(
 61        registry.select_language("zed/a.cars").map(|l| l.name()),
 62        None
 63    );
 64    assert_eq!(registry.select_language("zed/sumk").map(|l| l.name()), None);
 65}
 66
 67#[gpui::test]
 68fn test_edit_events(cx: &mut gpui::MutableAppContext) {
 69    let mut now = Instant::now();
 70    let buffer_1_events = Rc::new(RefCell::new(Vec::new()));
 71    let buffer_2_events = Rc::new(RefCell::new(Vec::new()));
 72
 73    let buffer1 = cx.add_model(|cx| Buffer::new(0, "abcdef", cx));
 74    let buffer2 = cx.add_model(|cx| Buffer::new(1, "abcdef", cx));
 75    let buffer_ops = buffer1.update(cx, |buffer, cx| {
 76        let buffer_1_events = buffer_1_events.clone();
 77        cx.subscribe(&buffer1, move |_, _, event, _| {
 78            buffer_1_events.borrow_mut().push(event.clone())
 79        })
 80        .detach();
 81        let buffer_2_events = buffer_2_events.clone();
 82        cx.subscribe(&buffer2, move |_, _, event, _| {
 83            buffer_2_events.borrow_mut().push(event.clone())
 84        })
 85        .detach();
 86
 87        // An edit emits an edited event, followed by a dirtied event,
 88        // since the buffer was previously in a clean state.
 89        buffer.edit(Some(2..4), "XYZ", cx);
 90
 91        // An empty transaction does not emit any events.
 92        buffer.start_transaction();
 93        buffer.end_transaction(cx);
 94
 95        // A transaction containing two edits emits one edited event.
 96        now += Duration::from_secs(1);
 97        buffer.start_transaction_at(now);
 98        buffer.edit(Some(5..5), "u", cx);
 99        buffer.edit(Some(6..6), "w", cx);
100        buffer.end_transaction_at(now, cx);
101
102        // Undoing a transaction emits one edited event.
103        buffer.undo(cx);
104
105        buffer.operations.clone()
106    });
107
108    // Incorporating a set of remote ops emits a single edited event,
109    // followed by a dirtied event.
110    buffer2.update(cx, |buffer, cx| {
111        buffer.apply_ops(buffer_ops, cx).unwrap();
112    });
113
114    let buffer_1_events = buffer_1_events.borrow();
115    assert_eq!(
116        *buffer_1_events,
117        vec![Event::Edited, Event::Dirtied, Event::Edited, Event::Edited]
118    );
119
120    let buffer_2_events = buffer_2_events.borrow();
121    assert_eq!(*buffer_2_events, vec![Event::Edited, Event::Dirtied]);
122}
123
124#[gpui::test]
125async fn test_apply_diff(mut cx: gpui::TestAppContext) {
126    let text = "a\nbb\nccc\ndddd\neeeee\nffffff\n";
127    let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
128
129    let text = "a\nccc\ndddd\nffffff\n";
130    let diff = buffer.read_with(&cx, |b, cx| b.diff(text.into(), cx)).await;
131    buffer.update(&mut cx, |b, cx| b.apply_diff(diff, cx));
132    cx.read(|cx| assert_eq!(buffer.read(cx).text(), text));
133
134    let text = "a\n1\n\nccc\ndd2dd\nffffff\n";
135    let diff = buffer.read_with(&cx, |b, cx| b.diff(text.into(), cx)).await;
136    buffer.update(&mut cx, |b, cx| b.apply_diff(diff, cx));
137    cx.read(|cx| assert_eq!(buffer.read(cx).text(), text));
138}
139
140#[gpui::test]
141async fn test_reparse(mut cx: gpui::TestAppContext) {
142    let text = "fn a() {}";
143    let buffer = cx.add_model(|cx| {
144        Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx)
145    });
146
147    // Wait for the initial text to parse
148    buffer
149        .condition(&cx, |buffer, _| !buffer.is_parsing())
150        .await;
151    assert_eq!(
152        get_tree_sexp(&buffer, &cx),
153        concat!(
154            "(source_file (function_item name: (identifier) ",
155            "parameters: (parameters) ",
156            "body: (block)))"
157        )
158    );
159
160    buffer.update(&mut cx, |buffer, _| {
161        buffer.set_sync_parse_timeout(Duration::ZERO)
162    });
163
164    // Perform some edits (add parameter and variable reference)
165    // Parsing doesn't begin until the transaction is complete
166    buffer.update(&mut cx, |buf, cx| {
167        buf.start_transaction();
168
169        let offset = buf.text().find(")").unwrap();
170        buf.edit(vec![offset..offset], "b: C", cx);
171        assert!(!buf.is_parsing());
172
173        let offset = buf.text().find("}").unwrap();
174        buf.edit(vec![offset..offset], " d; ", cx);
175        assert!(!buf.is_parsing());
176
177        buf.end_transaction(cx);
178        assert_eq!(buf.text(), "fn a(b: C) { d; }");
179        assert!(buf.is_parsing());
180    });
181    buffer
182        .condition(&cx, |buffer, _| !buffer.is_parsing())
183        .await;
184    assert_eq!(
185        get_tree_sexp(&buffer, &cx),
186        concat!(
187            "(source_file (function_item name: (identifier) ",
188            "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
189            "body: (block (identifier))))"
190        )
191    );
192
193    // Perform a series of edits without waiting for the current parse to complete:
194    // * turn identifier into a field expression
195    // * turn field expression into a method call
196    // * add a turbofish to the method call
197    buffer.update(&mut cx, |buf, cx| {
198        let offset = buf.text().find(";").unwrap();
199        buf.edit(vec![offset..offset], ".e", cx);
200        assert_eq!(buf.text(), "fn a(b: C) { d.e; }");
201        assert!(buf.is_parsing());
202    });
203    buffer.update(&mut cx, |buf, cx| {
204        let offset = buf.text().find(";").unwrap();
205        buf.edit(vec![offset..offset], "(f)", cx);
206        assert_eq!(buf.text(), "fn a(b: C) { d.e(f); }");
207        assert!(buf.is_parsing());
208    });
209    buffer.update(&mut cx, |buf, cx| {
210        let offset = buf.text().find("(f)").unwrap();
211        buf.edit(vec![offset..offset], "::<G>", cx);
212        assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }");
213        assert!(buf.is_parsing());
214    });
215    buffer
216        .condition(&cx, |buffer, _| !buffer.is_parsing())
217        .await;
218    assert_eq!(
219        get_tree_sexp(&buffer, &cx),
220        concat!(
221            "(source_file (function_item name: (identifier) ",
222            "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
223            "body: (block (call_expression ",
224            "function: (generic_function ",
225            "function: (field_expression value: (identifier) field: (field_identifier)) ",
226            "type_arguments: (type_arguments (type_identifier))) ",
227            "arguments: (arguments (identifier))))))",
228        )
229    );
230
231    buffer.update(&mut cx, |buf, cx| {
232        buf.undo(cx);
233        assert_eq!(buf.text(), "fn a() {}");
234        assert!(buf.is_parsing());
235    });
236    buffer
237        .condition(&cx, |buffer, _| !buffer.is_parsing())
238        .await;
239    assert_eq!(
240        get_tree_sexp(&buffer, &cx),
241        concat!(
242            "(source_file (function_item name: (identifier) ",
243            "parameters: (parameters) ",
244            "body: (block)))"
245        )
246    );
247
248    buffer.update(&mut cx, |buf, cx| {
249        buf.redo(cx);
250        assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }");
251        assert!(buf.is_parsing());
252    });
253    buffer
254        .condition(&cx, |buffer, _| !buffer.is_parsing())
255        .await;
256    assert_eq!(
257        get_tree_sexp(&buffer, &cx),
258        concat!(
259            "(source_file (function_item name: (identifier) ",
260            "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
261            "body: (block (call_expression ",
262            "function: (generic_function ",
263            "function: (field_expression value: (identifier) field: (field_identifier)) ",
264            "type_arguments: (type_arguments (type_identifier))) ",
265            "arguments: (arguments (identifier))))))",
266        )
267    );
268
269    fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> String {
270        buffer.read_with(cx, |buffer, _| {
271            buffer.syntax_tree().unwrap().root_node().to_sexp()
272        })
273    }
274}
275
276#[gpui::test]
277fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
278    let buffer = cx.add_model(|cx| {
279        let text = "
280            mod x {
281                mod y {
282
283                }
284            }
285        "
286        .unindent();
287        Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx)
288    });
289    let buffer = buffer.read(cx);
290    assert_eq!(
291        buffer.enclosing_bracket_point_ranges(Point::new(1, 6)..Point::new(1, 6)),
292        Some((
293            Point::new(0, 6)..Point::new(0, 7),
294            Point::new(4, 0)..Point::new(4, 1)
295        ))
296    );
297    assert_eq!(
298        buffer.enclosing_bracket_point_ranges(Point::new(1, 10)..Point::new(1, 10)),
299        Some((
300            Point::new(1, 10)..Point::new(1, 11),
301            Point::new(3, 4)..Point::new(3, 5)
302        ))
303    );
304    assert_eq!(
305        buffer.enclosing_bracket_point_ranges(Point::new(3, 5)..Point::new(3, 5)),
306        Some((
307            Point::new(1, 10)..Point::new(1, 11),
308            Point::new(3, 4)..Point::new(3, 5)
309        ))
310    );
311}
312
313#[gpui::test]
314fn test_edit_with_autoindent(cx: &mut MutableAppContext) {
315    cx.add_model(|cx| {
316        let text = "fn a() {}";
317        let mut buffer =
318            Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
319
320        buffer.edit_with_autoindent([8..8], "\n\n", cx);
321        assert_eq!(buffer.text(), "fn a() {\n    \n}");
322
323        buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 4)], "b()\n", cx);
324        assert_eq!(buffer.text(), "fn a() {\n    b()\n    \n}");
325
326        buffer.edit_with_autoindent([Point::new(2, 4)..Point::new(2, 4)], ".c", cx);
327        assert_eq!(buffer.text(), "fn a() {\n    b()\n        .c\n}");
328
329        buffer
330    });
331}
332
333// We need another approach to managing selections with auto-indent
334
335// #[gpui::test]
336// fn test_autoindent_moves_selections(cx: &mut MutableAppContext) {
337//     cx.add_model(|cx| {
338//         let text = "fn a() {}";
339
340//         let mut buffer =
341//             Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
342
343//         let selection_set_id = buffer.add_selection_set::<usize>(&[], cx);
344//         buffer.start_transaction();
345//         buffer.edit_with_autoindent([5..5, 9..9], "\n\n", cx);
346//         buffer
347//             .update_selection_set(
348//                 selection_set_id,
349//                 &[
350//                     Selection {
351//                         id: 0,
352//                         start: Point::new(1, 0),
353//                         end: Point::new(1, 0),
354//                         reversed: false,
355//                         goal: SelectionGoal::None,
356//                     },
357//                     Selection {
358//                         id: 1,
359//                         start: Point::new(4, 0),
360//                         end: Point::new(4, 0),
361//                         reversed: false,
362//                         goal: SelectionGoal::None,
363//                     },
364//                 ],
365//                 cx,
366//             )
367//             .unwrap();
368//         assert_eq!(buffer.text(), "fn a(\n\n) {}\n\n");
369
370//         // TODO! Come up with a different approach to moving selections now that we don't manage selection sets in the buffer
371
372//         // Ending the transaction runs the auto-indent. The selection
373//         // at the start of the auto-indented row is pushed to the right.
374//         buffer.end_transaction(cx);
375//         assert_eq!(buffer.text(), "fn a(\n    \n) {}\n\n");
376//         let selection_ranges = buffer
377//             .selection_set(selection_set_id)
378//             .unwrap()
379//             .selections::<Point>(&buffer)
380//             .map(|selection| selection.start.to_point(&buffer)..selection.end.to_point(&buffer))
381//             .collect::<Vec<_>>();
382
383//         assert_eq!(selection_ranges[0], empty(Point::new(1, 4)));
384//         assert_eq!(selection_ranges[1], empty(Point::new(4, 0)));
385
386//         buffer
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                    DiagnosticEntry {
517                        range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10),
518                        diagnostic: Diagnostic {
519                            severity: DiagnosticSeverity::ERROR,
520                            message: "undefined variable 'A'".to_string(),
521                            group_id: 0,
522                            is_primary: true,
523                            ..Default::default()
524                        },
525                    },
526                    DiagnosticEntry {
527                        range: PointUtf16::new(1, 9)..PointUtf16::new(1, 11),
528                        diagnostic: Diagnostic {
529                            severity: DiagnosticSeverity::ERROR,
530                            message: "undefined variable 'BB'".to_string(),
531                            group_id: 1,
532                            is_primary: true,
533                            ..Default::default()
534                        },
535                    },
536                    DiagnosticEntry {
537                        range: PointUtf16::new(2, 9)..PointUtf16::new(2, 12),
538                        diagnostic: Diagnostic {
539                            severity: DiagnosticSeverity::ERROR,
540                            message: "undefined variable 'CCC'".to_string(),
541                            group_id: 2,
542                            is_primary: true,
543                            ..Default::default()
544                        },
545                    },
546                ],
547                cx,
548            )
549            .unwrap();
550
551        // The diagnostics have moved down since they were created.
552        assert_eq!(
553            buffer
554                .snapshot()
555                .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0))
556                .collect::<Vec<_>>(),
557            &[
558                DiagnosticEntry {
559                    range: Point::new(3, 9)..Point::new(3, 11),
560                    diagnostic: Diagnostic {
561                        severity: DiagnosticSeverity::ERROR,
562                        message: "undefined variable 'BB'".to_string(),
563                        group_id: 1,
564                        is_primary: true,
565                        ..Default::default()
566                    },
567                },
568                DiagnosticEntry {
569                    range: Point::new(4, 9)..Point::new(4, 12),
570                    diagnostic: Diagnostic {
571                        severity: DiagnosticSeverity::ERROR,
572                        message: "undefined variable 'CCC'".to_string(),
573                        group_id: 2,
574                        is_primary: true,
575                        ..Default::default()
576                    }
577                }
578            ]
579        );
580        assert_eq!(
581            chunks_with_diagnostics(buffer, 0..buffer.len()),
582            [
583                ("\n\nfn a() { ".to_string(), None),
584                ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
585                (" }\nfn b() { ".to_string(), None),
586                ("BB".to_string(), Some(DiagnosticSeverity::ERROR)),
587                (" }\nfn c() { ".to_string(), None),
588                ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)),
589                (" }\n".to_string(), None),
590            ]
591        );
592        assert_eq!(
593            chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)),
594            [
595                ("B".to_string(), Some(DiagnosticSeverity::ERROR)),
596                (" }\nfn c() { ".to_string(), None),
597                ("CC".to_string(), Some(DiagnosticSeverity::ERROR)),
598            ]
599        );
600
601        // Ensure overlapping diagnostics are highlighted correctly.
602        buffer
603            .update_diagnostics(
604                Some(open_notification.text_document.version),
605                vec![
606                    DiagnosticEntry {
607                        range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10),
608                        diagnostic: Diagnostic {
609                            severity: DiagnosticSeverity::ERROR,
610                            message: "undefined variable 'A'".to_string(),
611                            group_id: 0,
612                            is_primary: true,
613                            ..Default::default()
614                        },
615                    },
616                    DiagnosticEntry {
617                        range: PointUtf16::new(0, 9)..PointUtf16::new(0, 12),
618                        diagnostic: Diagnostic {
619                            severity: DiagnosticSeverity::WARNING,
620                            message: "unreachable statement".to_string(),
621                            group_id: 1,
622                            is_primary: true,
623                            ..Default::default()
624                        },
625                    },
626                ],
627                cx,
628            )
629            .unwrap();
630        assert_eq!(
631            buffer
632                .snapshot()
633                .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0))
634                .collect::<Vec<_>>(),
635            &[
636                DiagnosticEntry {
637                    range: Point::new(2, 9)..Point::new(2, 12),
638                    diagnostic: Diagnostic {
639                        severity: DiagnosticSeverity::WARNING,
640                        message: "unreachable statement".to_string(),
641                        group_id: 1,
642                        is_primary: true,
643                        ..Default::default()
644                    }
645                },
646                DiagnosticEntry {
647                    range: Point::new(2, 9)..Point::new(2, 10),
648                    diagnostic: Diagnostic {
649                        severity: DiagnosticSeverity::ERROR,
650                        message: "undefined variable 'A'".to_string(),
651                        group_id: 0,
652                        is_primary: true,
653                        ..Default::default()
654                    },
655                }
656            ]
657        );
658        assert_eq!(
659            chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)),
660            [
661                ("fn a() { ".to_string(), None),
662                ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
663                (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
664                ("\n".to_string(), None),
665            ]
666        );
667        assert_eq!(
668            chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)),
669            [
670                (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
671                ("\n".to_string(), None),
672            ]
673        );
674    });
675
676    // Keep editing the buffer and ensure disk-based diagnostics get translated according to the
677    // changes since the last save.
678    buffer.update(&mut cx, |buffer, cx| {
679        buffer.edit(Some(Point::new(2, 0)..Point::new(2, 0)), "    ", cx);
680        buffer.edit(Some(Point::new(2, 8)..Point::new(2, 10)), "(x: usize)", cx);
681    });
682    let change_notification_2 = fake
683        .receive_notification::<lsp::notification::DidChangeTextDocument>()
684        .await;
685    assert!(
686        change_notification_2.text_document.version > change_notification_1.text_document.version
687    );
688
689    buffer.update(&mut cx, |buffer, cx| {
690        buffer
691            .update_diagnostics(
692                Some(change_notification_2.text_document.version),
693                vec![
694                    DiagnosticEntry {
695                        range: PointUtf16::new(1, 9)..PointUtf16::new(1, 11),
696                        diagnostic: Diagnostic {
697                            severity: DiagnosticSeverity::ERROR,
698                            message: "undefined variable 'BB'".to_string(),
699                            source: Some("disk".to_string()),
700                            group_id: 1,
701                            is_primary: true,
702                            ..Default::default()
703                        },
704                    },
705                    DiagnosticEntry {
706                        range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10),
707                        diagnostic: Diagnostic {
708                            severity: DiagnosticSeverity::ERROR,
709                            message: "undefined variable 'A'".to_string(),
710                            source: Some("disk".to_string()),
711                            group_id: 0,
712                            is_primary: true,
713                            ..Default::default()
714                        },
715                    },
716                ],
717                cx,
718            )
719            .unwrap();
720        assert_eq!(
721            buffer
722                .snapshot()
723                .diagnostics_in_range::<_, Point>(0..buffer.len())
724                .collect::<Vec<_>>(),
725            &[
726                DiagnosticEntry {
727                    range: Point::new(2, 21)..Point::new(2, 22),
728                    diagnostic: Diagnostic {
729                        severity: DiagnosticSeverity::ERROR,
730                        message: "undefined variable 'A'".to_string(),
731                        source: Some("disk".to_string()),
732                        group_id: 0,
733                        is_primary: true,
734                        ..Default::default()
735                    }
736                },
737                DiagnosticEntry {
738                    range: Point::new(3, 9)..Point::new(3, 11),
739                    diagnostic: Diagnostic {
740                        severity: DiagnosticSeverity::ERROR,
741                        message: "undefined variable 'BB'".to_string(),
742                        source: Some("disk".to_string()),
743                        group_id: 1,
744                        is_primary: true,
745                        ..Default::default()
746                    },
747                }
748            ]
749        );
750    });
751}
752
753#[gpui::test]
754async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
755    cx.add_model(|cx| {
756        let text = concat!(
757            "let one = ;\n", //
758            "let two = \n",
759            "let three = 3;\n",
760        );
761
762        let mut buffer = Buffer::new(0, text, cx);
763        buffer.set_language(Some(Arc::new(rust_lang())), None, cx);
764        buffer
765            .update_diagnostics(
766                None,
767                vec![
768                    DiagnosticEntry {
769                        range: PointUtf16::new(0, 10)..PointUtf16::new(0, 10),
770                        diagnostic: Diagnostic {
771                            severity: DiagnosticSeverity::ERROR,
772                            message: "syntax error 1".to_string(),
773                            ..Default::default()
774                        },
775                    },
776                    DiagnosticEntry {
777                        range: PointUtf16::new(1, 10)..PointUtf16::new(1, 10),
778                        diagnostic: Diagnostic {
779                            severity: DiagnosticSeverity::ERROR,
780                            message: "syntax error 2".to_string(),
781                            ..Default::default()
782                        },
783                    },
784                ],
785                cx,
786            )
787            .unwrap();
788
789        // An empty range is extended forward to include the following character.
790        // At the end of a line, an empty range is extended backward to include
791        // the preceding character.
792        let chunks = chunks_with_diagnostics(&buffer, 0..buffer.len());
793        assert_eq!(
794            chunks
795                .iter()
796                .map(|(s, d)| (s.as_str(), *d))
797                .collect::<Vec<_>>(),
798            &[
799                ("let one = ", None),
800                (";", Some(DiagnosticSeverity::ERROR)),
801                ("\nlet two =", None),
802                (" ", Some(DiagnosticSeverity::ERROR)),
803                ("\nlet three = 3;\n", None)
804            ]
805        );
806        buffer
807    });
808}
809
810fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
811    buffer: &Buffer,
812    range: Range<T>,
813) -> Vec<(String, Option<DiagnosticSeverity>)> {
814    let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
815    for chunk in buffer.snapshot().chunks(range, Some(&Default::default())) {
816        if chunks
817            .last()
818            .map_or(false, |prev_chunk| prev_chunk.1 == chunk.diagnostic)
819        {
820            chunks.last_mut().unwrap().0.push_str(chunk.text);
821        } else {
822            chunks.push((chunk.text.to_string(), chunk.diagnostic));
823        }
824    }
825    chunks
826}
827
828#[test]
829fn test_contiguous_ranges() {
830    assert_eq!(
831        contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12].into_iter(), 100).collect::<Vec<_>>(),
832        &[1..4, 5..7, 9..13]
833    );
834
835    // Respects the `max_len` parameter
836    assert_eq!(
837        contiguous_ranges(
838            [2, 3, 4, 5, 6, 7, 8, 9, 23, 24, 25, 26, 30, 31].into_iter(),
839            3
840        )
841        .collect::<Vec<_>>(),
842        &[2..5, 5..8, 8..10, 23..26, 26..27, 30..32],
843    );
844}
845
846impl Buffer {
847    pub fn enclosing_bracket_point_ranges<T: ToOffset>(
848        &self,
849        range: Range<T>,
850    ) -> Option<(Range<Point>, Range<Point>)> {
851        self.snapshot()
852            .enclosing_bracket_ranges(range)
853            .map(|(start, end)| {
854                let point_start = start.start.to_point(self)..start.end.to_point(self);
855                let point_end = end.start.to_point(self)..end.end.to_point(self);
856                (point_start, point_end)
857            })
858    }
859}
860
861fn rust_lang() -> Language {
862    Language::new(
863        LanguageConfig {
864            name: "Rust".to_string(),
865            path_suffixes: vec!["rs".to_string()],
866            language_server: None,
867            ..Default::default()
868        },
869        Some(tree_sitter_rust::language()),
870    )
871    .with_indents_query(
872        r#"
873                (call_expression) @indent
874                (field_expression) @indent
875                (_ "(" ")" @end) @indent
876                (_ "{" "}" @end) @indent
877            "#,
878    )
879    .unwrap()
880    .with_brackets_query(r#" ("{" @open "}" @close) "#)
881    .unwrap()
882}
883
884fn empty(point: Point) -> Range<Point> {
885    point..point
886}