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