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
390#[gpui::test]
391fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut MutableAppContext) {
392    cx.add_model(|cx| {
393        let text = "
394            fn a() {
395            c;
396            d;
397            }
398        "
399        .unindent();
400
401        let mut buffer =
402            Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
403
404        // Lines 2 and 3 don't match the indentation suggestion. When editing these lines,
405        // their indentation is not adjusted.
406        buffer.edit_with_autoindent([empty(Point::new(1, 1)), empty(Point::new(2, 1))], "()", cx);
407        assert_eq!(
408            buffer.text(),
409            "
410            fn a() {
411            c();
412            d();
413            }
414            "
415            .unindent()
416        );
417
418        // When appending new content after these lines, the indentation is based on the
419        // preceding lines' actual indentation.
420        buffer.edit_with_autoindent(
421            [empty(Point::new(1, 1)), empty(Point::new(2, 1))],
422            "\n.f\n.g",
423            cx,
424        );
425        assert_eq!(
426            buffer.text(),
427            "
428            fn a() {
429            c
430                .f
431                .g();
432            d
433                .f
434                .g();
435            }
436            "
437            .unindent()
438        );
439        buffer
440    });
441}
442
443#[gpui::test]
444fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppContext) {
445    cx.add_model(|cx| {
446        let text = "
447            fn a() {}
448        "
449        .unindent();
450
451        let mut buffer =
452            Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
453
454        buffer.edit_with_autoindent([5..5], "\nb", cx);
455        assert_eq!(
456            buffer.text(),
457            "
458                fn a(
459                    b) {}
460            "
461            .unindent()
462        );
463
464        // The indentation suggestion changed because `@end` node (a close paren)
465        // is now at the beginning of the line.
466        buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 5)], "", cx);
467        assert_eq!(
468            buffer.text(),
469            "
470                fn a(
471                ) {}
472            "
473            .unindent()
474        );
475
476        buffer
477    });
478}
479
480#[gpui::test]
481async fn test_diagnostics(mut cx: gpui::TestAppContext) {
482    let (language_server, mut fake) = lsp::LanguageServer::fake(cx.background()).await;
483    let mut rust_lang = rust_lang();
484    rust_lang.config.language_server = Some(LanguageServerConfig {
485        disk_based_diagnostic_sources: HashSet::from_iter(["disk".to_string()]),
486        ..Default::default()
487    });
488
489    let text = "
490        fn a() { A }
491        fn b() { BB }
492        fn c() { CCC }
493    "
494    .unindent();
495
496    let buffer = cx.add_model(|cx| {
497        Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang)), Some(language_server), cx)
498    });
499
500    let open_notification = fake
501        .receive_notification::<lsp::notification::DidOpenTextDocument>()
502        .await;
503
504    // Edit the buffer, moving the content down
505    buffer.update(&mut cx, |buffer, cx| buffer.edit([0..0], "\n\n", cx));
506    let change_notification_1 = fake
507        .receive_notification::<lsp::notification::DidChangeTextDocument>()
508        .await;
509    assert!(change_notification_1.text_document.version > open_notification.text_document.version);
510
511    buffer.update(&mut cx, |buffer, cx| {
512        // Receive diagnostics for an earlier version of the buffer.
513        buffer
514            .update_diagnostics(
515                "lsp".into(),
516                Some(open_notification.text_document.version),
517                vec![
518                    DiagnosticEntry {
519                        range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10),
520                        diagnostic: Diagnostic {
521                            severity: DiagnosticSeverity::ERROR,
522                            message: "undefined variable 'A'".to_string(),
523                            is_disk_based: true,
524                            group_id: 0,
525                            is_primary: true,
526                            ..Default::default()
527                        },
528                    },
529                    DiagnosticEntry {
530                        range: PointUtf16::new(1, 9)..PointUtf16::new(1, 11),
531                        diagnostic: Diagnostic {
532                            severity: DiagnosticSeverity::ERROR,
533                            message: "undefined variable 'BB'".to_string(),
534                            is_disk_based: true,
535                            group_id: 1,
536                            is_primary: true,
537                            ..Default::default()
538                        },
539                    },
540                    DiagnosticEntry {
541                        range: PointUtf16::new(2, 9)..PointUtf16::new(2, 12),
542                        diagnostic: Diagnostic {
543                            severity: DiagnosticSeverity::ERROR,
544                            is_disk_based: true,
545                            message: "undefined variable 'CCC'".to_string(),
546                            group_id: 2,
547                            is_primary: true,
548                            ..Default::default()
549                        },
550                    },
551                ],
552                cx,
553            )
554            .unwrap();
555
556        // The diagnostics have moved down since they were created.
557        assert_eq!(
558            buffer
559                .snapshot()
560                .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0))
561                .collect::<Vec<_>>(),
562            &[
563                (
564                    "lsp",
565                    DiagnosticEntry {
566                        range: Point::new(3, 9)..Point::new(3, 11),
567                        diagnostic: Diagnostic {
568                            severity: DiagnosticSeverity::ERROR,
569                            message: "undefined variable 'BB'".to_string(),
570                            is_disk_based: true,
571                            group_id: 1,
572                            is_primary: true,
573                            ..Default::default()
574                        },
575                    }
576                ),
577                (
578                    "lsp",
579                    DiagnosticEntry {
580                        range: Point::new(4, 9)..Point::new(4, 12),
581                        diagnostic: Diagnostic {
582                            severity: DiagnosticSeverity::ERROR,
583                            message: "undefined variable 'CCC'".to_string(),
584                            is_disk_based: true,
585                            group_id: 2,
586                            is_primary: true,
587                            ..Default::default()
588                        }
589                    }
590                )
591            ]
592        );
593        assert_eq!(
594            chunks_with_diagnostics(buffer, 0..buffer.len()),
595            [
596                ("\n\nfn a() { ".to_string(), None),
597                ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
598                (" }\nfn b() { ".to_string(), None),
599                ("BB".to_string(), Some(DiagnosticSeverity::ERROR)),
600                (" }\nfn c() { ".to_string(), None),
601                ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)),
602                (" }\n".to_string(), None),
603            ]
604        );
605        assert_eq!(
606            chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)),
607            [
608                ("B".to_string(), Some(DiagnosticSeverity::ERROR)),
609                (" }\nfn c() { ".to_string(), None),
610                ("CC".to_string(), Some(DiagnosticSeverity::ERROR)),
611            ]
612        );
613
614        // Ensure overlapping diagnostics are highlighted correctly.
615        buffer
616            .update_diagnostics(
617                "lsp".into(),
618                Some(open_notification.text_document.version),
619                vec![
620                    DiagnosticEntry {
621                        range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10),
622                        diagnostic: Diagnostic {
623                            severity: DiagnosticSeverity::ERROR,
624                            message: "undefined variable 'A'".to_string(),
625                            is_disk_based: true,
626                            group_id: 0,
627                            is_primary: true,
628                            ..Default::default()
629                        },
630                    },
631                    DiagnosticEntry {
632                        range: PointUtf16::new(0, 9)..PointUtf16::new(0, 12),
633                        diagnostic: Diagnostic {
634                            severity: DiagnosticSeverity::WARNING,
635                            message: "unreachable statement".to_string(),
636                            group_id: 1,
637                            is_primary: true,
638                            ..Default::default()
639                        },
640                    },
641                ],
642                cx,
643            )
644            .unwrap();
645        assert_eq!(
646            buffer
647                .snapshot()
648                .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0))
649                .collect::<Vec<_>>(),
650            &[
651                (
652                    "lsp",
653                    DiagnosticEntry {
654                        range: Point::new(2, 9)..Point::new(2, 12),
655                        diagnostic: Diagnostic {
656                            severity: DiagnosticSeverity::WARNING,
657                            message: "unreachable statement".to_string(),
658                            group_id: 1,
659                            is_primary: true,
660                            ..Default::default()
661                        }
662                    }
663                ),
664                (
665                    "lsp",
666                    DiagnosticEntry {
667                        range: Point::new(2, 9)..Point::new(2, 10),
668                        diagnostic: Diagnostic {
669                            severity: DiagnosticSeverity::ERROR,
670                            message: "undefined variable 'A'".to_string(),
671                            is_disk_based: true,
672                            group_id: 0,
673                            is_primary: true,
674                            ..Default::default()
675                        },
676                    }
677                )
678            ]
679        );
680        assert_eq!(
681            chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)),
682            [
683                ("fn a() { ".to_string(), None),
684                ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
685                (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
686                ("\n".to_string(), None),
687            ]
688        );
689        assert_eq!(
690            chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)),
691            [
692                (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
693                ("\n".to_string(), None),
694            ]
695        );
696    });
697
698    // Keep editing the buffer and ensure disk-based diagnostics get translated according to the
699    // changes since the last save.
700    buffer.update(&mut cx, |buffer, cx| {
701        buffer.edit(Some(Point::new(2, 0)..Point::new(2, 0)), "    ", cx);
702        buffer.edit(Some(Point::new(2, 8)..Point::new(2, 10)), "(x: usize)", cx);
703    });
704    let change_notification_2 = fake
705        .receive_notification::<lsp::notification::DidChangeTextDocument>()
706        .await;
707    assert!(
708        change_notification_2.text_document.version > change_notification_1.text_document.version
709    );
710
711    buffer.update(&mut cx, |buffer, cx| {
712        buffer
713            .update_diagnostics(
714                "lsp".into(),
715                Some(change_notification_2.text_document.version),
716                vec![
717                    DiagnosticEntry {
718                        range: PointUtf16::new(1, 9)..PointUtf16::new(1, 11),
719                        diagnostic: Diagnostic {
720                            severity: DiagnosticSeverity::ERROR,
721                            message: "undefined variable 'BB'".to_string(),
722                            is_disk_based: true,
723                            group_id: 1,
724                            is_primary: true,
725                            ..Default::default()
726                        },
727                    },
728                    DiagnosticEntry {
729                        range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10),
730                        diagnostic: Diagnostic {
731                            severity: DiagnosticSeverity::ERROR,
732                            message: "undefined variable 'A'".to_string(),
733                            is_disk_based: true,
734                            group_id: 0,
735                            is_primary: true,
736                            ..Default::default()
737                        },
738                    },
739                ],
740                cx,
741            )
742            .unwrap();
743        assert_eq!(
744            buffer
745                .snapshot()
746                .diagnostics_in_range::<_, Point>(0..buffer.len())
747                .collect::<Vec<_>>(),
748            &[
749                (
750                    "lsp",
751                    DiagnosticEntry {
752                        range: Point::new(2, 21)..Point::new(2, 22),
753                        diagnostic: Diagnostic {
754                            severity: DiagnosticSeverity::ERROR,
755                            message: "undefined variable 'A'".to_string(),
756                            is_disk_based: true,
757                            group_id: 0,
758                            is_primary: true,
759                            ..Default::default()
760                        }
761                    }
762                ),
763                (
764                    "lsp",
765                    DiagnosticEntry {
766                        range: Point::new(3, 9)..Point::new(3, 11),
767                        diagnostic: Diagnostic {
768                            severity: DiagnosticSeverity::ERROR,
769                            message: "undefined variable 'BB'".to_string(),
770                            is_disk_based: true,
771                            group_id: 1,
772                            is_primary: true,
773                            ..Default::default()
774                        },
775                    }
776                )
777            ]
778        );
779    });
780}
781
782#[gpui::test]
783async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
784    cx.add_model(|cx| {
785        let text = concat!(
786            "let one = ;\n", //
787            "let two = \n",
788            "let three = 3;\n",
789        );
790
791        let mut buffer = Buffer::new(0, text, cx);
792        buffer.set_language(Some(Arc::new(rust_lang())), None, cx);
793        buffer
794            .update_diagnostics(
795                "lsp".into(),
796                None,
797                vec![
798                    DiagnosticEntry {
799                        range: PointUtf16::new(0, 10)..PointUtf16::new(0, 10),
800                        diagnostic: Diagnostic {
801                            severity: DiagnosticSeverity::ERROR,
802                            message: "syntax error 1".to_string(),
803                            ..Default::default()
804                        },
805                    },
806                    DiagnosticEntry {
807                        range: PointUtf16::new(1, 10)..PointUtf16::new(1, 10),
808                        diagnostic: Diagnostic {
809                            severity: DiagnosticSeverity::ERROR,
810                            message: "syntax error 2".to_string(),
811                            ..Default::default()
812                        },
813                    },
814                ],
815                cx,
816            )
817            .unwrap();
818
819        // An empty range is extended forward to include the following character.
820        // At the end of a line, an empty range is extended backward to include
821        // the preceding character.
822        let chunks = chunks_with_diagnostics(&buffer, 0..buffer.len());
823        assert_eq!(
824            chunks
825                .iter()
826                .map(|(s, d)| (s.as_str(), *d))
827                .collect::<Vec<_>>(),
828            &[
829                ("let one = ", None),
830                (";", Some(DiagnosticSeverity::ERROR)),
831                ("\nlet two =", None),
832                (" ", Some(DiagnosticSeverity::ERROR)),
833                ("\nlet three = 3;\n", None)
834            ]
835        );
836        buffer
837    });
838}
839
840fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
841    buffer: &Buffer,
842    range: Range<T>,
843) -> Vec<(String, Option<DiagnosticSeverity>)> {
844    let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
845    for chunk in buffer.snapshot().chunks(range, Some(&Default::default())) {
846        if chunks
847            .last()
848            .map_or(false, |prev_chunk| prev_chunk.1 == chunk.diagnostic)
849        {
850            chunks.last_mut().unwrap().0.push_str(chunk.text);
851        } else {
852            chunks.push((chunk.text.to_string(), chunk.diagnostic));
853        }
854    }
855    chunks
856}
857
858#[test]
859fn test_contiguous_ranges() {
860    assert_eq!(
861        contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12].into_iter(), 100).collect::<Vec<_>>(),
862        &[1..4, 5..7, 9..13]
863    );
864
865    // Respects the `max_len` parameter
866    assert_eq!(
867        contiguous_ranges(
868            [2, 3, 4, 5, 6, 7, 8, 9, 23, 24, 25, 26, 30, 31].into_iter(),
869            3
870        )
871        .collect::<Vec<_>>(),
872        &[2..5, 5..8, 8..10, 23..26, 26..27, 30..32],
873    );
874}
875
876impl Buffer {
877    pub fn enclosing_bracket_point_ranges<T: ToOffset>(
878        &self,
879        range: Range<T>,
880    ) -> Option<(Range<Point>, Range<Point>)> {
881        self.snapshot()
882            .enclosing_bracket_ranges(range)
883            .map(|(start, end)| {
884                let point_start = start.start.to_point(self)..start.end.to_point(self);
885                let point_end = end.start.to_point(self)..end.end.to_point(self);
886                (point_start, point_end)
887            })
888    }
889}
890
891fn rust_lang() -> Language {
892    Language::new(
893        LanguageConfig {
894            name: "Rust".to_string(),
895            path_suffixes: vec!["rs".to_string()],
896            language_server: None,
897            ..Default::default()
898        },
899        Some(tree_sitter_rust::language()),
900    )
901    .with_indents_query(
902        r#"
903                (call_expression) @indent
904                (field_expression) @indent
905                (_ "(" ")" @end) @indent
906                (_ "{" "}" @end) @indent
907            "#,
908    )
909    .unwrap()
910    .with_brackets_query(r#" ("{" @open "}" @close) "#)
911    .unwrap()
912}
913
914fn empty(point: Point) -> Range<Point> {
915    point..point
916}