1use super::*;
2use collections::{HashMap, HashSet};
3use editor::{
4 DisplayPoint, EditorSettings, InlayId,
5 actions::{GoToDiagnostic, GoToPreviousDiagnostic, Hover, MoveToBeginning},
6 display_map::{DisplayRow, Inlay},
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::Rope;
15use lsp::LanguageServerId;
16use pretty_assertions::assert_eq;
17use project::FakeFs;
18use rand::{Rng, rngs::StdRng, seq::IteratorRandom as _};
19use serde_json::json;
20use settings::SettingsStore;
21use std::{
22 env,
23 path::{Path, PathBuf},
24};
25use unindent::Unindent as _;
26use util::{RandomCharIter, path, post_inc};
27
28#[ctor::ctor]
29fn init_logger() {
30 if env::var("RUST_LOG").is_ok() {
31 env_logger::init();
32 }
33}
34
35#[gpui::test]
36async fn test_diagnostics(cx: &mut TestAppContext) {
37 init_test(cx);
38
39 let fs = FakeFs::new(cx.executor());
40 fs.insert_tree(
41 path!("/test"),
42 json!({
43 "consts.rs": "
44 const a: i32 = 'a';
45 const b: i32 = c;
46 "
47 .unindent(),
48
49 "main.rs": "
50 fn main() {
51 let x = vec![];
52 let y = vec![];
53 a(x);
54 b(y);
55 // comment 1
56 // comment 2
57 c(y);
58 d(x);
59 }
60 "
61 .unindent(),
62 }),
63 )
64 .await;
65
66 let language_server_id = LanguageServerId(0);
67 let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
68 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
69 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
70 let cx = &mut VisualTestContext::from_window(*window, cx);
71 let workspace = window.root(cx).unwrap();
72 let uri = lsp::Url::from_file_path(path!("/test/main.rs")).unwrap();
73
74 // Create some diagnostics
75 lsp_store.update(cx, |lsp_store, cx| {
76 lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
77 uri: uri.clone(),
78 diagnostics: vec![lsp::Diagnostic{
79 range: lsp::Range::new(lsp::Position::new(7, 6),lsp::Position::new(7, 7)),
80 severity:Some(lsp::DiagnosticSeverity::ERROR),
81 message: "use of moved value\nvalue used here after move".to_string(),
82 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
83 location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(2,8),lsp::Position::new(2,9))),
84 message: "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
85 },
86 lsp::DiagnosticRelatedInformation {
87 location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(4,6),lsp::Position::new(4,7))),
88 message: "value moved here".to_string()
89 },
90 ]),
91 ..Default::default()
92 },
93 lsp::Diagnostic{
94 range: lsp::Range::new(lsp::Position::new(8, 6),lsp::Position::new(8, 7)),
95 severity:Some(lsp::DiagnosticSeverity::ERROR),
96 message: "use of moved value\nvalue used here after move".to_string(),
97 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
98 location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(1,8),lsp::Position::new(1,9))),
99 message: "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
100 },
101 lsp::DiagnosticRelatedInformation {
102 location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(3,6),lsp::Position::new(3,7))),
103 message: "value moved here".to_string()
104 },
105 ]),
106 ..Default::default()
107 }
108 ],
109 version: None
110 }, &[], cx).unwrap();
111 });
112
113 // Open the project diagnostics view while there are already diagnostics.
114 let diagnostics = window.build_entity(cx, |window, cx| {
115 ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
116 });
117 let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
118
119 diagnostics
120 .next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
121 .await;
122
123 pretty_assertions::assert_eq!(
124 editor_content_with_blocks(&editor, cx),
125 indoc::indoc! {
126 "§ main.rs
127 § -----
128 fn main() {
129 let x = vec![];
130 § move occurs because `x` has type `Vec<char>`, which does not implement
131 § the `Copy` trait (back)
132 let y = vec![];
133 § move occurs because `y` has type `Vec<char>`, which does not implement
134 § the `Copy` trait (back)
135 a(x); § value moved here (back)
136 b(y); § value moved here
137 // comment 1
138 // comment 2
139 c(y);
140 § use of moved value
141 § value used here after move
142 § hint: move occurs because `y` has type `Vec<char>`, which does not
143 § implement the `Copy` trait
144 d(x);
145 § use of moved value
146 § value used here after move
147 § hint: move occurs because `x` has type `Vec<char>`, which does not
148 § implement the `Copy` trait
149 § hint: value moved here
150 }"
151 }
152 );
153
154 // Cursor is at the first diagnostic
155 editor.update(cx, |editor, cx| {
156 assert_eq!(
157 editor.selections.display_ranges(cx),
158 [DisplayPoint::new(DisplayRow(3), 8)..DisplayPoint::new(DisplayRow(3), 8)]
159 );
160 });
161
162 // Diagnostics are added for another earlier path.
163 lsp_store.update(cx, |lsp_store, cx| {
164 lsp_store.disk_based_diagnostics_started(language_server_id, cx);
165 lsp_store
166 .update_diagnostics(
167 language_server_id,
168 lsp::PublishDiagnosticsParams {
169 uri: lsp::Url::from_file_path(path!("/test/consts.rs")).unwrap(),
170 diagnostics: vec![lsp::Diagnostic {
171 range: lsp::Range::new(
172 lsp::Position::new(0, 15),
173 lsp::Position::new(0, 15),
174 ),
175 severity: Some(lsp::DiagnosticSeverity::ERROR),
176 message: "mismatched types expected `usize`, found `char`".to_string(),
177 ..Default::default()
178 }],
179 version: None,
180 },
181 &[],
182 cx,
183 )
184 .unwrap();
185 lsp_store.disk_based_diagnostics_finished(language_server_id, cx);
186 });
187
188 diagnostics
189 .next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
190 .await;
191
192 pretty_assertions::assert_eq!(
193 editor_content_with_blocks(&editor, cx),
194 indoc::indoc! {
195 "§ consts.rs
196 § -----
197 const a: i32 = 'a'; § mismatched types expected `usize`, found `char`
198 const b: i32 = c;
199
200 § main.rs
201 § -----
202 fn main() {
203 let x = vec![];
204 § move occurs because `x` has type `Vec<char>`, which does not implement
205 § the `Copy` trait (back)
206 let y = vec![];
207 § move occurs because `y` has type `Vec<char>`, which does not implement
208 § the `Copy` trait (back)
209 a(x); § value moved here (back)
210 b(y); § value moved here
211 // comment 1
212 // comment 2
213 c(y);
214 § use of moved value
215 § value used here after move
216 § hint: move occurs because `y` has type `Vec<char>`, which does not
217 § implement the `Copy` trait
218 d(x);
219 § use of moved value
220 § value used here after move
221 § hint: move occurs because `x` has type `Vec<char>`, which does not
222 § implement the `Copy` trait
223 § hint: value moved here
224 }"
225 }
226 );
227
228 // Cursor keeps its position.
229 editor.update(cx, |editor, cx| {
230 assert_eq!(
231 editor.selections.display_ranges(cx),
232 [DisplayPoint::new(DisplayRow(8), 8)..DisplayPoint::new(DisplayRow(8), 8)]
233 );
234 });
235
236 // Diagnostics are added to the first path
237 lsp_store.update(cx, |lsp_store, cx| {
238 lsp_store.disk_based_diagnostics_started(language_server_id, cx);
239 lsp_store
240 .update_diagnostics(
241 language_server_id,
242 lsp::PublishDiagnosticsParams {
243 uri: lsp::Url::from_file_path(path!("/test/consts.rs")).unwrap(),
244 diagnostics: vec![
245 lsp::Diagnostic {
246 range: lsp::Range::new(
247 lsp::Position::new(0, 15),
248 lsp::Position::new(0, 15),
249 ),
250 severity: Some(lsp::DiagnosticSeverity::ERROR),
251 message: "mismatched types expected `usize`, found `char`".to_string(),
252 ..Default::default()
253 },
254 lsp::Diagnostic {
255 range: lsp::Range::new(
256 lsp::Position::new(1, 15),
257 lsp::Position::new(1, 15),
258 ),
259 severity: Some(lsp::DiagnosticSeverity::ERROR),
260 message: "unresolved name `c`".to_string(),
261 ..Default::default()
262 },
263 ],
264 version: None,
265 },
266 &[],
267 cx,
268 )
269 .unwrap();
270 lsp_store.disk_based_diagnostics_finished(language_server_id, cx);
271 });
272
273 diagnostics
274 .next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
275 .await;
276
277 pretty_assertions::assert_eq!(
278 editor_content_with_blocks(&editor, cx),
279 indoc::indoc! {
280 "§ consts.rs
281 § -----
282 const a: i32 = 'a'; § mismatched types expected `usize`, found `char`
283 const b: i32 = c; § unresolved name `c`
284
285 § main.rs
286 § -----
287 fn main() {
288 let x = vec![];
289 § move occurs because `x` has type `Vec<char>`, which does not implement
290 § the `Copy` trait (back)
291 let y = vec![];
292 § move occurs because `y` has type `Vec<char>`, which does not implement
293 § the `Copy` trait (back)
294 a(x); § value moved here (back)
295 b(y); § value moved here
296 // comment 1
297 // comment 2
298 c(y);
299 § use of moved value
300 § value used here after move
301 § hint: move occurs because `y` has type `Vec<char>`, which does not
302 § implement the `Copy` trait
303 d(x);
304 § use of moved value
305 § value used here after move
306 § hint: move occurs because `x` has type `Vec<char>`, which does not
307 § implement the `Copy` trait
308 § hint: value moved here
309 }"
310 }
311 );
312}
313
314#[gpui::test]
315async fn test_diagnostics_with_folds(cx: &mut TestAppContext) {
316 init_test(cx);
317
318 let fs = FakeFs::new(cx.executor());
319 fs.insert_tree(
320 path!("/test"),
321 json!({
322 "main.js": "
323 function test() {
324 return 1
325 };
326
327 tset();
328 ".unindent()
329 }),
330 )
331 .await;
332
333 let server_id_1 = LanguageServerId(100);
334 let server_id_2 = LanguageServerId(101);
335 let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
336 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
337 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
338 let cx = &mut VisualTestContext::from_window(*window, cx);
339 let workspace = window.root(cx).unwrap();
340
341 let diagnostics = window.build_entity(cx, |window, cx| {
342 ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
343 });
344 let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
345
346 // Two language servers start updating diagnostics
347 lsp_store.update(cx, |lsp_store, cx| {
348 lsp_store.disk_based_diagnostics_started(server_id_1, cx);
349 lsp_store.disk_based_diagnostics_started(server_id_2, cx);
350 lsp_store
351 .update_diagnostics(
352 server_id_1,
353 lsp::PublishDiagnosticsParams {
354 uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
355 diagnostics: vec![lsp::Diagnostic {
356 range: lsp::Range::new(lsp::Position::new(4, 0), lsp::Position::new(4, 4)),
357 severity: Some(lsp::DiagnosticSeverity::WARNING),
358 message: "no method `tset`".to_string(),
359 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
360 location: lsp::Location::new(
361 lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
362 lsp::Range::new(
363 lsp::Position::new(0, 9),
364 lsp::Position::new(0, 13),
365 ),
366 ),
367 message: "method `test` defined here".to_string(),
368 }]),
369 ..Default::default()
370 }],
371 version: None,
372 },
373 &[],
374 cx,
375 )
376 .unwrap();
377 });
378
379 // The first language server finishes
380 lsp_store.update(cx, |lsp_store, cx| {
381 lsp_store.disk_based_diagnostics_finished(server_id_1, cx);
382 });
383
384 // Only the first language server's diagnostics are shown.
385 cx.executor()
386 .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
387 cx.executor().run_until_parked();
388 editor.update_in(cx, |editor, window, cx| {
389 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(3, 0)], false, window, cx);
390 });
391
392 pretty_assertions::assert_eq!(
393 editor_content_with_blocks(&editor, cx),
394 indoc::indoc! {
395 "§ main.js
396 § -----
397 ⋯
398
399 tset(); § no method `tset`"
400 }
401 );
402
403 editor.update(cx, |editor, cx| {
404 editor.unfold_ranges(&[Point::new(0, 0)..Point::new(3, 0)], false, false, cx);
405 });
406
407 pretty_assertions::assert_eq!(
408 editor_content_with_blocks(&editor, cx),
409 indoc::indoc! {
410 "§ main.js
411 § -----
412 function test() { § method `test` defined here
413 return 1
414 };
415
416 tset(); § no method `tset`"
417 }
418 );
419}
420
421#[gpui::test]
422async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
423 init_test(cx);
424
425 let fs = FakeFs::new(cx.executor());
426 fs.insert_tree(
427 path!("/test"),
428 json!({
429 "main.js": "
430 a();
431 b();
432 c();
433 d();
434 e();
435 ".unindent()
436 }),
437 )
438 .await;
439
440 let server_id_1 = LanguageServerId(100);
441 let server_id_2 = LanguageServerId(101);
442 let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
443 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
444 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
445 let cx = &mut VisualTestContext::from_window(*window, cx);
446 let workspace = window.root(cx).unwrap();
447
448 let diagnostics = window.build_entity(cx, |window, cx| {
449 ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
450 });
451 let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
452
453 // Two language servers start updating diagnostics
454 lsp_store.update(cx, |lsp_store, cx| {
455 lsp_store.disk_based_diagnostics_started(server_id_1, cx);
456 lsp_store.disk_based_diagnostics_started(server_id_2, cx);
457 lsp_store
458 .update_diagnostics(
459 server_id_1,
460 lsp::PublishDiagnosticsParams {
461 uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
462 diagnostics: vec![lsp::Diagnostic {
463 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 1)),
464 severity: Some(lsp::DiagnosticSeverity::WARNING),
465 message: "error 1".to_string(),
466 ..Default::default()
467 }],
468 version: None,
469 },
470 &[],
471 cx,
472 )
473 .unwrap();
474 });
475
476 // The first language server finishes
477 lsp_store.update(cx, |lsp_store, cx| {
478 lsp_store.disk_based_diagnostics_finished(server_id_1, cx);
479 });
480
481 // Only the first language server's diagnostics are shown.
482 cx.executor()
483 .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
484 cx.executor().run_until_parked();
485
486 pretty_assertions::assert_eq!(
487 editor_content_with_blocks(&editor, cx),
488 indoc::indoc! {
489 "§ main.js
490 § -----
491 a(); § error 1
492 b();
493 c();"
494 }
495 );
496
497 // The second language server finishes
498 lsp_store.update(cx, |lsp_store, cx| {
499 lsp_store
500 .update_diagnostics(
501 server_id_2,
502 lsp::PublishDiagnosticsParams {
503 uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
504 diagnostics: vec![lsp::Diagnostic {
505 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 1)),
506 severity: Some(lsp::DiagnosticSeverity::ERROR),
507 message: "warning 1".to_string(),
508 ..Default::default()
509 }],
510 version: None,
511 },
512 &[],
513 cx,
514 )
515 .unwrap();
516 lsp_store.disk_based_diagnostics_finished(server_id_2, cx);
517 });
518
519 // Both language server's diagnostics are shown.
520 cx.executor()
521 .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
522 cx.executor().run_until_parked();
523
524 pretty_assertions::assert_eq!(
525 editor_content_with_blocks(&editor, cx),
526 indoc::indoc! {
527 "§ main.js
528 § -----
529 a(); § error 1
530 b(); § warning 1
531 c();
532 d();"
533 }
534 );
535
536 // Both language servers start updating diagnostics, and the first server finishes.
537 lsp_store.update(cx, |lsp_store, cx| {
538 lsp_store.disk_based_diagnostics_started(server_id_1, cx);
539 lsp_store.disk_based_diagnostics_started(server_id_2, cx);
540 lsp_store
541 .update_diagnostics(
542 server_id_1,
543 lsp::PublishDiagnosticsParams {
544 uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
545 diagnostics: vec![lsp::Diagnostic {
546 range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 1)),
547 severity: Some(lsp::DiagnosticSeverity::WARNING),
548 message: "warning 2".to_string(),
549 ..Default::default()
550 }],
551 version: None,
552 },
553 &[],
554 cx,
555 )
556 .unwrap();
557 lsp_store
558 .update_diagnostics(
559 server_id_2,
560 lsp::PublishDiagnosticsParams {
561 uri: lsp::Url::from_file_path(path!("/test/main.rs")).unwrap(),
562 diagnostics: vec![],
563 version: None,
564 },
565 &[],
566 cx,
567 )
568 .unwrap();
569 lsp_store.disk_based_diagnostics_finished(server_id_1, cx);
570 });
571
572 // Only the first language server's diagnostics are updated.
573 cx.executor()
574 .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
575 cx.executor().run_until_parked();
576
577 pretty_assertions::assert_eq!(
578 editor_content_with_blocks(&editor, cx),
579 indoc::indoc! {
580 "§ main.js
581 § -----
582 a();
583 b(); § warning 1
584 c(); § warning 2
585 d();
586 e();"
587 }
588 );
589
590 // The second language server finishes.
591 lsp_store.update(cx, |lsp_store, cx| {
592 lsp_store
593 .update_diagnostics(
594 server_id_2,
595 lsp::PublishDiagnosticsParams {
596 uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
597 diagnostics: vec![lsp::Diagnostic {
598 range: lsp::Range::new(lsp::Position::new(3, 0), lsp::Position::new(3, 1)),
599 severity: Some(lsp::DiagnosticSeverity::WARNING),
600 message: "warning 2".to_string(),
601 ..Default::default()
602 }],
603 version: None,
604 },
605 &[],
606 cx,
607 )
608 .unwrap();
609 lsp_store.disk_based_diagnostics_finished(server_id_2, cx);
610 });
611
612 // Both language servers' diagnostics are updated.
613 cx.executor()
614 .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
615 cx.executor().run_until_parked();
616
617 pretty_assertions::assert_eq!(
618 editor_content_with_blocks(&editor, cx),
619 indoc::indoc! {
620 "§ main.js
621 § -----
622 a();
623 b();
624 c(); § warning 2
625 d(); § warning 2
626 e();"
627 }
628 );
629}
630
631#[gpui::test(iterations = 20)]
632async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng) {
633 init_test(cx);
634
635 let operations = env::var("OPERATIONS")
636 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
637 .unwrap_or(10);
638
639 let fs = FakeFs::new(cx.executor());
640 fs.insert_tree(path!("/test"), json!({})).await;
641
642 let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
643 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
644 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
645 let cx = &mut VisualTestContext::from_window(*window, cx);
646 let workspace = window.root(cx).unwrap();
647
648 let mutated_diagnostics = window.build_entity(cx, |window, cx| {
649 ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
650 });
651
652 workspace.update_in(cx, |workspace, window, cx| {
653 workspace.add_item_to_center(Box::new(mutated_diagnostics.clone()), window, cx);
654 });
655 mutated_diagnostics.update_in(cx, |diagnostics, window, _cx| {
656 assert!(diagnostics.focus_handle.is_focused(window));
657 });
658
659 let mut next_id = 0;
660 let mut next_filename = 0;
661 let mut language_server_ids = vec![LanguageServerId(0)];
662 let mut updated_language_servers = HashSet::default();
663 let mut current_diagnostics: HashMap<(PathBuf, LanguageServerId), Vec<lsp::Diagnostic>> =
664 Default::default();
665
666 for _ in 0..operations {
667 match rng.gen_range(0..100) {
668 // language server completes its diagnostic check
669 0..=20 if !updated_language_servers.is_empty() => {
670 let server_id = *updated_language_servers.iter().choose(&mut rng).unwrap();
671 log::info!("finishing diagnostic check for language server {server_id}");
672 lsp_store.update(cx, |lsp_store, cx| {
673 lsp_store.disk_based_diagnostics_finished(server_id, cx)
674 });
675
676 if rng.gen_bool(0.5) {
677 cx.run_until_parked();
678 }
679 }
680
681 // language server updates diagnostics
682 _ => {
683 let (path, server_id, diagnostics) =
684 match current_diagnostics.iter_mut().choose(&mut rng) {
685 // update existing set of diagnostics
686 Some(((path, server_id), diagnostics)) if rng.gen_bool(0.5) => {
687 (path.clone(), *server_id, diagnostics)
688 }
689
690 // insert a set of diagnostics for a new path
691 _ => {
692 let path: PathBuf =
693 format!(path!("/test/{}.rs"), post_inc(&mut next_filename)).into();
694 let len = rng.gen_range(128..256);
695 let content =
696 RandomCharIter::new(&mut rng).take(len).collect::<String>();
697 fs.insert_file(&path, content.into_bytes()).await;
698
699 let server_id = match language_server_ids.iter().choose(&mut rng) {
700 Some(server_id) if rng.gen_bool(0.5) => *server_id,
701 _ => {
702 let id = LanguageServerId(language_server_ids.len());
703 language_server_ids.push(id);
704 id
705 }
706 };
707
708 (
709 path.clone(),
710 server_id,
711 current_diagnostics.entry((path, server_id)).or_default(),
712 )
713 }
714 };
715
716 updated_language_servers.insert(server_id);
717
718 lsp_store.update(cx, |lsp_store, cx| {
719 log::info!("updating diagnostics. language server {server_id} path {path:?}");
720 randomly_update_diagnostics_for_path(
721 &fs,
722 &path,
723 diagnostics,
724 &mut next_id,
725 &mut rng,
726 );
727 lsp_store
728 .update_diagnostics(
729 server_id,
730 lsp::PublishDiagnosticsParams {
731 uri: lsp::Url::from_file_path(&path).unwrap_or_else(|_| {
732 lsp::Url::parse("file:///test/fallback.rs").unwrap()
733 }),
734 diagnostics: diagnostics.clone(),
735 version: None,
736 },
737 &[],
738 cx,
739 )
740 .unwrap()
741 });
742 cx.executor()
743 .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
744
745 cx.run_until_parked();
746 }
747 }
748 }
749
750 log::info!("updating mutated diagnostics view");
751 mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
752 diagnostics.update_stale_excerpts(window, cx)
753 });
754
755 log::info!("constructing reference diagnostics view");
756 let reference_diagnostics = window.build_entity(cx, |window, cx| {
757 ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
758 });
759 cx.executor()
760 .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
761 cx.run_until_parked();
762
763 let mutated_excerpts =
764 editor_content_with_blocks(&mutated_diagnostics.update(cx, |d, _| d.editor.clone()), cx);
765 let reference_excerpts = editor_content_with_blocks(
766 &reference_diagnostics.update(cx, |d, _| d.editor.clone()),
767 cx,
768 );
769
770 // The mutated view may contain more than the reference view as
771 // we don't currently shrink excerpts when diagnostics were removed.
772 let mut ref_iter = reference_excerpts.lines().filter(|line| *line != "§ -----");
773 let mut next_ref_line = ref_iter.next();
774 let mut skipped_block = false;
775
776 for mut_line in mutated_excerpts.lines() {
777 if let Some(ref_line) = next_ref_line {
778 if mut_line == ref_line {
779 next_ref_line = ref_iter.next();
780 } else if mut_line.contains('§') && mut_line != "§ -----" {
781 skipped_block = true;
782 }
783 }
784 }
785
786 if next_ref_line.is_some() || skipped_block {
787 pretty_assertions::assert_eq!(mutated_excerpts, reference_excerpts);
788 }
789}
790
791// similar to above, but with inlays. Used to find panics when mixing diagnostics and inlays.
792#[gpui::test]
793async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: StdRng) {
794 init_test(cx);
795
796 let operations = env::var("OPERATIONS")
797 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
798 .unwrap_or(10);
799
800 let fs = FakeFs::new(cx.executor());
801 fs.insert_tree(path!("/test"), json!({})).await;
802
803 let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
804 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
805 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
806 let cx = &mut VisualTestContext::from_window(*window, cx);
807 let workspace = window.root(cx).unwrap();
808
809 let mutated_diagnostics = window.build_entity(cx, |window, cx| {
810 ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
811 });
812
813 workspace.update_in(cx, |workspace, window, cx| {
814 workspace.add_item_to_center(Box::new(mutated_diagnostics.clone()), window, cx);
815 });
816 mutated_diagnostics.update_in(cx, |diagnostics, window, _cx| {
817 assert!(diagnostics.focus_handle.is_focused(window));
818 });
819
820 let mut next_id = 0;
821 let mut next_filename = 0;
822 let mut language_server_ids = vec![LanguageServerId(0)];
823 let mut updated_language_servers = HashSet::default();
824 let mut current_diagnostics: HashMap<(PathBuf, LanguageServerId), Vec<lsp::Diagnostic>> =
825 Default::default();
826 let mut next_inlay_id = 0;
827
828 for _ in 0..operations {
829 match rng.gen_range(0..100) {
830 // language server completes its diagnostic check
831 0..=20 if !updated_language_servers.is_empty() => {
832 let server_id = *updated_language_servers.iter().choose(&mut rng).unwrap();
833 log::info!("finishing diagnostic check for language server {server_id}");
834 lsp_store.update(cx, |lsp_store, cx| {
835 lsp_store.disk_based_diagnostics_finished(server_id, cx)
836 });
837
838 if rng.gen_bool(0.5) {
839 cx.run_until_parked();
840 }
841 }
842
843 21..=50 => mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
844 diagnostics.editor.update(cx, |editor, cx| {
845 let snapshot = editor.snapshot(window, cx);
846 if snapshot.buffer_snapshot.len() > 0 {
847 let position = rng.gen_range(0..snapshot.buffer_snapshot.len());
848 let position = snapshot.buffer_snapshot.clip_offset(position, Bias::Left);
849 log::info!(
850 "adding inlay at {position}/{}: {:?}",
851 snapshot.buffer_snapshot.len(),
852 snapshot.buffer_snapshot.text(),
853 );
854
855 editor.splice_inlays(
856 &[],
857 vec![Inlay {
858 id: InlayId::InlineCompletion(post_inc(&mut next_inlay_id)),
859 position: snapshot.buffer_snapshot.anchor_before(position),
860 text: Rope::from(format!("Test inlay {next_inlay_id}")),
861 }],
862 cx,
863 );
864 }
865 });
866 }),
867
868 // language server updates diagnostics
869 _ => {
870 let (path, server_id, diagnostics) =
871 match current_diagnostics.iter_mut().choose(&mut rng) {
872 // update existing set of diagnostics
873 Some(((path, server_id), diagnostics)) if rng.gen_bool(0.5) => {
874 (path.clone(), *server_id, diagnostics)
875 }
876
877 // insert a set of diagnostics for a new path
878 _ => {
879 let path: PathBuf =
880 format!(path!("/test/{}.rs"), post_inc(&mut next_filename)).into();
881 let len = rng.gen_range(128..256);
882 let content =
883 RandomCharIter::new(&mut rng).take(len).collect::<String>();
884 fs.insert_file(&path, content.into_bytes()).await;
885
886 let server_id = match language_server_ids.iter().choose(&mut rng) {
887 Some(server_id) if rng.gen_bool(0.5) => *server_id,
888 _ => {
889 let id = LanguageServerId(language_server_ids.len());
890 language_server_ids.push(id);
891 id
892 }
893 };
894
895 (
896 path.clone(),
897 server_id,
898 current_diagnostics.entry((path, server_id)).or_default(),
899 )
900 }
901 };
902
903 updated_language_servers.insert(server_id);
904
905 lsp_store.update(cx, |lsp_store, cx| {
906 log::info!("updating diagnostics. language server {server_id} path {path:?}");
907 randomly_update_diagnostics_for_path(
908 &fs,
909 &path,
910 diagnostics,
911 &mut next_id,
912 &mut rng,
913 );
914 lsp_store
915 .update_diagnostics(
916 server_id,
917 lsp::PublishDiagnosticsParams {
918 uri: lsp::Url::from_file_path(&path).unwrap_or_else(|_| {
919 lsp::Url::parse("file:///test/fallback.rs").unwrap()
920 }),
921 diagnostics: diagnostics.clone(),
922 version: None,
923 },
924 &[],
925 cx,
926 )
927 .unwrap()
928 });
929 cx.executor()
930 .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
931
932 cx.run_until_parked();
933 }
934 }
935 }
936
937 log::info!("updating mutated diagnostics view");
938 mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
939 diagnostics.update_stale_excerpts(window, cx)
940 });
941
942 cx.executor()
943 .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
944 cx.run_until_parked();
945}
946
947#[gpui::test]
948async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext) {
949 init_test(cx);
950
951 let mut cx = EditorTestContext::new(cx).await;
952 let lsp_store =
953 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
954
955 cx.set_state(indoc! {"
956 ˇfn func(abc def: i32) -> u32 {
957 }
958 "});
959
960 let message = "Something's wrong!";
961 cx.update(|_, cx| {
962 lsp_store.update(cx, |lsp_store, cx| {
963 lsp_store
964 .update_diagnostics(
965 LanguageServerId(0),
966 lsp::PublishDiagnosticsParams {
967 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
968 version: None,
969 diagnostics: vec![lsp::Diagnostic {
970 range: lsp::Range::new(
971 lsp::Position::new(0, 11),
972 lsp::Position::new(0, 12),
973 ),
974 severity: Some(lsp::DiagnosticSeverity::ERROR),
975 message: message.to_string(),
976 ..Default::default()
977 }],
978 },
979 &[],
980 cx,
981 )
982 .unwrap()
983 });
984 });
985 cx.run_until_parked();
986
987 cx.update_editor(|editor, window, cx| {
988 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
989 assert_eq!(
990 editor
991 .active_diagnostic_group()
992 .map(|diagnostics_group| diagnostics_group.active_message.as_str()),
993 Some(message),
994 "Should have a diagnostics group activated"
995 );
996 });
997 cx.assert_editor_state(indoc! {"
998 fn func(abcˇ def: i32) -> u32 {
999 }
1000 "});
1001
1002 cx.update(|_, cx| {
1003 lsp_store.update(cx, |lsp_store, cx| {
1004 lsp_store
1005 .update_diagnostics(
1006 LanguageServerId(0),
1007 lsp::PublishDiagnosticsParams {
1008 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
1009 version: None,
1010 diagnostics: Vec::new(),
1011 },
1012 &[],
1013 cx,
1014 )
1015 .unwrap()
1016 });
1017 });
1018 cx.run_until_parked();
1019 cx.update_editor(|editor, _, _| {
1020 assert_eq!(editor.active_diagnostic_group(), None);
1021 });
1022 cx.assert_editor_state(indoc! {"
1023 fn func(abcˇ def: i32) -> u32 {
1024 }
1025 "});
1026
1027 cx.update_editor(|editor, window, cx| {
1028 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
1029 assert_eq!(editor.active_diagnostic_group(), None);
1030 });
1031 cx.assert_editor_state(indoc! {"
1032 fn func(abcˇ def: i32) -> u32 {
1033 }
1034 "});
1035}
1036
1037#[gpui::test]
1038async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
1039 init_test(cx);
1040
1041 let mut cx = EditorTestContext::new(cx).await;
1042 let lsp_store =
1043 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
1044
1045 cx.set_state(indoc! {"
1046 ˇfn func(abc def: i32) -> u32 {
1047 }
1048 "});
1049
1050 cx.update(|_, cx| {
1051 lsp_store.update(cx, |lsp_store, cx| {
1052 lsp_store
1053 .update_diagnostics(
1054 LanguageServerId(0),
1055 lsp::PublishDiagnosticsParams {
1056 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
1057 version: None,
1058 diagnostics: vec![
1059 lsp::Diagnostic {
1060 range: lsp::Range::new(
1061 lsp::Position::new(0, 11),
1062 lsp::Position::new(0, 12),
1063 ),
1064 severity: Some(lsp::DiagnosticSeverity::ERROR),
1065 ..Default::default()
1066 },
1067 lsp::Diagnostic {
1068 range: lsp::Range::new(
1069 lsp::Position::new(0, 12),
1070 lsp::Position::new(0, 15),
1071 ),
1072 severity: Some(lsp::DiagnosticSeverity::ERROR),
1073 ..Default::default()
1074 },
1075 lsp::Diagnostic {
1076 range: lsp::Range::new(
1077 lsp::Position::new(0, 12),
1078 lsp::Position::new(0, 15),
1079 ),
1080 severity: Some(lsp::DiagnosticSeverity::ERROR),
1081 ..Default::default()
1082 },
1083 lsp::Diagnostic {
1084 range: lsp::Range::new(
1085 lsp::Position::new(0, 25),
1086 lsp::Position::new(0, 28),
1087 ),
1088 severity: Some(lsp::DiagnosticSeverity::ERROR),
1089 ..Default::default()
1090 },
1091 ],
1092 },
1093 &[],
1094 cx,
1095 )
1096 .unwrap()
1097 });
1098 });
1099 cx.run_until_parked();
1100
1101 //// Backward
1102
1103 // Fourth diagnostic
1104 cx.update_editor(|editor, window, cx| {
1105 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
1106 });
1107 cx.assert_editor_state(indoc! {"
1108 fn func(abc def: i32) -> ˇu32 {
1109 }
1110 "});
1111
1112 // Third diagnostic
1113 cx.update_editor(|editor, window, cx| {
1114 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
1115 });
1116 cx.assert_editor_state(indoc! {"
1117 fn func(abc ˇdef: i32) -> u32 {
1118 }
1119 "});
1120
1121 // Second diagnostic, same place
1122 cx.update_editor(|editor, window, cx| {
1123 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
1124 });
1125 cx.assert_editor_state(indoc! {"
1126 fn func(abc ˇdef: i32) -> u32 {
1127 }
1128 "});
1129
1130 // First diagnostic
1131 cx.update_editor(|editor, window, cx| {
1132 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
1133 });
1134 cx.assert_editor_state(indoc! {"
1135 fn func(abcˇ def: i32) -> u32 {
1136 }
1137 "});
1138
1139 // Wrapped over, fourth diagnostic
1140 cx.update_editor(|editor, window, cx| {
1141 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
1142 });
1143 cx.assert_editor_state(indoc! {"
1144 fn func(abc def: i32) -> ˇu32 {
1145 }
1146 "});
1147
1148 cx.update_editor(|editor, window, cx| {
1149 editor.move_to_beginning(&MoveToBeginning, window, cx);
1150 });
1151 cx.assert_editor_state(indoc! {"
1152 ˇfn func(abc def: i32) -> u32 {
1153 }
1154 "});
1155
1156 //// Forward
1157
1158 // First diagnostic
1159 cx.update_editor(|editor, window, cx| {
1160 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
1161 });
1162 cx.assert_editor_state(indoc! {"
1163 fn func(abcˇ def: i32) -> u32 {
1164 }
1165 "});
1166
1167 // Second diagnostic
1168 cx.update_editor(|editor, window, cx| {
1169 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
1170 });
1171 cx.assert_editor_state(indoc! {"
1172 fn func(abc ˇdef: i32) -> u32 {
1173 }
1174 "});
1175
1176 // Third diagnostic, same place
1177 cx.update_editor(|editor, window, cx| {
1178 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
1179 });
1180 cx.assert_editor_state(indoc! {"
1181 fn func(abc ˇdef: i32) -> u32 {
1182 }
1183 "});
1184
1185 // Fourth diagnostic
1186 cx.update_editor(|editor, window, cx| {
1187 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
1188 });
1189 cx.assert_editor_state(indoc! {"
1190 fn func(abc def: i32) -> ˇu32 {
1191 }
1192 "});
1193
1194 // Wrapped around, first diagnostic
1195 cx.update_editor(|editor, window, cx| {
1196 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
1197 });
1198 cx.assert_editor_state(indoc! {"
1199 fn func(abcˇ def: i32) -> u32 {
1200 }
1201 "});
1202}
1203
1204#[gpui::test]
1205async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
1206 init_test(cx);
1207
1208 let mut cx = EditorTestContext::new(cx).await;
1209
1210 cx.set_state(indoc! {"
1211 fn func(abˇc def: i32) -> u32 {
1212 }
1213 "});
1214 let lsp_store =
1215 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
1216
1217 cx.update(|_, cx| {
1218 lsp_store.update(cx, |lsp_store, cx| {
1219 lsp_store.update_diagnostics(
1220 LanguageServerId(0),
1221 lsp::PublishDiagnosticsParams {
1222 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
1223 version: None,
1224 diagnostics: vec![lsp::Diagnostic {
1225 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
1226 severity: Some(lsp::DiagnosticSeverity::ERROR),
1227 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
1228 ..Default::default()
1229 }],
1230 },
1231 &[],
1232 cx,
1233 )
1234 })
1235 }).unwrap();
1236 cx.run_until_parked();
1237 cx.update_editor(|editor, window, cx| {
1238 editor::hover_popover::hover(editor, &Default::default(), window, cx)
1239 });
1240 cx.run_until_parked();
1241 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
1242}
1243
1244#[gpui::test]
1245async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
1246 init_test(cx);
1247
1248 let mut cx = EditorLspTestContext::new_rust(
1249 lsp::ServerCapabilities {
1250 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1251 ..Default::default()
1252 },
1253 cx,
1254 )
1255 .await;
1256
1257 // Hover with just diagnostic, pops DiagnosticPopover immediately and then
1258 // info popover once request completes
1259 cx.set_state(indoc! {"
1260 fn teˇst() { println!(); }
1261 "});
1262 // Send diagnostic to client
1263 let range = cx.lsp_range(indoc! {"
1264 fn «test»() { println!(); }
1265 "});
1266 let lsp_store =
1267 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
1268 cx.update(|_, cx| {
1269 lsp_store.update(cx, |lsp_store, cx| {
1270 lsp_store.update_diagnostics(
1271 LanguageServerId(0),
1272 lsp::PublishDiagnosticsParams {
1273 uri: lsp::Url::from_file_path(path!("/root/dir/file.rs")).unwrap(),
1274 version: None,
1275 diagnostics: vec![lsp::Diagnostic {
1276 range,
1277 severity: Some(lsp::DiagnosticSeverity::ERROR),
1278 message: "A test diagnostic message.".to_string(),
1279 ..Default::default()
1280 }],
1281 },
1282 &[],
1283 cx,
1284 )
1285 })
1286 })
1287 .unwrap();
1288 cx.run_until_parked();
1289
1290 // Hover pops diagnostic immediately
1291 cx.update_editor(|editor, window, cx| editor::hover_popover::hover(editor, &Hover, window, cx));
1292 cx.background_executor.run_until_parked();
1293
1294 cx.editor(|Editor { hover_state, .. }, _, _| {
1295 assert!(hover_state.diagnostic_popover.is_some());
1296 assert!(hover_state.info_popovers.is_empty());
1297 });
1298
1299 // Info Popover shows after request responded to
1300 let range = cx.lsp_range(indoc! {"
1301 fn «test»() { println!(); }
1302 "});
1303 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1304 Ok(Some(lsp::Hover {
1305 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1306 kind: lsp::MarkupKind::Markdown,
1307 value: "some new docs".to_string(),
1308 }),
1309 range: Some(range),
1310 }))
1311 });
1312 let delay = cx.update(|_, cx| EditorSettings::get_global(cx).hover_popover_delay + 1);
1313 cx.background_executor
1314 .advance_clock(Duration::from_millis(delay));
1315
1316 cx.background_executor.run_until_parked();
1317 cx.editor(|Editor { hover_state, .. }, _, _| {
1318 hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
1319 });
1320}
1321#[gpui::test]
1322async fn test_diagnostics_with_code(cx: &mut TestAppContext) {
1323 init_test(cx);
1324
1325 let fs = FakeFs::new(cx.executor());
1326 fs.insert_tree(
1327 path!("/root"),
1328 json!({
1329 "main.js": "
1330 function test() {
1331 const x = 10;
1332 const y = 20;
1333 return 1;
1334 }
1335 test();
1336 "
1337 .unindent(),
1338 }),
1339 )
1340 .await;
1341
1342 let language_server_id = LanguageServerId(0);
1343 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1344 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1345 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1346 let cx = &mut VisualTestContext::from_window(*window, cx);
1347 let workspace = window.root(cx).unwrap();
1348 let uri = lsp::Url::from_file_path(path!("/root/main.js")).unwrap();
1349
1350 // Create diagnostics with code fields
1351 lsp_store.update(cx, |lsp_store, cx| {
1352 lsp_store
1353 .update_diagnostics(
1354 language_server_id,
1355 lsp::PublishDiagnosticsParams {
1356 uri: uri.clone(),
1357 diagnostics: vec![
1358 lsp::Diagnostic {
1359 range: lsp::Range::new(
1360 lsp::Position::new(1, 4),
1361 lsp::Position::new(1, 14),
1362 ),
1363 severity: Some(lsp::DiagnosticSeverity::WARNING),
1364 code: Some(lsp::NumberOrString::String("no-unused-vars".to_string())),
1365 source: Some("eslint".to_string()),
1366 message: "'x' is assigned a value but never used".to_string(),
1367 ..Default::default()
1368 },
1369 lsp::Diagnostic {
1370 range: lsp::Range::new(
1371 lsp::Position::new(2, 4),
1372 lsp::Position::new(2, 14),
1373 ),
1374 severity: Some(lsp::DiagnosticSeverity::WARNING),
1375 code: Some(lsp::NumberOrString::String("no-unused-vars".to_string())),
1376 source: Some("eslint".to_string()),
1377 message: "'y' is assigned a value but never used".to_string(),
1378 ..Default::default()
1379 },
1380 ],
1381 version: None,
1382 },
1383 &[],
1384 cx,
1385 )
1386 .unwrap();
1387 });
1388
1389 // Open the project diagnostics view
1390 let diagnostics = window.build_entity(cx, |window, cx| {
1391 ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
1392 });
1393 let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
1394
1395 diagnostics
1396 .next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
1397 .await;
1398
1399 // Verify that the diagnostic codes are displayed correctly
1400 pretty_assertions::assert_eq!(
1401 editor_content_with_blocks(&editor, cx),
1402 indoc::indoc! {
1403 "§ main.js
1404 § -----
1405 function test() {
1406 const x = 10; § 'x' is assigned a value but never used (eslint no-unused-vars)
1407 const y = 20; § 'y' is assigned a value but never used (eslint no-unused-vars)
1408 return 1;
1409 }"
1410 }
1411 );
1412}
1413
1414fn init_test(cx: &mut TestAppContext) {
1415 cx.update(|cx| {
1416 env_logger::try_init().ok();
1417 let settings = SettingsStore::test(cx);
1418 cx.set_global(settings);
1419 theme::init(theme::LoadThemes::JustBase, cx);
1420 language::init(cx);
1421 client::init_settings(cx);
1422 workspace::init_settings(cx);
1423 Project::init_settings(cx);
1424 crate::init(cx);
1425 editor::init(cx);
1426 });
1427}
1428
1429fn randomly_update_diagnostics_for_path(
1430 fs: &FakeFs,
1431 path: &Path,
1432 diagnostics: &mut Vec<lsp::Diagnostic>,
1433 next_id: &mut usize,
1434 rng: &mut impl Rng,
1435) {
1436 let mutation_count = rng.gen_range(1..=3);
1437 for _ in 0..mutation_count {
1438 if rng.gen_bool(0.3) && !diagnostics.is_empty() {
1439 let idx = rng.gen_range(0..diagnostics.len());
1440 log::info!(" removing diagnostic at index {idx}");
1441 diagnostics.remove(idx);
1442 } else {
1443 let unique_id = *next_id;
1444 *next_id += 1;
1445
1446 let new_diagnostic = random_lsp_diagnostic(rng, fs, path, unique_id);
1447
1448 let ix = rng.gen_range(0..=diagnostics.len());
1449 log::info!(
1450 " inserting {} at index {ix}. {},{}..{},{}",
1451 new_diagnostic.message,
1452 new_diagnostic.range.start.line,
1453 new_diagnostic.range.start.character,
1454 new_diagnostic.range.end.line,
1455 new_diagnostic.range.end.character,
1456 );
1457 for related in new_diagnostic.related_information.iter().flatten() {
1458 log::info!(
1459 " {}. {},{}..{},{}",
1460 related.message,
1461 related.location.range.start.line,
1462 related.location.range.start.character,
1463 related.location.range.end.line,
1464 related.location.range.end.character,
1465 );
1466 }
1467 diagnostics.insert(ix, new_diagnostic);
1468 }
1469 }
1470}
1471
1472fn random_lsp_diagnostic(
1473 rng: &mut impl Rng,
1474 fs: &FakeFs,
1475 path: &Path,
1476 unique_id: usize,
1477) -> lsp::Diagnostic {
1478 // Intentionally allow erroneous ranges some of the time (that run off the end of the file),
1479 // because language servers can potentially give us those, and we should handle them gracefully.
1480 const ERROR_MARGIN: usize = 10;
1481
1482 let file_content = fs.read_file_sync(path).unwrap();
1483 let file_text = Rope::from(String::from_utf8_lossy(&file_content).as_ref());
1484
1485 let start = rng.gen_range(0..file_text.len().saturating_add(ERROR_MARGIN));
1486 let end = rng.gen_range(start..file_text.len().saturating_add(ERROR_MARGIN));
1487
1488 let start_point = file_text.offset_to_point_utf16(start);
1489 let end_point = file_text.offset_to_point_utf16(end);
1490
1491 let range = lsp::Range::new(
1492 lsp::Position::new(start_point.row, start_point.column),
1493 lsp::Position::new(end_point.row, end_point.column),
1494 );
1495
1496 let severity = if rng.gen_bool(0.5) {
1497 Some(lsp::DiagnosticSeverity::ERROR)
1498 } else {
1499 Some(lsp::DiagnosticSeverity::WARNING)
1500 };
1501
1502 let message = format!("diagnostic {unique_id}");
1503
1504 let related_information = if rng.gen_bool(0.3) {
1505 let info_count = rng.gen_range(1..=3);
1506 let mut related_info = Vec::with_capacity(info_count);
1507
1508 for i in 0..info_count {
1509 let info_start = rng.gen_range(0..file_text.len().saturating_add(ERROR_MARGIN));
1510 let info_end = rng.gen_range(info_start..file_text.len().saturating_add(ERROR_MARGIN));
1511
1512 let info_start_point = file_text.offset_to_point_utf16(info_start);
1513 let info_end_point = file_text.offset_to_point_utf16(info_end);
1514
1515 let info_range = lsp::Range::new(
1516 lsp::Position::new(info_start_point.row, info_start_point.column),
1517 lsp::Position::new(info_end_point.row, info_end_point.column),
1518 );
1519
1520 related_info.push(lsp::DiagnosticRelatedInformation {
1521 location: lsp::Location::new(lsp::Url::from_file_path(path).unwrap(), info_range),
1522 message: format!("related info {i} for diagnostic {unique_id}"),
1523 });
1524 }
1525
1526 Some(related_info)
1527 } else {
1528 None
1529 };
1530
1531 lsp::Diagnostic {
1532 range,
1533 severity,
1534 message,
1535 related_information,
1536 data: None,
1537 ..Default::default()
1538 }
1539}