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
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.random_range(0..snapshot.buffer_snapshot().len());
882 let position = snapshot.buffer_snapshot().clip_offset(position, Bias::Left);
883 log::info!(
884 "adding inlay at {position}/{}: {:?}",
885 snapshot.buffer_snapshot().len(),
886 snapshot.buffer_snapshot().text(),
887 );
888
889 editor.splice_inlays(
890 &[],
891 vec![Inlay::edit_prediction(
892 post_inc(&mut next_inlay_id),
893 snapshot.buffer_snapshot().anchor_before(position),
894 Rope::from_iter(["Test inlay ", "next_inlay_id"]),
895 )],
896 cx,
897 );
898 }
899 });
900 }),
901
902 // language server updates diagnostics
903 _ => {
904 let (path, server_id, diagnostics) =
905 match current_diagnostics.iter_mut().choose(&mut rng) {
906 // update existing set of diagnostics
907 Some(((path, server_id), diagnostics)) if rng.random_bool(0.5) => {
908 (path.clone(), *server_id, diagnostics)
909 }
910
911 // insert a set of diagnostics for a new path
912 _ => {
913 let path: PathBuf =
914 format!(path!("/test/{}.rs"), post_inc(&mut next_filename)).into();
915 let len = rng.random_range(128..256);
916 let content =
917 RandomCharIter::new(&mut rng).take(len).collect::<String>();
918 fs.insert_file(&path, content.into_bytes()).await;
919
920 let server_id = match language_server_ids.iter().choose(&mut rng) {
921 Some(server_id) if rng.random_bool(0.5) => *server_id,
922 _ => {
923 let id = LanguageServerId(language_server_ids.len());
924 language_server_ids.push(id);
925 id
926 }
927 };
928
929 (
930 path.clone(),
931 server_id,
932 current_diagnostics.entry((path, server_id)).or_default(),
933 )
934 }
935 };
936
937 updated_language_servers.insert(server_id);
938
939 lsp_store.update(cx, |lsp_store, cx| {
940 log::info!("updating diagnostics. language server {server_id} path {path:?}");
941 randomly_update_diagnostics_for_path(
942 &fs,
943 &path,
944 diagnostics,
945 &mut next_id,
946 &mut rng,
947 );
948 lsp_store
949 .update_diagnostics(
950 server_id,
951 lsp::PublishDiagnosticsParams {
952 uri: lsp::Uri::from_file_path(&path).unwrap_or_else(|_| {
953 lsp::Uri::from_str("file:///test/fallback.rs").unwrap()
954 }),
955 diagnostics: diagnostics.clone(),
956 version: None,
957 },
958 None,
959 DiagnosticSourceKind::Pushed,
960 &[],
961 cx,
962 )
963 .unwrap()
964 });
965 cx.executor()
966 .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
967
968 cx.run_until_parked();
969 }
970 }
971 }
972
973 log::info!("updating mutated diagnostics view");
974 mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
975 diagnostics.update_stale_excerpts(window, cx)
976 });
977
978 cx.executor()
979 .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
980 cx.run_until_parked();
981}
982
983#[gpui::test]
984async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext) {
985 init_test(cx);
986
987 let mut cx = EditorTestContext::new(cx).await;
988 let lsp_store =
989 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
990
991 cx.set_state(indoc! {"
992 ˇfn func(abc def: i32) -> u32 {
993 }
994 "});
995
996 let message = "Something's wrong!";
997 cx.update(|_, cx| {
998 lsp_store.update(cx, |lsp_store, cx| {
999 lsp_store
1000 .update_diagnostics(
1001 LanguageServerId(0),
1002 lsp::PublishDiagnosticsParams {
1003 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
1004 version: None,
1005 diagnostics: vec![lsp::Diagnostic {
1006 range: lsp::Range::new(
1007 lsp::Position::new(0, 11),
1008 lsp::Position::new(0, 12),
1009 ),
1010 severity: Some(lsp::DiagnosticSeverity::ERROR),
1011 message: message.to_string(),
1012 ..Default::default()
1013 }],
1014 },
1015 None,
1016 DiagnosticSourceKind::Pushed,
1017 &[],
1018 cx,
1019 )
1020 .unwrap()
1021 });
1022 });
1023 cx.run_until_parked();
1024
1025 cx.update_editor(|editor, window, cx| {
1026 editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
1027 assert_eq!(
1028 editor
1029 .active_diagnostic_group()
1030 .map(|diagnostics_group| diagnostics_group.active_message.as_str()),
1031 Some(message),
1032 "Should have a diagnostics group activated"
1033 );
1034 });
1035 cx.assert_editor_state(indoc! {"
1036 fn func(abcˇ def: i32) -> u32 {
1037 }
1038 "});
1039
1040 cx.update(|_, cx| {
1041 lsp_store.update(cx, |lsp_store, cx| {
1042 lsp_store
1043 .update_diagnostics(
1044 LanguageServerId(0),
1045 lsp::PublishDiagnosticsParams {
1046 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
1047 version: None,
1048 diagnostics: Vec::new(),
1049 },
1050 None,
1051 DiagnosticSourceKind::Pushed,
1052 &[],
1053 cx,
1054 )
1055 .unwrap()
1056 });
1057 });
1058 cx.run_until_parked();
1059 cx.update_editor(|editor, _, _| {
1060 assert_eq!(editor.active_diagnostic_group(), None);
1061 });
1062 cx.assert_editor_state(indoc! {"
1063 fn func(abcˇ def: i32) -> u32 {
1064 }
1065 "});
1066
1067 cx.update_editor(|editor, window, cx| {
1068 editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
1069 assert_eq!(editor.active_diagnostic_group(), None);
1070 });
1071 cx.assert_editor_state(indoc! {"
1072 fn func(abcˇ def: i32) -> u32 {
1073 }
1074 "});
1075}
1076
1077#[gpui::test]
1078async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
1079 init_test(cx);
1080
1081 let mut cx = EditorTestContext::new(cx).await;
1082 let lsp_store =
1083 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
1084
1085 cx.set_state(indoc! {"
1086 ˇfn func(abc def: i32) -> u32 {
1087 }
1088 "});
1089
1090 cx.update(|_, cx| {
1091 lsp_store.update(cx, |lsp_store, cx| {
1092 lsp_store
1093 .update_diagnostics(
1094 LanguageServerId(0),
1095 lsp::PublishDiagnosticsParams {
1096 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
1097 version: None,
1098 diagnostics: vec![
1099 lsp::Diagnostic {
1100 range: lsp::Range::new(
1101 lsp::Position::new(0, 11),
1102 lsp::Position::new(0, 12),
1103 ),
1104 severity: Some(lsp::DiagnosticSeverity::ERROR),
1105 ..Default::default()
1106 },
1107 lsp::Diagnostic {
1108 range: lsp::Range::new(
1109 lsp::Position::new(0, 12),
1110 lsp::Position::new(0, 15),
1111 ),
1112 severity: Some(lsp::DiagnosticSeverity::ERROR),
1113 ..Default::default()
1114 },
1115 lsp::Diagnostic {
1116 range: lsp::Range::new(
1117 lsp::Position::new(0, 12),
1118 lsp::Position::new(0, 15),
1119 ),
1120 severity: Some(lsp::DiagnosticSeverity::ERROR),
1121 ..Default::default()
1122 },
1123 lsp::Diagnostic {
1124 range: lsp::Range::new(
1125 lsp::Position::new(0, 25),
1126 lsp::Position::new(0, 28),
1127 ),
1128 severity: Some(lsp::DiagnosticSeverity::ERROR),
1129 ..Default::default()
1130 },
1131 ],
1132 },
1133 None,
1134 DiagnosticSourceKind::Pushed,
1135 &[],
1136 cx,
1137 )
1138 .unwrap()
1139 });
1140 });
1141 cx.run_until_parked();
1142
1143 //// Backward
1144
1145 // Fourth diagnostic
1146 cx.update_editor(|editor, window, cx| {
1147 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
1148 });
1149 cx.assert_editor_state(indoc! {"
1150 fn func(abc def: i32) -> ˇu32 {
1151 }
1152 "});
1153
1154 // Third diagnostic
1155 cx.update_editor(|editor, window, cx| {
1156 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
1157 });
1158 cx.assert_editor_state(indoc! {"
1159 fn func(abc ˇdef: i32) -> u32 {
1160 }
1161 "});
1162
1163 // Second diagnostic, same place
1164 cx.update_editor(|editor, window, cx| {
1165 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
1166 });
1167 cx.assert_editor_state(indoc! {"
1168 fn func(abc ˇdef: i32) -> u32 {
1169 }
1170 "});
1171
1172 // First diagnostic
1173 cx.update_editor(|editor, window, cx| {
1174 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
1175 });
1176 cx.assert_editor_state(indoc! {"
1177 fn func(abcˇ def: i32) -> u32 {
1178 }
1179 "});
1180
1181 // Wrapped over, fourth diagnostic
1182 cx.update_editor(|editor, window, cx| {
1183 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
1184 });
1185 cx.assert_editor_state(indoc! {"
1186 fn func(abc def: i32) -> ˇu32 {
1187 }
1188 "});
1189
1190 cx.update_editor(|editor, window, cx| {
1191 editor.move_to_beginning(&MoveToBeginning, window, cx);
1192 });
1193 cx.assert_editor_state(indoc! {"
1194 ˇfn func(abc def: i32) -> u32 {
1195 }
1196 "});
1197
1198 //// Forward
1199
1200 // First diagnostic
1201 cx.update_editor(|editor, window, cx| {
1202 editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
1203 });
1204 cx.assert_editor_state(indoc! {"
1205 fn func(abcˇ def: i32) -> u32 {
1206 }
1207 "});
1208
1209 // Second diagnostic
1210 cx.update_editor(|editor, window, cx| {
1211 editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
1212 });
1213 cx.assert_editor_state(indoc! {"
1214 fn func(abc ˇdef: i32) -> u32 {
1215 }
1216 "});
1217
1218 // Third diagnostic, same place
1219 cx.update_editor(|editor, window, cx| {
1220 editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
1221 });
1222 cx.assert_editor_state(indoc! {"
1223 fn func(abc ˇdef: i32) -> u32 {
1224 }
1225 "});
1226
1227 // Fourth diagnostic
1228 cx.update_editor(|editor, window, cx| {
1229 editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
1230 });
1231 cx.assert_editor_state(indoc! {"
1232 fn func(abc def: i32) -> ˇu32 {
1233 }
1234 "});
1235
1236 // Wrapped around, first diagnostic
1237 cx.update_editor(|editor, window, cx| {
1238 editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
1239 });
1240 cx.assert_editor_state(indoc! {"
1241 fn func(abcˇ def: i32) -> u32 {
1242 }
1243 "});
1244}
1245
1246#[gpui::test]
1247async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
1248 init_test(cx);
1249
1250 let mut cx = EditorTestContext::new(cx).await;
1251
1252 cx.set_state(indoc! {"
1253 fn func(abˇc def: i32) -> u32 {
1254 }
1255 "});
1256 let lsp_store =
1257 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
1258
1259 cx.update(|_, cx| {
1260 lsp_store.update(cx, |lsp_store, cx| {
1261 lsp_store.update_diagnostics(
1262 LanguageServerId(0),
1263 lsp::PublishDiagnosticsParams {
1264 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
1265 version: None,
1266 diagnostics: vec![lsp::Diagnostic {
1267 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
1268 severity: Some(lsp::DiagnosticSeverity::ERROR),
1269 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
1270 ..Default::default()
1271 }],
1272 },
1273 None,
1274 DiagnosticSourceKind::Pushed,
1275 &[],
1276 cx,
1277 )
1278 })
1279 }).unwrap();
1280 cx.run_until_parked();
1281 cx.update_editor(|editor, window, cx| {
1282 editor::hover_popover::hover(editor, &Default::default(), window, cx)
1283 });
1284 cx.run_until_parked();
1285 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
1286}
1287
1288#[gpui::test]
1289async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
1290 init_test(cx);
1291
1292 let mut cx = EditorLspTestContext::new_rust(
1293 lsp::ServerCapabilities {
1294 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1295 ..Default::default()
1296 },
1297 cx,
1298 )
1299 .await;
1300
1301 // Hover with just diagnostic, pops DiagnosticPopover immediately and then
1302 // info popover once request completes
1303 cx.set_state(indoc! {"
1304 fn teˇst() { println!(); }
1305 "});
1306 // Send diagnostic to client
1307 let range = cx.lsp_range(indoc! {"
1308 fn «test»() { println!(); }
1309 "});
1310 let lsp_store =
1311 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
1312 cx.update(|_, cx| {
1313 lsp_store.update(cx, |lsp_store, cx| {
1314 lsp_store.update_diagnostics(
1315 LanguageServerId(0),
1316 lsp::PublishDiagnosticsParams {
1317 uri: lsp::Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
1318 version: None,
1319 diagnostics: vec![lsp::Diagnostic {
1320 range,
1321 severity: Some(lsp::DiagnosticSeverity::ERROR),
1322 message: "A test diagnostic message.".to_string(),
1323 ..Default::default()
1324 }],
1325 },
1326 None,
1327 DiagnosticSourceKind::Pushed,
1328 &[],
1329 cx,
1330 )
1331 })
1332 })
1333 .unwrap();
1334 cx.run_until_parked();
1335
1336 // Hover pops diagnostic immediately
1337 cx.update_editor(|editor, window, cx| editor::hover_popover::hover(editor, &Hover, window, cx));
1338 cx.background_executor.run_until_parked();
1339
1340 cx.editor(|Editor { hover_state, .. }, _, _| {
1341 assert!(hover_state.diagnostic_popover.is_some());
1342 assert!(hover_state.info_popovers.is_empty());
1343 });
1344
1345 // Info Popover shows after request responded to
1346 let range = cx.lsp_range(indoc! {"
1347 fn «test»() { println!(); }
1348 "});
1349 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1350 Ok(Some(lsp::Hover {
1351 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1352 kind: lsp::MarkupKind::Markdown,
1353 value: "some new docs".to_string(),
1354 }),
1355 range: Some(range),
1356 }))
1357 });
1358 let delay = cx.update(|_, cx| EditorSettings::get_global(cx).hover_popover_delay.0 + 1);
1359 cx.background_executor
1360 .advance_clock(Duration::from_millis(delay));
1361
1362 cx.background_executor.run_until_parked();
1363 cx.editor(|Editor { hover_state, .. }, _, _| {
1364 hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
1365 });
1366}
1367#[gpui::test]
1368async fn test_diagnostics_with_code(cx: &mut TestAppContext) {
1369 init_test(cx);
1370
1371 let fs = FakeFs::new(cx.executor());
1372 fs.insert_tree(
1373 path!("/root"),
1374 json!({
1375 "main.js": "
1376 function test() {
1377 const x = 10;
1378 const y = 20;
1379 return 1;
1380 }
1381 test();
1382 "
1383 .unindent(),
1384 }),
1385 )
1386 .await;
1387
1388 let language_server_id = LanguageServerId(0);
1389 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1390 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1391 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1392 let cx = &mut VisualTestContext::from_window(*window, cx);
1393 let workspace = window.root(cx).unwrap();
1394 let uri = lsp::Uri::from_file_path(path!("/root/main.js")).unwrap();
1395
1396 // Create diagnostics with code fields
1397 lsp_store.update(cx, |lsp_store, cx| {
1398 lsp_store
1399 .update_diagnostics(
1400 language_server_id,
1401 lsp::PublishDiagnosticsParams {
1402 uri: uri.clone(),
1403 diagnostics: vec![
1404 lsp::Diagnostic {
1405 range: lsp::Range::new(
1406 lsp::Position::new(1, 4),
1407 lsp::Position::new(1, 14),
1408 ),
1409 severity: Some(lsp::DiagnosticSeverity::WARNING),
1410 code: Some(lsp::NumberOrString::String("no-unused-vars".to_string())),
1411 source: Some("eslint".to_string()),
1412 message: "'x' is assigned a value but never used".to_string(),
1413 ..Default::default()
1414 },
1415 lsp::Diagnostic {
1416 range: lsp::Range::new(
1417 lsp::Position::new(2, 4),
1418 lsp::Position::new(2, 14),
1419 ),
1420 severity: Some(lsp::DiagnosticSeverity::WARNING),
1421 code: Some(lsp::NumberOrString::String("no-unused-vars".to_string())),
1422 source: Some("eslint".to_string()),
1423 message: "'y' is assigned a value but never used".to_string(),
1424 ..Default::default()
1425 },
1426 ],
1427 version: None,
1428 },
1429 None,
1430 DiagnosticSourceKind::Pushed,
1431 &[],
1432 cx,
1433 )
1434 .unwrap();
1435 });
1436
1437 // Open the project diagnostics view
1438 let diagnostics = window.build_entity(cx, |window, cx| {
1439 ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
1440 });
1441 let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
1442
1443 diagnostics
1444 .next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
1445 .await;
1446
1447 // Verify that the diagnostic codes are displayed correctly
1448 pretty_assertions::assert_eq!(
1449 editor_content_with_blocks(&editor, cx),
1450 indoc::indoc! {
1451 "§ main.js
1452 § -----
1453 function test() {
1454 const x = 10; § 'x' is assigned a value but never used (eslint no-unused-vars)
1455 const y = 20; § 'y' is assigned a value but never used (eslint no-unused-vars)
1456 return 1;
1457 }"
1458 }
1459 );
1460}
1461
1462#[gpui::test]
1463async fn go_to_diagnostic_with_severity(cx: &mut TestAppContext) {
1464 init_test(cx);
1465
1466 let mut cx = EditorTestContext::new(cx).await;
1467 let lsp_store =
1468 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
1469
1470 cx.set_state(indoc! {"error warning info hiˇnt"});
1471
1472 cx.update(|_, cx| {
1473 lsp_store.update(cx, |lsp_store, cx| {
1474 lsp_store
1475 .update_diagnostics(
1476 LanguageServerId(0),
1477 lsp::PublishDiagnosticsParams {
1478 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
1479 version: None,
1480 diagnostics: vec![
1481 lsp::Diagnostic {
1482 range: lsp::Range::new(
1483 lsp::Position::new(0, 0),
1484 lsp::Position::new(0, 5),
1485 ),
1486 severity: Some(lsp::DiagnosticSeverity::ERROR),
1487 ..Default::default()
1488 },
1489 lsp::Diagnostic {
1490 range: lsp::Range::new(
1491 lsp::Position::new(0, 6),
1492 lsp::Position::new(0, 13),
1493 ),
1494 severity: Some(lsp::DiagnosticSeverity::WARNING),
1495 ..Default::default()
1496 },
1497 lsp::Diagnostic {
1498 range: lsp::Range::new(
1499 lsp::Position::new(0, 14),
1500 lsp::Position::new(0, 18),
1501 ),
1502 severity: Some(lsp::DiagnosticSeverity::INFORMATION),
1503 ..Default::default()
1504 },
1505 lsp::Diagnostic {
1506 range: lsp::Range::new(
1507 lsp::Position::new(0, 19),
1508 lsp::Position::new(0, 23),
1509 ),
1510 severity: Some(lsp::DiagnosticSeverity::HINT),
1511 ..Default::default()
1512 },
1513 ],
1514 },
1515 None,
1516 DiagnosticSourceKind::Pushed,
1517 &[],
1518 cx,
1519 )
1520 .unwrap()
1521 });
1522 });
1523 cx.run_until_parked();
1524
1525 macro_rules! go {
1526 ($severity:expr) => {
1527 cx.update_editor(|editor, window, cx| {
1528 editor.go_to_diagnostic(
1529 &GoToDiagnostic {
1530 severity: $severity,
1531 },
1532 window,
1533 cx,
1534 );
1535 });
1536 };
1537 }
1538
1539 // Default, should cycle through all diagnostics
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 go!(GoToDiagnosticSeverityFilter::default());
1547 cx.assert_editor_state(indoc! {"error warning info ˇhint"});
1548 go!(GoToDiagnosticSeverityFilter::default());
1549 cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
1550
1551 let only_info = GoToDiagnosticSeverityFilter::Only(GoToDiagnosticSeverity::Information);
1552 go!(only_info);
1553 cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
1554 go!(only_info);
1555 cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
1556
1557 let no_hints = GoToDiagnosticSeverityFilter::Range {
1558 min: GoToDiagnosticSeverity::Information,
1559 max: GoToDiagnosticSeverity::Error,
1560 };
1561
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 go!(no_hints);
1567 cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
1568 go!(no_hints);
1569 cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
1570
1571 let warning_info = GoToDiagnosticSeverityFilter::Range {
1572 min: GoToDiagnosticSeverity::Information,
1573 max: GoToDiagnosticSeverity::Warning,
1574 };
1575
1576 go!(warning_info);
1577 cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
1578 go!(warning_info);
1579 cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
1580 go!(warning_info);
1581 cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
1582}
1583
1584#[gpui::test]
1585async fn test_buffer_diagnostics(cx: &mut TestAppContext) {
1586 init_test(cx);
1587
1588 // We'll be creating two different files, both with diagnostics, so we can
1589 // later verify that, since the `BufferDiagnosticsEditor` only shows
1590 // diagnostics for the provided path, the diagnostics for the other file
1591 // will not be shown, contrary to what happens with
1592 // `ProjectDiagnosticsEditor`.
1593 let fs = FakeFs::new(cx.executor());
1594 fs.insert_tree(
1595 path!("/test"),
1596 json!({
1597 "main.rs": "
1598 fn main() {
1599 let x = vec![];
1600 let y = vec![];
1601 a(x);
1602 b(y);
1603 c(y);
1604 d(x);
1605 }
1606 "
1607 .unindent(),
1608 "other.rs": "
1609 fn other() {
1610 let unused = 42;
1611 undefined_function();
1612 }
1613 "
1614 .unindent(),
1615 }),
1616 )
1617 .await;
1618
1619 let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
1620 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1621 let cx = &mut VisualTestContext::from_window(*window, cx);
1622 let project_path = project::ProjectPath {
1623 worktree_id: project.read_with(cx, |project, cx| {
1624 project.worktrees(cx).next().unwrap().read(cx).id()
1625 }),
1626 path: rel_path("main.rs").into(),
1627 };
1628 let buffer = project
1629 .update(cx, |project, cx| {
1630 project.open_buffer(project_path.clone(), cx)
1631 })
1632 .await
1633 .ok();
1634
1635 // Create the diagnostics for `main.rs`.
1636 let language_server_id = LanguageServerId(0);
1637 let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap();
1638 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1639
1640 lsp_store.update(cx, |lsp_store, cx| {
1641 lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
1642 uri: uri.clone(),
1643 diagnostics: vec![
1644 lsp::Diagnostic{
1645 range: lsp::Range::new(lsp::Position::new(5, 6), lsp::Position::new(5, 7)),
1646 severity: Some(lsp::DiagnosticSeverity::WARNING),
1647 message: "use of moved value\nvalue used here after move".to_string(),
1648 related_information: Some(vec![
1649 lsp::DiagnosticRelatedInformation {
1650 location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 9))),
1651 message: "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
1652 },
1653 lsp::DiagnosticRelatedInformation {
1654 location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 7))),
1655 message: "value moved here".to_string()
1656 },
1657 ]),
1658 ..Default::default()
1659 },
1660 lsp::Diagnostic{
1661 range: lsp::Range::new(lsp::Position::new(6, 6), lsp::Position::new(6, 7)),
1662 severity: Some(lsp::DiagnosticSeverity::ERROR),
1663 message: "use of moved value\nvalue used here after move".to_string(),
1664 related_information: Some(vec![
1665 lsp::DiagnosticRelatedInformation {
1666 location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9))),
1667 message: "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
1668 },
1669 lsp::DiagnosticRelatedInformation {
1670 location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(3, 6), lsp::Position::new(3, 7))),
1671 message: "value moved here".to_string()
1672 },
1673 ]),
1674 ..Default::default()
1675 }
1676 ],
1677 version: None
1678 }, None, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
1679
1680 // Create diagnostics for other.rs to ensure that the file and
1681 // diagnostics are not included in `BufferDiagnosticsEditor` when it is
1682 // deployed for main.rs.
1683 lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
1684 uri: lsp::Uri::from_file_path(path!("/test/other.rs")).unwrap(),
1685 diagnostics: vec![
1686 lsp::Diagnostic{
1687 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 14)),
1688 severity: Some(lsp::DiagnosticSeverity::WARNING),
1689 message: "unused variable: `unused`".to_string(),
1690 ..Default::default()
1691 },
1692 lsp::Diagnostic{
1693 range: lsp::Range::new(lsp::Position::new(2, 4), lsp::Position::new(2, 22)),
1694 severity: Some(lsp::DiagnosticSeverity::ERROR),
1695 message: "cannot find function `undefined_function` in this scope".to_string(),
1696 ..Default::default()
1697 }
1698 ],
1699 version: None
1700 }, None, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
1701 });
1702
1703 let buffer_diagnostics = window.build_entity(cx, |window, cx| {
1704 BufferDiagnosticsEditor::new(
1705 project_path.clone(),
1706 project.clone(),
1707 buffer,
1708 true,
1709 window,
1710 cx,
1711 )
1712 });
1713 let editor = buffer_diagnostics.update(cx, |buffer_diagnostics, _| {
1714 buffer_diagnostics.editor().clone()
1715 });
1716
1717 // Since the excerpt updates is handled by a background task, we need to
1718 // wait a little bit to ensure that the buffer diagnostic's editor content
1719 // is rendered.
1720 cx.executor()
1721 .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
1722
1723 pretty_assertions::assert_eq!(
1724 editor_content_with_blocks(&editor, cx),
1725 indoc::indoc! {
1726 "§ main.rs
1727 § -----
1728 fn main() {
1729 let x = vec![];
1730 § move occurs because `x` has type `Vec<char>`, which does not implement
1731 § the `Copy` trait (back)
1732 let y = vec![];
1733 § move occurs because `y` has type `Vec<char>`, which does not implement
1734 § the `Copy` trait
1735 a(x); § value moved here
1736 b(y); § value moved here
1737 c(y);
1738 § use of moved value
1739 § value used here after move
1740 d(x);
1741 § use of moved value
1742 § value used here after move
1743 § hint: move occurs because `x` has type `Vec<char>`, which does not
1744 § implement the `Copy` trait
1745 }"
1746 }
1747 );
1748}
1749
1750#[gpui::test]
1751async fn test_buffer_diagnostics_without_warnings(cx: &mut TestAppContext) {
1752 init_test(cx);
1753
1754 let fs = FakeFs::new(cx.executor());
1755 fs.insert_tree(
1756 path!("/test"),
1757 json!({
1758 "main.rs": "
1759 fn main() {
1760 let x = vec![];
1761 let y = vec![];
1762 a(x);
1763 b(y);
1764 c(y);
1765 d(x);
1766 }
1767 "
1768 .unindent(),
1769 }),
1770 )
1771 .await;
1772
1773 let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
1774 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1775 let cx = &mut VisualTestContext::from_window(*window, cx);
1776 let project_path = project::ProjectPath {
1777 worktree_id: project.read_with(cx, |project, cx| {
1778 project.worktrees(cx).next().unwrap().read(cx).id()
1779 }),
1780 path: rel_path("main.rs").into(),
1781 };
1782 let buffer = project
1783 .update(cx, |project, cx| {
1784 project.open_buffer(project_path.clone(), cx)
1785 })
1786 .await
1787 .ok();
1788
1789 let language_server_id = LanguageServerId(0);
1790 let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap();
1791 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1792
1793 lsp_store.update(cx, |lsp_store, cx| {
1794 lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
1795 uri: uri.clone(),
1796 diagnostics: vec![
1797 lsp::Diagnostic{
1798 range: lsp::Range::new(lsp::Position::new(5, 6), lsp::Position::new(5, 7)),
1799 severity: Some(lsp::DiagnosticSeverity::WARNING),
1800 message: "use of moved value\nvalue used here after move".to_string(),
1801 related_information: Some(vec![
1802 lsp::DiagnosticRelatedInformation {
1803 location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 9))),
1804 message: "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
1805 },
1806 lsp::DiagnosticRelatedInformation {
1807 location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 7))),
1808 message: "value moved here".to_string()
1809 },
1810 ]),
1811 ..Default::default()
1812 },
1813 lsp::Diagnostic{
1814 range: lsp::Range::new(lsp::Position::new(6, 6), lsp::Position::new(6, 7)),
1815 severity: Some(lsp::DiagnosticSeverity::ERROR),
1816 message: "use of moved value\nvalue used here after move".to_string(),
1817 related_information: Some(vec![
1818 lsp::DiagnosticRelatedInformation {
1819 location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9))),
1820 message: "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
1821 },
1822 lsp::DiagnosticRelatedInformation {
1823 location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(3, 6), lsp::Position::new(3, 7))),
1824 message: "value moved here".to_string()
1825 },
1826 ]),
1827 ..Default::default()
1828 }
1829 ],
1830 version: None
1831 }, None, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
1832 });
1833
1834 let include_warnings = false;
1835 let buffer_diagnostics = window.build_entity(cx, |window, cx| {
1836 BufferDiagnosticsEditor::new(
1837 project_path.clone(),
1838 project.clone(),
1839 buffer,
1840 include_warnings,
1841 window,
1842 cx,
1843 )
1844 });
1845
1846 let editor = buffer_diagnostics.update(cx, |buffer_diagnostics, _cx| {
1847 buffer_diagnostics.editor().clone()
1848 });
1849
1850 // Since the excerpt updates is handled by a background task, we need to
1851 // wait a little bit to ensure that the buffer diagnostic's editor content
1852 // is rendered.
1853 cx.executor()
1854 .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
1855
1856 pretty_assertions::assert_eq!(
1857 editor_content_with_blocks(&editor, cx),
1858 indoc::indoc! {
1859 "§ main.rs
1860 § -----
1861 fn main() {
1862 let x = vec![];
1863 § move occurs because `x` has type `Vec<char>`, which does not implement
1864 § the `Copy` trait (back)
1865 let y = vec![];
1866 a(x); § value moved here
1867 b(y);
1868 c(y);
1869 d(x);
1870 § use of moved value
1871 § value used here after move
1872 § hint: move occurs because `x` has type `Vec<char>`, which does not
1873 § implement the `Copy` trait
1874 }"
1875 }
1876 );
1877}
1878
1879#[gpui::test]
1880async fn test_buffer_diagnostics_multiple_servers(cx: &mut TestAppContext) {
1881 init_test(cx);
1882
1883 let fs = FakeFs::new(cx.executor());
1884 fs.insert_tree(
1885 path!("/test"),
1886 json!({
1887 "main.rs": "
1888 fn main() {
1889 let x = vec![];
1890 let y = vec![];
1891 a(x);
1892 b(y);
1893 c(y);
1894 d(x);
1895 }
1896 "
1897 .unindent(),
1898 }),
1899 )
1900 .await;
1901
1902 let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
1903 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1904 let cx = &mut VisualTestContext::from_window(*window, cx);
1905 let project_path = project::ProjectPath {
1906 worktree_id: project.read_with(cx, |project, cx| {
1907 project.worktrees(cx).next().unwrap().read(cx).id()
1908 }),
1909 path: rel_path("main.rs").into(),
1910 };
1911 let buffer = project
1912 .update(cx, |project, cx| {
1913 project.open_buffer(project_path.clone(), cx)
1914 })
1915 .await
1916 .ok();
1917
1918 // Create the diagnostics for `main.rs`.
1919 // Two warnings are being created, one for each language server, in order to
1920 // assert that both warnings are rendered in the editor.
1921 let language_server_id_a = LanguageServerId(0);
1922 let language_server_id_b = LanguageServerId(1);
1923 let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap();
1924 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1925
1926 lsp_store.update(cx, |lsp_store, cx| {
1927 lsp_store
1928 .update_diagnostics(
1929 language_server_id_a,
1930 lsp::PublishDiagnosticsParams {
1931 uri: uri.clone(),
1932 diagnostics: vec![lsp::Diagnostic {
1933 range: lsp::Range::new(lsp::Position::new(5, 6), lsp::Position::new(5, 7)),
1934 severity: Some(lsp::DiagnosticSeverity::WARNING),
1935 message: "use of moved value\nvalue used here after move".to_string(),
1936 related_information: None,
1937 ..Default::default()
1938 }],
1939 version: None,
1940 },
1941 None,
1942 DiagnosticSourceKind::Pushed,
1943 &[],
1944 cx,
1945 )
1946 .unwrap();
1947
1948 lsp_store
1949 .update_diagnostics(
1950 language_server_id_b,
1951 lsp::PublishDiagnosticsParams {
1952 uri: uri.clone(),
1953 diagnostics: vec![lsp::Diagnostic {
1954 range: lsp::Range::new(lsp::Position::new(6, 6), lsp::Position::new(6, 7)),
1955 severity: Some(lsp::DiagnosticSeverity::WARNING),
1956 message: "use of moved value\nvalue used here after move".to_string(),
1957 related_information: None,
1958 ..Default::default()
1959 }],
1960 version: None,
1961 },
1962 None,
1963 DiagnosticSourceKind::Pushed,
1964 &[],
1965 cx,
1966 )
1967 .unwrap();
1968 });
1969
1970 let buffer_diagnostics = window.build_entity(cx, |window, cx| {
1971 BufferDiagnosticsEditor::new(
1972 project_path.clone(),
1973 project.clone(),
1974 buffer,
1975 true,
1976 window,
1977 cx,
1978 )
1979 });
1980 let editor = buffer_diagnostics.update(cx, |buffer_diagnostics, _| {
1981 buffer_diagnostics.editor().clone()
1982 });
1983
1984 // Since the excerpt updates is handled by a background task, we need to
1985 // wait a little bit to ensure that the buffer diagnostic's editor content
1986 // is rendered.
1987 cx.executor()
1988 .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
1989
1990 pretty_assertions::assert_eq!(
1991 editor_content_with_blocks(&editor, cx),
1992 indoc::indoc! {
1993 "§ main.rs
1994 § -----
1995 a(x);
1996 b(y);
1997 c(y);
1998 § use of moved value
1999 § value used here after move
2000 d(x);
2001 § use of moved value
2002 § value used here after move
2003 }"
2004 }
2005 );
2006
2007 buffer_diagnostics.update(cx, |buffer_diagnostics, _cx| {
2008 assert_eq!(
2009 *buffer_diagnostics.summary(),
2010 DiagnosticSummary {
2011 warning_count: 2,
2012 error_count: 0
2013 }
2014 );
2015 })
2016}
2017
2018fn init_test(cx: &mut TestAppContext) {
2019 cx.update(|cx| {
2020 zlog::init_test();
2021 let settings = SettingsStore::test(cx);
2022 cx.set_global(settings);
2023 theme::init(theme::LoadThemes::JustBase, 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(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}