ZIm/crates/diagnostics/src/diagnostics_tests.rs
Finn Evers 3c5d5a1d57
editor: Add access method for project (#36266)
This resolves a `TODO` that I've stumbled upon too many times whilst
looking at the editor code.

Release Notes:

- N/A
2025-08-15 18:34:22 +00:00

1694 lines
62 KiB
Rust

use super::*;
use collections::{HashMap, HashSet};
use editor::{
DisplayPoint, EditorSettings,
actions::{GoToDiagnostic, GoToPreviousDiagnostic, Hover, MoveToBeginning},
display_map::{DisplayRow, Inlay},
test::{
editor_content_with_blocks, editor_lsp_test_context::EditorLspTestContext,
editor_test_context::EditorTestContext,
},
};
use gpui::{TestAppContext, VisualTestContext};
use indoc::indoc;
use language::{DiagnosticSourceKind, Rope};
use lsp::LanguageServerId;
use pretty_assertions::assert_eq;
use project::{
FakeFs,
project_settings::{GoToDiagnosticSeverity, GoToDiagnosticSeverityFilter},
};
use rand::{Rng, rngs::StdRng, seq::IteratorRandom as _};
use serde_json::json;
use settings::SettingsStore;
use std::{
env,
path::{Path, PathBuf},
};
use unindent::Unindent as _;
use util::{RandomCharIter, path, post_inc};
#[ctor::ctor]
fn init_logger() {
zlog::init_test();
}
#[gpui::test]
async fn test_diagnostics(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/test"),
json!({
"consts.rs": "
const a: i32 = 'a';
const b: i32 = c;
"
.unindent(),
"main.rs": "
fn main() {
let x = vec![];
let y = vec![];
a(x);
b(y);
// comment 1
// comment 2
c(y);
d(x);
}
"
.unindent(),
}),
)
.await;
let language_server_id = LanguageServerId(0);
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx);
let workspace = window.root(cx).unwrap();
let uri = lsp::Url::from_file_path(path!("/test/main.rs")).unwrap();
// Create some diagnostics
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
uri: uri.clone(),
diagnostics: vec![lsp::Diagnostic{
range: lsp::Range::new(lsp::Position::new(7, 6),lsp::Position::new(7, 7)),
severity:Some(lsp::DiagnosticSeverity::ERROR),
message: "use of moved value\nvalue used here after move".to_string(),
related_information: Some(vec![lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(2,8),lsp::Position::new(2,9))),
message: "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
},
lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(4,6),lsp::Position::new(4,7))),
message: "value moved here".to_string()
},
]),
..Default::default()
},
lsp::Diagnostic{
range: lsp::Range::new(lsp::Position::new(8, 6),lsp::Position::new(8, 7)),
severity:Some(lsp::DiagnosticSeverity::ERROR),
message: "use of moved value\nvalue used here after move".to_string(),
related_information: Some(vec![lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(1,8),lsp::Position::new(1,9))),
message: "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
},
lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(3,6),lsp::Position::new(3,7))),
message: "value moved here".to_string()
},
]),
..Default::default()
}
],
version: None
}, None, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
});
// Open the project diagnostics view while there are already diagnostics.
let diagnostics = window.build_entity(cx, |window, cx| {
ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
});
let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
diagnostics
.next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
.await;
pretty_assertions::assert_eq!(
editor_content_with_blocks(&editor, cx),
indoc::indoc! {
"§ main.rs
§ -----
fn main() {
let x = vec![];
§ move occurs because `x` has type `Vec<char>`, which does not implement
§ the `Copy` trait (back)
let y = vec![];
§ move occurs because `y` has type `Vec<char>`, which does not implement
§ the `Copy` trait (back)
a(x); § value moved here (back)
b(y); § value moved here
// comment 1
// comment 2
c(y);
§ use of moved value
§ value used here after move
§ hint: move occurs because `y` has type `Vec<char>`, which does not
§ implement the `Copy` trait
d(x);
§ use of moved value
§ value used here after move
§ hint: move occurs because `x` has type `Vec<char>`, which does not
§ implement the `Copy` trait
§ hint: value moved here
}"
}
);
// Cursor is at the first diagnostic
editor.update(cx, |editor, cx| {
assert_eq!(
editor.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(3), 8)..DisplayPoint::new(DisplayRow(3), 8)]
);
});
// Diagnostics are added for another earlier path.
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.disk_based_diagnostics_started(language_server_id, cx);
lsp_store
.update_diagnostics(
language_server_id,
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/test/consts.rs")).unwrap(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(0, 15),
lsp::Position::new(0, 15),
),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "mismatched types expected `usize`, found `char`".to_string(),
..Default::default()
}],
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
.unwrap();
lsp_store.disk_based_diagnostics_finished(language_server_id, cx);
});
diagnostics
.next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
.await;
pretty_assertions::assert_eq!(
editor_content_with_blocks(&editor, cx),
indoc::indoc! {
"§ consts.rs
§ -----
const a: i32 = 'a'; § mismatched types expected `usize`, found `char`
const b: i32 = c;
§ main.rs
§ -----
fn main() {
let x = vec![];
§ move occurs because `x` has type `Vec<char>`, which does not implement
§ the `Copy` trait (back)
let y = vec![];
§ move occurs because `y` has type `Vec<char>`, which does not implement
§ the `Copy` trait (back)
a(x); § value moved here (back)
b(y); § value moved here
// comment 1
// comment 2
c(y);
§ use of moved value
§ value used here after move
§ hint: move occurs because `y` has type `Vec<char>`, which does not
§ implement the `Copy` trait
d(x);
§ use of moved value
§ value used here after move
§ hint: move occurs because `x` has type `Vec<char>`, which does not
§ implement the `Copy` trait
§ hint: value moved here
}"
}
);
// Cursor keeps its position.
editor.update(cx, |editor, cx| {
assert_eq!(
editor.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(8), 8)..DisplayPoint::new(DisplayRow(8), 8)]
);
});
// Diagnostics are added to the first path
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.disk_based_diagnostics_started(language_server_id, cx);
lsp_store
.update_diagnostics(
language_server_id,
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/test/consts.rs")).unwrap(),
diagnostics: vec![
lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(0, 15),
lsp::Position::new(0, 15),
),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "mismatched types expected `usize`, found `char`".to_string(),
..Default::default()
},
lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(1, 15),
lsp::Position::new(1, 15),
),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "unresolved name `c`".to_string(),
..Default::default()
},
],
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
.unwrap();
lsp_store.disk_based_diagnostics_finished(language_server_id, cx);
});
diagnostics
.next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
.await;
pretty_assertions::assert_eq!(
editor_content_with_blocks(&editor, cx),
indoc::indoc! {
"§ consts.rs
§ -----
const a: i32 = 'a'; § mismatched types expected `usize`, found `char`
const b: i32 = c; § unresolved name `c`
§ main.rs
§ -----
fn main() {
let x = vec![];
§ move occurs because `x` has type `Vec<char>`, which does not implement
§ the `Copy` trait (back)
let y = vec![];
§ move occurs because `y` has type `Vec<char>`, which does not implement
§ the `Copy` trait (back)
a(x); § value moved here (back)
b(y); § value moved here
// comment 1
// comment 2
c(y);
§ use of moved value
§ value used here after move
§ hint: move occurs because `y` has type `Vec<char>`, which does not
§ implement the `Copy` trait
d(x);
§ use of moved value
§ value used here after move
§ hint: move occurs because `x` has type `Vec<char>`, which does not
§ implement the `Copy` trait
§ hint: value moved here
}"
}
);
}
#[gpui::test]
async fn test_diagnostics_with_folds(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/test"),
json!({
"main.js": "
function test() {
return 1
};
tset();
".unindent()
}),
)
.await;
let server_id_1 = LanguageServerId(100);
let server_id_2 = LanguageServerId(101);
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx);
let workspace = window.root(cx).unwrap();
let diagnostics = window.build_entity(cx, |window, cx| {
ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
});
let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
// Two language servers start updating diagnostics
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.disk_based_diagnostics_started(server_id_1, cx);
lsp_store.disk_based_diagnostics_started(server_id_2, cx);
lsp_store
.update_diagnostics(
server_id_1,
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(4, 0), lsp::Position::new(4, 4)),
severity: Some(lsp::DiagnosticSeverity::WARNING),
message: "no method `tset`".to_string(),
related_information: Some(vec![lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(
lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
lsp::Range::new(
lsp::Position::new(0, 9),
lsp::Position::new(0, 13),
),
),
message: "method `test` defined here".to_string(),
}]),
..Default::default()
}],
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
.unwrap();
});
// The first language server finishes
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.disk_based_diagnostics_finished(server_id_1, cx);
});
// Only the first language server's diagnostics are shown.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.executor().run_until_parked();
editor.update_in(cx, |editor, window, cx| {
editor.fold_ranges(vec![Point::new(0, 0)..Point::new(3, 0)], false, window, cx);
});
pretty_assertions::assert_eq!(
editor_content_with_blocks(&editor, cx),
indoc::indoc! {
"§ main.js
§ -----
tset(); § no method `tset`"
}
);
editor.update(cx, |editor, cx| {
editor.unfold_ranges(&[Point::new(0, 0)..Point::new(3, 0)], false, false, cx);
});
pretty_assertions::assert_eq!(
editor_content_with_blocks(&editor, cx),
indoc::indoc! {
"§ main.js
§ -----
function test() { § method `test` defined here
return 1
};
tset(); § no method `tset`"
}
);
}
#[gpui::test]
async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/test"),
json!({
"main.js": "
a();
b();
c();
d();
e();
".unindent()
}),
)
.await;
let server_id_1 = LanguageServerId(100);
let server_id_2 = LanguageServerId(101);
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx);
let workspace = window.root(cx).unwrap();
let diagnostics = window.build_entity(cx, |window, cx| {
ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
});
let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
// Two language servers start updating diagnostics
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.disk_based_diagnostics_started(server_id_1, cx);
lsp_store.disk_based_diagnostics_started(server_id_2, cx);
lsp_store
.update_diagnostics(
server_id_1,
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 1)),
severity: Some(lsp::DiagnosticSeverity::WARNING),
message: "error 1".to_string(),
..Default::default()
}],
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
.unwrap();
});
// The first language server finishes
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.disk_based_diagnostics_finished(server_id_1, cx);
});
// Only the first language server's diagnostics are shown.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.executor().run_until_parked();
pretty_assertions::assert_eq!(
editor_content_with_blocks(&editor, cx),
indoc::indoc! {
"§ main.js
§ -----
a(); § error 1
b();
c();"
}
);
// The second language server finishes
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostics(
server_id_2,
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 1)),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "warning 1".to_string(),
..Default::default()
}],
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
.unwrap();
lsp_store.disk_based_diagnostics_finished(server_id_2, cx);
});
// Both language server's diagnostics are shown.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.executor().run_until_parked();
pretty_assertions::assert_eq!(
editor_content_with_blocks(&editor, cx),
indoc::indoc! {
"§ main.js
§ -----
a(); § error 1
b(); § warning 1
c();
d();"
}
);
// Both language servers start updating diagnostics, and the first server finishes.
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.disk_based_diagnostics_started(server_id_1, cx);
lsp_store.disk_based_diagnostics_started(server_id_2, cx);
lsp_store
.update_diagnostics(
server_id_1,
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 1)),
severity: Some(lsp::DiagnosticSeverity::WARNING),
message: "warning 2".to_string(),
..Default::default()
}],
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
.unwrap();
lsp_store
.update_diagnostics(
server_id_2,
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/test/main.rs")).unwrap(),
diagnostics: vec![],
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
.unwrap();
lsp_store.disk_based_diagnostics_finished(server_id_1, cx);
});
// Only the first language server's diagnostics are updated.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.executor().run_until_parked();
pretty_assertions::assert_eq!(
editor_content_with_blocks(&editor, cx),
indoc::indoc! {
"§ main.js
§ -----
a();
b(); § warning 1
c(); § warning 2
d();
e();"
}
);
// The second language server finishes.
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostics(
server_id_2,
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(3, 0), lsp::Position::new(3, 1)),
severity: Some(lsp::DiagnosticSeverity::WARNING),
message: "warning 2".to_string(),
..Default::default()
}],
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
.unwrap();
lsp_store.disk_based_diagnostics_finished(server_id_2, cx);
});
// Both language servers' diagnostics are updated.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.executor().run_until_parked();
pretty_assertions::assert_eq!(
editor_content_with_blocks(&editor, cx),
indoc::indoc! {
"§ main.js
§ -----
a();
b();
c(); § warning 2
d(); § warning 2
e();"
}
);
}
#[gpui::test(iterations = 20)]
async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng) {
init_test(cx);
let operations = env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/test"), json!({})).await;
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx);
let workspace = window.root(cx).unwrap();
let mutated_diagnostics = window.build_entity(cx, |window, cx| {
ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
});
workspace.update_in(cx, |workspace, window, cx| {
workspace.add_item_to_center(Box::new(mutated_diagnostics.clone()), window, cx);
});
mutated_diagnostics.update_in(cx, |diagnostics, window, _cx| {
assert!(diagnostics.focus_handle.is_focused(window));
});
let mut next_id = 0;
let mut next_filename = 0;
let mut language_server_ids = vec![LanguageServerId(0)];
let mut updated_language_servers = HashSet::default();
let mut current_diagnostics: HashMap<(PathBuf, LanguageServerId), Vec<lsp::Diagnostic>> =
Default::default();
for _ in 0..operations {
match rng.gen_range(0..100) {
// language server completes its diagnostic check
0..=20 if !updated_language_servers.is_empty() => {
let server_id = *updated_language_servers.iter().choose(&mut rng).unwrap();
log::info!("finishing diagnostic check for language server {server_id}");
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.disk_based_diagnostics_finished(server_id, cx)
});
if rng.gen_bool(0.5) {
cx.run_until_parked();
}
}
// language server updates diagnostics
_ => {
let (path, server_id, diagnostics) =
match current_diagnostics.iter_mut().choose(&mut rng) {
// update existing set of diagnostics
Some(((path, server_id), diagnostics)) if rng.gen_bool(0.5) => {
(path.clone(), *server_id, diagnostics)
}
// insert a set of diagnostics for a new path
_ => {
let path: PathBuf =
format!(path!("/test/{}.rs"), post_inc(&mut next_filename)).into();
let len = rng.gen_range(128..256);
let content =
RandomCharIter::new(&mut rng).take(len).collect::<String>();
fs.insert_file(&path, content.into_bytes()).await;
let server_id = match language_server_ids.iter().choose(&mut rng) {
Some(server_id) if rng.gen_bool(0.5) => *server_id,
_ => {
let id = LanguageServerId(language_server_ids.len());
language_server_ids.push(id);
id
}
};
(
path.clone(),
server_id,
current_diagnostics.entry((path, server_id)).or_default(),
)
}
};
updated_language_servers.insert(server_id);
lsp_store.update(cx, |lsp_store, cx| {
log::info!("updating diagnostics. language server {server_id} path {path:?}");
randomly_update_diagnostics_for_path(
&fs,
&path,
diagnostics,
&mut next_id,
&mut rng,
);
lsp_store
.update_diagnostics(
server_id,
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(&path).unwrap_or_else(|_| {
lsp::Url::parse("file:///test/fallback.rs").unwrap()
}),
diagnostics: diagnostics.clone(),
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
.unwrap()
});
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.run_until_parked();
}
}
}
log::info!("updating mutated diagnostics view");
mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
diagnostics.update_stale_excerpts(window, cx)
});
log::info!("constructing reference diagnostics view");
let reference_diagnostics = window.build_entity(cx, |window, cx| {
ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
});
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.run_until_parked();
let mutated_excerpts =
editor_content_with_blocks(&mutated_diagnostics.update(cx, |d, _| d.editor.clone()), cx);
let reference_excerpts = editor_content_with_blocks(
&reference_diagnostics.update(cx, |d, _| d.editor.clone()),
cx,
);
// The mutated view may contain more than the reference view as
// we don't currently shrink excerpts when diagnostics were removed.
let mut ref_iter = reference_excerpts.lines().filter(|line| *line != "§ -----");
let mut next_ref_line = ref_iter.next();
let mut skipped_block = false;
for mut_line in mutated_excerpts.lines() {
if let Some(ref_line) = next_ref_line {
if mut_line == ref_line {
next_ref_line = ref_iter.next();
} else if mut_line.contains('§') && mut_line != "§ -----" {
skipped_block = true;
}
}
}
if next_ref_line.is_some() || skipped_block {
pretty_assertions::assert_eq!(mutated_excerpts, reference_excerpts);
}
}
// similar to above, but with inlays. Used to find panics when mixing diagnostics and inlays.
#[gpui::test]
async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: StdRng) {
init_test(cx);
let operations = env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/test"), json!({})).await;
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx);
let workspace = window.root(cx).unwrap();
let mutated_diagnostics = window.build_entity(cx, |window, cx| {
ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
});
workspace.update_in(cx, |workspace, window, cx| {
workspace.add_item_to_center(Box::new(mutated_diagnostics.clone()), window, cx);
});
mutated_diagnostics.update_in(cx, |diagnostics, window, _cx| {
assert!(diagnostics.focus_handle.is_focused(window));
});
let mut next_id = 0;
let mut next_filename = 0;
let mut language_server_ids = vec![LanguageServerId(0)];
let mut updated_language_servers = HashSet::default();
let mut current_diagnostics: HashMap<(PathBuf, LanguageServerId), Vec<lsp::Diagnostic>> =
Default::default();
let mut next_inlay_id = 0;
for _ in 0..operations {
match rng.gen_range(0..100) {
// language server completes its diagnostic check
0..=20 if !updated_language_servers.is_empty() => {
let server_id = *updated_language_servers.iter().choose(&mut rng).unwrap();
log::info!("finishing diagnostic check for language server {server_id}");
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.disk_based_diagnostics_finished(server_id, cx)
});
if rng.gen_bool(0.5) {
cx.run_until_parked();
}
}
21..=50 => mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
diagnostics.editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
if snapshot.buffer_snapshot.len() > 0 {
let position = rng.gen_range(0..snapshot.buffer_snapshot.len());
let position = snapshot.buffer_snapshot.clip_offset(position, Bias::Left);
log::info!(
"adding inlay at {position}/{}: {:?}",
snapshot.buffer_snapshot.len(),
snapshot.buffer_snapshot.text(),
);
editor.splice_inlays(
&[],
vec![Inlay::edit_prediction(
post_inc(&mut next_inlay_id),
snapshot.buffer_snapshot.anchor_before(position),
Rope::from_iter(["Test inlay ", "next_inlay_id"]),
)],
cx,
);
}
});
}),
// language server updates diagnostics
_ => {
let (path, server_id, diagnostics) =
match current_diagnostics.iter_mut().choose(&mut rng) {
// update existing set of diagnostics
Some(((path, server_id), diagnostics)) if rng.gen_bool(0.5) => {
(path.clone(), *server_id, diagnostics)
}
// insert a set of diagnostics for a new path
_ => {
let path: PathBuf =
format!(path!("/test/{}.rs"), post_inc(&mut next_filename)).into();
let len = rng.gen_range(128..256);
let content =
RandomCharIter::new(&mut rng).take(len).collect::<String>();
fs.insert_file(&path, content.into_bytes()).await;
let server_id = match language_server_ids.iter().choose(&mut rng) {
Some(server_id) if rng.gen_bool(0.5) => *server_id,
_ => {
let id = LanguageServerId(language_server_ids.len());
language_server_ids.push(id);
id
}
};
(
path.clone(),
server_id,
current_diagnostics.entry((path, server_id)).or_default(),
)
}
};
updated_language_servers.insert(server_id);
lsp_store.update(cx, |lsp_store, cx| {
log::info!("updating diagnostics. language server {server_id} path {path:?}");
randomly_update_diagnostics_for_path(
&fs,
&path,
diagnostics,
&mut next_id,
&mut rng,
);
lsp_store
.update_diagnostics(
server_id,
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(&path).unwrap_or_else(|_| {
lsp::Url::parse("file:///test/fallback.rs").unwrap()
}),
diagnostics: diagnostics.clone(),
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
.unwrap()
});
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.run_until_parked();
}
}
}
log::info!("updating mutated diagnostics view");
mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
diagnostics.update_stale_excerpts(window, cx)
});
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.run_until_parked();
}
#[gpui::test]
async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext) {
init_test(cx);
let mut cx = EditorTestContext::new(cx).await;
let lsp_store =
cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
cx.set_state(indoc! {"
ˇfn func(abc def: i32) -> u32 {
}
"});
let message = "Something's wrong!";
cx.update(|_, cx| {
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(0, 11),
lsp::Position::new(0, 12),
),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: message.to_string(),
..Default::default()
}],
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
.unwrap()
});
});
cx.run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
assert_eq!(
editor
.active_diagnostic_group()
.map(|diagnostics_group| diagnostics_group.active_message.as_str()),
Some(message),
"Should have a diagnostics group activated"
);
});
cx.assert_editor_state(indoc! {"
fn func(abcˇ def: i32) -> u32 {
}
"});
cx.update(|_, cx| {
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
version: None,
diagnostics: Vec::new(),
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
.unwrap()
});
});
cx.run_until_parked();
cx.update_editor(|editor, _, _| {
assert_eq!(editor.active_diagnostic_group(), None);
});
cx.assert_editor_state(indoc! {"
fn func(abcˇ def: i32) -> u32 {
}
"});
cx.update_editor(|editor, window, cx| {
editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
assert_eq!(editor.active_diagnostic_group(), None);
});
cx.assert_editor_state(indoc! {"
fn func(abcˇ def: i32) -> u32 {
}
"});
}
#[gpui::test]
async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
init_test(cx);
let mut cx = EditorTestContext::new(cx).await;
let lsp_store =
cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
cx.set_state(indoc! {"
ˇfn func(abc def: i32) -> u32 {
}
"});
cx.update(|_, cx| {
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
version: None,
diagnostics: vec![
lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(0, 11),
lsp::Position::new(0, 12),
),
severity: Some(lsp::DiagnosticSeverity::ERROR),
..Default::default()
},
lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(0, 12),
lsp::Position::new(0, 15),
),
severity: Some(lsp::DiagnosticSeverity::ERROR),
..Default::default()
},
lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(0, 12),
lsp::Position::new(0, 15),
),
severity: Some(lsp::DiagnosticSeverity::ERROR),
..Default::default()
},
lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(0, 25),
lsp::Position::new(0, 28),
),
severity: Some(lsp::DiagnosticSeverity::ERROR),
..Default::default()
},
],
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
.unwrap()
});
});
cx.run_until_parked();
//// Backward
// Fourth diagnostic
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abc def: i32) -> ˇu32 {
}
"});
// Third diagnostic
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abc ˇdef: i32) -> u32 {
}
"});
// Second diagnostic, same place
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abc ˇdef: i32) -> u32 {
}
"});
// First diagnostic
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abcˇ def: i32) -> u32 {
}
"});
// Wrapped over, fourth diagnostic
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abc def: i32) -> ˇu32 {
}
"});
cx.update_editor(|editor, window, cx| {
editor.move_to_beginning(&MoveToBeginning, window, cx);
});
cx.assert_editor_state(indoc! {"
ˇfn func(abc def: i32) -> u32 {
}
"});
//// Forward
// First diagnostic
cx.update_editor(|editor, window, cx| {
editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abcˇ def: i32) -> u32 {
}
"});
// Second diagnostic
cx.update_editor(|editor, window, cx| {
editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abc ˇdef: i32) -> u32 {
}
"});
// Third diagnostic, same place
cx.update_editor(|editor, window, cx| {
editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abc ˇdef: i32) -> u32 {
}
"});
// Fourth diagnostic
cx.update_editor(|editor, window, cx| {
editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abc def: i32) -> ˇu32 {
}
"});
// Wrapped around, first diagnostic
cx.update_editor(|editor, window, cx| {
editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abcˇ def: i32) -> u32 {
}
"});
}
#[gpui::test]
async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
init_test(cx);
let mut cx = EditorTestContext::new(cx).await;
cx.set_state(indoc! {"
fn func(abˇc def: i32) -> u32 {
}
"});
let lsp_store =
cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
cx.update(|_, cx| {
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
..Default::default()
}],
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
})
}).unwrap();
cx.run_until_parked();
cx.update_editor(|editor, window, cx| {
editor::hover_popover::hover(editor, &Default::default(), window, cx)
});
cx.run_until_parked();
cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
}
#[gpui::test]
async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
init_test(cx);
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
..Default::default()
},
cx,
)
.await;
// Hover with just diagnostic, pops DiagnosticPopover immediately and then
// info popover once request completes
cx.set_state(indoc! {"
fn teˇst() { println!(); }
"});
// Send diagnostic to client
let range = cx.lsp_range(indoc! {"
fn «test»() { println!(); }
"});
let lsp_store =
cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
cx.update(|_, cx| {
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/root/dir/file.rs")).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
range,
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "A test diagnostic message.".to_string(),
..Default::default()
}],
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
})
})
.unwrap();
cx.run_until_parked();
// Hover pops diagnostic immediately
cx.update_editor(|editor, window, cx| editor::hover_popover::hover(editor, &Hover, window, cx));
cx.background_executor.run_until_parked();
cx.editor(|Editor { hover_state, .. }, _, _| {
assert!(hover_state.diagnostic_popover.is_some());
assert!(hover_state.info_popovers.is_empty());
});
// Info Popover shows after request responded to
let range = cx.lsp_range(indoc! {"
fn «test»() { println!(); }
"});
cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
Ok(Some(lsp::Hover {
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: "some new docs".to_string(),
}),
range: Some(range),
}))
});
let delay = cx.update(|_, cx| EditorSettings::get_global(cx).hover_popover_delay + 1);
cx.background_executor
.advance_clock(Duration::from_millis(delay));
cx.background_executor.run_until_parked();
cx.editor(|Editor { hover_state, .. }, _, _| {
hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
});
}
#[gpui::test]
async fn test_diagnostics_with_code(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/root"),
json!({
"main.js": "
function test() {
const x = 10;
const y = 20;
return 1;
}
test();
"
.unindent(),
}),
)
.await;
let language_server_id = LanguageServerId(0);
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx);
let workspace = window.root(cx).unwrap();
let uri = lsp::Url::from_file_path(path!("/root/main.js")).unwrap();
// Create diagnostics with code fields
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostics(
language_server_id,
lsp::PublishDiagnosticsParams {
uri: uri.clone(),
diagnostics: vec![
lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(1, 4),
lsp::Position::new(1, 14),
),
severity: Some(lsp::DiagnosticSeverity::WARNING),
code: Some(lsp::NumberOrString::String("no-unused-vars".to_string())),
source: Some("eslint".to_string()),
message: "'x' is assigned a value but never used".to_string(),
..Default::default()
},
lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(2, 4),
lsp::Position::new(2, 14),
),
severity: Some(lsp::DiagnosticSeverity::WARNING),
code: Some(lsp::NumberOrString::String("no-unused-vars".to_string())),
source: Some("eslint".to_string()),
message: "'y' is assigned a value but never used".to_string(),
..Default::default()
},
],
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
.unwrap();
});
// Open the project diagnostics view
let diagnostics = window.build_entity(cx, |window, cx| {
ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
});
let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
diagnostics
.next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
.await;
// Verify that the diagnostic codes are displayed correctly
pretty_assertions::assert_eq!(
editor_content_with_blocks(&editor, cx),
indoc::indoc! {
"§ main.js
§ -----
function test() {
const x = 10; § 'x' is assigned a value but never used (eslint no-unused-vars)
const y = 20; § 'y' is assigned a value but never used (eslint no-unused-vars)
return 1;
}"
}
);
}
#[gpui::test]
async fn go_to_diagnostic_with_severity(cx: &mut TestAppContext) {
init_test(cx);
let mut cx = EditorTestContext::new(cx).await;
let lsp_store =
cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
cx.set_state(indoc! {"error warning info hiˇnt"});
cx.update(|_, cx| {
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
version: None,
diagnostics: vec![
lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(0, 0),
lsp::Position::new(0, 5),
),
severity: Some(lsp::DiagnosticSeverity::ERROR),
..Default::default()
},
lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(0, 6),
lsp::Position::new(0, 13),
),
severity: Some(lsp::DiagnosticSeverity::WARNING),
..Default::default()
},
lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(0, 14),
lsp::Position::new(0, 18),
),
severity: Some(lsp::DiagnosticSeverity::INFORMATION),
..Default::default()
},
lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(0, 19),
lsp::Position::new(0, 23),
),
severity: Some(lsp::DiagnosticSeverity::HINT),
..Default::default()
},
],
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
.unwrap()
});
});
cx.run_until_parked();
macro_rules! go {
($severity:expr) => {
cx.update_editor(|editor, window, cx| {
editor.go_to_diagnostic(
&GoToDiagnostic {
severity: $severity,
},
window,
cx,
);
});
};
}
// Default, should cycle through all diagnostics
go!(GoToDiagnosticSeverityFilter::default());
cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
go!(GoToDiagnosticSeverityFilter::default());
cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
go!(GoToDiagnosticSeverityFilter::default());
cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
go!(GoToDiagnosticSeverityFilter::default());
cx.assert_editor_state(indoc! {"error warning info ˇhint"});
go!(GoToDiagnosticSeverityFilter::default());
cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
let only_info = GoToDiagnosticSeverityFilter::Only(GoToDiagnosticSeverity::Information);
go!(only_info);
cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
go!(only_info);
cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
let no_hints = GoToDiagnosticSeverityFilter::Range {
min: GoToDiagnosticSeverity::Information,
max: GoToDiagnosticSeverity::Error,
};
go!(no_hints);
cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
go!(no_hints);
cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
go!(no_hints);
cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
go!(no_hints);
cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
let warning_info = GoToDiagnosticSeverityFilter::Range {
min: GoToDiagnosticSeverity::Information,
max: GoToDiagnosticSeverity::Warning,
};
go!(warning_info);
cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
go!(warning_info);
cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
go!(warning_info);
cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
zlog::init_test();
let settings = SettingsStore::test(cx);
cx.set_global(settings);
theme::init(theme::LoadThemes::JustBase, cx);
language::init(cx);
client::init_settings(cx);
workspace::init_settings(cx);
Project::init_settings(cx);
crate::init(cx);
editor::init(cx);
});
}
fn randomly_update_diagnostics_for_path(
fs: &FakeFs,
path: &Path,
diagnostics: &mut Vec<lsp::Diagnostic>,
next_id: &mut usize,
rng: &mut impl Rng,
) {
let mutation_count = rng.gen_range(1..=3);
for _ in 0..mutation_count {
if rng.gen_bool(0.3) && !diagnostics.is_empty() {
let idx = rng.gen_range(0..diagnostics.len());
log::info!(" removing diagnostic at index {idx}");
diagnostics.remove(idx);
} else {
let unique_id = *next_id;
*next_id += 1;
let new_diagnostic = random_lsp_diagnostic(rng, fs, path, unique_id);
let ix = rng.gen_range(0..=diagnostics.len());
log::info!(
" inserting {} at index {ix}. {},{}..{},{}",
new_diagnostic.message,
new_diagnostic.range.start.line,
new_diagnostic.range.start.character,
new_diagnostic.range.end.line,
new_diagnostic.range.end.character,
);
for related in new_diagnostic.related_information.iter().flatten() {
log::info!(
" {}. {},{}..{},{}",
related.message,
related.location.range.start.line,
related.location.range.start.character,
related.location.range.end.line,
related.location.range.end.character,
);
}
diagnostics.insert(ix, new_diagnostic);
}
}
}
fn random_lsp_diagnostic(
rng: &mut impl Rng,
fs: &FakeFs,
path: &Path,
unique_id: usize,
) -> lsp::Diagnostic {
// Intentionally allow erroneous ranges some of the time (that run off the end of the file),
// because language servers can potentially give us those, and we should handle them gracefully.
const ERROR_MARGIN: usize = 10;
let file_content = fs.read_file_sync(path).unwrap();
let file_text = Rope::from(String::from_utf8_lossy(&file_content).as_ref());
let start = rng.gen_range(0..file_text.len().saturating_add(ERROR_MARGIN));
let end = rng.gen_range(start..file_text.len().saturating_add(ERROR_MARGIN));
let start_point = file_text.offset_to_point_utf16(start);
let end_point = file_text.offset_to_point_utf16(end);
let range = lsp::Range::new(
lsp::Position::new(start_point.row, start_point.column),
lsp::Position::new(end_point.row, end_point.column),
);
let severity = if rng.gen_bool(0.5) {
Some(lsp::DiagnosticSeverity::ERROR)
} else {
Some(lsp::DiagnosticSeverity::WARNING)
};
let message = format!("diagnostic {unique_id}");
let related_information = if rng.gen_bool(0.3) {
let info_count = rng.gen_range(1..=3);
let mut related_info = Vec::with_capacity(info_count);
for i in 0..info_count {
let info_start = rng.gen_range(0..file_text.len().saturating_add(ERROR_MARGIN));
let info_end = rng.gen_range(info_start..file_text.len().saturating_add(ERROR_MARGIN));
let info_start_point = file_text.offset_to_point_utf16(info_start);
let info_end_point = file_text.offset_to_point_utf16(info_end);
let info_range = lsp::Range::new(
lsp::Position::new(info_start_point.row, info_start_point.column),
lsp::Position::new(info_end_point.row, info_end_point.column),
);
related_info.push(lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(lsp::Url::from_file_path(path).unwrap(), info_range),
message: format!("related info {i} for diagnostic {unique_id}"),
});
}
Some(related_info)
} else {
None
};
lsp::Diagnostic {
range,
severity,
message,
related_information,
data: None,
..Default::default()
}
}