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