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}