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