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 DiagnosticEntry {
564 range: Point::new(3, 9)..Point::new(3, 11),
565 diagnostic: Diagnostic {
566 severity: DiagnosticSeverity::ERROR,
567 message: "undefined variable 'BB'".to_string(),
568 is_disk_based: true,
569 group_id: 1,
570 is_primary: true,
571 ..Default::default()
572 },
573 },
574 DiagnosticEntry {
575 range: Point::new(4, 9)..Point::new(4, 12),
576 diagnostic: Diagnostic {
577 severity: DiagnosticSeverity::ERROR,
578 message: "undefined variable 'CCC'".to_string(),
579 is_disk_based: true,
580 group_id: 2,
581 is_primary: true,
582 ..Default::default()
583 }
584 }
585 ]
586 );
587 assert_eq!(
588 chunks_with_diagnostics(buffer, 0..buffer.len()),
589 [
590 ("\n\nfn a() { ".to_string(), None),
591 ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
592 (" }\nfn b() { ".to_string(), None),
593 ("BB".to_string(), Some(DiagnosticSeverity::ERROR)),
594 (" }\nfn c() { ".to_string(), None),
595 ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)),
596 (" }\n".to_string(), None),
597 ]
598 );
599 assert_eq!(
600 chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)),
601 [
602 ("B".to_string(), Some(DiagnosticSeverity::ERROR)),
603 (" }\nfn c() { ".to_string(), None),
604 ("CC".to_string(), Some(DiagnosticSeverity::ERROR)),
605 ]
606 );
607
608 // Ensure overlapping diagnostics are highlighted correctly.
609 buffer
610 .update_diagnostics(
611 "lsp".into(),
612 Some(open_notification.text_document.version),
613 vec![
614 DiagnosticEntry {
615 range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10),
616 diagnostic: Diagnostic {
617 severity: DiagnosticSeverity::ERROR,
618 message: "undefined variable 'A'".to_string(),
619 is_disk_based: true,
620 group_id: 0,
621 is_primary: true,
622 ..Default::default()
623 },
624 },
625 DiagnosticEntry {
626 range: PointUtf16::new(0, 9)..PointUtf16::new(0, 12),
627 diagnostic: Diagnostic {
628 severity: DiagnosticSeverity::WARNING,
629 message: "unreachable statement".to_string(),
630 group_id: 1,
631 is_primary: true,
632 ..Default::default()
633 },
634 },
635 ],
636 cx,
637 )
638 .unwrap();
639 assert_eq!(
640 buffer
641 .snapshot()
642 .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0))
643 .collect::<Vec<_>>(),
644 &[
645 DiagnosticEntry {
646 range: Point::new(2, 9)..Point::new(2, 12),
647 diagnostic: Diagnostic {
648 severity: DiagnosticSeverity::WARNING,
649 message: "unreachable statement".to_string(),
650 group_id: 3,
651 is_primary: true,
652 ..Default::default()
653 }
654 },
655 DiagnosticEntry {
656 range: Point::new(2, 9)..Point::new(2, 10),
657 diagnostic: Diagnostic {
658 severity: DiagnosticSeverity::ERROR,
659 message: "undefined variable 'A'".to_string(),
660 is_disk_based: true,
661 group_id: 0,
662 is_primary: true,
663 ..Default::default()
664 },
665 }
666 ]
667 );
668 assert_eq!(
669 chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)),
670 [
671 ("fn a() { ".to_string(), None),
672 ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
673 (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
674 ("\n".to_string(), None),
675 ]
676 );
677 assert_eq!(
678 chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)),
679 [
680 (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
681 ("\n".to_string(), None),
682 ]
683 );
684 });
685
686 // Keep editing the buffer and ensure disk-based diagnostics get translated according to the
687 // changes since the last save.
688 buffer.update(&mut cx, |buffer, cx| {
689 buffer.edit(Some(Point::new(2, 0)..Point::new(2, 0)), " ", cx);
690 buffer.edit(Some(Point::new(2, 8)..Point::new(2, 10)), "(x: usize)", cx);
691 });
692 let change_notification_2 = fake
693 .receive_notification::<lsp::notification::DidChangeTextDocument>()
694 .await;
695 assert!(
696 change_notification_2.text_document.version > change_notification_1.text_document.version
697 );
698
699 buffer.update(&mut cx, |buffer, cx| {
700 buffer
701 .update_diagnostics(
702 "lsp".into(),
703 Some(change_notification_2.text_document.version),
704 vec![
705 DiagnosticEntry {
706 range: PointUtf16::new(1, 9)..PointUtf16::new(1, 11),
707 diagnostic: Diagnostic {
708 severity: DiagnosticSeverity::ERROR,
709 message: "undefined variable 'BB'".to_string(),
710 is_disk_based: true,
711 group_id: 1,
712 is_primary: true,
713 ..Default::default()
714 },
715 },
716 DiagnosticEntry {
717 range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10),
718 diagnostic: Diagnostic {
719 severity: DiagnosticSeverity::ERROR,
720 message: "undefined variable 'A'".to_string(),
721 is_disk_based: true,
722 group_id: 0,
723 is_primary: true,
724 ..Default::default()
725 },
726 },
727 ],
728 cx,
729 )
730 .unwrap();
731 assert_eq!(
732 buffer
733 .snapshot()
734 .diagnostics_in_range::<_, Point>(0..buffer.len())
735 .collect::<Vec<_>>(),
736 &[
737 DiagnosticEntry {
738 range: Point::new(2, 21)..Point::new(2, 22),
739 diagnostic: Diagnostic {
740 severity: DiagnosticSeverity::ERROR,
741 message: "undefined variable 'A'".to_string(),
742 is_disk_based: true,
743 group_id: 0,
744 is_primary: true,
745 ..Default::default()
746 }
747 },
748 DiagnosticEntry {
749 range: Point::new(3, 9)..Point::new(3, 11),
750 diagnostic: Diagnostic {
751 severity: DiagnosticSeverity::ERROR,
752 message: "undefined variable 'BB'".to_string(),
753 is_disk_based: true,
754 group_id: 4,
755 is_primary: true,
756 ..Default::default()
757 },
758 }
759 ]
760 );
761 });
762}
763
764#[gpui::test]
765async fn test_preserving_old_group_ids_and_disk_based_diagnostics(mut cx: gpui::TestAppContext) {
766 let buffer = cx.add_model(|cx| {
767 let text = "
768 use a::*;
769 const b: i32 = c::;
770 const c: i32 = d;
771 const e: i32 = f +;
772 "
773 .unindent();
774
775 let mut rust_lang = rust_lang();
776 rust_lang.config.language_server = Some(LanguageServerConfig {
777 disk_based_diagnostic_sources: HashSet::from_iter(["disk".to_string()]),
778 ..Default::default()
779 });
780
781 let mut buffer = Buffer::new(0, text, cx);
782 buffer.set_language(Some(Arc::new(rust_lang)), None, cx);
783 buffer
784 });
785
786 // Initially, there are three errors. The second one is disk-based.
787 let diagnostics = vec![
788 DiagnosticEntry {
789 range: PointUtf16::new(1, 16)..PointUtf16::new(1, 18),
790 diagnostic: Diagnostic {
791 severity: DiagnosticSeverity::ERROR,
792 message: "syntax error 1".to_string(),
793 group_id: 0,
794 is_primary: true,
795 is_valid: true,
796 ..Default::default()
797 },
798 },
799 DiagnosticEntry {
800 range: PointUtf16::new(2, 15)..PointUtf16::new(2, 16),
801 diagnostic: Diagnostic {
802 severity: DiagnosticSeverity::ERROR,
803 message: "cannot find value `d` in this scope".to_string(),
804 is_disk_based: true,
805 group_id: 1,
806 is_primary: true,
807 is_valid: true,
808 ..Default::default()
809 },
810 },
811 DiagnosticEntry {
812 range: PointUtf16::new(3, 17)..PointUtf16::new(3, 18),
813 diagnostic: Diagnostic {
814 severity: DiagnosticSeverity::ERROR,
815 message: "syntax error 2".to_string(),
816 group_id: 2,
817 is_primary: true,
818 is_valid: true,
819 ..Default::default()
820 },
821 },
822 ];
823 buffer.update(&mut cx, |buffer, cx| {
824 buffer
825 .update_diagnostics("lsp".into(), None, diagnostics.clone(), cx)
826 .unwrap();
827 assert_eq!(
828 buffer
829 .snapshot()
830 .diagnostics_in_range::<_, PointUtf16>(PointUtf16::new(0, 0)..PointUtf16::new(4, 0))
831 .collect::<Vec<_>>(),
832 diagnostics.as_slice(),
833 );
834 });
835
836 // The diagnostics are updated. The disk-based diagnostic is omitted, and one
837 // other diagnostic has changed its message.
838 let mut new_diagnostics = vec![diagnostics[0].clone(), diagnostics[2].clone()];
839 new_diagnostics[0].diagnostic.message = "another syntax error".to_string();
840
841 buffer.update(&mut cx, |buffer, cx| {
842 buffer
843 .update_diagnostics("lsp".into(), None, new_diagnostics.clone(), cx)
844 .unwrap();
845 assert_eq!(
846 buffer
847 .snapshot()
848 .diagnostics_in_range::<_, PointUtf16>(PointUtf16::new(0, 0)..PointUtf16::new(4, 0))
849 .collect::<Vec<_>>(),
850 &[
851 // The changed diagnostic is given a new group id.
852 DiagnosticEntry {
853 range: new_diagnostics[0].range.clone(),
854 diagnostic: Diagnostic {
855 group_id: 3,
856 ..new_diagnostics[0].diagnostic.clone()
857 },
858 },
859 // The old disk-based diagnostic is marked as invalid, but keeps
860 // its original group id.
861 DiagnosticEntry {
862 range: diagnostics[1].range.clone(),
863 diagnostic: Diagnostic {
864 is_valid: false,
865 ..diagnostics[1].diagnostic.clone()
866 },
867 },
868 // The unchanged diagnostic keeps its original group id
869 new_diagnostics[1].clone(),
870 ],
871 );
872 });
873}
874
875#[gpui::test]
876async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
877 cx.add_model(|cx| {
878 let text = concat!(
879 "let one = ;\n", //
880 "let two = \n",
881 "let three = 3;\n",
882 );
883
884 let mut buffer = Buffer::new(0, text, cx);
885 buffer.set_language(Some(Arc::new(rust_lang())), None, cx);
886 buffer
887 .update_diagnostics(
888 "lsp".into(),
889 None,
890 vec![
891 DiagnosticEntry {
892 range: PointUtf16::new(0, 10)..PointUtf16::new(0, 10),
893 diagnostic: Diagnostic {
894 severity: DiagnosticSeverity::ERROR,
895 message: "syntax error 1".to_string(),
896 ..Default::default()
897 },
898 },
899 DiagnosticEntry {
900 range: PointUtf16::new(1, 10)..PointUtf16::new(1, 10),
901 diagnostic: Diagnostic {
902 severity: DiagnosticSeverity::ERROR,
903 message: "syntax error 2".to_string(),
904 ..Default::default()
905 },
906 },
907 ],
908 cx,
909 )
910 .unwrap();
911
912 // An empty range is extended forward to include the following character.
913 // At the end of a line, an empty range is extended backward to include
914 // the preceding character.
915 let chunks = chunks_with_diagnostics(&buffer, 0..buffer.len());
916 assert_eq!(
917 chunks
918 .iter()
919 .map(|(s, d)| (s.as_str(), *d))
920 .collect::<Vec<_>>(),
921 &[
922 ("let one = ", None),
923 (";", Some(DiagnosticSeverity::ERROR)),
924 ("\nlet two =", None),
925 (" ", Some(DiagnosticSeverity::ERROR)),
926 ("\nlet three = 3;\n", None)
927 ]
928 );
929 buffer
930 });
931}
932
933fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
934 buffer: &Buffer,
935 range: Range<T>,
936) -> Vec<(String, Option<DiagnosticSeverity>)> {
937 let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
938 for chunk in buffer.snapshot().chunks(range, Some(&Default::default())) {
939 if chunks
940 .last()
941 .map_or(false, |prev_chunk| prev_chunk.1 == chunk.diagnostic)
942 {
943 chunks.last_mut().unwrap().0.push_str(chunk.text);
944 } else {
945 chunks.push((chunk.text.to_string(), chunk.diagnostic));
946 }
947 }
948 chunks
949}
950
951#[test]
952fn test_contiguous_ranges() {
953 assert_eq!(
954 contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12].into_iter(), 100).collect::<Vec<_>>(),
955 &[1..4, 5..7, 9..13]
956 );
957
958 // Respects the `max_len` parameter
959 assert_eq!(
960 contiguous_ranges(
961 [2, 3, 4, 5, 6, 7, 8, 9, 23, 24, 25, 26, 30, 31].into_iter(),
962 3
963 )
964 .collect::<Vec<_>>(),
965 &[2..5, 5..8, 8..10, 23..26, 26..27, 30..32],
966 );
967}
968
969impl Buffer {
970 pub fn enclosing_bracket_point_ranges<T: ToOffset>(
971 &self,
972 range: Range<T>,
973 ) -> Option<(Range<Point>, Range<Point>)> {
974 self.snapshot()
975 .enclosing_bracket_ranges(range)
976 .map(|(start, end)| {
977 let point_start = start.start.to_point(self)..start.end.to_point(self);
978 let point_end = end.start.to_point(self)..end.end.to_point(self);
979 (point_start, point_end)
980 })
981 }
982}
983
984fn rust_lang() -> Language {
985 Language::new(
986 LanguageConfig {
987 name: "Rust".to_string(),
988 path_suffixes: vec!["rs".to_string()],
989 language_server: None,
990 ..Default::default()
991 },
992 Some(tree_sitter_rust::language()),
993 )
994 .with_indents_query(
995 r#"
996 (call_expression) @indent
997 (field_expression) @indent
998 (_ "(" ")" @end) @indent
999 (_ "{" "}" @end) @indent
1000 "#,
1001 )
1002 .unwrap()
1003 .with_brackets_query(r#" ("{" @open "}" @close) "#)
1004 .unwrap()
1005}
1006
1007fn empty(point: Point) -> Range<Point> {
1008 point..point
1009}