1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
use crate::db::SourceDb;
use crate::files::{SourceFileId, Utf8PathBuf};
use crate::Span;
pub use codespan_reporting::diagnostic as cs;
use codespan_reporting::files::Error as CsError;
use codespan_reporting::term;
pub use cs::Severity;
use std::ops::Range;
use std::rc::Rc;
use term::termcolor::{BufferWriter, ColorChoice};

#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct Diagnostic {
    pub severity: Severity,
    pub message: String,
    pub labels: Vec<Label>,
    pub notes: Vec<String>,
}
impl Diagnostic {
    pub fn into_cs(self) -> cs::Diagnostic<SourceFileId> {
        cs::Diagnostic {
            severity: self.severity,
            code: None,
            message: self.message,
            labels: self.labels.into_iter().map(Label::into_cs_label).collect(),
            notes: self.notes,
        }
    }
    pub fn error(message: String) -> Self {
        Self {
            severity: Severity::Error,
            message,
            labels: vec![],
            notes: vec![],
        }
    }
}

#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum LabelStyle {
    Primary,
    Secondary,
}
impl From<LabelStyle> for cs::LabelStyle {
    fn from(other: LabelStyle) -> cs::LabelStyle {
        match other {
            LabelStyle::Primary => cs::LabelStyle::Primary,
            LabelStyle::Secondary => cs::LabelStyle::Secondary,
        }
    }
}

#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct Label {
    pub style: LabelStyle,
    pub span: Span,
    pub message: String,
}
impl Label {
    /// Create a primary label with the given message. This will underline the
    /// given span with carets (`^^^^`).
    pub fn primary<S: Into<String>>(span: Span, message: S) -> Self {
        Label {
            style: LabelStyle::Primary,
            span,
            message: message.into(),
        }
    }

    /// Create a secondary label with the given message. This will underline the
    /// given span with hyphens (`----`).
    pub fn secondary<S: Into<String>>(span: Span, message: S) -> Self {
        Label {
            style: LabelStyle::Secondary,
            span,
            message: message.into(),
        }
    }

    /// Convert into a [`codespan_reporting::Diagnostic::Label`]
    pub fn into_cs_label(self) -> cs::Label<SourceFileId> {
        cs::Label {
            style: self.style.into(),
            file_id: self.span.file_id,
            range: self.span.into(),
            message: self.message,
        }
    }
}

/// Print the given diagnostics to stderr.
pub fn print_diagnostics(db: &dyn SourceDb, diagnostics: &[Diagnostic]) {
    let writer = BufferWriter::stderr(ColorChoice::Auto);
    let mut buffer = writer.buffer();
    let config = term::Config::default();
    let files = SourceDbWrapper(db);

    for diag in diagnostics {
        term::emit(&mut buffer, &config, &files, &diag.clone().into_cs()).unwrap();
    }
    // If we use `writer` here, the output won't be captured by rust's test system.
    eprintln!("{}", std::str::from_utf8(buffer.as_slice()).unwrap());
}

/// Format the given diagnostics as a string.
pub fn diagnostics_string(db: &dyn SourceDb, diagnostics: &[Diagnostic]) -> String {
    let writer = BufferWriter::stderr(ColorChoice::Never);
    let mut buffer = writer.buffer();
    let config = term::Config::default();
    let files = SourceDbWrapper(db);

    for diag in diagnostics {
        term::emit(&mut buffer, &config, &files, &diag.clone().into_cs())
            .expect("failed to emit diagnostic");
    }
    std::str::from_utf8(buffer.as_slice()).unwrap().to_string()
}

struct SourceDbWrapper<'a>(pub &'a dyn SourceDb);

impl<'a> codespan_reporting::files::Files<'_> for SourceDbWrapper<'a> {
    type FileId = SourceFileId;
    type Name = Rc<Utf8PathBuf>;
    type Source = Rc<str>;

    fn name(&self, file: SourceFileId) -> Result<Self::Name, CsError> {
        Ok(file.path(self.0))
    }

    fn source(&self, file: SourceFileId) -> Result<Self::Source, CsError> {
        Ok(file.content(self.0))
    }

    fn line_index(&self, file: SourceFileId, byte_index: usize) -> Result<usize, CsError> {
        Ok(file.line_index(self.0, byte_index))
    }

    fn line_range(&self, file: SourceFileId, line_index: usize) -> Result<Range<usize>, CsError> {
        file.line_range(self.0, line_index)
            .ok_or(CsError::LineTooLarge {
                given: line_index,
                max: self.0.file_line_starts(file).len() - 1,
            })
    }
}