use crate::{
world::{DocumentState, WorkspaceState, World},
NAME,
};
use jsona::dom::Node;
use jsona_util::environment::Environment;
use lsp_async_stub::{util::LspExt, Context, RequestWriter};
use lsp_types::{
notification, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, Location,
PublishDiagnosticsParams, Url,
};
#[tracing::instrument(skip_all)]
pub(crate) async fn publish_diagnostics<E: Environment>(
mut context: Context<World<E>>,
ws_uri: Url,
document_uri: Url,
) {
let mut diags = Vec::new();
let workspaces = context.workspaces.read().await;
let ws = match workspaces.get(&ws_uri) {
Some(d) => d,
None => {
tracing::warn!(%document_uri, "workspace not found");
return;
}
};
let doc = match ws.documents.get(&document_uri) {
Some(doc) => doc,
None => return,
};
collect_syntax_errors(doc, &mut diags);
drop(workspaces);
context
.write_notification::<notification::PublishDiagnostics, _>(Some(PublishDiagnosticsParams {
uri: document_uri.clone(),
diagnostics: diags.clone(),
version: None,
}))
.await
.unwrap_or_else(|err| tracing::error!("{err}"));
if !diags.is_empty() {
return;
}
let workspaces = context.workspaces.read().await;
let ws = match workspaces.get(&ws_uri) {
Some(d) => d,
None => {
tracing::warn!(%document_uri, "workspace not found");
return;
}
};
let doc = match ws.documents.get(&document_uri) {
Some(doc) => doc,
None => return,
};
let dom = doc.dom.clone();
collect_dom_errors(doc, &dom, &document_uri, &mut diags);
drop(workspaces);
context
.write_notification::<notification::PublishDiagnostics, _>(Some(PublishDiagnosticsParams {
uri: document_uri.clone(),
diagnostics: diags.clone(),
version: None,
}))
.await
.unwrap_or_else(|err| tracing::error!("{err}"));
if !diags.is_empty() {
return;
}
let workspaces = context.workspaces.read().await;
let ws = match workspaces.get(&ws_uri) {
Some(d) => d,
None => {
tracing::warn!(%document_uri, "workspace not found");
return;
}
};
let doc = match ws.documents.get(&document_uri) {
Some(doc) => doc,
None => return,
};
collect_schema_errors(ws, doc, &dom, &document_uri, &mut diags).await;
drop(workspaces);
context
.write_notification::<notification::PublishDiagnostics, _>(Some(PublishDiagnosticsParams {
uri: document_uri.clone(),
diagnostics: diags.clone(),
version: None,
}))
.await
.unwrap_or_else(|err| tracing::error!("{err}"));
}
#[tracing::instrument(skip_all)]
pub(crate) async fn clear_diagnostics<E: Environment>(
mut context: Context<World<E>>,
document_uri: Url,
) {
context
.write_notification::<notification::PublishDiagnostics, _>(Some(PublishDiagnosticsParams {
uri: document_uri,
diagnostics: Vec::new(),
version: None,
}))
.await
.unwrap_or_else(|err| tracing::error!("{}", err));
}
#[tracing::instrument(skip_all)]
fn collect_syntax_errors(doc: &DocumentState, diags: &mut Vec<Diagnostic>) {
diags.extend(doc.parse.errors.iter().map(|e| {
let range = doc.mapper.range(e.range).unwrap_or_default().into_lsp();
Diagnostic {
range,
severity: Some(DiagnosticSeverity::ERROR),
code: None,
code_description: None,
source: Some(NAME.into()),
message: e.message.clone(),
related_information: None,
tags: None,
data: None,
}
}));
}
#[tracing::instrument(skip_all)]
fn collect_dom_errors(
doc: &DocumentState,
dom: &Node,
document_uri: &Url,
diags: &mut Vec<Diagnostic>,
) {
if let Err(errors) = dom.validate() {
for error in errors {
match &error {
jsona::dom::DomError::ConflictingKeys { key, other_key } => {
let range = doc
.mapper
.range(key.text_range().unwrap())
.unwrap()
.into_lsp();
let other_range = doc
.mapper
.range(other_key.text_range().unwrap())
.unwrap()
.into_lsp();
diags.push(Diagnostic {
range,
severity: Some(DiagnosticSeverity::WARNING),
source: Some(NAME.into()),
message: error.to_string(),
related_information: Some(Vec::from([DiagnosticRelatedInformation {
location: Location {
uri: document_uri.clone(),
range: other_range,
},
message: "other key defined here".into(),
}])),
..Default::default()
});
diags.push(Diagnostic {
range: other_range,
severity: Some(DiagnosticSeverity::WARNING),
source: Some(NAME.into()),
message: error.to_string(),
related_information: Some(Vec::from([DiagnosticRelatedInformation {
location: Location {
uri: document_uri.clone(),
range,
},
message: "other key defined here".into(),
}])),
..Default::default()
});
}
jsona::dom::DomError::InvalidNode { syntax } => {
let range = doc
.mapper
.range(syntax.text_range())
.unwrap_or_default()
.into_lsp();
diags.push(Diagnostic {
range,
severity: Some(DiagnosticSeverity::ERROR),
code: None,
code_description: None,
source: Some(NAME.into()),
message: error.to_string(),
related_information: None,
tags: None,
data: None,
});
}
jsona::dom::DomError::InvalidString { syntax: _ }
| jsona::dom::DomError::InvalidNumber { syntax: _ } => {}
}
}
}
}
#[tracing::instrument(skip_all, fields(%document_uri))]
async fn collect_schema_errors<E: Environment>(
ws: &WorkspaceState<E>,
doc: &DocumentState,
dom: &Node,
document_uri: &Url,
diags: &mut Vec<Diagnostic>,
) {
if !ws.lsp_config.schema.enabled {
return;
}
if let Some(schema_association) = ws.schemas.associations().query_for(document_uri) {
tracing::debug!(
schema.url = %schema_association.url,
schema.name = schema_association.meta["name"].as_str().unwrap_or(""),
schema.source = schema_association.meta["source"].as_str().unwrap_or(""),
"using schema"
);
match ws.schemas.validate(&schema_association.url, dom).await {
Ok(errors) => diags.extend(errors.into_iter().map(|err| {
let text_range = dom
.path(&err.keys)
.and_then(|v| v.text_range())
.or_else(|| err.keys.last_text_range())
.unwrap_or_default();
let range = doc.mapper.range(text_range).unwrap_or_default().into_lsp();
Diagnostic {
range,
severity: Some(DiagnosticSeverity::ERROR),
code: None,
code_description: None,
source: Some(NAME.into()),
message: err.kind.to_string(),
related_information: None,
tags: None,
data: None,
}
})),
Err(error) => {
tracing::error!(?error, "schema validation failed");
}
}
}
}