#![feature(new_range_api)]
#![warn(missing_docs)]
use oak_core::{
errors::{OakError, OakErrorKind},
source::Source,
};
use oak_vfs::LineMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Severity {
Error,
Warning,
Advice,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Label {
pub message: Option<String>,
#[cfg_attr(feature = "serde", serde(with = "oak_core::serde_range"))]
pub span: core::range::Range<usize>,
pub color: Option<String>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Diagnostic {
pub code: Option<String>,
pub message: String,
pub i18n_key: Option<String>,
pub i18n_args: std::collections::HashMap<String, String>,
pub severity: Severity,
pub labels: Vec<Label>,
pub help: Option<String>,
}
impl Diagnostic {
pub fn error(message: impl Into<String>) -> Self {
Self { code: None, message: message.into(), i18n_key: None, i18n_args: std::collections::HashMap::new(), severity: Severity::Error, labels: Vec::new(), help: None }
}
pub fn warning(message: impl Into<String>) -> Self {
Self { code: None, message: message.into(), i18n_key: None, i18n_args: std::collections::HashMap::new(), severity: Severity::Warning, labels: Vec::new(), help: None }
}
pub fn with_i18n(mut self, key: impl Into<String>) -> Self {
self.i18n_key = Some(key.into());
self
}
pub fn with_arg(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.i18n_args.insert(key.into(), value.into());
self
}
pub fn from_provider<P: DiagnosticProvider, S: Source + ?Sized>(provider: &P, source: &S) -> Self {
provider.to_diagnostic(source)
}
pub fn with_label(mut self, span: core::range::Range<usize>, message: impl Into<String>) -> Self {
self.labels.push(Label { message: Some(message.into()), span, color: None });
self
}
pub fn with_help(mut self, help: impl Into<String>) -> Self {
self.help = Some(help.into());
self
}
pub fn with_code(mut self, code: impl Into<String>) -> Self {
self.code = Some(code.into());
self
}
}
impl From<&OakError> for Diagnostic {
fn from(error: &OakError) -> Self {
let message = format!("{}", error);
Diagnostic::error(message)
}
}
pub trait DiagnosticProvider {
fn to_diagnostic<S: Source + ?Sized>(&self, source: &S) -> Diagnostic;
}
pub trait OakDiagnosticsProvider<L: oak_core::Language> {
fn emit_diagnostics<S: Source + ?Sized>(&self, uri: &str, root: &oak_core::tree::RedNode<L>, source: &S) -> Vec<Diagnostic>;
fn emit_all_diagnostics<S: Source + ?Sized>(&self, uri: &str, source: &S) -> Vec<Diagnostic> {
let _ = (uri, source);
Vec::new()
}
}
impl DiagnosticProvider for OakError {
fn to_diagnostic<S: Source + ?Sized>(&self, source: &S) -> Diagnostic {
let kind = self.kind();
let message = kind.to_string();
let code = Some(kind.key().to_string());
let mut diag = Diagnostic::error(message).with_code(code.unwrap()).with_i18n(kind.key());
match kind {
OakErrorKind::IoError { .. } => {}
OakErrorKind::SyntaxError { offset, .. } | OakErrorKind::UnexpectedCharacter { offset, .. } => {
let start = (*offset).min(source.length());
let end = (start + 1).min(source.length());
diag = diag.with_label(core::range::Range { start, end }, "here");
}
OakErrorKind::UnexpectedToken { token, offset, .. } => {
let start = (*offset).min(source.length());
let end = (start + 1).min(source.length());
diag = diag.with_label(core::range::Range { start, end }, "here").with_arg("token", token.clone());
}
OakErrorKind::ExpectedToken { expected, offset, .. } => {
let start = (*offset).min(source.length());
let end = (start + 1).min(source.length());
diag = diag.with_label(core::range::Range { start, end }, "here").with_arg("expected", expected.clone());
}
OakErrorKind::ExpectedName { name_kind, offset, .. } => {
let start = (*offset).min(source.length());
let end = (start + 1).min(source.length());
diag = diag.with_label(core::range::Range { start, end }, "here").with_arg("name_kind", name_kind.clone());
}
OakErrorKind::TrailingCommaNotAllowed { offset, .. } => {
let start = (*offset).min(source.length());
let end = (start + 1).min(source.length());
diag = diag.with_label(core::range::Range { start, end }, "here");
}
_ => {}
}
diag
}
}
pub trait Localizer {
fn localize(&self, key: &str, args: &std::collections::HashMap<String, String>) -> String;
}
impl Localizer for () {
fn localize(&self, _key: &str, _args: &std::collections::HashMap<String, String>) -> String {
String::new()
}
}
pub trait Emitter {
fn render<S: Source + ?Sized>(&self, source: &S, diagnostic: &Diagnostic) -> String {
self.render_localized::<S, ()>(source, diagnostic, None, None)
}
fn render_localized<S: Source + ?Sized, L: Localizer + ?Sized>(&self, source: &S, diagnostic: &Diagnostic, localizer: Option<&L>, uri: Option<&str>) -> String;
}
pub struct ConsoleEmitter {
pub unicode: bool,
}
impl Default for ConsoleEmitter {
fn default() -> Self {
Self { unicode: true }
}
}
impl Emitter for ConsoleEmitter {
fn render_localized<S: Source + ?Sized, L: Localizer + ?Sized>(&self, source: &S, diagnostic: &Diagnostic, localizer: Option<&L>, uri: Option<&str>) -> String {
let mut out = String::new();
let line_map = LineMap::from_source(source);
let full_text = source.get_text_in(core::range::Range { start: 0, end: source.length() }).into_owned();
let lines: Vec<&str> = full_text.lines().collect();
let sev_name = match diagnostic.severity {
Severity::Error => "error",
Severity::Warning => "warning",
Severity::Advice => "advice",
};
let sev_color = match diagnostic.severity {
Severity::Error => "\x1b[31;1m",
Severity::Warning => "\x1b[33;1m",
Severity::Advice => "\x1b[36;1m",
};
let message = if let (Some(key), Some(loc)) = (&diagnostic.i18n_key, localizer) { loc.localize(key, &diagnostic.i18n_args) } else { diagnostic.message.clone() };
if let Some(code) = &diagnostic.code {
out.push_str(&format!("{}[{}]\x1b[0m {}\n", sev_color, code, message));
}
else {
out.push_str(&format!("{}[{}]\x1b[0m {}\n", sev_color, sev_name, message));
}
for label in &diagnostic.labels {
self.render_snippet(&mut out, source, &line_map, &full_text, &lines, label, uri);
}
if let Some(help) = &diagnostic.help {
out.push_str(&format!("\n\x1b[36;1mhelp\x1b[0m: {}\n", help));
}
out
}
}
struct Characters {
vbar: &'static str,
hbar: &'static str,
ltop: &'static str,
lbot: &'static str,
}
impl Characters {
fn unicode() -> Self {
Self { vbar: "│", hbar: "─", ltop: "┌", lbot: "└" }
}
fn ascii() -> Self {
Self { vbar: "|", hbar: "_", ltop: "/", lbot: "|" }
}
}
impl ConsoleEmitter {
fn render_snippet<S: Source + ?Sized>(&self, out: &mut String, source: &S, line_map: &LineMap, full_text: &str, lines: &[&str], label: &Label, uri: Option<&str>) {
let chars = if self.unicode { Characters::unicode() } else { Characters::ascii() };
let (start_line, _) = line_map.offset_to_line_col_utf16(source, label.span.start);
let (end_line, _) = line_map.offset_to_line_col_utf16(source, label.span.end);
let start_line = start_line as usize;
let end_line = end_line as usize;
let start_line_start = line_map.line_start(start_line as u32).unwrap_or(0);
let end_line_start = line_map.line_start(end_line as u32).unwrap_or(0);
let start_col = full_text.get(start_line_start..label.span.start.min(full_text.len())).unwrap_or("").chars().count();
let end_col = full_text.get(end_line_start..label.span.end.min(full_text.len())).unwrap_or("").chars().count();
let line_num_width = (end_line + 1).to_string().len();
let padding = " ".repeat(line_num_width);
let url_str = uri.unwrap_or("<anonymous>");
let pos_str = format!("{}:{}", start_line + 1, start_col + 1);
out.push_str(&format!(" \x1b[34m{}\x1b[0m at {}:{}\n", chars.ltop, url_str, pos_str));
out.push_str(&format!("{} \x1b[34m{}\x1b[0m\n", padding, chars.vbar));
if start_line == end_line {
if let Some(line_text) = lines.get(start_line) {
out.push_str(&format!("{:>width$} \x1b[34m{}\x1b[0m {}\n", start_line + 1, chars.vbar, line_text, width = line_num_width));
let underline_padding = " ".repeat(start_col);
let underline_len = full_text.get(label.span.start.min(full_text.len())..label.span.end.min(full_text.len())).unwrap_or("").chars().count().max(1);
let underline = "^".repeat(underline_len);
let color = label.color.as_deref().unwrap_or("\x1b[31;1m");
out.push_str(&format!("{} \x1b[34m{}\x1b[0m {}{}{}\x1b[0m", padding, chars.vbar, underline_padding, color, underline));
if let Some(msg) = &label.message {
out.push_str(&format!(" {}\n", msg));
}
else {
out.push_str("\n");
}
}
}
else {
let color = label.color.as_deref().unwrap_or("\x1b[31;1m");
for i in start_line..=end_line {
if let Some(line_text) = lines.get(i) {
let line_num = i + 1;
if i == start_line {
out.push_str(&format!("{:>width$} \x1b[34m{}\x1b[0m {}{}{} {}\n", line_num, chars.vbar, color, chars.ltop, "\x1b[0m", line_text, width = line_num_width));
}
else if i == end_line {
out.push_str(&format!("{:>width$} \x1b[34m{}\x1b[0m {}{}{} {}\n", line_num, chars.vbar, color, chars.vbar, "\x1b[0m", line_text, width = line_num_width));
let underline_len = end_col.max(1);
let underline = "^".repeat(underline_len);
out.push_str(&format!("{} \x1b[34m{}\x1b[0m {}{}{}{}", padding, chars.vbar, color, chars.lbot, chars.hbar.repeat(end_col), underline));
if let Some(msg) = &label.message {
out.push_str(&format!(" {}\x1b[0m\n", msg));
}
else {
out.push_str("\x1b[0m\n");
}
}
else {
out.push_str(&format!("{:>width$} \x1b[34m{}\x1b[0m {}{}{} {}\n", line_num, chars.vbar, color, chars.vbar, "\x1b[0m", line_text, width = line_num_width));
}
}
}
}
out.push_str(&format!("{} \x1b[34m{}\x1b[0m\n", padding, chars.vbar));
}
}
pub struct PlainTextEmitter {
pub unicode: bool,
}
impl Default for PlainTextEmitter {
fn default() -> Self {
Self { unicode: false }
}
}
impl Emitter for PlainTextEmitter {
fn render_localized<S: Source + ?Sized, L: Localizer + ?Sized>(&self, source: &S, diagnostic: &Diagnostic, localizer: Option<&L>, uri: Option<&str>) -> String {
let mut out = String::new();
let line_map = LineMap::from_source(source);
let full_text = source.get_text_in(core::range::Range { start: 0, end: source.length() }).into_owned();
let lines: Vec<&str> = full_text.lines().collect();
let sev_name = match diagnostic.severity {
Severity::Error => "error",
Severity::Warning => "warning",
Severity::Advice => "advice",
};
let message = if let (Some(key), Some(loc)) = (&diagnostic.i18n_key, localizer) { loc.localize(key, &diagnostic.i18n_args) } else { diagnostic.message.clone() };
if let Some(code) = &diagnostic.code {
out.push_str(&format!("[{}] {}\n", code, message));
}
else {
out.push_str(&format!("[{}] {}\n", sev_name, message));
}
for label in &diagnostic.labels {
self.render_snippet(&mut out, source, &line_map, &full_text, &lines, label, uri);
}
if let Some(help) = &diagnostic.help {
out.push_str(&format!("\nhelp: {}\n", help));
}
out
}
}
impl PlainTextEmitter {
fn render_snippet<S: Source + ?Sized>(&self, out: &mut String, source: &S, line_map: &LineMap, full_text: &str, lines: &[&str], label: &Label, uri: Option<&str>) {
let chars = if self.unicode { Characters::unicode() } else { Characters::ascii() };
let (start_line, _) = line_map.offset_to_line_col_utf16(source, label.span.start);
let (end_line, _) = line_map.offset_to_line_col_utf16(source, label.span.end);
let start_line = start_line as usize;
let end_line = end_line as usize;
let start_line_start = line_map.line_start(start_line as u32).unwrap_or(0);
let end_line_start = line_map.line_start(end_line as u32).unwrap_or(0);
let start_col = full_text.get(start_line_start..label.span.start.min(full_text.len())).unwrap_or("").chars().count();
let end_col = full_text.get(end_line_start..label.span.end.min(full_text.len())).unwrap_or("").chars().count();
let line_num_width = (end_line + 1).to_string().len();
let padding = " ".repeat(line_num_width);
let url_str = uri.unwrap_or("<anonymous>");
let pos_str = format!("{}:{}", start_line + 1, start_col + 1);
out.push_str(&format!(" {} at {}:{}\n", chars.ltop, url_str, pos_str));
out.push_str(&format!("{} {}\n", padding, chars.vbar));
if start_line == end_line {
if let Some(line_text) = lines.get(start_line) {
out.push_str(&format!("{:>width$} {} {}\n", start_line + 1, chars.vbar, line_text, width = line_num_width));
let underline_padding = " ".repeat(start_col);
let underline_len = full_text.get(label.span.start.min(full_text.len())..label.span.end.min(full_text.len())).unwrap_or("").chars().count().max(1);
let underline = "^".repeat(underline_len);
out.push_str(&format!("{} {} {}{}", padding, chars.vbar, underline_padding, underline));
if let Some(msg) = &label.message {
out.push_str(&format!(" {}\n", msg));
}
else {
out.push_str("\n");
}
}
}
else {
for i in start_line..=end_line {
if let Some(line_text) = lines.get(i) {
let line_num = i + 1;
if i == start_line {
out.push_str(&format!("{:>width$} {} {}{}\n", line_num, chars.vbar, chars.ltop, line_text, width = line_num_width));
}
else if i == end_line {
out.push_str(&format!("{:>width$} {} {}{}\n", line_num, chars.vbar, chars.vbar, line_text, width = line_num_width));
let underline_len = end_col.max(1);
let underline = "^".repeat(underline_len);
out.push_str(&format!("{} {} {}{}{}", padding, chars.vbar, chars.lbot, chars.hbar.repeat(end_col), underline));
if let Some(msg) = &label.message {
out.push_str(&format!(" {}\n", msg));
}
else {
out.push_str("\n");
}
}
else {
out.push_str(&format!("{:>width$} {} {}{}\n", line_num, chars.vbar, chars.vbar, line_text, width = line_num_width));
}
}
}
}
out.push_str(&format!("{} {}\n", padding, chars.vbar));
}
}
pub struct HtmlEmitter;
impl Emitter for HtmlEmitter {
fn render_localized<S: Source + ?Sized, L: Localizer + ?Sized>(&self, source: &S, diagnostic: &Diagnostic, localizer: Option<&L>, uri: Option<&str>) -> String {
let mut out = String::new();
let line_map = LineMap::from_source(source);
let full_text = source.get_text_in(core::range::Range { start: 0, end: source.length() }).into_owned();
let lines: Vec<&str> = full_text.lines().collect();
let sev_class = match diagnostic.severity {
Severity::Error => "error",
Severity::Warning => "warning",
Severity::Advice => "advice",
};
let message = if let (Some(key), Some(loc)) = (&diagnostic.i18n_key, localizer) { loc.localize(key, &diagnostic.i18n_args) } else { diagnostic.message.clone() };
out.push_str("<div class=\"diagnostic\">\n");
out.push_str(&format!(" <div class=\"header {}\">\n", sev_class));
if let Some(code) = &diagnostic.code {
out.push_str(&format!(" <span class=\"code\">[{}]</span> <span class=\"message\">{}</span>\n", code, html_escape(&message)));
}
else {
out.push_str(&format!(" <span class=\"severity\">[{}]</span> <span class=\"message\">{}</span>\n", sev_class, html_escape(&message)));
}
out.push_str(" </div>\n");
for label in &diagnostic.labels {
self.render_snippet(&mut out, source, &line_map, &full_text, &lines, label, uri);
}
if let Some(help) = &diagnostic.help {
out.push_str(&format!(" <div class=\"help\">help: {}</div>\n", html_escape(help)));
}
out.push_str("</div>");
out
}
}
impl HtmlEmitter {
fn render_snippet<S: Source + ?Sized>(&self, out: &mut String, source: &S, line_map: &LineMap, full_text: &str, lines: &[&str], label: &Label, uri: Option<&str>) {
let (start_line, _) = line_map.offset_to_line_col_utf16(source, label.span.start);
let (end_line, _) = line_map.offset_to_line_col_utf16(source, label.span.end);
let start_line = start_line as usize;
let end_line = end_line as usize;
let start_line_start = line_map.line_start(start_line as u32).unwrap_or(0);
let end_line_start = line_map.line_start(end_line as u32).unwrap_or(0);
let start_col = full_text.get(start_line_start..label.span.start.min(full_text.len())).unwrap_or("").chars().count();
let end_col = full_text.get(end_line_start..label.span.end.min(full_text.len())).unwrap_or("").chars().count();
out.push_str(" <div class=\"snippet\">\n");
let location_prefix = "┌"; let url_str = uri.unwrap_or("<anonymous>");
let pos_str = format!("{}:{}", start_line + 1, start_col + 1);
out.push_str(&format!(" <div class=\"location\"> {} at {}:{}</div>\n", location_prefix, html_escape(url_str), pos_str));
out.push_str(" <pre><code>");
let line_num_width = (end_line + 1).to_string().len();
let padding = " ".repeat(line_num_width);
out.push_str(&format!("<span class=\"padding\">{}</span> <span class=\"vbar\">│</span>\n", padding));
if start_line == end_line {
if let Some(line_text) = lines.get(start_line) {
out.push_str(&format!("<span class=\"line-num\">{: >width$}</span> <span class=\"vbar\">│</span> {}\n", start_line + 1, html_escape(line_text), width = line_num_width));
let underline_padding = " ".repeat(start_col);
let underline_len = full_text.get(label.span.start.min(full_text.len())..label.span.end.min(full_text.len())).unwrap_or("").chars().count().max(1);
let underline = "^".repeat(underline_len);
out.push_str(&format!("<span class=\"padding\">{}</span> <span class=\"vbar\">│</span> <span class=\"underline\">{}{}</span>", padding, underline_padding, underline));
if let Some(msg) = &label.message {
out.push_str(&format!(" <span class=\"label-msg\">{}</span>", html_escape(msg)));
}
out.push_str("\n");
}
}
else {
for i in start_line..=end_line {
if let Some(line_text) = lines.get(i) {
let line_num = i + 1;
if i == start_line {
out.push_str(&format!("<span class=\"line-num\">{: >width$}</span> <span class=\"vbar\">│</span> <span class=\"multiline\">┌</span>{}\n", line_num, html_escape(line_text), width = line_num_width));
}
else if i == end_line {
out.push_str(&format!("<span class=\"line-num\">{: >width$}</span> <span class=\"vbar\">│</span> <span class=\"multiline\">│</span>{}\n", line_num, html_escape(line_text), width = line_num_width));
let underline_len = end_col.max(1);
let underline = "^".repeat(underline_len);
out.push_str(&format!("<span class=\"padding\">{}</span> <span class=\"vbar\">│</span> <span class=\"multiline\">└</span><span class=\"underline\">{}</span>", padding, "─".repeat(end_col)));
out.push_str(&format!("<span class=\"underline\">{}</span>", underline));
if let Some(msg) = &label.message {
out.push_str(&format!(" <span class=\"label-msg\">{}</span>", html_escape(msg)));
}
out.push_str("\n");
}
else {
out.push_str(&format!("<span class=\"line-num\">{: >width$}</span> <span class=\"vbar\">│</span> <span class=\"multiline\">│</span>{}\n", line_num, html_escape(line_text), width = line_num_width));
}
}
}
}
out.push_str(&format!("<span class=\"padding\">{}</span> <span class=\"vbar\">│</span>\n", padding));
out.push_str("</code></pre>\n");
out.push_str(" </div>\n");
}
}
fn html_escape(s: &str) -> String {
s.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """).replace('\'', "'")
}
#[cfg(feature = "serde")]
pub struct LspEmitter;
#[cfg(feature = "serde")]
impl Emitter for LspEmitter {
fn render_localized<S: Source + ?Sized, L: Localizer + ?Sized>(&self, source: &S, diagnostic: &Diagnostic, localizer: Option<&L>, uri: Option<&str>) -> String {
let line_map = LineMap::from_source(source);
let (start_line, start_character, end_line, end_character) = if let Some(label) = diagnostic.labels.first() {
let (sl, sc) = line_map.offset_to_line_col_utf16(source, label.span.start);
let (el, ec) = line_map.offset_to_line_col_utf16(source, label.span.end);
(sl, sc, el, ec)
}
else {
(0, 0, 0, 0)
};
let severity = match diagnostic.severity {
Severity::Error => 1, Severity::Warning => 2, Severity::Advice => 3, };
let message = if let (Some(key), Some(loc)) = (&diagnostic.i18n_key, localizer) { loc.localize(key, &diagnostic.i18n_args) } else { diagnostic.message.clone() };
let lsp_diag = serde_json::json!({
"range": {
"start": { "line": start_line, "character": start_character },
"end": { "line": end_line, "character": end_character }
},
"severity": severity,
"code": diagnostic.code.clone().unwrap_or_default(),
"source": "oak",
"message": message,
"relatedInformation": diagnostic.labels.iter().filter_map(|l| {
l.message.as_ref().map(|msg| {
let (sl, sc) = line_map.offset_to_line_col_utf16(source, l.span.start);
let (el, ec) = line_map.offset_to_line_col_utf16(source, l.span.end);
serde_json::json!({
"location": {
"uri": uri.unwrap_or(""),
"range": {
"start": { "line": sl, "character": sc },
"end": { "line": el, "character": ec }
}
},
"message": msg.clone()
})
})
}).collect::<Vec<serde_json::Value>>()
});
lsp_diag.to_string()
}
}