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