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