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