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