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