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