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