Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:32:51.588Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

diagnostics_tests.rs

2154 lines · 81.8 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.