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