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