1use super::*;
2use collections::{HashMap, HashSet};
3use editor::{
4 DisplayPoint, EditorSettings, Inlay,
5 actions::{GoToDiagnostic, GoToPreviousDiagnostic, Hover, MoveToBeginning},
6 display_map::DisplayRow,
7 test::{
8 editor_content_with_blocks, editor_lsp_test_context::EditorLspTestContext,
9 editor_test_context::EditorTestContext,
10 },
11};
12use gpui::{TestAppContext, VisualTestContext};
13use indoc::indoc;
14use language::{DiagnosticSourceKind, Rope};
15use lsp::LanguageServerId;
16use pretty_assertions::assert_eq;
17use project::{
18 FakeFs,
19 project_settings::{GoToDiagnosticSeverity, GoToDiagnosticSeverityFilter},
20};
21use rand::{Rng, rngs::StdRng, seq::IteratorRandom as _};
22use serde_json::json;
23use settings::SettingsStore;
24use std::{
25 env,
26 path::{Path, PathBuf},
27 str::FromStr,
28};
29use unindent::Unindent as _;
30use util::{RandomCharIter, path, post_inc, rel_path::rel_path};
31
32#[ctor::ctor]
33fn init_logger() {
34 zlog::init_test();
35}
36
37#[gpui::test]
38async fn test_diagnostics(cx: &mut TestAppContext) {
39 init_test(cx);
40
41 let fs = FakeFs::new(cx.executor());
42 fs.insert_tree(
43 path!("/test"),
44 json!({
45 "consts.rs": "
46 const a: i32 = 'a';
47 const b: i32 = c;
48 "
49 .unindent(),
50
51 "main.rs": "
52 fn main() {
53 let x = vec![];
54 let y = vec![];
55 a(x);
56 b(y);
57 // comment 1
58 // comment 2
59 c(y);
60 d(x);
61 }
62 "
63 .unindent(),
64 }),
65 )
66 .await;
67
68 let language_server_id = LanguageServerId(0);
69 let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
70 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
71 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
72 let cx = &mut VisualTestContext::from_window(*window, cx);
73 let workspace = window.root(cx).unwrap();
74 let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap();
75
76 // Create some diagnostics
77 lsp_store.update(cx, |lsp_store, cx| {
78 lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
79 uri: uri.clone(),
80 diagnostics: vec![lsp::Diagnostic{
81 range: lsp::Range::new(lsp::Position::new(7, 6),lsp::Position::new(7, 7)),
82 severity:Some(lsp::DiagnosticSeverity::ERROR),
83 message: "use of moved value\nvalue used here after move".to_string(),
84 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
85 location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(2,8),lsp::Position::new(2,9))),
86 message: "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
87 },
88 lsp::DiagnosticRelatedInformation {
89 location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(4,6),lsp::Position::new(4,7))),
90 message: "value moved here".to_string()
91 },
92 ]),
93 ..Default::default()
94 },
95 lsp::Diagnostic{
96 range: lsp::Range::new(lsp::Position::new(8, 6),lsp::Position::new(8, 7)),
97 severity:Some(lsp::DiagnosticSeverity::ERROR),
98 message: "use of moved value\nvalue used here after move".to_string(),
99 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
100 location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(1,8),lsp::Position::new(1,9))),
101 message: "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
102 },
103 lsp::DiagnosticRelatedInformation {
104 location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(3,6),lsp::Position::new(3,7))),
105 message: "value moved here".to_string()
106 },
107 ]),
108 ..Default::default()
109 }
110 ],
111 version: None
112 }, None, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
113 });
114
115 // Open the project diagnostics view while there are already diagnostics.
116 let diagnostics = window.build_entity(cx, |window, cx| {
117 ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
118 });
119 let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
120
121 diagnostics
122 .next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
123 .await;
124
125 pretty_assertions::assert_eq!(
126 editor_content_with_blocks(&editor, cx),
127 indoc::indoc! {
128 "§ main.rs
129 § -----
130 fn main() {
131 let x = vec![];
132 § move occurs because `x` has type `Vec<char>`, which does not implement
133 § the `Copy` trait (back)
134 let y = vec![];
135 § move occurs because `y` has type `Vec<char>`, which does not implement
136 § the `Copy` trait (back)
137 a(x); § value moved here (back)
138 b(y); § value moved here
139 // comment 1
140 // comment 2
141 c(y);
142 § use of moved value
143 § value used here after move
144 § hint: move occurs because `y` has type `Vec<char>`, which does not
145 § implement the `Copy` trait
146 d(x);
147 § use of moved value
148 § value used here after move
149 § hint: move occurs because `x` has type `Vec<char>`, which does not
150 § implement the `Copy` trait
151 § hint: value moved here
152 }"
153 }
154 );
155
156 // Cursor is at the first diagnostic
157 editor.update(cx, |editor, cx| {
158 assert_eq!(
159 editor.selections.display_ranges(cx),
160 [DisplayPoint::new(DisplayRow(3), 8)..DisplayPoint::new(DisplayRow(3), 8)]
161 );
162 });
163
164 // Diagnostics are added for another earlier path.
165 lsp_store.update(cx, |lsp_store, cx| {
166 lsp_store.disk_based_diagnostics_started(language_server_id, cx);
167 lsp_store
168 .update_diagnostics(
169 language_server_id,
170 lsp::PublishDiagnosticsParams {
171 uri: lsp::Uri::from_file_path(path!("/test/consts.rs")).unwrap(),
172 diagnostics: vec![lsp::Diagnostic {
173 range: lsp::Range::new(
174 lsp::Position::new(0, 15),
175 lsp::Position::new(0, 15),
176 ),
177 severity: Some(lsp::DiagnosticSeverity::ERROR),
178 message: "mismatched types expected `usize`, found `char`".to_string(),
179 ..Default::default()
180 }],
181 version: None,
182 },
183 None,
184 DiagnosticSourceKind::Pushed,
185 &[],
186 cx,
187 )
188 .unwrap();
189 lsp_store.disk_based_diagnostics_finished(language_server_id, cx);
190 });
191
192 diagnostics
193 .next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
194 .await;
195
196 pretty_assertions::assert_eq!(
197 editor_content_with_blocks(&editor, cx),
198 indoc::indoc! {
199 "§ consts.rs
200 § -----
201 const a: i32 = 'a'; § mismatched types expected `usize`, found `char`
202 const b: i32 = c;
203
204 § main.rs
205 § -----
206 fn main() {
207 let x = vec![];
208 § move occurs because `x` has type `Vec<char>`, which does not implement
209 § the `Copy` trait (back)
210 let y = vec![];
211 § move occurs because `y` has type `Vec<char>`, which does not implement
212 § the `Copy` trait (back)
213 a(x); § value moved here (back)
214 b(y); § value moved here
215 // comment 1
216 // comment 2
217 c(y);
218 § use of moved value
219 § value used here after move
220 § hint: move occurs because `y` has type `Vec<char>`, which does not
221 § implement the `Copy` trait
222 d(x);
223 § use of moved value
224 § value used here after move
225 § hint: move occurs because `x` has type `Vec<char>`, which does not
226 § implement the `Copy` trait
227 § hint: value moved here
228 }"
229 }
230 );
231
232 // Cursor keeps its position.
233 editor.update(cx, |editor, cx| {
234 assert_eq!(
235 editor.selections.display_ranges(cx),
236 [DisplayPoint::new(DisplayRow(8), 8)..DisplayPoint::new(DisplayRow(8), 8)]
237 );
238 });
239
240 // Diagnostics are added to the first path
241 lsp_store.update(cx, |lsp_store, cx| {
242 lsp_store.disk_based_diagnostics_started(language_server_id, cx);
243 lsp_store
244 .update_diagnostics(
245 language_server_id,
246 lsp::PublishDiagnosticsParams {
247 uri: lsp::Uri::from_file_path(path!("/test/consts.rs")).unwrap(),
248 diagnostics: vec![
249 lsp::Diagnostic {
250 range: lsp::Range::new(
251 lsp::Position::new(0, 15),
252 lsp::Position::new(0, 15),
253 ),
254 severity: Some(lsp::DiagnosticSeverity::ERROR),
255 message: "mismatched types expected `usize`, found `char`".to_string(),
256 ..Default::default()
257 },
258 lsp::Diagnostic {
259 range: lsp::Range::new(
260 lsp::Position::new(1, 15),
261 lsp::Position::new(1, 15),
262 ),
263 severity: Some(lsp::DiagnosticSeverity::ERROR),
264 message: "unresolved name `c`".to_string(),
265 ..Default::default()
266 },
267 ],
268 version: None,
269 },
270 None,
271 DiagnosticSourceKind::Pushed,
272 &[],
273 cx,
274 )
275 .unwrap();
276 lsp_store.disk_based_diagnostics_finished(language_server_id, cx);
277 });
278
279 diagnostics
280 .next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
281 .await;
282
283 pretty_assertions::assert_eq!(
284 editor_content_with_blocks(&editor, cx),
285 indoc::indoc! {
286 "§ consts.rs
287 § -----
288 const a: i32 = 'a'; § mismatched types expected `usize`, found `char`
289 const b: i32 = c; § unresolved name `c`
290
291 § main.rs
292 § -----
293 fn main() {
294 let x = vec![];
295 § move occurs because `x` has type `Vec<char>`, which does not implement
296 § the `Copy` trait (back)
297 let y = vec![];
298 § move occurs because `y` has type `Vec<char>`, which does not implement
299 § the `Copy` trait (back)
300 a(x); § value moved here (back)
301 b(y); § value moved here
302 // comment 1
303 // comment 2
304 c(y);
305 § use of moved value
306 § value used here after move
307 § hint: move occurs because `y` has type `Vec<char>`, which does not
308 § implement the `Copy` trait
309 d(x);
310 § use of moved value
311 § value used here after move
312 § hint: move occurs because `x` has type `Vec<char>`, which does not
313 § implement the `Copy` trait
314 § hint: value moved here
315 }"
316 }
317 );
318}
319
320#[gpui::test]
321async fn test_diagnostics_with_folds(cx: &mut TestAppContext) {
322 init_test(cx);
323
324 let fs = FakeFs::new(cx.executor());
325 fs.insert_tree(
326 path!("/test"),
327 json!({
328 "main.js": "
329 function test() {
330 return 1
331 };
332
333 tset();
334 ".unindent()
335 }),
336 )
337 .await;
338
339 let server_id_1 = LanguageServerId(100);
340 let server_id_2 = LanguageServerId(101);
341 let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
342 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
343 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
344 let cx = &mut VisualTestContext::from_window(*window, cx);
345 let workspace = window.root(cx).unwrap();
346
347 let diagnostics = window.build_entity(cx, |window, cx| {
348 ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
349 });
350 let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
351
352 // Two language servers start updating diagnostics
353 lsp_store.update(cx, |lsp_store, cx| {
354 lsp_store.disk_based_diagnostics_started(server_id_1, cx);
355 lsp_store.disk_based_diagnostics_started(server_id_2, cx);
356 lsp_store
357 .update_diagnostics(
358 server_id_1,
359 lsp::PublishDiagnosticsParams {
360 uri: lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(),
361 diagnostics: vec![lsp::Diagnostic {
362 range: lsp::Range::new(lsp::Position::new(4, 0), lsp::Position::new(4, 4)),
363 severity: Some(lsp::DiagnosticSeverity::WARNING),
364 message: "no method `tset`".to_string(),
365 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
366 location: lsp::Location::new(
367 lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(),
368 lsp::Range::new(
369 lsp::Position::new(0, 9),
370 lsp::Position::new(0, 13),
371 ),
372 ),
373 message: "method `test` defined here".to_string(),
374 }]),
375 ..Default::default()
376 }],
377 version: None,
378 },
379 None,
380 DiagnosticSourceKind::Pushed,
381 &[],
382 cx,
383 )
384 .unwrap();
385 });
386
387 // The first language server finishes
388 lsp_store.update(cx, |lsp_store, cx| {
389 lsp_store.disk_based_diagnostics_finished(server_id_1, cx);
390 });
391
392 // Only the first language server's diagnostics are shown.
393 cx.executor()
394 .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
395 cx.executor().run_until_parked();
396 editor.update_in(cx, |editor, window, cx| {
397 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(3, 0)], false, window, cx);
398 });
399
400 pretty_assertions::assert_eq!(
401 editor_content_with_blocks(&editor, cx),
402 indoc::indoc! {
403 "§ main.js
404 § -----
405 ⋯
406
407 tset(); § no method `tset`"
408 }
409 );
410
411 editor.update(cx, |editor, cx| {
412 editor.unfold_ranges(&[Point::new(0, 0)..Point::new(3, 0)], false, false, cx);
413 });
414
415 pretty_assertions::assert_eq!(
416 editor_content_with_blocks(&editor, cx),
417 indoc::indoc! {
418 "§ main.js
419 § -----
420 function test() { § method `test` defined here
421 return 1
422 };
423
424 tset(); § no method `tset`"
425 }
426 );
427}
428
429#[gpui::test]
430async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
431 init_test(cx);
432
433 let fs = FakeFs::new(cx.executor());
434 fs.insert_tree(
435 path!("/test"),
436 json!({
437 "main.js": "
438 a();
439 b();
440 c();
441 d();
442 e();
443 ".unindent()
444 }),
445 )
446 .await;
447
448 let server_id_1 = LanguageServerId(100);
449 let server_id_2 = LanguageServerId(101);
450 let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
451 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
452 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
453 let cx = &mut VisualTestContext::from_window(*window, cx);
454 let workspace = window.root(cx).unwrap();
455
456 let diagnostics = window.build_entity(cx, |window, cx| {
457 ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
458 });
459 let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
460
461 // Two language servers start updating diagnostics
462 lsp_store.update(cx, |lsp_store, cx| {
463 lsp_store.disk_based_diagnostics_started(server_id_1, cx);
464 lsp_store.disk_based_diagnostics_started(server_id_2, cx);
465 lsp_store
466 .update_diagnostics(
467 server_id_1,
468 lsp::PublishDiagnosticsParams {
469 uri: lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(),
470 diagnostics: vec![lsp::Diagnostic {
471 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 1)),
472 severity: Some(lsp::DiagnosticSeverity::WARNING),
473 message: "error 1".to_string(),
474 ..Default::default()
475 }],
476 version: None,
477 },
478 None,
479 DiagnosticSourceKind::Pushed,
480 &[],
481 cx,
482 )
483 .unwrap();
484 });
485
486 // The first language server finishes
487 lsp_store.update(cx, |lsp_store, cx| {
488 lsp_store.disk_based_diagnostics_finished(server_id_1, cx);
489 });
490
491 // Only the first language server's diagnostics are shown.
492 cx.executor()
493 .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
494 cx.executor().run_until_parked();
495
496 pretty_assertions::assert_eq!(
497 editor_content_with_blocks(&editor, cx),
498 indoc::indoc! {
499 "§ main.js
500 § -----
501 a(); § error 1
502 b();
503 c();"
504 }
505 );
506
507 // The second language server finishes
508 lsp_store.update(cx, |lsp_store, cx| {
509 lsp_store
510 .update_diagnostics(
511 server_id_2,
512 lsp::PublishDiagnosticsParams {
513 uri: lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(),
514 diagnostics: vec![lsp::Diagnostic {
515 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 1)),
516 severity: Some(lsp::DiagnosticSeverity::ERROR),
517 message: "warning 1".to_string(),
518 ..Default::default()
519 }],
520 version: None,
521 },
522 None,
523 DiagnosticSourceKind::Pushed,
524 &[],
525 cx,
526 )
527 .unwrap();
528 lsp_store.disk_based_diagnostics_finished(server_id_2, cx);
529 });
530
531 // Both language server's diagnostics are shown.
532 cx.executor()
533 .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
534 cx.executor().run_until_parked();
535
536 pretty_assertions::assert_eq!(
537 editor_content_with_blocks(&editor, cx),
538 indoc::indoc! {
539 "§ main.js
540 § -----
541 a(); § error 1
542 b(); § warning 1
543 c();
544 d();"
545 }
546 );
547
548 // Both language servers start updating diagnostics, and the first server finishes.
549 lsp_store.update(cx, |lsp_store, cx| {
550 lsp_store.disk_based_diagnostics_started(server_id_1, cx);
551 lsp_store.disk_based_diagnostics_started(server_id_2, cx);
552 lsp_store
553 .update_diagnostics(
554 server_id_1,
555 lsp::PublishDiagnosticsParams {
556 uri: lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(),
557 diagnostics: vec![lsp::Diagnostic {
558 range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 1)),
559 severity: Some(lsp::DiagnosticSeverity::WARNING),
560 message: "warning 2".to_string(),
561 ..Default::default()
562 }],
563 version: None,
564 },
565 None,
566 DiagnosticSourceKind::Pushed,
567 &[],
568 cx,
569 )
570 .unwrap();
571 lsp_store
572 .update_diagnostics(
573 server_id_2,
574 lsp::PublishDiagnosticsParams {
575 uri: lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap(),
576 diagnostics: vec![],
577 version: None,
578 },
579 None,
580 DiagnosticSourceKind::Pushed,
581 &[],
582 cx,
583 )
584 .unwrap();
585 lsp_store.disk_based_diagnostics_finished(server_id_1, cx);
586 });
587
588 // Only the first language server's diagnostics are updated.
589 cx.executor()
590 .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
591 cx.executor().run_until_parked();
592
593 pretty_assertions::assert_eq!(
594 editor_content_with_blocks(&editor, cx),
595 indoc::indoc! {
596 "§ main.js
597 § -----
598 a();
599 b(); § warning 1
600 c(); § warning 2
601 d();
602 e();"
603 }
604 );
605
606 // The second language server finishes.
607 lsp_store.update(cx, |lsp_store, cx| {
608 lsp_store
609 .update_diagnostics(
610 server_id_2,
611 lsp::PublishDiagnosticsParams {
612 uri: lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(),
613 diagnostics: vec![lsp::Diagnostic {
614 range: lsp::Range::new(lsp::Position::new(3, 0), lsp::Position::new(3, 1)),
615 severity: Some(lsp::DiagnosticSeverity::WARNING),
616 message: "warning 2".to_string(),
617 ..Default::default()
618 }],
619 version: None,
620 },
621 None,
622 DiagnosticSourceKind::Pushed,
623 &[],
624 cx,
625 )
626 .unwrap();
627 lsp_store.disk_based_diagnostics_finished(server_id_2, cx);
628 });
629
630 // Both language servers' diagnostics are updated.
631 cx.executor()
632 .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
633 cx.executor().run_until_parked();
634
635 pretty_assertions::assert_eq!(
636 editor_content_with_blocks(&editor, cx),
637 indoc::indoc! {
638 "§ main.js
639 § -----
640 a();
641 b();
642 c(); § warning 2
643 d(); § warning 2
644 e();"
645 }
646 );
647}
648
649#[gpui::test(iterations = 20)]
650async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng) {
651 init_test(cx);
652
653 let operations = env::var("OPERATIONS")
654 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
655 .unwrap_or(10);
656
657 let fs = FakeFs::new(cx.executor());
658 fs.insert_tree(path!("/test"), json!({})).await;
659
660 let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
661 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
662 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
663 let cx = &mut VisualTestContext::from_window(*window, cx);
664 let workspace = window.root(cx).unwrap();
665
666 let mutated_diagnostics = window.build_entity(cx, |window, cx| {
667 ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
668 });
669
670 workspace.update_in(cx, |workspace, window, cx| {
671 workspace.add_item_to_center(Box::new(mutated_diagnostics.clone()), window, cx);
672 });
673 mutated_diagnostics.update_in(cx, |diagnostics, window, _cx| {
674 assert!(diagnostics.focus_handle.is_focused(window));
675 });
676
677 let mut next_id = 0;
678 let mut next_filename = 0;
679 let mut language_server_ids = vec![LanguageServerId(0)];
680 let mut updated_language_servers = HashSet::default();
681 let mut current_diagnostics: HashMap<(PathBuf, LanguageServerId), Vec<lsp::Diagnostic>> =
682 Default::default();
683
684 for _ in 0..operations {
685 match rng.random_range(0..100) {
686 // language server completes its diagnostic check
687 0..=20 if !updated_language_servers.is_empty() => {
688 let server_id = *updated_language_servers.iter().choose(&mut rng).unwrap();
689 log::info!("finishing diagnostic check for language server {server_id}");
690 lsp_store.update(cx, |lsp_store, cx| {
691 lsp_store.disk_based_diagnostics_finished(server_id, cx)
692 });
693
694 if rng.random_bool(0.5) {
695 cx.run_until_parked();
696 }
697 }
698
699 // language server updates diagnostics
700 _ => {
701 let (path, server_id, diagnostics) =
702 match current_diagnostics.iter_mut().choose(&mut rng) {
703 // update existing set of diagnostics
704 Some(((path, server_id), diagnostics)) if rng.random_bool(0.5) => {
705 (path.clone(), *server_id, diagnostics)
706 }
707
708 // insert a set of diagnostics for a new path
709 _ => {
710 let path: PathBuf =
711 format!(path!("/test/{}.rs"), post_inc(&mut next_filename)).into();
712 let len = rng.random_range(128..256);
713 let content =
714 RandomCharIter::new(&mut rng).take(len).collect::<String>();
715 fs.insert_file(&path, content.into_bytes()).await;
716
717 let server_id = match language_server_ids.iter().choose(&mut rng) {
718 Some(server_id) if rng.random_bool(0.5) => *server_id,
719 _ => {
720 let id = LanguageServerId(language_server_ids.len());
721 language_server_ids.push(id);
722 id
723 }
724 };
725
726 (
727 path.clone(),
728 server_id,
729 current_diagnostics.entry((path, server_id)).or_default(),
730 )
731 }
732 };
733
734 updated_language_servers.insert(server_id);
735
736 lsp_store.update(cx, |lsp_store, cx| {
737 log::info!("updating diagnostics. language server {server_id} path {path:?}");
738 randomly_update_diagnostics_for_path(
739 &fs,
740 &path,
741 diagnostics,
742 &mut next_id,
743 &mut rng,
744 );
745 lsp_store
746 .update_diagnostics(
747 server_id,
748 lsp::PublishDiagnosticsParams {
749 uri: lsp::Uri::from_file_path(&path).unwrap_or_else(|_| {
750 lsp::Uri::from_str("file:///test/fallback.rs").unwrap()
751 }),
752 diagnostics: diagnostics.clone(),
753 version: None,
754 },
755 None,
756 DiagnosticSourceKind::Pushed,
757 &[],
758 cx,
759 )
760 .unwrap()
761 });
762 cx.executor()
763 .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
764
765 cx.run_until_parked();
766 }
767 }
768 }
769
770 log::info!("updating mutated diagnostics view");
771 mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
772 diagnostics.update_stale_excerpts(RetainExcerpts::No, window, cx)
773 });
774
775 log::info!("constructing reference diagnostics view");
776 let reference_diagnostics = window.build_entity(cx, |window, cx| {
777 ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
778 });
779 cx.executor()
780 .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
781 cx.run_until_parked();
782
783 let mutated_excerpts =
784 editor_content_with_blocks(&mutated_diagnostics.update(cx, |d, _| d.editor.clone()), cx);
785 let reference_excerpts = editor_content_with_blocks(
786 &reference_diagnostics.update(cx, |d, _| d.editor.clone()),
787 cx,
788 );
789
790 // The mutated view may contain more than the reference view as
791 // we don't currently shrink excerpts when diagnostics were removed.
792 let mut ref_iter = reference_excerpts.lines().filter(|line| {
793 // ignore $ ---- and $ <file>.rs
794 !line.starts_with('§')
795 || line.starts_with("§ diagnostic")
796 || line.starts_with("§ related info")
797 });
798 let mut next_ref_line = ref_iter.next();
799 let mut skipped_block = false;
800
801 for mut_line in mutated_excerpts.lines() {
802 if let Some(ref_line) = next_ref_line {
803 if mut_line == ref_line {
804 next_ref_line = ref_iter.next();
805 } else if mut_line.contains('§')
806 // ignore $ ---- and $ <file>.rs
807 && (!mut_line.starts_with('§')
808 || mut_line.starts_with("§ diagnostic")
809 || mut_line.starts_with("§ related info"))
810 {
811 skipped_block = true;
812 }
813 }
814 }
815
816 if next_ref_line.is_some() || skipped_block {
817 pretty_assertions::assert_eq!(mutated_excerpts, reference_excerpts);
818 }
819}
820
821// similar to above, but with inlays. Used to find panics when mixing diagnostics and inlays.
822#[gpui::test]
823async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: StdRng) {
824 init_test(cx);
825
826 let operations = env::var("OPERATIONS")
827 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
828 .unwrap_or(10);
829
830 let fs = FakeFs::new(cx.executor());
831 fs.insert_tree(path!("/test"), json!({})).await;
832
833 let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
834 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
835 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
836 let cx = &mut VisualTestContext::from_window(*window, cx);
837 let workspace = window.root(cx).unwrap();
838
839 let mutated_diagnostics = window.build_entity(cx, |window, cx| {
840 ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
841 });
842
843 workspace.update_in(cx, |workspace, window, cx| {
844 workspace.add_item_to_center(Box::new(mutated_diagnostics.clone()), window, cx);
845 });
846 mutated_diagnostics.update_in(cx, |diagnostics, window, _cx| {
847 assert!(diagnostics.focus_handle.is_focused(window));
848 });
849
850 let mut next_id = 0;
851 let mut next_filename = 0;
852 let mut language_server_ids = vec![LanguageServerId(0)];
853 let mut updated_language_servers = HashSet::default();
854 let mut current_diagnostics: HashMap<(PathBuf, LanguageServerId), Vec<lsp::Diagnostic>> =
855 Default::default();
856 let mut next_inlay_id = 0;
857
858 for _ in 0..operations {
859 match rng.random_range(0..100) {
860 // language server completes its diagnostic check
861 0..=20 if !updated_language_servers.is_empty() => {
862 let server_id = *updated_language_servers.iter().choose(&mut rng).unwrap();
863 log::info!("finishing diagnostic check for language server {server_id}");
864 lsp_store.update(cx, |lsp_store, cx| {
865 lsp_store.disk_based_diagnostics_finished(server_id, cx)
866 });
867
868 if rng.random_bool(0.5) {
869 cx.run_until_parked();
870 }
871 }
872
873 21..=50 => mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
874 diagnostics.editor.update(cx, |editor, cx| {
875 let snapshot = editor.snapshot(window, cx);
876 if !snapshot.buffer_snapshot().is_empty() {
877 let position = rng.random_range(0..snapshot.buffer_snapshot().len());
878 let position = snapshot.buffer_snapshot().clip_offset(position, Bias::Left);
879 log::info!(
880 "adding inlay at {position}/{}: {:?}",
881 snapshot.buffer_snapshot().len(),
882 snapshot.buffer_snapshot().text(),
883 );
884
885 editor.splice_inlays(
886 &[],
887 vec![Inlay::edit_prediction(
888 post_inc(&mut next_inlay_id),
889 snapshot.buffer_snapshot().anchor_before(position),
890 Rope::from_iter_small(["Test inlay ", "next_inlay_id"]),
891 )],
892 cx,
893 );
894 }
895 });
896 }),
897
898 // language server updates diagnostics
899 _ => {
900 let (path, server_id, diagnostics) =
901 match current_diagnostics.iter_mut().choose(&mut rng) {
902 // update existing set of diagnostics
903 Some(((path, server_id), diagnostics)) if rng.random_bool(0.5) => {
904 (path.clone(), *server_id, diagnostics)
905 }
906
907 // insert a set of diagnostics for a new path
908 _ => {
909 let path: PathBuf =
910 format!(path!("/test/{}.rs"), post_inc(&mut next_filename)).into();
911 let len = rng.random_range(128..256);
912 let content =
913 RandomCharIter::new(&mut rng).take(len).collect::<String>();
914 fs.insert_file(&path, content.into_bytes()).await;
915
916 let server_id = match language_server_ids.iter().choose(&mut rng) {
917 Some(server_id) if rng.random_bool(0.5) => *server_id,
918 _ => {
919 let id = LanguageServerId(language_server_ids.len());
920 language_server_ids.push(id);
921 id
922 }
923 };
924
925 (
926 path.clone(),
927 server_id,
928 current_diagnostics.entry((path, server_id)).or_default(),
929 )
930 }
931 };
932
933 updated_language_servers.insert(server_id);
934
935 lsp_store.update(cx, |lsp_store, cx| {
936 log::info!("updating diagnostics. language server {server_id} path {path:?}");
937 randomly_update_diagnostics_for_path(
938 &fs,
939 &path,
940 diagnostics,
941 &mut next_id,
942 &mut rng,
943 );
944 lsp_store
945 .update_diagnostics(
946 server_id,
947 lsp::PublishDiagnosticsParams {
948 uri: lsp::Uri::from_file_path(&path).unwrap_or_else(|_| {
949 lsp::Uri::from_str("file:///test/fallback.rs").unwrap()
950 }),
951 diagnostics: diagnostics.clone(),
952 version: None,
953 },
954 None,
955 DiagnosticSourceKind::Pushed,
956 &[],
957 cx,
958 )
959 .unwrap()
960 });
961 cx.executor()
962 .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
963
964 cx.run_until_parked();
965 }
966 }
967 }
968
969 log::info!("updating mutated diagnostics view");
970 mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
971 diagnostics.update_stale_excerpts(RetainExcerpts::No, window, cx)
972 });
973
974 cx.executor()
975 .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
976 cx.run_until_parked();
977}
978
979#[gpui::test]
980async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext) {
981 init_test(cx);
982
983 let mut cx = EditorTestContext::new(cx).await;
984 let lsp_store =
985 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
986
987 cx.set_state(indoc! {"
988 ˇfn func(abc def: i32) -> u32 {
989 }
990 "});
991
992 let message = "Something's wrong!";
993 cx.update(|_, cx| {
994 lsp_store.update(cx, |lsp_store, cx| {
995 lsp_store
996 .update_diagnostics(
997 LanguageServerId(0),
998 lsp::PublishDiagnosticsParams {
999 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
1000 version: None,
1001 diagnostics: vec![lsp::Diagnostic {
1002 range: lsp::Range::new(
1003 lsp::Position::new(0, 11),
1004 lsp::Position::new(0, 12),
1005 ),
1006 severity: Some(lsp::DiagnosticSeverity::ERROR),
1007 message: message.to_string(),
1008 ..Default::default()
1009 }],
1010 },
1011 None,
1012 DiagnosticSourceKind::Pushed,
1013 &[],
1014 cx,
1015 )
1016 .unwrap()
1017 });
1018 });
1019 cx.run_until_parked();
1020
1021 cx.update_editor(|editor, window, cx| {
1022 editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
1023 assert_eq!(
1024 editor
1025 .active_diagnostic_group()
1026 .map(|diagnostics_group| diagnostics_group.active_message.as_str()),
1027 Some(message),
1028 "Should have a diagnostics group activated"
1029 );
1030 });
1031 cx.assert_editor_state(indoc! {"
1032 fn func(abcˇ def: i32) -> u32 {
1033 }
1034 "});
1035
1036 cx.update(|_, cx| {
1037 lsp_store.update(cx, |lsp_store, cx| {
1038 lsp_store
1039 .update_diagnostics(
1040 LanguageServerId(0),
1041 lsp::PublishDiagnosticsParams {
1042 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
1043 version: None,
1044 diagnostics: Vec::new(),
1045 },
1046 None,
1047 DiagnosticSourceKind::Pushed,
1048 &[],
1049 cx,
1050 )
1051 .unwrap()
1052 });
1053 });
1054 cx.run_until_parked();
1055 cx.update_editor(|editor, _, _| {
1056 assert_eq!(editor.active_diagnostic_group(), None);
1057 });
1058 cx.assert_editor_state(indoc! {"
1059 fn func(abcˇ def: i32) -> u32 {
1060 }
1061 "});
1062
1063 cx.update_editor(|editor, window, cx| {
1064 editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
1065 assert_eq!(editor.active_diagnostic_group(), None);
1066 });
1067 cx.assert_editor_state(indoc! {"
1068 fn func(abcˇ def: i32) -> u32 {
1069 }
1070 "});
1071}
1072
1073#[gpui::test]
1074async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
1075 init_test(cx);
1076
1077 let mut cx = EditorTestContext::new(cx).await;
1078 let lsp_store =
1079 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
1080
1081 cx.set_state(indoc! {"
1082 ˇfn func(abc def: i32) -> u32 {
1083 }
1084 "});
1085
1086 cx.update(|_, cx| {
1087 lsp_store.update(cx, |lsp_store, cx| {
1088 lsp_store
1089 .update_diagnostics(
1090 LanguageServerId(0),
1091 lsp::PublishDiagnosticsParams {
1092 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
1093 version: None,
1094 diagnostics: vec![
1095 lsp::Diagnostic {
1096 range: lsp::Range::new(
1097 lsp::Position::new(0, 11),
1098 lsp::Position::new(0, 12),
1099 ),
1100 severity: Some(lsp::DiagnosticSeverity::ERROR),
1101 ..Default::default()
1102 },
1103 lsp::Diagnostic {
1104 range: lsp::Range::new(
1105 lsp::Position::new(0, 12),
1106 lsp::Position::new(0, 15),
1107 ),
1108 severity: Some(lsp::DiagnosticSeverity::ERROR),
1109 ..Default::default()
1110 },
1111 lsp::Diagnostic {
1112 range: lsp::Range::new(
1113 lsp::Position::new(0, 12),
1114 lsp::Position::new(0, 15),
1115 ),
1116 severity: Some(lsp::DiagnosticSeverity::ERROR),
1117 ..Default::default()
1118 },
1119 lsp::Diagnostic {
1120 range: lsp::Range::new(
1121 lsp::Position::new(0, 25),
1122 lsp::Position::new(0, 28),
1123 ),
1124 severity: Some(lsp::DiagnosticSeverity::ERROR),
1125 ..Default::default()
1126 },
1127 ],
1128 },
1129 None,
1130 DiagnosticSourceKind::Pushed,
1131 &[],
1132 cx,
1133 )
1134 .unwrap()
1135 });
1136 });
1137 cx.run_until_parked();
1138
1139 //// Backward
1140
1141 // Fourth diagnostic
1142 cx.update_editor(|editor, window, cx| {
1143 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
1144 });
1145 cx.assert_editor_state(indoc! {"
1146 fn func(abc def: i32) -> ˇu32 {
1147 }
1148 "});
1149
1150 // Third diagnostic
1151 cx.update_editor(|editor, window, cx| {
1152 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
1153 });
1154 cx.assert_editor_state(indoc! {"
1155 fn func(abc ˇdef: i32) -> u32 {
1156 }
1157 "});
1158
1159 // Second diagnostic, same place
1160 cx.update_editor(|editor, window, cx| {
1161 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
1162 });
1163 cx.assert_editor_state(indoc! {"
1164 fn func(abc ˇdef: i32) -> u32 {
1165 }
1166 "});
1167
1168 // First diagnostic
1169 cx.update_editor(|editor, window, cx| {
1170 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
1171 });
1172 cx.assert_editor_state(indoc! {"
1173 fn func(abcˇ def: i32) -> u32 {
1174 }
1175 "});
1176
1177 // Wrapped over, fourth diagnostic
1178 cx.update_editor(|editor, window, cx| {
1179 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
1180 });
1181 cx.assert_editor_state(indoc! {"
1182 fn func(abc def: i32) -> ˇu32 {
1183 }
1184 "});
1185
1186 cx.update_editor(|editor, window, cx| {
1187 editor.move_to_beginning(&MoveToBeginning, window, cx);
1188 });
1189 cx.assert_editor_state(indoc! {"
1190 ˇfn func(abc def: i32) -> u32 {
1191 }
1192 "});
1193
1194 //// Forward
1195
1196 // First diagnostic
1197 cx.update_editor(|editor, window, cx| {
1198 editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
1199 });
1200 cx.assert_editor_state(indoc! {"
1201 fn func(abcˇ def: i32) -> u32 {
1202 }
1203 "});
1204
1205 // Second diagnostic
1206 cx.update_editor(|editor, window, cx| {
1207 editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
1208 });
1209 cx.assert_editor_state(indoc! {"
1210 fn func(abc ˇdef: i32) -> u32 {
1211 }
1212 "});
1213
1214 // Third diagnostic, same place
1215 cx.update_editor(|editor, window, cx| {
1216 editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
1217 });
1218 cx.assert_editor_state(indoc! {"
1219 fn func(abc ˇdef: i32) -> u32 {
1220 }
1221 "});
1222
1223 // Fourth diagnostic
1224 cx.update_editor(|editor, window, cx| {
1225 editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
1226 });
1227 cx.assert_editor_state(indoc! {"
1228 fn func(abc def: i32) -> ˇu32 {
1229 }
1230 "});
1231
1232 // Wrapped around, first diagnostic
1233 cx.update_editor(|editor, window, cx| {
1234 editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
1235 });
1236 cx.assert_editor_state(indoc! {"
1237 fn func(abcˇ def: i32) -> u32 {
1238 }
1239 "});
1240}
1241
1242#[gpui::test]
1243async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
1244 init_test(cx);
1245
1246 let mut cx = EditorTestContext::new(cx).await;
1247
1248 cx.set_state(indoc! {"
1249 fn func(abˇc def: i32) -> u32 {
1250 }
1251 "});
1252 let lsp_store =
1253 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
1254
1255 cx.update(|_, cx| {
1256 lsp_store.update(cx, |lsp_store, cx| {
1257 lsp_store.update_diagnostics(
1258 LanguageServerId(0),
1259 lsp::PublishDiagnosticsParams {
1260 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
1261 version: None,
1262 diagnostics: vec![lsp::Diagnostic {
1263 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
1264 severity: Some(lsp::DiagnosticSeverity::ERROR),
1265 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
1266 ..Default::default()
1267 }],
1268 },
1269 None,
1270 DiagnosticSourceKind::Pushed,
1271 &[],
1272 cx,
1273 )
1274 })
1275 }).unwrap();
1276 cx.run_until_parked();
1277 cx.update_editor(|editor, window, cx| {
1278 editor::hover_popover::hover(editor, &Default::default(), window, cx)
1279 });
1280 cx.run_until_parked();
1281 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
1282}
1283
1284#[gpui::test]
1285async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
1286 init_test(cx);
1287
1288 let mut cx = EditorLspTestContext::new_rust(
1289 lsp::ServerCapabilities {
1290 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1291 ..Default::default()
1292 },
1293 cx,
1294 )
1295 .await;
1296
1297 // Hover with just diagnostic, pops DiagnosticPopover immediately and then
1298 // info popover once request completes
1299 cx.set_state(indoc! {"
1300 fn teˇst() { println!(); }
1301 "});
1302 // Send diagnostic to client
1303 let range = cx.lsp_range(indoc! {"
1304 fn «test»() { println!(); }
1305 "});
1306 let lsp_store =
1307 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
1308 cx.update(|_, cx| {
1309 lsp_store.update(cx, |lsp_store, cx| {
1310 lsp_store.update_diagnostics(
1311 LanguageServerId(0),
1312 lsp::PublishDiagnosticsParams {
1313 uri: lsp::Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
1314 version: None,
1315 diagnostics: vec![lsp::Diagnostic {
1316 range,
1317 severity: Some(lsp::DiagnosticSeverity::ERROR),
1318 message: "A test diagnostic message.".to_string(),
1319 ..Default::default()
1320 }],
1321 },
1322 None,
1323 DiagnosticSourceKind::Pushed,
1324 &[],
1325 cx,
1326 )
1327 })
1328 })
1329 .unwrap();
1330 cx.run_until_parked();
1331
1332 // Hover pops diagnostic immediately
1333 cx.update_editor(|editor, window, cx| editor::hover_popover::hover(editor, &Hover, window, cx));
1334 cx.background_executor.run_until_parked();
1335
1336 cx.editor(|Editor { hover_state, .. }, _, _| {
1337 assert!(hover_state.diagnostic_popover.is_some());
1338 assert!(hover_state.info_popovers.is_empty());
1339 });
1340
1341 // Info Popover shows after request responded to
1342 let range = cx.lsp_range(indoc! {"
1343 fn «test»() { println!(); }
1344 "});
1345 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1346 Ok(Some(lsp::Hover {
1347 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1348 kind: lsp::MarkupKind::Markdown,
1349 value: "some new docs".to_string(),
1350 }),
1351 range: Some(range),
1352 }))
1353 });
1354 let delay = cx.update(|_, cx| EditorSettings::get_global(cx).hover_popover_delay.0 + 1);
1355 cx.background_executor
1356 .advance_clock(Duration::from_millis(delay));
1357
1358 cx.background_executor.run_until_parked();
1359 cx.editor(|Editor { hover_state, .. }, _, _| {
1360 hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
1361 });
1362}
1363#[gpui::test]
1364async fn test_diagnostics_with_code(cx: &mut TestAppContext) {
1365 init_test(cx);
1366
1367 let fs = FakeFs::new(cx.executor());
1368 fs.insert_tree(
1369 path!("/root"),
1370 json!({
1371 "main.js": "
1372 function test() {
1373 const x = 10;
1374 const y = 20;
1375 return 1;
1376 }
1377 test();
1378 "
1379 .unindent(),
1380 }),
1381 )
1382 .await;
1383
1384 let language_server_id = LanguageServerId(0);
1385 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1386 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1387 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1388 let cx = &mut VisualTestContext::from_window(*window, cx);
1389 let workspace = window.root(cx).unwrap();
1390 let uri = lsp::Uri::from_file_path(path!("/root/main.js")).unwrap();
1391
1392 // Create diagnostics with code fields
1393 lsp_store.update(cx, |lsp_store, cx| {
1394 lsp_store
1395 .update_diagnostics(
1396 language_server_id,
1397 lsp::PublishDiagnosticsParams {
1398 uri: uri.clone(),
1399 diagnostics: vec![
1400 lsp::Diagnostic {
1401 range: lsp::Range::new(
1402 lsp::Position::new(1, 4),
1403 lsp::Position::new(1, 14),
1404 ),
1405 severity: Some(lsp::DiagnosticSeverity::WARNING),
1406 code: Some(lsp::NumberOrString::String("no-unused-vars".to_string())),
1407 source: Some("eslint".to_string()),
1408 message: "'x' is assigned a value but never used".to_string(),
1409 ..Default::default()
1410 },
1411 lsp::Diagnostic {
1412 range: lsp::Range::new(
1413 lsp::Position::new(2, 4),
1414 lsp::Position::new(2, 14),
1415 ),
1416 severity: Some(lsp::DiagnosticSeverity::WARNING),
1417 code: Some(lsp::NumberOrString::String("no-unused-vars".to_string())),
1418 source: Some("eslint".to_string()),
1419 message: "'y' is assigned a value but never used".to_string(),
1420 ..Default::default()
1421 },
1422 ],
1423 version: None,
1424 },
1425 None,
1426 DiagnosticSourceKind::Pushed,
1427 &[],
1428 cx,
1429 )
1430 .unwrap();
1431 });
1432
1433 // Open the project diagnostics view
1434 let diagnostics = window.build_entity(cx, |window, cx| {
1435 ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
1436 });
1437 let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
1438
1439 diagnostics
1440 .next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
1441 .await;
1442
1443 // Verify that the diagnostic codes are displayed correctly
1444 pretty_assertions::assert_eq!(
1445 editor_content_with_blocks(&editor, cx),
1446 indoc::indoc! {
1447 "§ main.js
1448 § -----
1449 function test() {
1450 const x = 10; § 'x' is assigned a value but never used (eslint no-unused-vars)
1451 const y = 20; § 'y' is assigned a value but never used (eslint no-unused-vars)
1452 return 1;
1453 }"
1454 }
1455 );
1456}
1457
1458#[gpui::test]
1459async fn go_to_diagnostic_with_severity(cx: &mut TestAppContext) {
1460 init_test(cx);
1461
1462 let mut cx = EditorTestContext::new(cx).await;
1463 let lsp_store =
1464 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
1465
1466 cx.set_state(indoc! {"error warning info hiˇnt"});
1467
1468 cx.update(|_, cx| {
1469 lsp_store.update(cx, |lsp_store, cx| {
1470 lsp_store
1471 .update_diagnostics(
1472 LanguageServerId(0),
1473 lsp::PublishDiagnosticsParams {
1474 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
1475 version: None,
1476 diagnostics: vec![
1477 lsp::Diagnostic {
1478 range: lsp::Range::new(
1479 lsp::Position::new(0, 0),
1480 lsp::Position::new(0, 5),
1481 ),
1482 severity: Some(lsp::DiagnosticSeverity::ERROR),
1483 ..Default::default()
1484 },
1485 lsp::Diagnostic {
1486 range: lsp::Range::new(
1487 lsp::Position::new(0, 6),
1488 lsp::Position::new(0, 13),
1489 ),
1490 severity: Some(lsp::DiagnosticSeverity::WARNING),
1491 ..Default::default()
1492 },
1493 lsp::Diagnostic {
1494 range: lsp::Range::new(
1495 lsp::Position::new(0, 14),
1496 lsp::Position::new(0, 18),
1497 ),
1498 severity: Some(lsp::DiagnosticSeverity::INFORMATION),
1499 ..Default::default()
1500 },
1501 lsp::Diagnostic {
1502 range: lsp::Range::new(
1503 lsp::Position::new(0, 19),
1504 lsp::Position::new(0, 23),
1505 ),
1506 severity: Some(lsp::DiagnosticSeverity::HINT),
1507 ..Default::default()
1508 },
1509 ],
1510 },
1511 None,
1512 DiagnosticSourceKind::Pushed,
1513 &[],
1514 cx,
1515 )
1516 .unwrap()
1517 });
1518 });
1519 cx.run_until_parked();
1520
1521 macro_rules! go {
1522 ($severity:expr) => {
1523 cx.update_editor(|editor, window, cx| {
1524 editor.go_to_diagnostic(
1525 &GoToDiagnostic {
1526 severity: $severity,
1527 },
1528 window,
1529 cx,
1530 );
1531 });
1532 };
1533 }
1534
1535 // Default, should cycle through all diagnostics
1536 go!(GoToDiagnosticSeverityFilter::default());
1537 cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
1538 go!(GoToDiagnosticSeverityFilter::default());
1539 cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
1540 go!(GoToDiagnosticSeverityFilter::default());
1541 cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
1542 go!(GoToDiagnosticSeverityFilter::default());
1543 cx.assert_editor_state(indoc! {"error warning info ˇhint"});
1544 go!(GoToDiagnosticSeverityFilter::default());
1545 cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
1546
1547 let only_info = GoToDiagnosticSeverityFilter::Only(GoToDiagnosticSeverity::Information);
1548 go!(only_info);
1549 cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
1550 go!(only_info);
1551 cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
1552
1553 let no_hints = GoToDiagnosticSeverityFilter::Range {
1554 min: GoToDiagnosticSeverity::Information,
1555 max: GoToDiagnosticSeverity::Error,
1556 };
1557
1558 go!(no_hints);
1559 cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
1560 go!(no_hints);
1561 cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
1562 go!(no_hints);
1563 cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
1564 go!(no_hints);
1565 cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
1566
1567 let warning_info = GoToDiagnosticSeverityFilter::Range {
1568 min: GoToDiagnosticSeverity::Information,
1569 max: GoToDiagnosticSeverity::Warning,
1570 };
1571
1572 go!(warning_info);
1573 cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
1574 go!(warning_info);
1575 cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
1576 go!(warning_info);
1577 cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
1578}
1579
1580#[gpui::test]
1581async fn test_buffer_diagnostics(cx: &mut TestAppContext) {
1582 init_test(cx);
1583
1584 // We'll be creating two different files, both with diagnostics, so we can
1585 // later verify that, since the `BufferDiagnosticsEditor` only shows
1586 // diagnostics for the provided path, the diagnostics for the other file
1587 // will not be shown, contrary to what happens with
1588 // `ProjectDiagnosticsEditor`.
1589 let fs = FakeFs::new(cx.executor());
1590 fs.insert_tree(
1591 path!("/test"),
1592 json!({
1593 "main.rs": "
1594 fn main() {
1595 let x = vec![];
1596 let y = vec![];
1597 a(x);
1598 b(y);
1599 c(y);
1600 d(x);
1601 }
1602 "
1603 .unindent(),
1604 "other.rs": "
1605 fn other() {
1606 let unused = 42;
1607 undefined_function();
1608 }
1609 "
1610 .unindent(),
1611 }),
1612 )
1613 .await;
1614
1615 let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
1616 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1617 let cx = &mut VisualTestContext::from_window(*window, cx);
1618 let project_path = project::ProjectPath {
1619 worktree_id: project.read_with(cx, |project, cx| {
1620 project.worktrees(cx).next().unwrap().read(cx).id()
1621 }),
1622 path: rel_path("main.rs").into(),
1623 };
1624 let buffer = project
1625 .update(cx, |project, cx| {
1626 project.open_buffer(project_path.clone(), cx)
1627 })
1628 .await
1629 .ok();
1630
1631 // Create the diagnostics for `main.rs`.
1632 let language_server_id = LanguageServerId(0);
1633 let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap();
1634 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1635
1636 lsp_store.update(cx, |lsp_store, cx| {
1637 lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
1638 uri: uri.clone(),
1639 diagnostics: vec![
1640 lsp::Diagnostic{
1641 range: lsp::Range::new(lsp::Position::new(5, 6), lsp::Position::new(5, 7)),
1642 severity: Some(lsp::DiagnosticSeverity::WARNING),
1643 message: "use of moved value\nvalue used here after move".to_string(),
1644 related_information: Some(vec![
1645 lsp::DiagnosticRelatedInformation {
1646 location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 9))),
1647 message: "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
1648 },
1649 lsp::DiagnosticRelatedInformation {
1650 location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 7))),
1651 message: "value moved here".to_string()
1652 },
1653 ]),
1654 ..Default::default()
1655 },
1656 lsp::Diagnostic{
1657 range: lsp::Range::new(lsp::Position::new(6, 6), lsp::Position::new(6, 7)),
1658 severity: Some(lsp::DiagnosticSeverity::ERROR),
1659 message: "use of moved value\nvalue used here after move".to_string(),
1660 related_information: Some(vec![
1661 lsp::DiagnosticRelatedInformation {
1662 location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9))),
1663 message: "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
1664 },
1665 lsp::DiagnosticRelatedInformation {
1666 location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(3, 6), lsp::Position::new(3, 7))),
1667 message: "value moved here".to_string()
1668 },
1669 ]),
1670 ..Default::default()
1671 }
1672 ],
1673 version: None
1674 }, None, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
1675
1676 // Create diagnostics for other.rs to ensure that the file and
1677 // diagnostics are not included in `BufferDiagnosticsEditor` when it is
1678 // deployed for main.rs.
1679 lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
1680 uri: lsp::Uri::from_file_path(path!("/test/other.rs")).unwrap(),
1681 diagnostics: vec![
1682 lsp::Diagnostic{
1683 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 14)),
1684 severity: Some(lsp::DiagnosticSeverity::WARNING),
1685 message: "unused variable: `unused`".to_string(),
1686 ..Default::default()
1687 },
1688 lsp::Diagnostic{
1689 range: lsp::Range::new(lsp::Position::new(2, 4), lsp::Position::new(2, 22)),
1690 severity: Some(lsp::DiagnosticSeverity::ERROR),
1691 message: "cannot find function `undefined_function` in this scope".to_string(),
1692 ..Default::default()
1693 }
1694 ],
1695 version: None
1696 }, None, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
1697 });
1698
1699 let buffer_diagnostics = window.build_entity(cx, |window, cx| {
1700 BufferDiagnosticsEditor::new(
1701 project_path.clone(),
1702 project.clone(),
1703 buffer,
1704 true,
1705 window,
1706 cx,
1707 )
1708 });
1709 let editor = buffer_diagnostics.update(cx, |buffer_diagnostics, _| {
1710 buffer_diagnostics.editor().clone()
1711 });
1712
1713 // Since the excerpt updates is handled by a background task, we need to
1714 // wait a little bit to ensure that the buffer diagnostic's editor content
1715 // is rendered.
1716 cx.executor()
1717 .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
1718
1719 pretty_assertions::assert_eq!(
1720 editor_content_with_blocks(&editor, cx),
1721 indoc::indoc! {
1722 "§ main.rs
1723 § -----
1724 fn main() {
1725 let x = vec![];
1726 § move occurs because `x` has type `Vec<char>`, which does not implement
1727 § the `Copy` trait (back)
1728 let y = vec![];
1729 § move occurs because `y` has type `Vec<char>`, which does not implement
1730 § the `Copy` trait
1731 a(x); § value moved here
1732 b(y); § value moved here
1733 c(y);
1734 § use of moved value
1735 § value used here after move
1736 d(x);
1737 § use of moved value
1738 § value used here after move
1739 § hint: move occurs because `x` has type `Vec<char>`, which does not
1740 § implement the `Copy` trait
1741 }"
1742 }
1743 );
1744}
1745
1746#[gpui::test]
1747async fn test_buffer_diagnostics_without_warnings(cx: &mut TestAppContext) {
1748 init_test(cx);
1749
1750 let fs = FakeFs::new(cx.executor());
1751 fs.insert_tree(
1752 path!("/test"),
1753 json!({
1754 "main.rs": "
1755 fn main() {
1756 let x = vec![];
1757 let y = vec![];
1758 a(x);
1759 b(y);
1760 c(y);
1761 d(x);
1762 }
1763 "
1764 .unindent(),
1765 }),
1766 )
1767 .await;
1768
1769 let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
1770 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1771 let cx = &mut VisualTestContext::from_window(*window, cx);
1772 let project_path = project::ProjectPath {
1773 worktree_id: project.read_with(cx, |project, cx| {
1774 project.worktrees(cx).next().unwrap().read(cx).id()
1775 }),
1776 path: rel_path("main.rs").into(),
1777 };
1778 let buffer = project
1779 .update(cx, |project, cx| {
1780 project.open_buffer(project_path.clone(), cx)
1781 })
1782 .await
1783 .ok();
1784
1785 let language_server_id = LanguageServerId(0);
1786 let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap();
1787 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1788
1789 lsp_store.update(cx, |lsp_store, cx| {
1790 lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
1791 uri: uri.clone(),
1792 diagnostics: vec![
1793 lsp::Diagnostic{
1794 range: lsp::Range::new(lsp::Position::new(5, 6), lsp::Position::new(5, 7)),
1795 severity: Some(lsp::DiagnosticSeverity::WARNING),
1796 message: "use of moved value\nvalue used here after move".to_string(),
1797 related_information: Some(vec![
1798 lsp::DiagnosticRelatedInformation {
1799 location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 9))),
1800 message: "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
1801 },
1802 lsp::DiagnosticRelatedInformation {
1803 location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 7))),
1804 message: "value moved here".to_string()
1805 },
1806 ]),
1807 ..Default::default()
1808 },
1809 lsp::Diagnostic{
1810 range: lsp::Range::new(lsp::Position::new(6, 6), lsp::Position::new(6, 7)),
1811 severity: Some(lsp::DiagnosticSeverity::ERROR),
1812 message: "use of moved value\nvalue used here after move".to_string(),
1813 related_information: Some(vec![
1814 lsp::DiagnosticRelatedInformation {
1815 location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9))),
1816 message: "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
1817 },
1818 lsp::DiagnosticRelatedInformation {
1819 location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(3, 6), lsp::Position::new(3, 7))),
1820 message: "value moved here".to_string()
1821 },
1822 ]),
1823 ..Default::default()
1824 }
1825 ],
1826 version: None
1827 }, None, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
1828 });
1829
1830 let include_warnings = false;
1831 let buffer_diagnostics = window.build_entity(cx, |window, cx| {
1832 BufferDiagnosticsEditor::new(
1833 project_path.clone(),
1834 project.clone(),
1835 buffer,
1836 include_warnings,
1837 window,
1838 cx,
1839 )
1840 });
1841
1842 let editor = buffer_diagnostics.update(cx, |buffer_diagnostics, _cx| {
1843 buffer_diagnostics.editor().clone()
1844 });
1845
1846 // Since the excerpt updates is handled by a background task, we need to
1847 // wait a little bit to ensure that the buffer diagnostic's editor content
1848 // is rendered.
1849 cx.executor()
1850 .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
1851
1852 pretty_assertions::assert_eq!(
1853 editor_content_with_blocks(&editor, cx),
1854 indoc::indoc! {
1855 "§ main.rs
1856 § -----
1857 fn main() {
1858 let x = vec![];
1859 § move occurs because `x` has type `Vec<char>`, which does not implement
1860 § the `Copy` trait (back)
1861 let y = vec![];
1862 a(x); § value moved here
1863 b(y);
1864 c(y);
1865 d(x);
1866 § use of moved value
1867 § value used here after move
1868 § hint: move occurs because `x` has type `Vec<char>`, which does not
1869 § implement the `Copy` trait
1870 }"
1871 }
1872 );
1873}
1874
1875#[gpui::test]
1876async fn test_buffer_diagnostics_multiple_servers(cx: &mut TestAppContext) {
1877 init_test(cx);
1878
1879 let fs = FakeFs::new(cx.executor());
1880 fs.insert_tree(
1881 path!("/test"),
1882 json!({
1883 "main.rs": "
1884 fn main() {
1885 let x = vec![];
1886 let y = vec![];
1887 a(x);
1888 b(y);
1889 c(y);
1890 d(x);
1891 }
1892 "
1893 .unindent(),
1894 }),
1895 )
1896 .await;
1897
1898 let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
1899 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1900 let cx = &mut VisualTestContext::from_window(*window, cx);
1901 let project_path = project::ProjectPath {
1902 worktree_id: project.read_with(cx, |project, cx| {
1903 project.worktrees(cx).next().unwrap().read(cx).id()
1904 }),
1905 path: rel_path("main.rs").into(),
1906 };
1907 let buffer = project
1908 .update(cx, |project, cx| {
1909 project.open_buffer(project_path.clone(), cx)
1910 })
1911 .await
1912 .ok();
1913
1914 // Create the diagnostics for `main.rs`.
1915 // Two warnings are being created, one for each language server, in order to
1916 // assert that both warnings are rendered in the editor.
1917 let language_server_id_a = LanguageServerId(0);
1918 let language_server_id_b = LanguageServerId(1);
1919 let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap();
1920 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1921
1922 lsp_store.update(cx, |lsp_store, cx| {
1923 lsp_store
1924 .update_diagnostics(
1925 language_server_id_a,
1926 lsp::PublishDiagnosticsParams {
1927 uri: uri.clone(),
1928 diagnostics: vec![lsp::Diagnostic {
1929 range: lsp::Range::new(lsp::Position::new(5, 6), lsp::Position::new(5, 7)),
1930 severity: Some(lsp::DiagnosticSeverity::WARNING),
1931 message: "use of moved value\nvalue used here after move".to_string(),
1932 related_information: None,
1933 ..Default::default()
1934 }],
1935 version: None,
1936 },
1937 None,
1938 DiagnosticSourceKind::Pushed,
1939 &[],
1940 cx,
1941 )
1942 .unwrap();
1943
1944 lsp_store
1945 .update_diagnostics(
1946 language_server_id_b,
1947 lsp::PublishDiagnosticsParams {
1948 uri: uri.clone(),
1949 diagnostics: vec![lsp::Diagnostic {
1950 range: lsp::Range::new(lsp::Position::new(6, 6), lsp::Position::new(6, 7)),
1951 severity: Some(lsp::DiagnosticSeverity::WARNING),
1952 message: "use of moved value\nvalue used here after move".to_string(),
1953 related_information: None,
1954 ..Default::default()
1955 }],
1956 version: None,
1957 },
1958 None,
1959 DiagnosticSourceKind::Pushed,
1960 &[],
1961 cx,
1962 )
1963 .unwrap();
1964 });
1965
1966 let buffer_diagnostics = window.build_entity(cx, |window, cx| {
1967 BufferDiagnosticsEditor::new(
1968 project_path.clone(),
1969 project.clone(),
1970 buffer,
1971 true,
1972 window,
1973 cx,
1974 )
1975 });
1976 let editor = buffer_diagnostics.update(cx, |buffer_diagnostics, _| {
1977 buffer_diagnostics.editor().clone()
1978 });
1979
1980 // Since the excerpt updates is handled by a background task, we need to
1981 // wait a little bit to ensure that the buffer diagnostic's editor content
1982 // is rendered.
1983 cx.executor()
1984 .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
1985
1986 pretty_assertions::assert_eq!(
1987 editor_content_with_blocks(&editor, cx),
1988 indoc::indoc! {
1989 "§ main.rs
1990 § -----
1991 a(x);
1992 b(y);
1993 c(y);
1994 § use of moved value
1995 § value used here after move
1996 d(x);
1997 § use of moved value
1998 § value used here after move
1999 }"
2000 }
2001 );
2002
2003 buffer_diagnostics.update(cx, |buffer_diagnostics, _cx| {
2004 assert_eq!(
2005 *buffer_diagnostics.summary(),
2006 DiagnosticSummary {
2007 warning_count: 2,
2008 error_count: 0
2009 }
2010 );
2011 })
2012}
2013
2014fn init_test(cx: &mut TestAppContext) {
2015 cx.update(|cx| {
2016 zlog::init_test();
2017 let settings = SettingsStore::test(cx);
2018 cx.set_global(settings);
2019 theme::init(theme::LoadThemes::JustBase, cx);
2020 language::init(cx);
2021 client::init_settings(cx);
2022 workspace::init_settings(cx);
2023 Project::init_settings(cx);
2024 crate::init(cx);
2025 editor::init(cx);
2026 });
2027}
2028
2029fn randomly_update_diagnostics_for_path(
2030 fs: &FakeFs,
2031 path: &Path,
2032 diagnostics: &mut Vec<lsp::Diagnostic>,
2033 next_id: &mut usize,
2034 rng: &mut impl Rng,
2035) {
2036 let mutation_count = rng.random_range(1..=3);
2037 for _ in 0..mutation_count {
2038 if rng.random_bool(0.3) && !diagnostics.is_empty() {
2039 let idx = rng.random_range(0..diagnostics.len());
2040 log::info!(" removing diagnostic at index {idx}");
2041 diagnostics.remove(idx);
2042 } else {
2043 let unique_id = *next_id;
2044 *next_id += 1;
2045
2046 let new_diagnostic = random_lsp_diagnostic(rng, fs, path, unique_id);
2047
2048 let ix = rng.random_range(0..=diagnostics.len());
2049 log::info!(
2050 " inserting {} at index {ix}. {},{}..{},{}",
2051 new_diagnostic.message,
2052 new_diagnostic.range.start.line,
2053 new_diagnostic.range.start.character,
2054 new_diagnostic.range.end.line,
2055 new_diagnostic.range.end.character,
2056 );
2057 for related in new_diagnostic.related_information.iter().flatten() {
2058 log::info!(
2059 " {}. {},{}..{},{}",
2060 related.message,
2061 related.location.range.start.line,
2062 related.location.range.start.character,
2063 related.location.range.end.line,
2064 related.location.range.end.character,
2065 );
2066 }
2067 diagnostics.insert(ix, new_diagnostic);
2068 }
2069 }
2070}
2071
2072fn random_lsp_diagnostic(
2073 rng: &mut impl Rng,
2074 fs: &FakeFs,
2075 path: &Path,
2076 unique_id: usize,
2077) -> lsp::Diagnostic {
2078 // Intentionally allow erroneous ranges some of the time (that run off the end of the file),
2079 // because language servers can potentially give us those, and we should handle them gracefully.
2080 const ERROR_MARGIN: usize = 10;
2081
2082 let file_content = fs.read_file_sync(path).unwrap();
2083 let file_text = Rope::from_str_small(String::from_utf8_lossy(&file_content).as_ref());
2084
2085 let start = rng.random_range(0..file_text.len().saturating_add(ERROR_MARGIN));
2086 let end = rng.random_range(start..file_text.len().saturating_add(ERROR_MARGIN));
2087
2088 let start_point = file_text.offset_to_point_utf16(start);
2089 let end_point = file_text.offset_to_point_utf16(end);
2090
2091 let range = lsp::Range::new(
2092 lsp::Position::new(start_point.row, start_point.column),
2093 lsp::Position::new(end_point.row, end_point.column),
2094 );
2095
2096 let severity = if rng.random_bool(0.5) {
2097 Some(lsp::DiagnosticSeverity::ERROR)
2098 } else {
2099 Some(lsp::DiagnosticSeverity::WARNING)
2100 };
2101
2102 let message = format!("diagnostic {unique_id}");
2103
2104 let related_information = if rng.random_bool(0.3) {
2105 let info_count = rng.random_range(1..=3);
2106 let mut related_info = Vec::with_capacity(info_count);
2107
2108 for i in 0..info_count {
2109 let info_start = rng.random_range(0..file_text.len().saturating_add(ERROR_MARGIN));
2110 let info_end =
2111 rng.random_range(info_start..file_text.len().saturating_add(ERROR_MARGIN));
2112
2113 let info_start_point = file_text.offset_to_point_utf16(info_start);
2114 let info_end_point = file_text.offset_to_point_utf16(info_end);
2115
2116 let info_range = lsp::Range::new(
2117 lsp::Position::new(info_start_point.row, info_start_point.column),
2118 lsp::Position::new(info_end_point.row, info_end_point.column),
2119 );
2120
2121 related_info.push(lsp::DiagnosticRelatedInformation {
2122 location: lsp::Location::new(lsp::Uri::from_file_path(path).unwrap(), info_range),
2123 message: format!("related info {i} for diagnostic {unique_id}"),
2124 });
2125 }
2126
2127 Some(related_info)
2128 } else {
2129 None
2130 };
2131
2132 lsp::Diagnostic {
2133 range,
2134 severity,
2135 message,
2136 related_information,
2137 data: None,
2138 ..Default::default()
2139 }
2140}