1use super::*;
2use gpui::{ModelHandle, MutableAppContext};
3use std::{iter::FromIterator, rc::Rc};
4use unindent::Unindent as _;
5
6#[gpui::test]
7fn test_edit_events(cx: &mut gpui::MutableAppContext) {
8 let mut now = Instant::now();
9 let buffer_1_events = Rc::new(RefCell::new(Vec::new()));
10 let buffer_2_events = Rc::new(RefCell::new(Vec::new()));
11
12 let buffer1 = cx.add_model(|cx| Buffer::new(0, "abcdef", cx));
13 let buffer2 = cx.add_model(|cx| Buffer::new(1, "abcdef", cx));
14 let buffer_ops = buffer1.update(cx, |buffer, cx| {
15 let buffer_1_events = buffer_1_events.clone();
16 cx.subscribe(&buffer1, move |_, _, event, _| {
17 buffer_1_events.borrow_mut().push(event.clone())
18 })
19 .detach();
20 let buffer_2_events = buffer_2_events.clone();
21 cx.subscribe(&buffer2, move |_, _, event, _| {
22 buffer_2_events.borrow_mut().push(event.clone())
23 })
24 .detach();
25
26 // An edit emits an edited event, followed by a dirtied event,
27 // since the buffer was previously in a clean state.
28 buffer.edit(Some(2..4), "XYZ", cx);
29
30 // An empty transaction does not emit any events.
31 buffer.start_transaction(None).unwrap();
32 buffer.end_transaction(None, cx).unwrap();
33
34 // A transaction containing two edits emits one edited event.
35 now += Duration::from_secs(1);
36 buffer.start_transaction_at(None, now).unwrap();
37 buffer.edit(Some(5..5), "u", cx);
38 buffer.edit(Some(6..6), "w", cx);
39 buffer.end_transaction_at(None, now, cx).unwrap();
40
41 // Undoing a transaction emits one edited event.
42 buffer.undo(cx);
43
44 buffer.operations.clone()
45 });
46
47 // Incorporating a set of remote ops emits a single edited event,
48 // followed by a dirtied event.
49 buffer2.update(cx, |buffer, cx| {
50 buffer.apply_ops(buffer_ops, cx).unwrap();
51 });
52
53 let buffer_1_events = buffer_1_events.borrow();
54 assert_eq!(
55 *buffer_1_events,
56 vec![Event::Edited, Event::Dirtied, Event::Edited, Event::Edited]
57 );
58
59 let buffer_2_events = buffer_2_events.borrow();
60 assert_eq!(*buffer_2_events, vec![Event::Edited, Event::Dirtied]);
61}
62
63#[gpui::test]
64async fn test_apply_diff(mut cx: gpui::TestAppContext) {
65 let text = "a\nbb\nccc\ndddd\neeeee\nffffff\n";
66 let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
67
68 let text = "a\nccc\ndddd\nffffff\n";
69 let diff = buffer.read_with(&cx, |b, cx| b.diff(text.into(), cx)).await;
70 buffer.update(&mut cx, |b, cx| b.apply_diff(diff, cx));
71 cx.read(|cx| assert_eq!(buffer.read(cx).text(), text));
72
73 let text = "a\n1\n\nccc\ndd2dd\nffffff\n";
74 let diff = buffer.read_with(&cx, |b, cx| b.diff(text.into(), cx)).await;
75 buffer.update(&mut cx, |b, cx| b.apply_diff(diff, cx));
76 cx.read(|cx| assert_eq!(buffer.read(cx).text(), text));
77}
78
79#[gpui::test]
80async fn test_reparse(mut cx: gpui::TestAppContext) {
81 let text = "fn a() {}";
82 let buffer = cx.add_model(|cx| {
83 Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx)
84 });
85
86 // Wait for the initial text to parse
87 buffer
88 .condition(&cx, |buffer, _| !buffer.is_parsing())
89 .await;
90 assert_eq!(
91 get_tree_sexp(&buffer, &cx),
92 concat!(
93 "(source_file (function_item name: (identifier) ",
94 "parameters: (parameters) ",
95 "body: (block)))"
96 )
97 );
98
99 buffer.update(&mut cx, |buffer, _| {
100 buffer.set_sync_parse_timeout(Duration::ZERO)
101 });
102
103 // Perform some edits (add parameter and variable reference)
104 // Parsing doesn't begin until the transaction is complete
105 buffer.update(&mut cx, |buf, cx| {
106 buf.start_transaction(None).unwrap();
107
108 let offset = buf.text().find(")").unwrap();
109 buf.edit(vec![offset..offset], "b: C", cx);
110 assert!(!buf.is_parsing());
111
112 let offset = buf.text().find("}").unwrap();
113 buf.edit(vec![offset..offset], " d; ", cx);
114 assert!(!buf.is_parsing());
115
116 buf.end_transaction(None, cx).unwrap();
117 assert_eq!(buf.text(), "fn a(b: C) { d; }");
118 assert!(buf.is_parsing());
119 });
120 buffer
121 .condition(&cx, |buffer, _| !buffer.is_parsing())
122 .await;
123 assert_eq!(
124 get_tree_sexp(&buffer, &cx),
125 concat!(
126 "(source_file (function_item name: (identifier) ",
127 "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
128 "body: (block (identifier))))"
129 )
130 );
131
132 // Perform a series of edits without waiting for the current parse to complete:
133 // * turn identifier into a field expression
134 // * turn field expression into a method call
135 // * add a turbofish to the method call
136 buffer.update(&mut cx, |buf, cx| {
137 let offset = buf.text().find(";").unwrap();
138 buf.edit(vec![offset..offset], ".e", cx);
139 assert_eq!(buf.text(), "fn a(b: C) { d.e; }");
140 assert!(buf.is_parsing());
141 });
142 buffer.update(&mut cx, |buf, cx| {
143 let offset = buf.text().find(";").unwrap();
144 buf.edit(vec![offset..offset], "(f)", cx);
145 assert_eq!(buf.text(), "fn a(b: C) { d.e(f); }");
146 assert!(buf.is_parsing());
147 });
148 buffer.update(&mut cx, |buf, cx| {
149 let offset = buf.text().find("(f)").unwrap();
150 buf.edit(vec![offset..offset], "::<G>", cx);
151 assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }");
152 assert!(buf.is_parsing());
153 });
154 buffer
155 .condition(&cx, |buffer, _| !buffer.is_parsing())
156 .await;
157 assert_eq!(
158 get_tree_sexp(&buffer, &cx),
159 concat!(
160 "(source_file (function_item name: (identifier) ",
161 "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
162 "body: (block (call_expression ",
163 "function: (generic_function ",
164 "function: (field_expression value: (identifier) field: (field_identifier)) ",
165 "type_arguments: (type_arguments (type_identifier))) ",
166 "arguments: (arguments (identifier))))))",
167 )
168 );
169
170 buffer.update(&mut cx, |buf, cx| {
171 buf.undo(cx);
172 assert_eq!(buf.text(), "fn a() {}");
173 assert!(buf.is_parsing());
174 });
175 buffer
176 .condition(&cx, |buffer, _| !buffer.is_parsing())
177 .await;
178 assert_eq!(
179 get_tree_sexp(&buffer, &cx),
180 concat!(
181 "(source_file (function_item name: (identifier) ",
182 "parameters: (parameters) ",
183 "body: (block)))"
184 )
185 );
186
187 buffer.update(&mut cx, |buf, cx| {
188 buf.redo(cx);
189 assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }");
190 assert!(buf.is_parsing());
191 });
192 buffer
193 .condition(&cx, |buffer, _| !buffer.is_parsing())
194 .await;
195 assert_eq!(
196 get_tree_sexp(&buffer, &cx),
197 concat!(
198 "(source_file (function_item name: (identifier) ",
199 "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
200 "body: (block (call_expression ",
201 "function: (generic_function ",
202 "function: (field_expression value: (identifier) field: (field_identifier)) ",
203 "type_arguments: (type_arguments (type_identifier))) ",
204 "arguments: (arguments (identifier))))))",
205 )
206 );
207
208 fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> String {
209 buffer.read_with(cx, |buffer, _| {
210 buffer.syntax_tree().unwrap().root_node().to_sexp()
211 })
212 }
213}
214
215#[gpui::test]
216fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
217 let buffer = cx.add_model(|cx| {
218 let text = "
219 mod x {
220 mod y {
221
222 }
223 }
224 "
225 .unindent();
226 Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx)
227 });
228 let buffer = buffer.read(cx);
229 assert_eq!(
230 buffer.enclosing_bracket_point_ranges(Point::new(1, 6)..Point::new(1, 6)),
231 Some((
232 Point::new(0, 6)..Point::new(0, 7),
233 Point::new(4, 0)..Point::new(4, 1)
234 ))
235 );
236 assert_eq!(
237 buffer.enclosing_bracket_point_ranges(Point::new(1, 10)..Point::new(1, 10)),
238 Some((
239 Point::new(1, 10)..Point::new(1, 11),
240 Point::new(3, 4)..Point::new(3, 5)
241 ))
242 );
243 assert_eq!(
244 buffer.enclosing_bracket_point_ranges(Point::new(3, 5)..Point::new(3, 5)),
245 Some((
246 Point::new(1, 10)..Point::new(1, 11),
247 Point::new(3, 4)..Point::new(3, 5)
248 ))
249 );
250}
251
252#[gpui::test]
253fn test_edit_with_autoindent(cx: &mut MutableAppContext) {
254 cx.add_model(|cx| {
255 let text = "fn a() {}";
256 let mut buffer =
257 Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
258
259 buffer.edit_with_autoindent([8..8], "\n\n", cx);
260 assert_eq!(buffer.text(), "fn a() {\n \n}");
261
262 buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 4)], "b()\n", cx);
263 assert_eq!(buffer.text(), "fn a() {\n b()\n \n}");
264
265 buffer.edit_with_autoindent([Point::new(2, 4)..Point::new(2, 4)], ".c", cx);
266 assert_eq!(buffer.text(), "fn a() {\n b()\n .c\n}");
267
268 buffer
269 });
270}
271
272#[gpui::test]
273fn test_autoindent_moves_selections(cx: &mut MutableAppContext) {
274 cx.add_model(|cx| {
275 let text = "fn a() {}";
276
277 let mut buffer =
278 Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
279
280 let selection_set_id = buffer.add_selection_set::<usize>(&[], cx);
281 buffer.start_transaction(Some(selection_set_id)).unwrap();
282 buffer.edit_with_autoindent([5..5, 9..9], "\n\n", cx);
283 buffer
284 .update_selection_set(
285 selection_set_id,
286 &[
287 Selection {
288 id: 0,
289 start: Point::new(1, 0),
290 end: Point::new(1, 0),
291 reversed: false,
292 goal: SelectionGoal::None,
293 },
294 Selection {
295 id: 1,
296 start: Point::new(4, 0),
297 end: Point::new(4, 0),
298 reversed: false,
299 goal: SelectionGoal::None,
300 },
301 ],
302 cx,
303 )
304 .unwrap();
305 assert_eq!(buffer.text(), "fn a(\n\n) {}\n\n");
306
307 // Ending the transaction runs the auto-indent. The selection
308 // at the start of the auto-indented row is pushed to the right.
309 buffer.end_transaction(Some(selection_set_id), cx).unwrap();
310 assert_eq!(buffer.text(), "fn a(\n \n) {}\n\n");
311 let selection_ranges = buffer
312 .selection_set(selection_set_id)
313 .unwrap()
314 .selections::<Point, _>(&buffer)
315 .map(|selection| selection.point_range(&buffer))
316 .collect::<Vec<_>>();
317
318 assert_eq!(selection_ranges[0], empty(Point::new(1, 4)));
319 assert_eq!(selection_ranges[1], empty(Point::new(4, 0)));
320
321 buffer
322 });
323}
324
325#[gpui::test]
326fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut MutableAppContext) {
327 cx.add_model(|cx| {
328 let text = "
329 fn a() {
330 c;
331 d;
332 }
333 "
334 .unindent();
335
336 let mut buffer =
337 Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
338
339 // Lines 2 and 3 don't match the indentation suggestion. When editing these lines,
340 // their indentation is not adjusted.
341 buffer.edit_with_autoindent([empty(Point::new(1, 1)), empty(Point::new(2, 1))], "()", cx);
342 assert_eq!(
343 buffer.text(),
344 "
345 fn a() {
346 c();
347 d();
348 }
349 "
350 .unindent()
351 );
352
353 // When appending new content after these lines, the indentation is based on the
354 // preceding lines' actual indentation.
355 buffer.edit_with_autoindent(
356 [empty(Point::new(1, 1)), empty(Point::new(2, 1))],
357 "\n.f\n.g",
358 cx,
359 );
360 assert_eq!(
361 buffer.text(),
362 "
363 fn a() {
364 c
365 .f
366 .g();
367 d
368 .f
369 .g();
370 }
371 "
372 .unindent()
373 );
374 buffer
375 });
376}
377
378#[gpui::test]
379fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppContext) {
380 cx.add_model(|cx| {
381 let text = "
382 fn a() {}
383 "
384 .unindent();
385
386 let mut buffer =
387 Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
388
389 buffer.edit_with_autoindent([5..5], "\nb", cx);
390 assert_eq!(
391 buffer.text(),
392 "
393 fn a(
394 b) {}
395 "
396 .unindent()
397 );
398
399 // The indentation suggestion changed because `@end` node (a close paren)
400 // is now at the beginning of the line.
401 buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 5)], "", cx);
402 assert_eq!(
403 buffer.text(),
404 "
405 fn a(
406 ) {}
407 "
408 .unindent()
409 );
410
411 buffer
412 });
413}
414
415#[gpui::test]
416async fn test_diagnostics(mut cx: gpui::TestAppContext) {
417 let (language_server, mut fake) = lsp::LanguageServer::fake(cx.background()).await;
418 let mut rust_lang = rust_lang();
419 rust_lang.config.language_server = Some(LanguageServerConfig {
420 disk_based_diagnostic_sources: HashSet::from_iter(["disk".to_string()]),
421 ..Default::default()
422 });
423
424 let text = "
425 fn a() { A }
426 fn b() { BB }
427 fn c() { CCC }
428 "
429 .unindent();
430
431 let buffer = cx.add_model(|cx| {
432 Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang)), Some(language_server), cx)
433 });
434
435 let open_notification = fake
436 .receive_notification::<lsp::notification::DidOpenTextDocument>()
437 .await;
438
439 // Edit the buffer, moving the content down
440 buffer.update(&mut cx, |buffer, cx| buffer.edit([0..0], "\n\n", cx));
441 let change_notification_1 = fake
442 .receive_notification::<lsp::notification::DidChangeTextDocument>()
443 .await;
444 assert!(change_notification_1.text_document.version > open_notification.text_document.version);
445
446 buffer.update(&mut cx, |buffer, cx| {
447 // Receive diagnostics for an earlier version of the buffer.
448 buffer
449 .update_diagnostics(
450 Some(open_notification.text_document.version),
451 vec![
452 lsp::Diagnostic {
453 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
454 severity: Some(lsp::DiagnosticSeverity::ERROR),
455 message: "undefined variable 'A'".to_string(),
456 ..Default::default()
457 },
458 lsp::Diagnostic {
459 range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
460 severity: Some(lsp::DiagnosticSeverity::ERROR),
461 message: "undefined variable 'BB'".to_string(),
462 ..Default::default()
463 },
464 lsp::Diagnostic {
465 range: lsp::Range::new(lsp::Position::new(2, 9), lsp::Position::new(2, 12)),
466 severity: Some(lsp::DiagnosticSeverity::ERROR),
467 message: "undefined variable 'CCC'".to_string(),
468 ..Default::default()
469 },
470 ],
471 cx,
472 )
473 .unwrap();
474
475 // The diagnostics have moved down since they were created.
476 assert_eq!(
477 buffer
478 .diagnostics_in_range(Point::new(3, 0)..Point::new(5, 0))
479 .collect::<Vec<_>>(),
480 &[
481 (
482 Point::new(3, 9)..Point::new(3, 11),
483 &Diagnostic {
484 severity: DiagnosticSeverity::ERROR,
485 message: "undefined variable 'BB'".to_string(),
486 group_id: 1,
487 is_primary: true,
488 },
489 ),
490 (
491 Point::new(4, 9)..Point::new(4, 12),
492 &Diagnostic {
493 severity: DiagnosticSeverity::ERROR,
494 message: "undefined variable 'CCC'".to_string(),
495 group_id: 2,
496 is_primary: true,
497 }
498 )
499 ]
500 );
501 assert_eq!(
502 chunks_with_diagnostics(buffer, 0..buffer.len()),
503 [
504 ("\n\nfn a() { ".to_string(), None),
505 ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
506 (" }\nfn b() { ".to_string(), None),
507 ("BB".to_string(), Some(DiagnosticSeverity::ERROR)),
508 (" }\nfn c() { ".to_string(), None),
509 ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)),
510 (" }\n".to_string(), None),
511 ]
512 );
513 assert_eq!(
514 chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)),
515 [
516 ("B".to_string(), Some(DiagnosticSeverity::ERROR)),
517 (" }\nfn c() { ".to_string(), None),
518 ("CC".to_string(), Some(DiagnosticSeverity::ERROR)),
519 ]
520 );
521
522 // Ensure overlapping diagnostics are highlighted correctly.
523 buffer
524 .update_diagnostics(
525 Some(open_notification.text_document.version),
526 vec![
527 lsp::Diagnostic {
528 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
529 severity: Some(lsp::DiagnosticSeverity::ERROR),
530 message: "undefined variable 'A'".to_string(),
531 ..Default::default()
532 },
533 lsp::Diagnostic {
534 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 12)),
535 severity: Some(lsp::DiagnosticSeverity::WARNING),
536 message: "unreachable statement".to_string(),
537 ..Default::default()
538 },
539 ],
540 cx,
541 )
542 .unwrap();
543 assert_eq!(
544 buffer
545 .diagnostics_in_range(Point::new(2, 0)..Point::new(3, 0))
546 .collect::<Vec<_>>(),
547 &[
548 (
549 Point::new(2, 9)..Point::new(2, 12),
550 &Diagnostic {
551 severity: DiagnosticSeverity::WARNING,
552 message: "unreachable statement".to_string(),
553 group_id: 1,
554 is_primary: true,
555 }
556 ),
557 (
558 Point::new(2, 9)..Point::new(2, 10),
559 &Diagnostic {
560 severity: DiagnosticSeverity::ERROR,
561 message: "undefined variable 'A'".to_string(),
562 group_id: 0,
563 is_primary: true,
564 },
565 )
566 ]
567 );
568 assert_eq!(
569 chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)),
570 [
571 ("fn a() { ".to_string(), None),
572 ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
573 (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
574 ("\n".to_string(), None),
575 ]
576 );
577 assert_eq!(
578 chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)),
579 [
580 (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
581 ("\n".to_string(), None),
582 ]
583 );
584 });
585
586 // Keep editing the buffer and ensure disk-based diagnostics get translated according to the
587 // changes since the last save.
588 buffer.update(&mut cx, |buffer, cx| {
589 buffer.edit(Some(Point::new(2, 0)..Point::new(2, 0)), " ", cx);
590 buffer.edit(Some(Point::new(2, 8)..Point::new(2, 10)), "(x: usize)", cx);
591 });
592 let change_notification_2 = fake
593 .receive_notification::<lsp::notification::DidChangeTextDocument>()
594 .await;
595 assert!(
596 change_notification_2.text_document.version > change_notification_1.text_document.version
597 );
598
599 buffer.update(&mut cx, |buffer, cx| {
600 buffer
601 .update_diagnostics(
602 Some(change_notification_2.text_document.version),
603 vec![
604 lsp::Diagnostic {
605 range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
606 severity: Some(lsp::DiagnosticSeverity::ERROR),
607 message: "undefined variable 'BB'".to_string(),
608 source: Some("disk".to_string()),
609 ..Default::default()
610 },
611 lsp::Diagnostic {
612 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
613 severity: Some(lsp::DiagnosticSeverity::ERROR),
614 message: "undefined variable 'A'".to_string(),
615 source: Some("disk".to_string()),
616 ..Default::default()
617 },
618 ],
619 cx,
620 )
621 .unwrap();
622 assert_eq!(
623 buffer
624 .diagnostics_in_range(0..buffer.len())
625 .collect::<Vec<_>>(),
626 &[
627 (
628 Point::new(2, 21)..Point::new(2, 22),
629 &Diagnostic {
630 severity: DiagnosticSeverity::ERROR,
631 message: "undefined variable 'A'".to_string(),
632 group_id: 0,
633 is_primary: true,
634 }
635 ),
636 (
637 Point::new(3, 9)..Point::new(3, 11),
638 &Diagnostic {
639 severity: DiagnosticSeverity::ERROR,
640 message: "undefined variable 'BB'".to_string(),
641 group_id: 1,
642 is_primary: true,
643 },
644 )
645 ]
646 );
647 });
648}
649
650#[gpui::test]
651async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
652 cx.add_model(|cx| {
653 let text = concat!(
654 "let one = ;\n", //
655 "let two = \n",
656 "let three = 3;\n",
657 );
658
659 let mut buffer = Buffer::new(0, text, cx);
660 buffer.set_language(Some(Arc::new(rust_lang())), None, cx);
661 buffer
662 .update_diagnostics(
663 None,
664 vec![
665 lsp::Diagnostic {
666 range: lsp::Range::new(
667 lsp::Position::new(0, 10),
668 lsp::Position::new(0, 10),
669 ),
670 severity: Some(lsp::DiagnosticSeverity::ERROR),
671 message: "syntax error 1".to_string(),
672 ..Default::default()
673 },
674 lsp::Diagnostic {
675 range: lsp::Range::new(
676 lsp::Position::new(1, 10),
677 lsp::Position::new(1, 10),
678 ),
679 severity: Some(lsp::DiagnosticSeverity::ERROR),
680 message: "syntax error 2".to_string(),
681 ..Default::default()
682 },
683 ],
684 cx,
685 )
686 .unwrap();
687
688 // An empty range is extended forward to include the following character.
689 // At the end of a line, an empty range is extended backward to include
690 // the preceding character.
691 let chunks = chunks_with_diagnostics(&buffer, 0..buffer.len());
692 assert_eq!(
693 chunks
694 .iter()
695 .map(|(s, d)| (s.as_str(), *d))
696 .collect::<Vec<_>>(),
697 &[
698 ("let one = ", None),
699 (";", Some(lsp::DiagnosticSeverity::ERROR)),
700 ("\nlet two =", None),
701 (" ", Some(lsp::DiagnosticSeverity::ERROR)),
702 ("\nlet three = 3;\n", None)
703 ]
704 );
705 buffer
706 });
707}
708
709#[gpui::test]
710async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) {
711 cx.add_model(|cx| {
712 let text = "
713 fn foo(mut v: Vec<usize>) {
714 for x in &v {
715 v.push(1);
716 }
717 }
718 "
719 .unindent();
720
721 let file = FakeFile::new("/example.rs");
722 let mut buffer = Buffer::from_file(0, text, Box::new(file.clone()), cx);
723 buffer.set_language(Some(Arc::new(rust_lang())), None, cx);
724 let diagnostics = vec![
725 lsp::Diagnostic {
726 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
727 severity: Some(DiagnosticSeverity::WARNING),
728 message: "error 1".to_string(),
729 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
730 location: lsp::Location {
731 uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
732 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
733 },
734 message: "error 1 hint 1".to_string(),
735 }]),
736 ..Default::default()
737 },
738 lsp::Diagnostic {
739 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
740 severity: Some(DiagnosticSeverity::HINT),
741 message: "error 1 hint 1".to_string(),
742 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
743 location: lsp::Location {
744 uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
745 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
746 },
747 message: "original diagnostic".to_string(),
748 }]),
749 ..Default::default()
750 },
751 lsp::Diagnostic {
752 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
753 severity: Some(DiagnosticSeverity::ERROR),
754 message: "error 2".to_string(),
755 related_information: Some(vec![
756 lsp::DiagnosticRelatedInformation {
757 location: lsp::Location {
758 uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
759 range: lsp::Range::new(
760 lsp::Position::new(1, 13),
761 lsp::Position::new(1, 15),
762 ),
763 },
764 message: "error 2 hint 1".to_string(),
765 },
766 lsp::DiagnosticRelatedInformation {
767 location: lsp::Location {
768 uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
769 range: lsp::Range::new(
770 lsp::Position::new(1, 13),
771 lsp::Position::new(1, 15),
772 ),
773 },
774 message: "error 2 hint 2".to_string(),
775 },
776 ]),
777 ..Default::default()
778 },
779 lsp::Diagnostic {
780 range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
781 severity: Some(DiagnosticSeverity::HINT),
782 message: "error 2 hint 1".to_string(),
783 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
784 location: lsp::Location {
785 uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
786 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
787 },
788 message: "original diagnostic".to_string(),
789 }]),
790 ..Default::default()
791 },
792 lsp::Diagnostic {
793 range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
794 severity: Some(DiagnosticSeverity::HINT),
795 message: "error 2 hint 2".to_string(),
796 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
797 location: lsp::Location {
798 uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
799 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
800 },
801 message: "original diagnostic".to_string(),
802 }]),
803 ..Default::default()
804 },
805 ];
806 buffer.update_diagnostics(None, diagnostics, cx).unwrap();
807 assert_eq!(
808 buffer
809 .diagnostics_in_range::<_, Point>(0..buffer.len())
810 .collect::<Vec<_>>(),
811 &[
812 (
813 Point::new(1, 8)..Point::new(1, 9),
814 &Diagnostic {
815 severity: DiagnosticSeverity::WARNING,
816 message: "error 1".to_string(),
817 group_id: 0,
818 is_primary: true,
819 }
820 ),
821 (
822 Point::new(1, 8)..Point::new(1, 9),
823 &Diagnostic {
824 severity: DiagnosticSeverity::HINT,
825 message: "error 1 hint 1".to_string(),
826 group_id: 0,
827 is_primary: false,
828 }
829 ),
830 (
831 Point::new(1, 13)..Point::new(1, 15),
832 &Diagnostic {
833 severity: DiagnosticSeverity::HINT,
834 message: "error 2 hint 1".to_string(),
835 group_id: 1,
836 is_primary: false,
837 }
838 ),
839 (
840 Point::new(1, 13)..Point::new(1, 15),
841 &Diagnostic {
842 severity: DiagnosticSeverity::HINT,
843 message: "error 2 hint 2".to_string(),
844 group_id: 1,
845 is_primary: false,
846 }
847 ),
848 (
849 Point::new(2, 8)..Point::new(2, 17),
850 &Diagnostic {
851 severity: DiagnosticSeverity::ERROR,
852 message: "error 2".to_string(),
853 group_id: 1,
854 is_primary: true,
855 }
856 )
857 ]
858 );
859
860 assert_eq!(
861 buffer.diagnostic_group(0).collect::<Vec<_>>(),
862 &[
863 (
864 Point::new(1, 8)..Point::new(1, 9),
865 &Diagnostic {
866 severity: DiagnosticSeverity::WARNING,
867 message: "error 1".to_string(),
868 group_id: 0,
869 is_primary: true,
870 }
871 ),
872 (
873 Point::new(1, 8)..Point::new(1, 9),
874 &Diagnostic {
875 severity: DiagnosticSeverity::HINT,
876 message: "error 1 hint 1".to_string(),
877 group_id: 0,
878 is_primary: false,
879 }
880 ),
881 ]
882 );
883 assert_eq!(
884 buffer.diagnostic_group(1).collect::<Vec<_>>(),
885 &[
886 (
887 Point::new(1, 13)..Point::new(1, 15),
888 &Diagnostic {
889 severity: DiagnosticSeverity::HINT,
890 message: "error 2 hint 1".to_string(),
891 group_id: 1,
892 is_primary: false,
893 }
894 ),
895 (
896 Point::new(1, 13)..Point::new(1, 15),
897 &Diagnostic {
898 severity: DiagnosticSeverity::HINT,
899 message: "error 2 hint 2".to_string(),
900 group_id: 1,
901 is_primary: false,
902 }
903 ),
904 (
905 Point::new(2, 8)..Point::new(2, 17),
906 &Diagnostic {
907 severity: DiagnosticSeverity::ERROR,
908 message: "error 2".to_string(),
909 group_id: 1,
910 is_primary: true,
911 }
912 )
913 ]
914 );
915
916 buffer
917 });
918}
919
920fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
921 buffer: &Buffer,
922 range: Range<T>,
923) -> Vec<(String, Option<DiagnosticSeverity>)> {
924 let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
925 for chunk in buffer.snapshot().chunks(range, Some(&Default::default())) {
926 if chunks
927 .last()
928 .map_or(false, |prev_chunk| prev_chunk.1 == chunk.diagnostic)
929 {
930 chunks.last_mut().unwrap().0.push_str(chunk.text);
931 } else {
932 chunks.push((chunk.text.to_string(), chunk.diagnostic));
933 }
934 }
935 chunks
936}
937
938#[test]
939fn test_contiguous_ranges() {
940 assert_eq!(
941 contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12], 100).collect::<Vec<_>>(),
942 &[1..4, 5..7, 9..13]
943 );
944
945 // Respects the `max_len` parameter
946 assert_eq!(
947 contiguous_ranges([2, 3, 4, 5, 6, 7, 8, 9, 23, 24, 25, 26, 30, 31], 3).collect::<Vec<_>>(),
948 &[2..5, 5..8, 8..10, 23..26, 26..27, 30..32],
949 );
950}
951
952impl Buffer {
953 pub fn enclosing_bracket_point_ranges<T: ToOffset>(
954 &self,
955 range: Range<T>,
956 ) -> Option<(Range<Point>, Range<Point>)> {
957 self.enclosing_bracket_ranges(range).map(|(start, end)| {
958 let point_start = start.start.to_point(self)..start.end.to_point(self);
959 let point_end = end.start.to_point(self)..end.end.to_point(self);
960 (point_start, point_end)
961 })
962 }
963}
964
965fn rust_lang() -> Language {
966 Language::new(
967 LanguageConfig {
968 name: "Rust".to_string(),
969 path_suffixes: vec!["rs".to_string()],
970 language_server: None,
971 ..Default::default()
972 },
973 tree_sitter_rust::language(),
974 )
975 .with_indents_query(
976 r#"
977 (call_expression) @indent
978 (field_expression) @indent
979 (_ "(" ")" @end) @indent
980 (_ "{" "}" @end) @indent
981 "#,
982 )
983 .unwrap()
984 .with_brackets_query(r#" ("{" @open "}" @close) "#)
985 .unwrap()
986}
987
988fn empty(point: Point) -> Range<Point> {
989 point..point
990}
991
992#[derive(Clone)]
993struct FakeFile {
994 abs_path: PathBuf,
995}
996
997impl FakeFile {
998 fn new(abs_path: impl Into<PathBuf>) -> Self {
999 Self {
1000 abs_path: abs_path.into(),
1001 }
1002 }
1003}
1004
1005impl File for FakeFile {
1006 fn worktree_id(&self) -> usize {
1007 todo!()
1008 }
1009
1010 fn entry_id(&self) -> Option<usize> {
1011 todo!()
1012 }
1013
1014 fn mtime(&self) -> SystemTime {
1015 SystemTime::now()
1016 }
1017
1018 fn path(&self) -> &Arc<Path> {
1019 todo!()
1020 }
1021
1022 fn abs_path(&self) -> Option<PathBuf> {
1023 Some(self.abs_path.clone())
1024 }
1025
1026 fn full_path(&self) -> PathBuf {
1027 todo!()
1028 }
1029
1030 fn file_name(&self) -> Option<OsString> {
1031 todo!()
1032 }
1033
1034 fn is_deleted(&self) -> bool {
1035 todo!()
1036 }
1037
1038 fn save(
1039 &self,
1040 _: u64,
1041 _: Rope,
1042 _: clock::Global,
1043 _: &mut MutableAppContext,
1044 ) -> Task<Result<(clock::Global, SystemTime)>> {
1045 todo!()
1046 }
1047
1048 fn load_local(&self, _: &AppContext) -> Option<Task<Result<String>>> {
1049 todo!()
1050 }
1051
1052 fn buffer_updated(&self, _: u64, _: super::Operation, _: &mut MutableAppContext) {
1053 todo!()
1054 }
1055
1056 fn buffer_removed(&self, _: u64, _: &mut MutableAppContext) {
1057 todo!()
1058 }
1059
1060 fn boxed_clone(&self) -> Box<dyn File> {
1061 todo!()
1062 }
1063
1064 fn as_any(&self) -> &dyn Any {
1065 todo!()
1066 }
1067}