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