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