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