1#![feature(new_range_api)]
2#![warn(missing_docs)]
3use oak_core::{
10 errors::{OakError, OakErrorKind},
11 source::Source,
12};
13use oak_vfs::LineMap;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18pub enum Severity {
19 Error,
21 Warning,
23 Advice,
25}
26
27#[derive(Debug, Clone)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub struct Label {
31 pub message: Option<String>,
33 #[cfg_attr(feature = "serde", serde(with = "oak_core::serde_range"))]
35 pub span: core::range::Range<usize>,
36 pub color: Option<String>,
38}
39
40#[derive(Debug, Clone)]
42#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
43pub struct Diagnostic {
44 pub code: Option<String>,
46 pub message: String,
48 pub i18n_key: Option<String>,
50 pub i18n_args: std::collections::HashMap<String, String>,
52 pub severity: Severity,
54 pub labels: Vec<Label>,
56 pub help: Option<String>,
58}
59
60impl Diagnostic {
61 pub fn error(message: impl Into<String>) -> Self {
63 Self { code: None, message: message.into(), i18n_key: None, i18n_args: std::collections::HashMap::new(), severity: Severity::Error, labels: Vec::new(), help: None }
64 }
65
66 pub fn warning(message: impl Into<String>) -> Self {
68 Self { code: None, message: message.into(), i18n_key: None, i18n_args: std::collections::HashMap::new(), severity: Severity::Warning, labels: Vec::new(), help: None }
69 }
70
71 pub fn with_i18n(mut self, key: impl Into<String>) -> Self {
73 self.i18n_key = Some(key.into());
74 self
75 }
76
77 pub fn with_arg(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
79 self.i18n_args.insert(key.into(), value.into());
80 self
81 }
82
83 pub fn from_provider<P: DiagnosticProvider, S: Source + ?Sized>(provider: &P, source: &S) -> Self {
85 provider.to_diagnostic(source)
86 }
87
88 pub fn with_label(mut self, span: core::range::Range<usize>, message: impl Into<String>) -> Self {
90 self.labels.push(Label { message: Some(message.into()), span, color: None });
91 self
92 }
93
94 pub fn with_help(mut self, help: impl Into<String>) -> Self {
96 self.help = Some(help.into());
97 self
98 }
99
100 pub fn with_code(mut self, code: impl Into<String>) -> Self {
102 self.code = Some(code.into());
103 self
104 }
105}
106
107impl From<&OakError> for Diagnostic {
108 fn from(error: &OakError) -> Self {
109 let message = format!("{}", error);
110 Diagnostic::error(message)
111 }
112}
113
114pub trait DiagnosticProvider {
116 fn to_diagnostic<S: Source + ?Sized>(&self, source: &S) -> Diagnostic;
118}
119
120pub trait OakDiagnosticsProvider<L: oak_core::Language> {
122 fn emit_diagnostics<S: Source + ?Sized>(&self, uri: &str, root: &oak_core::tree::RedNode<L>, source: &S) -> Vec<Diagnostic>;
124 fn emit_all_diagnostics<S: Source + ?Sized>(&self, uri: &str, source: &S) -> Vec<Diagnostic> {
126 let _ = (uri, source);
127 Vec::new()
128 }
129}
130
131impl DiagnosticProvider for OakError {
132 fn to_diagnostic<S: Source + ?Sized>(&self, source: &S) -> Diagnostic {
133 let kind = self.kind();
134 let message = kind.to_string();
135 let code = Some(kind.key().to_string());
136
137 let mut diag = Diagnostic::error(message).with_code(code.unwrap()).with_i18n(kind.key());
138
139 match kind {
140 OakErrorKind::IoError { .. } => {}
141 OakErrorKind::SyntaxError { offset, .. } | OakErrorKind::UnexpectedCharacter { offset, .. } => {
142 let start = (*offset).min(source.length());
143 let end = (start + 1).min(source.length());
144 diag = diag.with_label(core::range::Range { start, end }, "here");
145 }
146 OakErrorKind::UnexpectedToken { token, offset, .. } => {
147 let start = (*offset).min(source.length());
148 let end = (start + 1).min(source.length());
149 diag = diag.with_label(core::range::Range { start, end }, "here").with_arg("token", token.clone());
150 }
151 OakErrorKind::ExpectedToken { expected, offset, .. } => {
152 let start = (*offset).min(source.length());
153 let end = (start + 1).min(source.length());
154 diag = diag.with_label(core::range::Range { start, end }, "here").with_arg("expected", expected.clone());
155 }
156 OakErrorKind::ExpectedName { name_kind, offset, .. } => {
157 let start = (*offset).min(source.length());
158 let end = (start + 1).min(source.length());
159 diag = diag.with_label(core::range::Range { start, end }, "here").with_arg("name_kind", name_kind.clone());
160 }
161 OakErrorKind::TrailingCommaNotAllowed { offset, .. } => {
162 let start = (*offset).min(source.length());
163 let end = (start + 1).min(source.length());
164 diag = diag.with_label(core::range::Range { start, end }, "here");
165 }
166 _ => {}
167 }
168
169 diag
170 }
171}
172
173pub trait Localizer {
175 fn localize(&self, key: &str, args: &std::collections::HashMap<String, String>) -> String;
177}
178
179impl Localizer for () {
180 fn localize(&self, _key: &str, _args: &std::collections::HashMap<String, String>) -> String {
181 String::new()
182 }
183}
184
185pub trait Emitter {
187 fn render<S: Source + ?Sized>(&self, source: &S, diagnostic: &Diagnostic) -> String {
189 self.render_localized::<S, ()>(source, diagnostic, None, None)
190 }
191
192 fn render_localized<S: Source + ?Sized, L: Localizer + ?Sized>(&self, source: &S, diagnostic: &Diagnostic, localizer: Option<&L>, uri: Option<&str>) -> String;
194}
195
196pub struct ConsoleEmitter {
198 pub unicode: bool,
200}
201
202impl Default for ConsoleEmitter {
203 fn default() -> Self {
204 Self { unicode: true }
205 }
206}
207
208impl Emitter for ConsoleEmitter {
209 fn render_localized<S: Source + ?Sized, L: Localizer + ?Sized>(&self, source: &S, diagnostic: &Diagnostic, localizer: Option<&L>, uri: Option<&str>) -> String {
210 let mut out = String::new();
211 let line_map = LineMap::from_source(source);
212 let full_text = source.get_text_in(core::range::Range { start: 0, end: source.length() }).into_owned();
213 let lines: Vec<&str> = full_text.lines().collect();
214
215 let sev_name = match diagnostic.severity {
217 Severity::Error => "error",
218 Severity::Warning => "warning",
219 Severity::Advice => "advice",
220 };
221 let sev_color = match diagnostic.severity {
222 Severity::Error => "\x1b[31;1m",
223 Severity::Warning => "\x1b[33;1m",
224 Severity::Advice => "\x1b[36;1m",
225 };
226
227 let message = if let (Some(key), Some(loc)) = (&diagnostic.i18n_key, localizer) { loc.localize(key, &diagnostic.i18n_args) } else { diagnostic.message.clone() };
228
229 if let Some(code) = &diagnostic.code {
230 out.push_str(&format!("{}[{}]\x1b[0m {}\n", sev_color, code, message));
231 }
232 else {
233 out.push_str(&format!("{}[{}]\x1b[0m {}\n", sev_color, sev_name, message));
234 }
235
236 for label in &diagnostic.labels {
238 self.render_snippet(&mut out, source, &line_map, &full_text, &lines, label, uri);
239 }
240
241 if let Some(help) = &diagnostic.help {
243 out.push_str(&format!("\n\x1b[36;1mhelp\x1b[0m: {}\n", help));
244 }
245
246 out
247 }
248}
249
250struct Characters {
251 vbar: &'static str,
252 hbar: &'static str,
253 ltop: &'static str,
254 lbot: &'static str,
255}
256
257impl Characters {
258 fn unicode() -> Self {
259 Self { vbar: "│", hbar: "─", ltop: "┌", lbot: "└" }
260 }
261
262 fn ascii() -> Self {
263 Self { vbar: "|", hbar: "_", ltop: "/", lbot: "|" }
264 }
265}
266
267impl ConsoleEmitter {
268 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>) {
269 let chars = if self.unicode { Characters::unicode() } else { Characters::ascii() };
270 let (start_line, _) = line_map.offset_to_line_col_utf16(source, label.span.start);
271 let (end_line, _) = line_map.offset_to_line_col_utf16(source, label.span.end);
272 let start_line = start_line as usize;
273 let end_line = end_line as usize;
274 let start_line_start = line_map.line_start(start_line as u32).unwrap_or(0);
275 let end_line_start = line_map.line_start(end_line as u32).unwrap_or(0);
276 let start_col = full_text.get(start_line_start..label.span.start.min(full_text.len())).unwrap_or("").chars().count();
277 let end_col = full_text.get(end_line_start..label.span.end.min(full_text.len())).unwrap_or("").chars().count();
278
279 let line_num_width = (end_line + 1).to_string().len();
280 let padding = " ".repeat(line_num_width);
281
282 let url_str = uri.unwrap_or("<anonymous>");
284 let pos_str = format!("{}:{}", start_line + 1, start_col + 1);
285 out.push_str(&format!(" \x1b[34m{}\x1b[0m at {}:{}\n", chars.ltop, url_str, pos_str));
286 out.push_str(&format!("{} \x1b[34m{}\x1b[0m\n", padding, chars.vbar));
287
288 if start_line == end_line {
289 if let Some(line_text) = lines.get(start_line) {
291 out.push_str(&format!("{:>width$} \x1b[34m{}\x1b[0m {}\n", start_line + 1, chars.vbar, line_text, width = line_num_width));
292
293 let underline_padding = " ".repeat(start_col);
294 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);
295 let underline = "^".repeat(underline_len);
296
297 let color = label.color.as_deref().unwrap_or("\x1b[31;1m");
298 out.push_str(&format!("{} \x1b[34m{}\x1b[0m {}{}{}\x1b[0m", padding, chars.vbar, underline_padding, color, underline));
299
300 if let Some(msg) = &label.message {
301 out.push_str(&format!(" {}\n", msg));
302 }
303 else {
304 out.push_str("\n");
305 }
306 }
307 }
308 else {
309 let color = label.color.as_deref().unwrap_or("\x1b[31;1m");
311 for i in start_line..=end_line {
312 if let Some(line_text) = lines.get(i) {
313 let line_num = i + 1;
314 if i == start_line {
315 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));
316 }
317 else if i == end_line {
318 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));
319
320 let underline_len = end_col.max(1);
321 let underline = "^".repeat(underline_len);
322 out.push_str(&format!("{} \x1b[34m{}\x1b[0m {}{}{}{}", padding, chars.vbar, color, chars.lbot, chars.hbar.repeat(end_col), underline));
323
324 if let Some(msg) = &label.message {
325 out.push_str(&format!(" {}\x1b[0m\n", msg));
326 }
327 else {
328 out.push_str("\x1b[0m\n");
329 }
330 }
331 else {
332 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));
333 }
334 }
335 }
336 }
337 out.push_str(&format!("{} \x1b[34m{}\x1b[0m\n", padding, chars.vbar));
338 }
339}
340
341pub struct PlainTextEmitter {
343 pub unicode: bool,
345}
346
347impl Default for PlainTextEmitter {
348 fn default() -> Self {
349 Self { unicode: false }
350 }
351}
352
353impl Emitter for PlainTextEmitter {
354 fn render_localized<S: Source + ?Sized, L: Localizer + ?Sized>(&self, source: &S, diagnostic: &Diagnostic, localizer: Option<&L>, uri: Option<&str>) -> String {
355 let mut out = String::new();
356 let line_map = LineMap::from_source(source);
357 let full_text = source.get_text_in(core::range::Range { start: 0, end: source.length() }).into_owned();
358 let lines: Vec<&str> = full_text.lines().collect();
359
360 let sev_name = match diagnostic.severity {
362 Severity::Error => "error",
363 Severity::Warning => "warning",
364 Severity::Advice => "advice",
365 };
366
367 let message = if let (Some(key), Some(loc)) = (&diagnostic.i18n_key, localizer) { loc.localize(key, &diagnostic.i18n_args) } else { diagnostic.message.clone() };
368
369 if let Some(code) = &diagnostic.code {
370 out.push_str(&format!("[{}] {}\n", code, message));
371 }
372 else {
373 out.push_str(&format!("[{}] {}\n", sev_name, message));
374 }
375
376 for label in &diagnostic.labels {
378 self.render_snippet(&mut out, source, &line_map, &full_text, &lines, label, uri);
379 }
380
381 if let Some(help) = &diagnostic.help {
383 out.push_str(&format!("\nhelp: {}\n", help));
384 }
385
386 out
387 }
388}
389
390impl PlainTextEmitter {
391 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>) {
392 let chars = if self.unicode { Characters::unicode() } else { Characters::ascii() };
393 let (start_line, _) = line_map.offset_to_line_col_utf16(source, label.span.start);
394 let (end_line, _) = line_map.offset_to_line_col_utf16(source, label.span.end);
395 let start_line = start_line as usize;
396 let end_line = end_line as usize;
397 let start_line_start = line_map.line_start(start_line as u32).unwrap_or(0);
398 let end_line_start = line_map.line_start(end_line as u32).unwrap_or(0);
399 let start_col = full_text.get(start_line_start..label.span.start.min(full_text.len())).unwrap_or("").chars().count();
400 let end_col = full_text.get(end_line_start..label.span.end.min(full_text.len())).unwrap_or("").chars().count();
401
402 let line_num_width = (end_line + 1).to_string().len();
403 let padding = " ".repeat(line_num_width);
404
405 let url_str = uri.unwrap_or("<anonymous>");
407 let pos_str = format!("{}:{}", start_line + 1, start_col + 1);
408 out.push_str(&format!(" {} at {}:{}\n", chars.ltop, url_str, pos_str));
409 out.push_str(&format!("{} {}\n", padding, chars.vbar));
410
411 if start_line == end_line {
412 if let Some(line_text) = lines.get(start_line) {
413 out.push_str(&format!("{:>width$} {} {}\n", start_line + 1, chars.vbar, line_text, width = line_num_width));
414 let underline_padding = " ".repeat(start_col);
415 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);
416 let underline = "^".repeat(underline_len);
417 out.push_str(&format!("{} {} {}{}", padding, chars.vbar, underline_padding, underline));
418 if let Some(msg) = &label.message {
419 out.push_str(&format!(" {}\n", msg));
420 }
421 else {
422 out.push_str("\n");
423 }
424 }
425 }
426 else {
427 for i in start_line..=end_line {
428 if let Some(line_text) = lines.get(i) {
429 let line_num = i + 1;
430 if i == start_line {
431 out.push_str(&format!("{:>width$} {} {}{}\n", line_num, chars.vbar, chars.ltop, line_text, width = line_num_width));
432 }
433 else if i == end_line {
434 out.push_str(&format!("{:>width$} {} {}{}\n", line_num, chars.vbar, chars.vbar, line_text, width = line_num_width));
435
436 let underline_len = end_col.max(1);
437 let underline = "^".repeat(underline_len);
438 out.push_str(&format!("{} {} {}{}{}", padding, chars.vbar, chars.lbot, chars.hbar.repeat(end_col), underline));
439
440 if let Some(msg) = &label.message {
441 out.push_str(&format!(" {}\n", msg));
442 }
443 else {
444 out.push_str("\n");
445 }
446 }
447 else {
448 out.push_str(&format!("{:>width$} {} {}{}\n", line_num, chars.vbar, chars.vbar, line_text, width = line_num_width));
449 }
450 }
451 }
452 }
453 out.push_str(&format!("{} {}\n", padding, chars.vbar));
454 }
455}
456
457pub struct HtmlEmitter;
459
460impl Emitter for HtmlEmitter {
461 fn render_localized<S: Source + ?Sized, L: Localizer + ?Sized>(&self, source: &S, diagnostic: &Diagnostic, localizer: Option<&L>, uri: Option<&str>) -> String {
462 let mut out = String::new();
463 let line_map = LineMap::from_source(source);
464 let full_text = source.get_text_in(core::range::Range { start: 0, end: source.length() }).into_owned();
465 let lines: Vec<&str> = full_text.lines().collect();
466 let sev_class = match diagnostic.severity {
467 Severity::Error => "error",
468 Severity::Warning => "warning",
469 Severity::Advice => "advice",
470 };
471
472 let message = if let (Some(key), Some(loc)) = (&diagnostic.i18n_key, localizer) { loc.localize(key, &diagnostic.i18n_args) } else { diagnostic.message.clone() };
473
474 out.push_str("<div class=\"diagnostic\">\n");
475 out.push_str(&format!(" <div class=\"header {}\">\n", sev_class));
476 if let Some(code) = &diagnostic.code {
477 out.push_str(&format!(" <span class=\"code\">[{}]</span> <span class=\"message\">{}</span>\n", code, html_escape(&message)));
478 }
479 else {
480 out.push_str(&format!(" <span class=\"severity\">[{}]</span> <span class=\"message\">{}</span>\n", sev_class, html_escape(&message)));
481 }
482 out.push_str(" </div>\n");
483
484 for label in &diagnostic.labels {
485 self.render_snippet(&mut out, source, &line_map, &full_text, &lines, label, uri);
486 }
487
488 if let Some(help) = &diagnostic.help {
489 out.push_str(&format!(" <div class=\"help\">help: {}</div>\n", html_escape(help)));
490 }
491 out.push_str("</div>");
492
493 out
494 }
495}
496
497impl HtmlEmitter {
498 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>) {
499 let (start_line, _) = line_map.offset_to_line_col_utf16(source, label.span.start);
500 let (end_line, _) = line_map.offset_to_line_col_utf16(source, label.span.end);
501 let start_line = start_line as usize;
502 let end_line = end_line as usize;
503 let start_line_start = line_map.line_start(start_line as u32).unwrap_or(0);
504 let end_line_start = line_map.line_start(end_line as u32).unwrap_or(0);
505 let start_col = full_text.get(start_line_start..label.span.start.min(full_text.len())).unwrap_or("").chars().count();
506 let end_col = full_text.get(end_line_start..label.span.end.min(full_text.len())).unwrap_or("").chars().count();
507
508 out.push_str(" <div class=\"snippet\">\n");
509 let location_prefix = "┌"; let url_str = uri.unwrap_or("<anonymous>");
511 let pos_str = format!("{}:{}", start_line + 1, start_col + 1);
512 out.push_str(&format!(" <div class=\"location\"> {} at {}:{}</div>\n", location_prefix, html_escape(url_str), pos_str));
513
514 out.push_str(" <pre><code>");
515 let line_num_width = (end_line + 1).to_string().len();
516 let padding = " ".repeat(line_num_width);
517
518 out.push_str(&format!("<span class=\"padding\">{}</span> <span class=\"vbar\">│</span>\n", padding));
519
520 if start_line == end_line {
521 if let Some(line_text) = lines.get(start_line) {
522 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));
523
524 let underline_padding = " ".repeat(start_col);
525 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);
526 let underline = "^".repeat(underline_len);
527 out.push_str(&format!("<span class=\"padding\">{}</span> <span class=\"vbar\">│</span> <span class=\"underline\">{}{}</span>", padding, underline_padding, underline));
528 if let Some(msg) = &label.message {
529 out.push_str(&format!(" <span class=\"label-msg\">{}</span>", html_escape(msg)));
530 }
531 out.push_str("\n");
532 }
533 }
534 else {
535 for i in start_line..=end_line {
536 if let Some(line_text) = lines.get(i) {
537 let line_num = i + 1;
538 if i == start_line {
539 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));
540 }
541 else if i == end_line {
542 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));
543
544 let underline_len = end_col.max(1);
545 let underline = "^".repeat(underline_len);
546 out.push_str(&format!("<span class=\"padding\">{}</span> <span class=\"vbar\">│</span> <span class=\"multiline\">└</span><span class=\"underline\">{}</span>", padding, "─".repeat(end_col)));
547 out.push_str(&format!("<span class=\"underline\">{}</span>", underline));
548 if let Some(msg) = &label.message {
549 out.push_str(&format!(" <span class=\"label-msg\">{}</span>", html_escape(msg)));
550 }
551 out.push_str("\n");
552 }
553 else {
554 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));
555 }
556 }
557 }
558 }
559 out.push_str(&format!("<span class=\"padding\">{}</span> <span class=\"vbar\">│</span>\n", padding));
560 out.push_str("</code></pre>\n");
561 out.push_str(" </div>\n");
562 }
563}
564
565fn html_escape(s: &str) -> String {
566 s.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """).replace('\'', "'")
567}
568
569#[cfg(feature = "serde")]
571pub struct LspEmitter;
572
573#[cfg(feature = "serde")]
574impl Emitter for LspEmitter {
575 fn render_localized<S: Source + ?Sized, L: Localizer + ?Sized>(&self, source: &S, diagnostic: &Diagnostic, localizer: Option<&L>, uri: Option<&str>) -> String {
576 let line_map = LineMap::from_source(source);
577 let (start_line, start_character, end_line, end_character) = if let Some(label) = diagnostic.labels.first() {
578 let (sl, sc) = line_map.offset_to_line_col_utf16(source, label.span.start);
579 let (el, ec) = line_map.offset_to_line_col_utf16(source, label.span.end);
580 (sl, sc, el, ec)
581 }
582 else {
583 (0, 0, 0, 0)
584 };
585
586 let severity = match diagnostic.severity {
587 Severity::Error => 1, Severity::Warning => 2, Severity::Advice => 3, };
591
592 let message = if let (Some(key), Some(loc)) = (&diagnostic.i18n_key, localizer) { loc.localize(key, &diagnostic.i18n_args) } else { diagnostic.message.clone() };
593
594 let lsp_diag = serde_json::json!({
595 "range": {
596 "start": { "line": start_line, "character": start_character },
597 "end": { "line": end_line, "character": end_character }
598 },
599 "severity": severity,
600 "code": diagnostic.code.clone().unwrap_or_default(),
601 "source": "oak",
602 "message": message,
603 "relatedInformation": diagnostic.labels.iter().filter_map(|l| {
604 l.message.as_ref().map(|msg| {
605 let (sl, sc) = line_map.offset_to_line_col_utf16(source, l.span.start);
606 let (el, ec) = line_map.offset_to_line_col_utf16(source, l.span.end);
607 serde_json::json!({
608 "location": {
609 "uri": uri.unwrap_or(""),
610 "range": {
611 "start": { "line": sl, "character": sc },
612 "end": { "line": el, "character": ec }
613 }
614 },
615 "message": msg.clone()
616 })
617 })
618 }).collect::<Vec<serde_json::Value>>()
619 });
620
621 lsp_diag.to_string()
622 }
623}