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