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                            ..Default::default()
522                        },
523                    },
524                    DiagnosticEntry {
525                        range: PointUtf16::new(1, 9)..PointUtf16::new(1, 11),
526                        diagnostic: Diagnostic {
527                            severity: DiagnosticSeverity::ERROR,
528                            message: "undefined variable 'BB'".to_string(),
529                            ..Default::default()
530                        },
531                    },
532                    DiagnosticEntry {
533                        range: PointUtf16::new(2, 9)..PointUtf16::new(2, 12),
534                        diagnostic: Diagnostic {
535                            severity: DiagnosticSeverity::ERROR,
536                            message: "undefined variable 'CCC'".to_string(),
537                            ..Default::default()
538                        },
539                    },
540                ],
541                cx,
542            )
543            .unwrap();
544
545        // The diagnostics have moved down since they were created.
546        assert_eq!(
547            buffer
548                .snapshot()
549                .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0))
550                .collect::<Vec<_>>(),
551            &[
552                DiagnosticEntry {
553                    range: Point::new(3, 9)..Point::new(3, 11),
554                    diagnostic: Diagnostic {
555                        severity: DiagnosticSeverity::ERROR,
556                        message: "undefined variable 'BB'".to_string(),
557                        group_id: 1,
558                        is_primary: true,
559                        ..Default::default()
560                    },
561                },
562                DiagnosticEntry {
563                    range: Point::new(4, 9)..Point::new(4, 12),
564                    diagnostic: Diagnostic {
565                        severity: DiagnosticSeverity::ERROR,
566                        message: "undefined variable 'CCC'".to_string(),
567                        group_id: 2,
568                        is_primary: true,
569                        ..Default::default()
570                    }
571                }
572            ]
573        );
574        assert_eq!(
575            chunks_with_diagnostics(buffer, 0..buffer.len()),
576            [
577                ("\n\nfn a() { ".to_string(), None),
578                ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
579                (" }\nfn b() { ".to_string(), None),
580                ("BB".to_string(), Some(DiagnosticSeverity::ERROR)),
581                (" }\nfn c() { ".to_string(), None),
582                ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)),
583                (" }\n".to_string(), None),
584            ]
585        );
586        assert_eq!(
587            chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)),
588            [
589                ("B".to_string(), Some(DiagnosticSeverity::ERROR)),
590                (" }\nfn c() { ".to_string(), None),
591                ("CC".to_string(), Some(DiagnosticSeverity::ERROR)),
592            ]
593        );
594
595        // Ensure overlapping diagnostics are highlighted correctly.
596        buffer
597            .update_diagnostics(
598                Some(open_notification.text_document.version),
599                vec![
600                    DiagnosticEntry {
601                        range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10),
602                        diagnostic: Diagnostic {
603                            severity: DiagnosticSeverity::ERROR,
604                            message: "undefined variable 'A'".to_string(),
605                            ..Default::default()
606                        },
607                    },
608                    DiagnosticEntry {
609                        range: PointUtf16::new(0, 9)..PointUtf16::new(0, 12),
610                        diagnostic: Diagnostic {
611                            severity: DiagnosticSeverity::WARNING,
612                            message: "unreachable statement".to_string(),
613                            ..Default::default()
614                        },
615                    },
616                ],
617                cx,
618            )
619            .unwrap();
620        assert_eq!(
621            buffer
622                .snapshot()
623                .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0))
624                .collect::<Vec<_>>(),
625            &[
626                DiagnosticEntry {
627                    range: Point::new(2, 9)..Point::new(2, 12),
628                    diagnostic: Diagnostic {
629                        severity: DiagnosticSeverity::WARNING,
630                        message: "unreachable statement".to_string(),
631                        group_id: 1,
632                        is_primary: true,
633                        ..Default::default()
634                    }
635                },
636                DiagnosticEntry {
637                    range: Point::new(2, 9)..Point::new(2, 10),
638                    diagnostic: Diagnostic {
639                        severity: DiagnosticSeverity::ERROR,
640                        message: "undefined variable 'A'".to_string(),
641                        group_id: 0,
642                        is_primary: true,
643                        ..Default::default()
644                    },
645                }
646            ]
647        );
648        assert_eq!(
649            chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)),
650            [
651                ("fn a() { ".to_string(), None),
652                ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
653                (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
654                ("\n".to_string(), None),
655            ]
656        );
657        assert_eq!(
658            chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)),
659            [
660                (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
661                ("\n".to_string(), None),
662            ]
663        );
664    });
665
666    // Keep editing the buffer and ensure disk-based diagnostics get translated according to the
667    // changes since the last save.
668    buffer.update(&mut cx, |buffer, cx| {
669        buffer.edit(Some(Point::new(2, 0)..Point::new(2, 0)), "    ", cx);
670        buffer.edit(Some(Point::new(2, 8)..Point::new(2, 10)), "(x: usize)", cx);
671    });
672    let change_notification_2 = fake
673        .receive_notification::<lsp::notification::DidChangeTextDocument>()
674        .await;
675    assert!(
676        change_notification_2.text_document.version > change_notification_1.text_document.version
677    );
678
679    buffer.update(&mut cx, |buffer, cx| {
680        buffer
681            .update_diagnostics(
682                Some(change_notification_2.text_document.version),
683                vec![
684                    DiagnosticEntry {
685                        range: PointUtf16::new(1, 9)..PointUtf16::new(1, 11),
686                        diagnostic: Diagnostic {
687                            severity: DiagnosticSeverity::ERROR,
688                            message: "undefined variable 'BB'".to_string(),
689                            source: Some("disk".to_string()),
690                            ..Default::default()
691                        },
692                    },
693                    DiagnosticEntry {
694                        range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10),
695                        diagnostic: Diagnostic {
696                            severity: DiagnosticSeverity::ERROR,
697                            message: "undefined variable 'A'".to_string(),
698                            source: Some("disk".to_string()),
699                            ..Default::default()
700                        },
701                    },
702                ],
703                cx,
704            )
705            .unwrap();
706        assert_eq!(
707            buffer
708                .snapshot()
709                .diagnostics_in_range::<_, Point>(0..buffer.len())
710                .collect::<Vec<_>>(),
711            &[
712                DiagnosticEntry {
713                    range: Point::new(2, 21)..Point::new(2, 22),
714                    diagnostic: Diagnostic {
715                        severity: DiagnosticSeverity::ERROR,
716                        message: "undefined variable 'A'".to_string(),
717                        group_id: 0,
718                        is_primary: true,
719                        ..Default::default()
720                    }
721                },
722                DiagnosticEntry {
723                    range: Point::new(3, 9)..Point::new(3, 11),
724                    diagnostic: Diagnostic {
725                        severity: DiagnosticSeverity::ERROR,
726                        message: "undefined variable 'BB'".to_string(),
727                        group_id: 1,
728                        is_primary: true,
729                        ..Default::default()
730                    },
731                }
732            ]
733        );
734    });
735}
736
737#[gpui::test]
738async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
739    cx.add_model(|cx| {
740        let text = concat!(
741            "let one = ;\n", //
742            "let two = \n",
743            "let three = 3;\n",
744        );
745
746        let mut buffer = Buffer::new(0, text, cx);
747        buffer.set_language(Some(Arc::new(rust_lang())), None, cx);
748        buffer
749            .update_diagnostics(
750                None,
751                vec![
752                    DiagnosticEntry {
753                        range: PointUtf16::new(0, 10)..PointUtf16::new(0, 10),
754                        diagnostic: Diagnostic {
755                            severity: DiagnosticSeverity::ERROR,
756                            message: "syntax error 1".to_string(),
757                            ..Default::default()
758                        },
759                    },
760                    DiagnosticEntry {
761                        range: PointUtf16::new(1, 10)..PointUtf16::new(1, 10),
762                        diagnostic: Diagnostic {
763                            severity: DiagnosticSeverity::ERROR,
764                            message: "syntax error 2".to_string(),
765                            ..Default::default()
766                        },
767                    },
768                ],
769                cx,
770            )
771            .unwrap();
772
773        // An empty range is extended forward to include the following character.
774        // At the end of a line, an empty range is extended backward to include
775        // the preceding character.
776        let chunks = chunks_with_diagnostics(&buffer, 0..buffer.len());
777        assert_eq!(
778            chunks
779                .iter()
780                .map(|(s, d)| (s.as_str(), *d))
781                .collect::<Vec<_>>(),
782            &[
783                ("let one = ", None),
784                (";", Some(DiagnosticSeverity::ERROR)),
785                ("\nlet two =", None),
786                (" ", Some(DiagnosticSeverity::ERROR)),
787                ("\nlet three = 3;\n", None)
788            ]
789        );
790        buffer
791    });
792}
793
794fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
795    buffer: &Buffer,
796    range: Range<T>,
797) -> Vec<(String, Option<DiagnosticSeverity>)> {
798    let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
799    for chunk in buffer.snapshot().chunks(range, Some(&Default::default())) {
800        if chunks
801            .last()
802            .map_or(false, |prev_chunk| prev_chunk.1 == chunk.diagnostic)
803        {
804            chunks.last_mut().unwrap().0.push_str(chunk.text);
805        } else {
806            chunks.push((chunk.text.to_string(), chunk.diagnostic));
807        }
808    }
809    chunks
810}
811
812#[test]
813fn test_contiguous_ranges() {
814    assert_eq!(
815        contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12].into_iter(), 100).collect::<Vec<_>>(),
816        &[1..4, 5..7, 9..13]
817    );
818
819    // Respects the `max_len` parameter
820    assert_eq!(
821        contiguous_ranges(
822            [2, 3, 4, 5, 6, 7, 8, 9, 23, 24, 25, 26, 30, 31].into_iter(),
823            3
824        )
825        .collect::<Vec<_>>(),
826        &[2..5, 5..8, 8..10, 23..26, 26..27, 30..32],
827    );
828}
829
830impl Buffer {
831    pub fn enclosing_bracket_point_ranges<T: ToOffset>(
832        &self,
833        range: Range<T>,
834    ) -> Option<(Range<Point>, Range<Point>)> {
835        self.snapshot()
836            .enclosing_bracket_ranges(range)
837            .map(|(start, end)| {
838                let point_start = start.start.to_point(self)..start.end.to_point(self);
839                let point_end = end.start.to_point(self)..end.end.to_point(self);
840                (point_start, point_end)
841            })
842    }
843}
844
845fn rust_lang() -> Language {
846    Language::new(
847        LanguageConfig {
848            name: "Rust".to_string(),
849            path_suffixes: vec!["rs".to_string()],
850            language_server: None,
851            ..Default::default()
852        },
853        Some(tree_sitter_rust::language()),
854    )
855    .with_indents_query(
856        r#"
857                (call_expression) @indent
858                (field_expression) @indent
859                (_ "(" ")" @end) @indent
860                (_ "{" "}" @end) @indent
861            "#,
862    )
863    .unwrap()
864    .with_brackets_query(r#" ("{" @open "}" @close) "#)
865    .unwrap()
866}
867
868fn empty(point: Point) -> Range<Point> {
869    point..point
870}