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