1use super::*;
2use collections::HashMap;
3use editor::{
4 display_map::{Block, BlockContext, DisplayRow},
5 DisplayPoint, GutterDimensions,
6};
7use gpui::{px, AvailableSpace, Stateful, TestAppContext, VisualTestContext};
8use language::{
9 Diagnostic, DiagnosticEntry, DiagnosticSeverity, OffsetRangeExt, PointUtf16, Rope, Unclipped,
10};
11use pretty_assertions::assert_eq;
12use project::FakeFs;
13use rand::{rngs::StdRng, seq::IteratorRandom as _, Rng};
14use serde_json::json;
15use settings::SettingsStore;
16use std::{
17 env,
18 path::{Path, PathBuf},
19};
20use unindent::Unindent as _;
21use util::{post_inc, RandomCharIter};
22
23#[ctor::ctor]
24fn init_logger() {
25 if env::var("RUST_LOG").is_ok() {
26 env_logger::init();
27 }
28}
29
30#[gpui::test]
31async fn test_diagnostics(cx: &mut TestAppContext) {
32 init_test(cx);
33
34 let fs = FakeFs::new(cx.executor());
35 fs.insert_tree(
36 "/test",
37 json!({
38 "consts.rs": "
39 const a: i32 = 'a';
40 const b: i32 = c;
41 "
42 .unindent(),
43
44 "main.rs": "
45 fn main() {
46 let x = vec![];
47 let y = vec![];
48 a(x);
49 b(y);
50 // comment 1
51 // comment 2
52 c(y);
53 d(x);
54 }
55 "
56 .unindent(),
57 }),
58 )
59 .await;
60
61 let language_server_id = LanguageServerId(0);
62 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
63 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
64 let cx = &mut VisualTestContext::from_window(*window, cx);
65 let workspace = window.root(cx).unwrap();
66
67 // Create some diagnostics
68 project.update(cx, |project, cx| {
69 project
70 .update_diagnostic_entries(
71 language_server_id,
72 PathBuf::from("/test/main.rs"),
73 None,
74 vec![
75 DiagnosticEntry {
76 range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
77 diagnostic: Diagnostic {
78 message:
79 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
80 .to_string(),
81 severity: DiagnosticSeverity::INFORMATION,
82 is_primary: false,
83 is_disk_based: true,
84 group_id: 1,
85 ..Default::default()
86 },
87 },
88 DiagnosticEntry {
89 range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
90 diagnostic: Diagnostic {
91 message:
92 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
93 .to_string(),
94 severity: DiagnosticSeverity::INFORMATION,
95 is_primary: false,
96 is_disk_based: true,
97 group_id: 0,
98 ..Default::default()
99 },
100 },
101 DiagnosticEntry {
102 range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)),
103 diagnostic: Diagnostic {
104 message: "value moved here".to_string(),
105 severity: DiagnosticSeverity::INFORMATION,
106 is_primary: false,
107 is_disk_based: true,
108 group_id: 1,
109 ..Default::default()
110 },
111 },
112 DiagnosticEntry {
113 range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
114 diagnostic: Diagnostic {
115 message: "value moved here".to_string(),
116 severity: DiagnosticSeverity::INFORMATION,
117 is_primary: false,
118 is_disk_based: true,
119 group_id: 0,
120 ..Default::default()
121 },
122 },
123 DiagnosticEntry {
124 range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)),
125 diagnostic: Diagnostic {
126 message: "use of moved value\nvalue used here after move".to_string(),
127 severity: DiagnosticSeverity::ERROR,
128 is_primary: true,
129 is_disk_based: true,
130 group_id: 0,
131 ..Default::default()
132 },
133 },
134 DiagnosticEntry {
135 range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
136 diagnostic: Diagnostic {
137 message: "use of moved value\nvalue used here after move".to_string(),
138 severity: DiagnosticSeverity::ERROR,
139 is_primary: true,
140 is_disk_based: true,
141 group_id: 1,
142 ..Default::default()
143 },
144 },
145 ],
146 cx,
147 )
148 .unwrap();
149 });
150
151 // Open the project diagnostics view while there are already diagnostics.
152 let view = window.build_view(cx, |cx| {
153 ProjectDiagnosticsEditor::new_with_context(1, project.clone(), workspace.downgrade(), cx)
154 });
155 let editor = view.update(cx, |view, _| view.editor.clone());
156
157 view.next_notification(cx).await;
158 assert_eq!(
159 editor_blocks(&editor, cx),
160 [
161 (DisplayRow(0), FILE_HEADER.into()),
162 (DisplayRow(2), DIAGNOSTIC_HEADER.into()),
163 (DisplayRow(15), EXCERPT_HEADER.into()),
164 (DisplayRow(16), DIAGNOSTIC_HEADER.into()),
165 (DisplayRow(25), EXCERPT_HEADER.into()),
166 ]
167 );
168 assert_eq!(
169 editor.update(cx, |editor, cx| editor.display_text(cx)),
170 concat!(
171 //
172 // main.rs
173 //
174 "\n", // filename
175 "\n", // padding
176 // diagnostic group 1
177 "\n", // primary message
178 "\n", // padding
179 " let x = vec![];\n",
180 " let y = vec![];\n",
181 "\n", // supporting diagnostic
182 " a(x);\n",
183 " b(y);\n",
184 "\n", // supporting diagnostic
185 " // comment 1\n",
186 " // comment 2\n",
187 " c(y);\n",
188 "\n", // supporting diagnostic
189 " d(x);\n",
190 "\n", // context ellipsis
191 // diagnostic group 2
192 "\n", // primary message
193 "\n", // padding
194 "fn main() {\n",
195 " let x = vec![];\n",
196 "\n", // supporting diagnostic
197 " let y = vec![];\n",
198 " a(x);\n",
199 "\n", // supporting diagnostic
200 " b(y);\n",
201 "\n", // context ellipsis
202 " c(y);\n",
203 " d(x);\n",
204 "\n", // supporting diagnostic
205 "}"
206 )
207 );
208
209 // Cursor is at the first diagnostic
210 editor.update(cx, |editor, cx| {
211 assert_eq!(
212 editor.selections.display_ranges(cx),
213 [DisplayPoint::new(DisplayRow(12), 6)..DisplayPoint::new(DisplayRow(12), 6)]
214 );
215 });
216
217 // Diagnostics are added for another earlier path.
218 project.update(cx, |project, cx| {
219 project.disk_based_diagnostics_started(language_server_id, cx);
220 project
221 .update_diagnostic_entries(
222 language_server_id,
223 PathBuf::from("/test/consts.rs"),
224 None,
225 vec![DiagnosticEntry {
226 range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
227 diagnostic: Diagnostic {
228 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
229 severity: DiagnosticSeverity::ERROR,
230 is_primary: true,
231 is_disk_based: true,
232 group_id: 0,
233 ..Default::default()
234 },
235 }],
236 cx,
237 )
238 .unwrap();
239 project.disk_based_diagnostics_finished(language_server_id, cx);
240 });
241
242 view.next_notification(cx).await;
243 assert_eq!(
244 editor_blocks(&editor, cx),
245 [
246 (DisplayRow(0), FILE_HEADER.into()),
247 (DisplayRow(2), DIAGNOSTIC_HEADER.into()),
248 (DisplayRow(7), FILE_HEADER.into()),
249 (DisplayRow(9), DIAGNOSTIC_HEADER.into()),
250 (DisplayRow(22), EXCERPT_HEADER.into()),
251 (DisplayRow(23), DIAGNOSTIC_HEADER.into()),
252 (DisplayRow(32), EXCERPT_HEADER.into()),
253 ]
254 );
255
256 assert_eq!(
257 editor.update(cx, |editor, cx| editor.display_text(cx)),
258 concat!(
259 //
260 // consts.rs
261 //
262 "\n", // filename
263 "\n", // padding
264 // diagnostic group 1
265 "\n", // primary message
266 "\n", // padding
267 "const a: i32 = 'a';\n",
268 "\n", // supporting diagnostic
269 "const b: i32 = c;\n",
270 //
271 // main.rs
272 //
273 "\n", // filename
274 "\n", // padding
275 // diagnostic group 1
276 "\n", // primary message
277 "\n", // padding
278 " let x = vec![];\n",
279 " let y = vec![];\n",
280 "\n", // supporting diagnostic
281 " a(x);\n",
282 " b(y);\n",
283 "\n", // supporting diagnostic
284 " // comment 1\n",
285 " // comment 2\n",
286 " c(y);\n",
287 "\n", // supporting diagnostic
288 " d(x);\n",
289 "\n", // collapsed context
290 // diagnostic group 2
291 "\n", // primary message
292 "\n", // filename
293 "fn main() {\n",
294 " let x = vec![];\n",
295 "\n", // supporting diagnostic
296 " let y = vec![];\n",
297 " a(x);\n",
298 "\n", // supporting diagnostic
299 " b(y);\n",
300 "\n", // context ellipsis
301 " c(y);\n",
302 " d(x);\n",
303 "\n", // supporting diagnostic
304 "}"
305 )
306 );
307
308 // Cursor keeps its position.
309 editor.update(cx, |editor, cx| {
310 assert_eq!(
311 editor.selections.display_ranges(cx),
312 [DisplayPoint::new(DisplayRow(19), 6)..DisplayPoint::new(DisplayRow(19), 6)]
313 );
314 });
315
316 // Diagnostics are added to the first path
317 project.update(cx, |project, cx| {
318 project.disk_based_diagnostics_started(language_server_id, cx);
319 project
320 .update_diagnostic_entries(
321 language_server_id,
322 PathBuf::from("/test/consts.rs"),
323 None,
324 vec![
325 DiagnosticEntry {
326 range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
327 diagnostic: Diagnostic {
328 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
329 severity: DiagnosticSeverity::ERROR,
330 is_primary: true,
331 is_disk_based: true,
332 group_id: 0,
333 ..Default::default()
334 },
335 },
336 DiagnosticEntry {
337 range: Unclipped(PointUtf16::new(1, 15))..Unclipped(PointUtf16::new(1, 15)),
338 diagnostic: Diagnostic {
339 message: "unresolved name `c`".to_string(),
340 severity: DiagnosticSeverity::ERROR,
341 is_primary: true,
342 is_disk_based: true,
343 group_id: 1,
344 ..Default::default()
345 },
346 },
347 ],
348 cx,
349 )
350 .unwrap();
351 project.disk_based_diagnostics_finished(language_server_id, cx);
352 });
353
354 view.next_notification(cx).await;
355 assert_eq!(
356 editor_blocks(&editor, cx),
357 [
358 (DisplayRow(0), FILE_HEADER.into()),
359 (DisplayRow(2), DIAGNOSTIC_HEADER.into()),
360 (DisplayRow(7), EXCERPT_HEADER.into()),
361 (DisplayRow(8), DIAGNOSTIC_HEADER.into()),
362 (DisplayRow(13), FILE_HEADER.into()),
363 (DisplayRow(15), DIAGNOSTIC_HEADER.into()),
364 (DisplayRow(28), EXCERPT_HEADER.into()),
365 (DisplayRow(29), DIAGNOSTIC_HEADER.into()),
366 (DisplayRow(38), EXCERPT_HEADER.into()),
367 ]
368 );
369
370 assert_eq!(
371 editor.update(cx, |editor, cx| editor.display_text(cx)),
372 concat!(
373 //
374 // consts.rs
375 //
376 "\n", // filename
377 "\n", // padding
378 // diagnostic group 1
379 "\n", // primary message
380 "\n", // padding
381 "const a: i32 = 'a';\n",
382 "\n", // supporting diagnostic
383 "const b: i32 = c;\n",
384 "\n", // context ellipsis
385 // diagnostic group 2
386 "\n", // primary message
387 "\n", // padding
388 "const a: i32 = 'a';\n",
389 "const b: i32 = c;\n",
390 "\n", // supporting diagnostic
391 //
392 // main.rs
393 //
394 "\n", // filename
395 "\n", // padding
396 // diagnostic group 1
397 "\n", // primary message
398 "\n", // padding
399 " let x = vec![];\n",
400 " let y = vec![];\n",
401 "\n", // supporting diagnostic
402 " a(x);\n",
403 " b(y);\n",
404 "\n", // supporting diagnostic
405 " // comment 1\n",
406 " // comment 2\n",
407 " c(y);\n",
408 "\n", // supporting diagnostic
409 " d(x);\n",
410 "\n", // context ellipsis
411 // diagnostic group 2
412 "\n", // primary message
413 "\n", // filename
414 "fn main() {\n",
415 " let x = vec![];\n",
416 "\n", // supporting diagnostic
417 " let y = vec![];\n",
418 " a(x);\n",
419 "\n", // supporting diagnostic
420 " b(y);\n",
421 "\n", // context ellipsis
422 " c(y);\n",
423 " d(x);\n",
424 "\n", // supporting diagnostic
425 "}"
426 )
427 );
428}
429
430#[gpui::test]
431async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
432 init_test(cx);
433
434 let fs = FakeFs::new(cx.executor());
435 fs.insert_tree(
436 "/test",
437 json!({
438 "main.js": "
439 a();
440 b();
441 c();
442 d();
443 e();
444 ".unindent()
445 }),
446 )
447 .await;
448
449 let server_id_1 = LanguageServerId(100);
450 let server_id_2 = LanguageServerId(101);
451 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
452 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
453 let cx = &mut VisualTestContext::from_window(*window, cx);
454 let workspace = window.root(cx).unwrap();
455
456 let view = window.build_view(cx, |cx| {
457 ProjectDiagnosticsEditor::new_with_context(1, project.clone(), workspace.downgrade(), cx)
458 });
459 let editor = view.update(cx, |view, _| view.editor.clone());
460
461 // Two language servers start updating diagnostics
462 project.update(cx, |project, cx| {
463 project.disk_based_diagnostics_started(server_id_1, cx);
464 project.disk_based_diagnostics_started(server_id_2, cx);
465 project
466 .update_diagnostic_entries(
467 server_id_1,
468 PathBuf::from("/test/main.js"),
469 None,
470 vec![DiagnosticEntry {
471 range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
472 diagnostic: Diagnostic {
473 message: "error 1".to_string(),
474 severity: DiagnosticSeverity::WARNING,
475 is_primary: true,
476 is_disk_based: true,
477 group_id: 1,
478 ..Default::default()
479 },
480 }],
481 cx,
482 )
483 .unwrap();
484 });
485
486 // The first language server finishes
487 project.update(cx, |project, cx| {
488 project.disk_based_diagnostics_finished(server_id_1, cx);
489 });
490
491 // Only the first language server's diagnostics are shown.
492 cx.executor().run_until_parked();
493 assert_eq!(
494 editor_blocks(&editor, cx),
495 [
496 (DisplayRow(0), FILE_HEADER.into()),
497 (DisplayRow(2), DIAGNOSTIC_HEADER.into()),
498 ]
499 );
500 assert_eq!(
501 editor.update(cx, |editor, cx| editor.display_text(cx)),
502 concat!(
503 "\n", // filename
504 "\n", // padding
505 // diagnostic group 1
506 "\n", // primary message
507 "\n", // padding
508 "a();\n", //
509 "b();",
510 )
511 );
512
513 // The second language server finishes
514 project.update(cx, |project, cx| {
515 project
516 .update_diagnostic_entries(
517 server_id_2,
518 PathBuf::from("/test/main.js"),
519 None,
520 vec![DiagnosticEntry {
521 range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
522 diagnostic: Diagnostic {
523 message: "warning 1".to_string(),
524 severity: DiagnosticSeverity::ERROR,
525 is_primary: true,
526 is_disk_based: true,
527 group_id: 2,
528 ..Default::default()
529 },
530 }],
531 cx,
532 )
533 .unwrap();
534 project.disk_based_diagnostics_finished(server_id_2, cx);
535 });
536
537 // Both language server's diagnostics are shown.
538 cx.executor().run_until_parked();
539 assert_eq!(
540 editor_blocks(&editor, cx),
541 [
542 (DisplayRow(0), FILE_HEADER.into()),
543 (DisplayRow(2), DIAGNOSTIC_HEADER.into()),
544 (DisplayRow(6), EXCERPT_HEADER.into()),
545 (DisplayRow(7), DIAGNOSTIC_HEADER.into()),
546 ]
547 );
548 assert_eq!(
549 editor.update(cx, |editor, cx| editor.display_text(cx)),
550 concat!(
551 "\n", // filename
552 "\n", // padding
553 // diagnostic group 1
554 "\n", // primary message
555 "\n", // padding
556 "a();\n", // location
557 "b();\n", //
558 "\n", // collapsed context
559 // diagnostic group 2
560 "\n", // primary message
561 "\n", // padding
562 "a();\n", // context
563 "b();\n", //
564 "c();", // context
565 )
566 );
567
568 // Both language servers start updating diagnostics, and the first server finishes.
569 project.update(cx, |project, cx| {
570 project.disk_based_diagnostics_started(server_id_1, cx);
571 project.disk_based_diagnostics_started(server_id_2, cx);
572 project
573 .update_diagnostic_entries(
574 server_id_1,
575 PathBuf::from("/test/main.js"),
576 None,
577 vec![DiagnosticEntry {
578 range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
579 diagnostic: Diagnostic {
580 message: "warning 2".to_string(),
581 severity: DiagnosticSeverity::WARNING,
582 is_primary: true,
583 is_disk_based: true,
584 group_id: 1,
585 ..Default::default()
586 },
587 }],
588 cx,
589 )
590 .unwrap();
591 project
592 .update_diagnostic_entries(
593 server_id_2,
594 PathBuf::from("/test/main.rs"),
595 None,
596 vec![],
597 cx,
598 )
599 .unwrap();
600 project.disk_based_diagnostics_finished(server_id_1, cx);
601 });
602
603 // Only the first language server's diagnostics are updated.
604 cx.executor().run_until_parked();
605 assert_eq!(
606 editor_blocks(&editor, cx),
607 [
608 (DisplayRow(0), FILE_HEADER.into()),
609 (DisplayRow(2), DIAGNOSTIC_HEADER.into()),
610 (DisplayRow(7), EXCERPT_HEADER.into()),
611 (DisplayRow(8), DIAGNOSTIC_HEADER.into()),
612 ]
613 );
614 assert_eq!(
615 editor.update(cx, |editor, cx| editor.display_text(cx)),
616 concat!(
617 "\n", // filename
618 "\n", // padding
619 // diagnostic group 1
620 "\n", // primary message
621 "\n", // padding
622 "a();\n", // location
623 "b();\n", //
624 "c();\n", // context
625 "\n", // collapsed context
626 // diagnostic group 2
627 "\n", // primary message
628 "\n", // padding
629 "b();\n", // context
630 "c();\n", //
631 "d();", // context
632 )
633 );
634
635 // The second language server finishes.
636 project.update(cx, |project, cx| {
637 project
638 .update_diagnostic_entries(
639 server_id_2,
640 PathBuf::from("/test/main.js"),
641 None,
642 vec![DiagnosticEntry {
643 range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
644 diagnostic: Diagnostic {
645 message: "warning 2".to_string(),
646 severity: DiagnosticSeverity::WARNING,
647 is_primary: true,
648 is_disk_based: true,
649 group_id: 1,
650 ..Default::default()
651 },
652 }],
653 cx,
654 )
655 .unwrap();
656 project.disk_based_diagnostics_finished(server_id_2, cx);
657 });
658
659 // Both language servers' diagnostics are updated.
660 cx.executor().run_until_parked();
661 assert_eq!(
662 editor_blocks(&editor, cx),
663 [
664 (DisplayRow(0), FILE_HEADER.into()),
665 (DisplayRow(2), DIAGNOSTIC_HEADER.into()),
666 (DisplayRow(7), EXCERPT_HEADER.into()),
667 (DisplayRow(8), DIAGNOSTIC_HEADER.into()),
668 ]
669 );
670 assert_eq!(
671 editor.update(cx, |editor, cx| editor.display_text(cx)),
672 concat!(
673 "\n", // filename
674 "\n", // padding
675 // diagnostic group 1
676 "\n", // primary message
677 "\n", // padding
678 "b();\n", // location
679 "c();\n", //
680 "d();\n", // context
681 "\n", // collapsed context
682 // diagnostic group 2
683 "\n", // primary message
684 "\n", // padding
685 "c();\n", // context
686 "d();\n", //
687 "e();", // context
688 )
689 );
690}
691
692#[gpui::test(iterations = 20)]
693async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
694 init_test(cx);
695
696 let operations = env::var("OPERATIONS")
697 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
698 .unwrap_or(10);
699
700 let fs = FakeFs::new(cx.executor());
701 fs.insert_tree("/test", json!({})).await;
702
703 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
704 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
705 let cx = &mut VisualTestContext::from_window(*window, cx);
706 let workspace = window.root(cx).unwrap();
707
708 let mutated_view = window.build_view(cx, |cx| {
709 ProjectDiagnosticsEditor::new_with_context(1, project.clone(), workspace.downgrade(), cx)
710 });
711
712 workspace.update(cx, |workspace, cx| {
713 workspace.add_item_to_center(Box::new(mutated_view.clone()), cx);
714 });
715 mutated_view.update(cx, |view, cx| {
716 assert!(view.focus_handle.is_focused(cx));
717 });
718
719 let mut next_group_id = 0;
720 let mut next_filename = 0;
721 let mut language_server_ids = vec![LanguageServerId(0)];
722 let mut updated_language_servers = HashSet::default();
723 let mut current_diagnostics: HashMap<
724 (PathBuf, LanguageServerId),
725 Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
726 > = Default::default();
727
728 for _ in 0..operations {
729 match rng.gen_range(0..100) {
730 // language server completes its diagnostic check
731 0..=20 if !updated_language_servers.is_empty() => {
732 let server_id = *updated_language_servers.iter().choose(&mut rng).unwrap();
733 log::info!("finishing diagnostic check for language server {server_id}");
734 project.update(cx, |project, cx| {
735 project.disk_based_diagnostics_finished(server_id, cx)
736 });
737
738 if rng.gen_bool(0.5) {
739 cx.run_until_parked();
740 }
741 }
742
743 // language server updates diagnostics
744 _ => {
745 let (path, server_id, diagnostics) =
746 match current_diagnostics.iter_mut().choose(&mut rng) {
747 // update existing set of diagnostics
748 Some(((path, server_id), diagnostics)) if rng.gen_bool(0.5) => {
749 (path.clone(), *server_id, diagnostics)
750 }
751
752 // insert a set of diagnostics for a new path
753 _ => {
754 let path: PathBuf =
755 format!("/test/{}.rs", post_inc(&mut next_filename)).into();
756 let len = rng.gen_range(128..256);
757 let content =
758 RandomCharIter::new(&mut rng).take(len).collect::<String>();
759 fs.insert_file(&path, content.into_bytes()).await;
760
761 let server_id = match language_server_ids.iter().choose(&mut rng) {
762 Some(server_id) if rng.gen_bool(0.5) => *server_id,
763 _ => {
764 let id = LanguageServerId(language_server_ids.len());
765 language_server_ids.push(id);
766 id
767 }
768 };
769
770 (
771 path.clone(),
772 server_id,
773 current_diagnostics
774 .entry((path, server_id))
775 .or_insert(vec![]),
776 )
777 }
778 };
779
780 updated_language_servers.insert(server_id);
781
782 project.update(cx, |project, cx| {
783 log::info!("updating diagnostics. language server {server_id} path {path:?}");
784 randomly_update_diagnostics_for_path(
785 &fs,
786 &path,
787 diagnostics,
788 &mut next_group_id,
789 &mut rng,
790 );
791 project
792 .update_diagnostic_entries(server_id, path, None, diagnostics.clone(), cx)
793 .unwrap()
794 });
795
796 cx.run_until_parked();
797 }
798 }
799 }
800
801 log::info!("updating mutated diagnostics view");
802 mutated_view.update(cx, |view, _| view.enqueue_update_stale_excerpts(None));
803 cx.run_until_parked();
804
805 log::info!("constructing reference diagnostics view");
806 let reference_view = window.build_view(cx, |cx| {
807 ProjectDiagnosticsEditor::new_with_context(1, project.clone(), workspace.downgrade(), cx)
808 });
809 cx.run_until_parked();
810
811 let mutated_excerpts = get_diagnostics_excerpts(&mutated_view, cx);
812 let reference_excerpts = get_diagnostics_excerpts(&reference_view, cx);
813 assert_eq!(mutated_excerpts, reference_excerpts);
814}
815
816fn init_test(cx: &mut TestAppContext) {
817 cx.update(|cx| {
818 let settings = SettingsStore::test(cx);
819 cx.set_global(settings);
820 theme::init(theme::LoadThemes::JustBase, cx);
821 language::init(cx);
822 client::init_settings(cx);
823 workspace::init_settings(cx);
824 Project::init_settings(cx);
825 crate::init(cx);
826 editor::init(cx);
827 });
828}
829
830#[derive(Debug, PartialEq, Eq)]
831struct ExcerptInfo {
832 path: PathBuf,
833 range: ExcerptRange<Point>,
834 group_id: usize,
835 primary: bool,
836 language_server: LanguageServerId,
837}
838
839fn get_diagnostics_excerpts(
840 view: &View<ProjectDiagnosticsEditor>,
841 cx: &mut VisualTestContext,
842) -> Vec<ExcerptInfo> {
843 view.update(cx, |view, cx| {
844 let mut result = vec![];
845 let mut excerpt_indices_by_id = HashMap::default();
846 view.excerpts.update(cx, |multibuffer, cx| {
847 let snapshot = multibuffer.snapshot(cx);
848 for (id, buffer, range) in snapshot.excerpts() {
849 excerpt_indices_by_id.insert(id, result.len());
850 result.push(ExcerptInfo {
851 path: buffer.file().unwrap().path().to_path_buf(),
852 range: ExcerptRange {
853 context: range.context.to_point(&buffer),
854 primary: range.primary.map(|range| range.to_point(&buffer)),
855 },
856 group_id: usize::MAX,
857 primary: false,
858 language_server: LanguageServerId(0),
859 });
860 }
861 });
862
863 for state in &view.path_states {
864 for group in &state.diagnostic_groups {
865 for (ix, excerpt_id) in group.excerpts.iter().enumerate() {
866 let excerpt_ix = excerpt_indices_by_id[excerpt_id];
867 let excerpt = &mut result[excerpt_ix];
868 excerpt.group_id = group.primary_diagnostic.diagnostic.group_id;
869 excerpt.language_server = group.language_server_id;
870 excerpt.primary = ix == group.primary_excerpt_ix;
871 }
872 }
873 }
874
875 result
876 })
877}
878
879fn randomly_update_diagnostics_for_path(
880 fs: &FakeFs,
881 path: &Path,
882 diagnostics: &mut Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
883 next_group_id: &mut usize,
884 rng: &mut impl Rng,
885) {
886 let file_content = fs.read_file_sync(path).unwrap();
887 let file_text = Rope::from(String::from_utf8_lossy(&file_content).as_ref());
888
889 let mut group_ids = diagnostics
890 .iter()
891 .map(|d| d.diagnostic.group_id)
892 .collect::<HashSet<_>>();
893
894 let mutation_count = rng.gen_range(1..=3);
895 for _ in 0..mutation_count {
896 if rng.gen_bool(0.5) && !group_ids.is_empty() {
897 let group_id = *group_ids.iter().choose(rng).unwrap();
898 log::info!(" removing diagnostic group {group_id}");
899 diagnostics.retain(|d| d.diagnostic.group_id != group_id);
900 group_ids.remove(&group_id);
901 } else {
902 let group_id = *next_group_id;
903 *next_group_id += 1;
904
905 let mut new_diagnostics = vec![random_diagnostic(rng, &file_text, group_id, true)];
906 for _ in 0..rng.gen_range(0..=1) {
907 new_diagnostics.push(random_diagnostic(rng, &file_text, group_id, false));
908 }
909
910 let ix = rng.gen_range(0..=diagnostics.len());
911 log::info!(
912 " inserting diagnostic group {group_id} at index {ix}. ranges: {:?}",
913 new_diagnostics
914 .iter()
915 .map(|d| (d.range.start.0, d.range.end.0))
916 .collect::<Vec<_>>()
917 );
918 diagnostics.splice(ix..ix, new_diagnostics);
919 }
920 }
921}
922
923fn random_diagnostic(
924 rng: &mut impl Rng,
925 file_text: &Rope,
926 group_id: usize,
927 is_primary: bool,
928) -> DiagnosticEntry<Unclipped<PointUtf16>> {
929 // Intentionally allow erroneous ranges some of the time (that run off the end of the file),
930 // because language servers can potentially give us those, and we should handle them gracefully.
931 const ERROR_MARGIN: usize = 10;
932
933 let start = rng.gen_range(0..file_text.len().saturating_add(ERROR_MARGIN));
934 let end = rng.gen_range(start..file_text.len().saturating_add(ERROR_MARGIN));
935 let range = Range {
936 start: Unclipped(file_text.offset_to_point_utf16(start)),
937 end: Unclipped(file_text.offset_to_point_utf16(end)),
938 };
939 let severity = if rng.gen_bool(0.5) {
940 DiagnosticSeverity::WARNING
941 } else {
942 DiagnosticSeverity::ERROR
943 };
944 let message = format!("diagnostic group {group_id}");
945
946 DiagnosticEntry {
947 range,
948 diagnostic: Diagnostic {
949 source: None, // (optional) service that created the diagnostic
950 code: None, // (optional) machine-readable code that identifies the diagnostic
951 severity,
952 message,
953 group_id,
954 is_primary,
955 is_disk_based: false,
956 is_unnecessary: false,
957 data: None,
958 },
959 }
960}
961
962const FILE_HEADER: &'static str = "file header";
963const EXCERPT_HEADER: &'static str = "excerpt header";
964const EXCERPT_FOOTER: &'static str = "excerpt footer";
965
966fn editor_blocks(
967 editor: &View<Editor>,
968 cx: &mut VisualTestContext,
969) -> Vec<(DisplayRow, SharedString)> {
970 let mut blocks = Vec::new();
971 cx.draw(gpui::Point::default(), AvailableSpace::min_size(), |cx| {
972 editor.update(cx, |editor, cx| {
973 let snapshot = editor.snapshot(cx);
974 blocks.extend(
975 snapshot
976 .blocks_in_range(DisplayRow(0)..snapshot.max_point().row())
977 .filter_map(|(row, block)| {
978 let block_id = block.id();
979 let name: SharedString = match block {
980 Block::Custom(block) => {
981 let mut element = block.render(&mut BlockContext {
982 context: cx,
983 anchor_x: px(0.),
984 gutter_dimensions: &GutterDimensions::default(),
985 line_height: px(0.),
986 em_width: px(0.),
987 max_width: px(0.),
988 block_id,
989 editor_style: &editor::EditorStyle::default(),
990 });
991 let element = element.downcast_mut::<Stateful<Div>>().unwrap();
992 element
993 .interactivity()
994 .element_id
995 .clone()?
996 .try_into()
997 .ok()?
998 }
999
1000 Block::ExcerptHeader {
1001 starts_new_buffer, ..
1002 } => {
1003 if *starts_new_buffer {
1004 FILE_HEADER.into()
1005 } else {
1006 EXCERPT_HEADER.into()
1007 }
1008 }
1009 Block::ExcerptFooter { .. } => EXCERPT_FOOTER.into(),
1010 };
1011
1012 Some((row, name))
1013 }),
1014 )
1015 });
1016
1017 div().into_any()
1018 });
1019 blocks
1020}