1use super::*;
2use collections::{HashMap, HashSet};
3use editor::{
4 DisplayPoint,
5 actions::{GoToDiagnostic, GoToPreviousDiagnostic, MoveToBeginning},
6 display_map::DisplayRow,
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(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#[gpui::test]
783async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext) {
784 init_test(cx);
785
786 let mut cx = EditorTestContext::new(cx).await;
787 let lsp_store =
788 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
789
790 cx.set_state(indoc! {"
791 ˇfn func(abc def: i32) -> u32 {
792 }
793 "});
794
795 let message = "Something's wrong!";
796 cx.update(|_, cx| {
797 lsp_store.update(cx, |lsp_store, cx| {
798 lsp_store
799 .update_diagnostics(
800 LanguageServerId(0),
801 lsp::PublishDiagnosticsParams {
802 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
803 version: None,
804 diagnostics: vec![lsp::Diagnostic {
805 range: lsp::Range::new(
806 lsp::Position::new(0, 11),
807 lsp::Position::new(0, 12),
808 ),
809 severity: Some(lsp::DiagnosticSeverity::ERROR),
810 message: message.to_string(),
811 ..Default::default()
812 }],
813 },
814 &[],
815 cx,
816 )
817 .unwrap()
818 });
819 });
820 cx.run_until_parked();
821
822 cx.update_editor(|editor, window, cx| {
823 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
824 assert_eq!(
825 editor
826 .active_diagnostic_group()
827 .map(|diagnostics_group| diagnostics_group.active_message.as_str()),
828 Some(message),
829 "Should have a diagnostics group activated"
830 );
831 });
832 cx.assert_editor_state(indoc! {"
833 fn func(abcˇ def: i32) -> u32 {
834 }
835 "});
836
837 cx.update(|_, cx| {
838 lsp_store.update(cx, |lsp_store, cx| {
839 lsp_store
840 .update_diagnostics(
841 LanguageServerId(0),
842 lsp::PublishDiagnosticsParams {
843 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
844 version: None,
845 diagnostics: Vec::new(),
846 },
847 &[],
848 cx,
849 )
850 .unwrap()
851 });
852 });
853 cx.run_until_parked();
854 cx.update_editor(|editor, _, _| {
855 assert_eq!(editor.active_diagnostic_group(), None);
856 });
857 cx.assert_editor_state(indoc! {"
858 fn func(abcˇ def: i32) -> u32 {
859 }
860 "});
861
862 cx.update_editor(|editor, window, cx| {
863 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
864 assert_eq!(editor.active_diagnostic_group(), None);
865 });
866 cx.assert_editor_state(indoc! {"
867 fn func(abcˇ def: i32) -> u32 {
868 }
869 "});
870}
871
872#[gpui::test]
873async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
874 init_test(cx);
875
876 let mut cx = EditorTestContext::new(cx).await;
877 let lsp_store =
878 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
879
880 cx.set_state(indoc! {"
881 ˇfn func(abc def: i32) -> u32 {
882 }
883 "});
884
885 cx.update(|_, cx| {
886 lsp_store.update(cx, |lsp_store, cx| {
887 lsp_store
888 .update_diagnostics(
889 LanguageServerId(0),
890 lsp::PublishDiagnosticsParams {
891 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
892 version: None,
893 diagnostics: vec![
894 lsp::Diagnostic {
895 range: lsp::Range::new(
896 lsp::Position::new(0, 11),
897 lsp::Position::new(0, 12),
898 ),
899 severity: Some(lsp::DiagnosticSeverity::ERROR),
900 ..Default::default()
901 },
902 lsp::Diagnostic {
903 range: lsp::Range::new(
904 lsp::Position::new(0, 12),
905 lsp::Position::new(0, 15),
906 ),
907 severity: Some(lsp::DiagnosticSeverity::ERROR),
908 ..Default::default()
909 },
910 lsp::Diagnostic {
911 range: lsp::Range::new(
912 lsp::Position::new(0, 12),
913 lsp::Position::new(0, 15),
914 ),
915 severity: Some(lsp::DiagnosticSeverity::ERROR),
916 ..Default::default()
917 },
918 lsp::Diagnostic {
919 range: lsp::Range::new(
920 lsp::Position::new(0, 25),
921 lsp::Position::new(0, 28),
922 ),
923 severity: Some(lsp::DiagnosticSeverity::ERROR),
924 ..Default::default()
925 },
926 ],
927 },
928 &[],
929 cx,
930 )
931 .unwrap()
932 });
933 });
934 cx.run_until_parked();
935
936 //// Backward
937
938 // Fourth diagnostic
939 cx.update_editor(|editor, window, cx| {
940 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
941 });
942 cx.assert_editor_state(indoc! {"
943 fn func(abc def: i32) -> ˇu32 {
944 }
945 "});
946
947 // Third diagnostic
948 cx.update_editor(|editor, window, cx| {
949 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
950 });
951 cx.assert_editor_state(indoc! {"
952 fn func(abc ˇdef: i32) -> u32 {
953 }
954 "});
955
956 // Second diagnostic, same place
957 cx.update_editor(|editor, window, cx| {
958 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
959 });
960 cx.assert_editor_state(indoc! {"
961 fn func(abc ˇdef: i32) -> u32 {
962 }
963 "});
964
965 // First diagnostic
966 cx.update_editor(|editor, window, cx| {
967 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
968 });
969 cx.assert_editor_state(indoc! {"
970 fn func(abcˇ def: i32) -> u32 {
971 }
972 "});
973
974 // Wrapped over, fourth diagnostic
975 cx.update_editor(|editor, window, cx| {
976 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
977 });
978 cx.assert_editor_state(indoc! {"
979 fn func(abc def: i32) -> ˇu32 {
980 }
981 "});
982
983 cx.update_editor(|editor, window, cx| {
984 editor.move_to_beginning(&MoveToBeginning, window, cx);
985 });
986 cx.assert_editor_state(indoc! {"
987 ˇfn func(abc def: i32) -> u32 {
988 }
989 "});
990
991 //// Forward
992
993 // First diagnostic
994 cx.update_editor(|editor, window, cx| {
995 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
996 });
997 cx.assert_editor_state(indoc! {"
998 fn func(abcˇ def: i32) -> u32 {
999 }
1000 "});
1001
1002 // Second diagnostic
1003 cx.update_editor(|editor, window, cx| {
1004 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
1005 });
1006 cx.assert_editor_state(indoc! {"
1007 fn func(abc ˇdef: i32) -> u32 {
1008 }
1009 "});
1010
1011 // Third diagnostic, same place
1012 cx.update_editor(|editor, window, cx| {
1013 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
1014 });
1015 cx.assert_editor_state(indoc! {"
1016 fn func(abc ˇdef: i32) -> u32 {
1017 }
1018 "});
1019
1020 // Fourth diagnostic
1021 cx.update_editor(|editor, window, cx| {
1022 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
1023 });
1024 cx.assert_editor_state(indoc! {"
1025 fn func(abc def: i32) -> ˇu32 {
1026 }
1027 "});
1028
1029 // Wrapped around, first diagnostic
1030 cx.update_editor(|editor, window, cx| {
1031 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
1032 });
1033 cx.assert_editor_state(indoc! {"
1034 fn func(abcˇ def: i32) -> u32 {
1035 }
1036 "});
1037}
1038
1039fn init_test(cx: &mut TestAppContext) {
1040 cx.update(|cx| {
1041 let settings = SettingsStore::test(cx);
1042 cx.set_global(settings);
1043 theme::init(theme::LoadThemes::JustBase, cx);
1044 language::init(cx);
1045 client::init_settings(cx);
1046 workspace::init_settings(cx);
1047 Project::init_settings(cx);
1048 crate::init(cx);
1049 editor::init(cx);
1050 });
1051}
1052
1053fn randomly_update_diagnostics_for_path(
1054 fs: &FakeFs,
1055 path: &Path,
1056 diagnostics: &mut Vec<lsp::Diagnostic>,
1057 next_id: &mut usize,
1058 rng: &mut impl Rng,
1059) {
1060 let mutation_count = rng.gen_range(1..=3);
1061 for _ in 0..mutation_count {
1062 if rng.gen_bool(0.3) && !diagnostics.is_empty() {
1063 let idx = rng.gen_range(0..diagnostics.len());
1064 log::info!(" removing diagnostic at index {idx}");
1065 diagnostics.remove(idx);
1066 } else {
1067 let unique_id = *next_id;
1068 *next_id += 1;
1069
1070 let new_diagnostic = random_lsp_diagnostic(rng, fs, path, unique_id);
1071
1072 let ix = rng.gen_range(0..=diagnostics.len());
1073 log::info!(
1074 " inserting {} at index {ix}. {},{}..{},{}",
1075 new_diagnostic.message,
1076 new_diagnostic.range.start.line,
1077 new_diagnostic.range.start.character,
1078 new_diagnostic.range.end.line,
1079 new_diagnostic.range.end.character,
1080 );
1081 for related in new_diagnostic.related_information.iter().flatten() {
1082 log::info!(
1083 " {}. {},{}..{},{}",
1084 related.message,
1085 related.location.range.start.line,
1086 related.location.range.start.character,
1087 related.location.range.end.line,
1088 related.location.range.end.character,
1089 );
1090 }
1091 diagnostics.insert(ix, new_diagnostic);
1092 }
1093 }
1094}
1095
1096fn random_lsp_diagnostic(
1097 rng: &mut impl Rng,
1098 fs: &FakeFs,
1099 path: &Path,
1100 unique_id: usize,
1101) -> lsp::Diagnostic {
1102 // Intentionally allow erroneous ranges some of the time (that run off the end of the file),
1103 // because language servers can potentially give us those, and we should handle them gracefully.
1104 const ERROR_MARGIN: usize = 10;
1105
1106 let file_content = fs.read_file_sync(path).unwrap();
1107 let file_text = Rope::from(String::from_utf8_lossy(&file_content).as_ref());
1108
1109 let start = rng.gen_range(0..file_text.len().saturating_add(ERROR_MARGIN));
1110 let end = rng.gen_range(start..file_text.len().saturating_add(ERROR_MARGIN));
1111
1112 let start_point = file_text.offset_to_point_utf16(start);
1113 let end_point = file_text.offset_to_point_utf16(end);
1114
1115 let range = lsp::Range::new(
1116 lsp::Position::new(start_point.row, start_point.column),
1117 lsp::Position::new(end_point.row, end_point.column),
1118 );
1119
1120 let severity = if rng.gen_bool(0.5) {
1121 Some(lsp::DiagnosticSeverity::ERROR)
1122 } else {
1123 Some(lsp::DiagnosticSeverity::WARNING)
1124 };
1125
1126 let message = format!("diagnostic {unique_id}");
1127
1128 let related_information = if rng.gen_bool(0.3) {
1129 let info_count = rng.gen_range(1..=3);
1130 let mut related_info = Vec::with_capacity(info_count);
1131
1132 for i in 0..info_count {
1133 let info_start = rng.gen_range(0..file_text.len().saturating_add(ERROR_MARGIN));
1134 let info_end = rng.gen_range(info_start..file_text.len().saturating_add(ERROR_MARGIN));
1135
1136 let info_start_point = file_text.offset_to_point_utf16(info_start);
1137 let info_end_point = file_text.offset_to_point_utf16(info_end);
1138
1139 let info_range = lsp::Range::new(
1140 lsp::Position::new(info_start_point.row, info_start_point.column),
1141 lsp::Position::new(info_end_point.row, info_end_point.column),
1142 );
1143
1144 related_info.push(lsp::DiagnosticRelatedInformation {
1145 location: lsp::Location::new(lsp::Url::from_file_path(path).unwrap(), info_range),
1146 message: format!("related info {i} for diagnostic {unique_id}"),
1147 });
1148 }
1149
1150 Some(related_info)
1151 } else {
1152 None
1153 };
1154
1155 lsp::Diagnostic {
1156 range,
1157 severity,
1158 message,
1159 related_information,
1160 data: None,
1161 ..Default::default()
1162 }
1163}