use std::collections::HashMap;
use cairo_lang_diagnostics::{
DiagnosticEntry, Diagnostics, PluginFileDiagnosticNotes, Severity, UserLocationWithPluginNotes,
};
use cairo_lang_filesystem::ids::{FileId, SpanInFile};
use lsp_types::{
Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, Location, NumberOrString, Range,
Url,
};
use salsa::Database;
use tracing::{error, trace};
use crate::lang::lsp::{LsProtoGroup, ToLsp};
pub fn map_cairo_diagnostics_to_lsp<'db, T>(
db: &'db dyn Database,
diags: &mut HashMap<(Url, FileId<'db>), Vec<Diagnostic>>,
diagnostics: &Diagnostics<'db, T>,
trace_macro_diagnostics: bool,
plugin_file_notes: &PluginFileDiagnosticNotes<'db>,
) where
T: DiagnosticEntry<'db> + salsa::Update,
{
for diagnostic in if trace_macro_diagnostics {
diagnostics.get_all()
} else {
diagnostics.get_diagnostics_without_duplicates(db)
} {
let mut message = diagnostic.format(db);
let span_in_file = diagnostic.location(db);
let (_, parent_file_notes) =
span_in_file.user_location_with_plugin_notes(db.as_dyn_database(), plugin_file_notes);
let mut related_information = vec![];
for note in diagnostic.notes(db).iter().chain(&parent_file_notes) {
if let Some(location) = ¬e.location {
let Some((range, file_id)) = get_mapped_range_and_add_mapping_note(
db.as_dyn_database(),
location,
trace_macro_diagnostics.then_some(&mut related_information),
"Next note mapped from here.",
) else {
continue;
};
let Some(uri) = db.url_for_file(file_id) else {
trace!("url for file not found: {:?}", file_id.long(db));
continue;
};
related_information.push(DiagnosticRelatedInformation {
location: Location { uri, range },
message: note.text.clone(),
});
} else {
message += &format!("\nnote: {}", note.text);
}
}
let Some((range, mapped_file_id)) = get_mapped_range_and_add_mapping_note(
db.as_dyn_database(),
&diagnostic.location(db),
trace_macro_diagnostics.then_some(&mut related_information),
"Diagnostic mapped from here.",
) else {
continue;
};
let diagnostic = Diagnostic {
range,
message,
related_information: (!related_information.is_empty()).then_some(related_information),
severity: Some(match diagnostic.severity() {
Severity::Error => DiagnosticSeverity::ERROR,
Severity::Warning => DiagnosticSeverity::WARNING,
}),
code: diagnostic.error_code().map(|code| NumberOrString::String(code.to_string())),
..Diagnostic::default()
};
let Some(mapped_file_url) = db.url_for_file(mapped_file_id) else {
continue;
};
diags.entry((mapped_file_url, mapped_file_id)).or_default().push(diagnostic);
}
}
fn get_mapped_range_and_add_mapping_note<'db>(
db: &'db dyn Database,
orig: &SpanInFile<'db>,
related_info: Option<&mut Vec<DiagnosticRelatedInformation>>,
message: &str,
) -> Option<(Range, FileId<'db>)> {
let mapped = orig.user_location(db);
let mapped_range = get_lsp_range(db, &mapped)?;
if let Some(related_info) = related_info
&& *orig != mapped
&& let Some(range) = get_lsp_range(db, orig)
{
related_info.push(DiagnosticRelatedInformation {
location: Location { uri: db.url_for_file(orig.file_id)?, range },
message: message.to_string(),
});
}
Some((mapped_range, mapped.file_id))
}
fn get_lsp_range<'db>(db: &'db dyn Database, location: &SpanInFile<'db>) -> Option<Range> {
let Some(span) = location.span.position_in_file(db, location.file_id) else {
error!("failed to get range for diagnostic");
return None;
};
Some(span.to_lsp())
}