1use super::*;
2use collections::HashMap;
3use editor::{
4 display_map::{BlockContext, TransformBlock},
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 (0, "path header block".into()),
162 (2, "diagnostic header".into()),
163 (15, "collapsed context".into()),
164 (16, "diagnostic header".into()),
165 (25, "collapsed context".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(12, 6)..DisplayPoint::new(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 (0, "path header block".into()),
247 (2, "diagnostic header".into()),
248 (7, "path header block".into()),
249 (9, "diagnostic header".into()),
250 (22, "collapsed context".into()),
251 (23, "diagnostic header".into()),
252 (32, "collapsed context".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(19, 6)..DisplayPoint::new(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 (0, "path header block".into()),
359 (2, "diagnostic header".into()),
360 (7, "collapsed context".into()),
361 (8, "diagnostic header".into()),
362 (13, "path header block".into()),
363 (15, "diagnostic header".into()),
364 (28, "collapsed context".into()),
365 (29, "diagnostic header".into()),
366 (38, "collapsed context".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 (0, "path header block".into()),
497 (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 (0, "path header block".into()),
543 (2, "diagnostic header".into()),
544 (6, "collapsed context".into()),
545 (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 (0, "path header block".into()),
609 (2, "diagnostic header".into()),
610 (7, "collapsed context".into()),
611 (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 (0, "path header block".into()),
665 (2, "diagnostic header".into()),
666 (7, "collapsed context".into()),
667 (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 },
958 }
959}
960
961fn editor_blocks(editor: &View<Editor>, cx: &mut VisualTestContext) -> Vec<(u32, SharedString)> {
962 let mut blocks = Vec::new();
963 cx.draw(gpui::Point::default(), AvailableSpace::min_size(), |cx| {
964 editor.update(cx, |editor, cx| {
965 let snapshot = editor.snapshot(cx);
966 blocks.extend(
967 snapshot
968 .blocks_in_range(0..snapshot.max_point().row())
969 .enumerate()
970 .filter_map(|(ix, (row, block))| {
971 let name: SharedString = match block {
972 TransformBlock::Custom(block) => {
973 let mut element = block.render(&mut BlockContext {
974 context: cx,
975 anchor_x: px(0.),
976 gutter_dimensions: &GutterDimensions::default(),
977 line_height: px(0.),
978 em_width: px(0.),
979 max_width: px(0.),
980 block_id: ix,
981 editor_style: &editor::EditorStyle::default(),
982 });
983 let element = element.downcast_mut::<Stateful<Div>>().unwrap();
984 element
985 .interactivity()
986 .element_id
987 .clone()?
988 .try_into()
989 .ok()?
990 }
991
992 TransformBlock::ExcerptHeader {
993 starts_new_buffer, ..
994 } => {
995 if *starts_new_buffer {
996 "path header block".into()
997 } else {
998 "collapsed context".into()
999 }
1000 }
1001 };
1002
1003 Some((row, name))
1004 }),
1005 )
1006 });
1007
1008 div().into_any()
1009 });
1010 blocks
1011}