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