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 lsp_store.update(cx, |lsp_store, 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 lsp_store
822 .update_diagnostic_entries(server_id, path, None, diagnostics.clone(), cx)
823 .unwrap()
824 });
825 cx.executor()
826 .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
827
828 cx.run_until_parked();
829 }
830 }
831 }
832
833 log::info!("updating mutated diagnostics view");
834 mutated_view.update(cx, |view, cx| view.update_stale_excerpts(cx));
835 cx.run_until_parked();
836
837 log::info!("constructing reference diagnostics view");
838 let reference_view = window.build_view(cx, |cx| {
839 ProjectDiagnosticsEditor::new_with_context(
840 1,
841 true,
842 project.clone(),
843 workspace.downgrade(),
844 cx,
845 )
846 });
847 cx.executor()
848 .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
849 cx.run_until_parked();
850
851 let mutated_excerpts = get_diagnostics_excerpts(&mutated_view, cx);
852 let reference_excerpts = get_diagnostics_excerpts(&reference_view, cx);
853
854 for ((path, language_server_id), diagnostics) in current_diagnostics {
855 for diagnostic in diagnostics {
856 let found_excerpt = reference_excerpts.iter().any(|info| {
857 let row_range = info.range.context.start.row..info.range.context.end.row;
858 info.path == path.strip_prefix("/test").unwrap()
859 && info.language_server == language_server_id
860 && row_range.contains(&diagnostic.range.start.0.row)
861 });
862 assert!(found_excerpt, "diagnostic not found in reference view");
863 }
864 }
865
866 assert_eq!(mutated_excerpts, reference_excerpts);
867}
868
869fn init_test(cx: &mut TestAppContext) {
870 cx.update(|cx| {
871 let settings = SettingsStore::test(cx);
872 cx.set_global(settings);
873 theme::init(theme::LoadThemes::JustBase, cx);
874 language::init(cx);
875 client::init_settings(cx);
876 workspace::init_settings(cx);
877 Project::init_settings(cx);
878 crate::init(cx);
879 editor::init(cx);
880 });
881}
882
883#[derive(Debug, PartialEq, Eq)]
884struct ExcerptInfo {
885 path: PathBuf,
886 range: ExcerptRange<Point>,
887 group_id: usize,
888 primary: bool,
889 language_server: LanguageServerId,
890}
891
892fn get_diagnostics_excerpts(
893 view: &View<ProjectDiagnosticsEditor>,
894 cx: &mut VisualTestContext,
895) -> Vec<ExcerptInfo> {
896 view.update(cx, |view, cx| {
897 let mut result = vec![];
898 let mut excerpt_indices_by_id = HashMap::default();
899 view.excerpts.update(cx, |multibuffer, cx| {
900 let snapshot = multibuffer.snapshot(cx);
901 for (id, buffer, range) in snapshot.excerpts() {
902 excerpt_indices_by_id.insert(id, result.len());
903 result.push(ExcerptInfo {
904 path: buffer.file().unwrap().path().to_path_buf(),
905 range: ExcerptRange {
906 context: range.context.to_point(buffer),
907 primary: range.primary.map(|range| range.to_point(buffer)),
908 },
909 group_id: usize::MAX,
910 primary: false,
911 language_server: LanguageServerId(0),
912 });
913 }
914 });
915
916 for state in &view.path_states {
917 for group in &state.diagnostic_groups {
918 for (ix, excerpt_id) in group.excerpts.iter().enumerate() {
919 let excerpt_ix = excerpt_indices_by_id[excerpt_id];
920 let excerpt = &mut result[excerpt_ix];
921 excerpt.group_id = group.primary_diagnostic.diagnostic.group_id;
922 excerpt.language_server = group.language_server_id;
923 excerpt.primary = ix == group.primary_excerpt_ix;
924 }
925 }
926 }
927
928 result
929 })
930}
931
932fn randomly_update_diagnostics_for_path(
933 fs: &FakeFs,
934 path: &Path,
935 diagnostics: &mut Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
936 next_group_id: &mut usize,
937 rng: &mut impl Rng,
938) {
939 let file_content = fs.read_file_sync(path).unwrap();
940 let file_text = Rope::from(String::from_utf8_lossy(&file_content).as_ref());
941
942 let mut group_ids = diagnostics
943 .iter()
944 .map(|d| d.diagnostic.group_id)
945 .collect::<HashSet<_>>();
946
947 let mutation_count = rng.gen_range(1..=3);
948 for _ in 0..mutation_count {
949 if rng.gen_bool(0.5) && !group_ids.is_empty() {
950 let group_id = *group_ids.iter().choose(rng).unwrap();
951 log::info!(" removing diagnostic group {group_id}");
952 diagnostics.retain(|d| d.diagnostic.group_id != group_id);
953 group_ids.remove(&group_id);
954 } else {
955 let group_id = *next_group_id;
956 *next_group_id += 1;
957
958 let mut new_diagnostics = vec![random_diagnostic(rng, &file_text, group_id, true)];
959 for _ in 0..rng.gen_range(0..=1) {
960 new_diagnostics.push(random_diagnostic(rng, &file_text, group_id, false));
961 }
962
963 let ix = rng.gen_range(0..=diagnostics.len());
964 log::info!(
965 " inserting diagnostic group {group_id} at index {ix}. ranges: {:?}",
966 new_diagnostics
967 .iter()
968 .map(|d| (d.range.start.0, d.range.end.0))
969 .collect::<Vec<_>>()
970 );
971 diagnostics.splice(ix..ix, new_diagnostics);
972 }
973 }
974}
975
976fn random_diagnostic(
977 rng: &mut impl Rng,
978 file_text: &Rope,
979 group_id: usize,
980 is_primary: bool,
981) -> DiagnosticEntry<Unclipped<PointUtf16>> {
982 // Intentionally allow erroneous ranges some of the time (that run off the end of the file),
983 // because language servers can potentially give us those, and we should handle them gracefully.
984 const ERROR_MARGIN: usize = 10;
985
986 let start = rng.gen_range(0..file_text.len().saturating_add(ERROR_MARGIN));
987 let end = rng.gen_range(start..file_text.len().saturating_add(ERROR_MARGIN));
988 let range = Range {
989 start: Unclipped(file_text.offset_to_point_utf16(start)),
990 end: Unclipped(file_text.offset_to_point_utf16(end)),
991 };
992 let severity = if rng.gen_bool(0.5) {
993 DiagnosticSeverity::WARNING
994 } else {
995 DiagnosticSeverity::ERROR
996 };
997 let message = format!("diagnostic group {group_id}");
998
999 DiagnosticEntry {
1000 range,
1001 diagnostic: Diagnostic {
1002 source: None, // (optional) service that created the diagnostic
1003 code: None, // (optional) machine-readable code that identifies the diagnostic
1004 severity,
1005 message,
1006 group_id,
1007 is_primary,
1008 is_disk_based: false,
1009 is_unnecessary: false,
1010 data: None,
1011 },
1012 }
1013}
1014
1015const FILE_HEADER: &str = "file header";
1016const EXCERPT_HEADER: &str = "excerpt header";
1017
1018fn editor_blocks(
1019 editor: &View<Editor>,
1020 cx: &mut VisualTestContext,
1021) -> Vec<(DisplayRow, SharedString)> {
1022 let mut blocks = Vec::new();
1023 cx.draw(gpui::Point::default(), AvailableSpace::min_size(), |cx| {
1024 editor.update(cx, |editor, cx| {
1025 let snapshot = editor.snapshot(cx);
1026 blocks.extend(
1027 snapshot
1028 .blocks_in_range(DisplayRow(0)..snapshot.max_point().row())
1029 .filter_map(|(row, block)| {
1030 let block_id = block.id();
1031 let name: SharedString = match block {
1032 Block::Custom(block) => {
1033 let mut element = block.render(&mut BlockContext {
1034 context: cx,
1035 anchor_x: px(0.),
1036 gutter_dimensions: &GutterDimensions::default(),
1037 line_height: px(0.),
1038 em_width: px(0.),
1039 max_width: px(0.),
1040 block_id,
1041 selected: false,
1042 editor_style: &editor::EditorStyle::default(),
1043 });
1044 let element = element.downcast_mut::<Stateful<Div>>().unwrap();
1045 element
1046 .interactivity()
1047 .element_id
1048 .clone()?
1049 .try_into()
1050 .ok()?
1051 }
1052
1053 Block::ExcerptBoundary {
1054 starts_new_buffer, ..
1055 } => {
1056 if *starts_new_buffer {
1057 FILE_HEADER.into()
1058 } else {
1059 EXCERPT_HEADER.into()
1060 }
1061 }
1062 };
1063
1064 Some((row, name))
1065 }),
1066 )
1067 });
1068
1069 div().into_any()
1070 });
1071 blocks
1072}