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