Skip to main content

fml_syntax/
diagnostic.rs

1//! Diagnostics and source positions.
2
3use rowan::{TextRange, TextSize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
6pub enum Severity {
7    Error,
8    Warning,
9}
10
11/// A problem found in the source, located by byte range.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Diagnostic {
14    pub severity: Severity,
15    pub range: TextRange,
16    pub message: String,
17}
18
19impl Diagnostic {
20    pub fn error(range: TextRange, message: impl Into<String>) -> Self {
21        Self {
22            severity: Severity::Error,
23            range,
24            message: message.into(),
25        }
26    }
27
28    pub fn warning(range: TextRange, message: impl Into<String>) -> Self {
29        Self {
30            severity: Severity::Warning,
31            range,
32            message: message.into(),
33        }
34    }
35}
36
37/// A position in the source: zero-based line, and zero-based column counted both in UTF-8 bytes
38/// and in UTF-16 code units (the unit LSP and the Mapping Language debug trace use).
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
40pub struct LineCol {
41    pub line: u32,
42    pub col: u32,
43    pub col_utf16: u32,
44}
45
46/// Converts byte offsets to line and column.
47#[derive(Debug, Clone)]
48pub struct LineIndex {
49    line_starts: Vec<TextSize>,
50}
51
52impl LineIndex {
53    pub fn new(text: &str) -> Self {
54        let mut line_starts = vec![TextSize::from(0)];
55        let bytes = text.as_bytes();
56        let mut i = 0;
57        while i < bytes.len() {
58            match bytes[i] {
59                b'\n' => line_starts.push(TextSize::from((i + 1) as u32)),
60                b'\r' if bytes.get(i + 1) != Some(&b'\n') => {
61                    line_starts.push(TextSize::from((i + 1) as u32))
62                }
63                _ => {}
64            }
65            i += 1;
66        }
67        Self { line_starts }
68    }
69
70    /// The position of `offset` in `text`, which must be the text this index was built from.
71    pub fn line_col(&self, text: &str, offset: TextSize) -> LineCol {
72        let line = self
73            .line_starts
74            .partition_point(|&start| start <= offset)
75            .saturating_sub(1);
76        let start = self.line_starts[line];
77        let prefix = &text[usize::from(start)..usize::from(offset)];
78        LineCol {
79            line: line as u32,
80            col: prefix.len() as u32,
81            col_utf16: prefix.encode_utf16().count() as u32,
82        }
83    }
84    /// The offset of a zero-based line and UTF-16 column in `text`, which must be the text this
85    /// index was built from. Positions past the end of a line or of the text are clamped.
86    pub fn offset_utf16(&self, text: &str, line: u32, col_utf16: u32) -> TextSize {
87        let Some(&start) = self.line_starts.get(line as usize) else {
88            return TextSize::of(text);
89        };
90        let end = self
91            .line_starts
92            .get(line as usize + 1)
93            .map_or(text.len(), |&next| usize::from(next));
94        let mut offset = usize::from(start);
95        let mut units = 0;
96        for c in text[offset..end].chars() {
97            if units >= col_utf16 || c == '\n' || c == '\r' {
98                break;
99            }
100            units += c.len_utf16() as u32;
101            offset += c.len_utf8();
102        }
103        TextSize::from(offset as u32)
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn line_and_columns() {
113        let text = "ab\nцd\r\nx";
114        let index = LineIndex::new(text);
115        let at = |offset: u32| index.line_col(text, TextSize::from(offset));
116        assert_eq!(
117            at(0),
118            LineCol {
119                line: 0,
120                col: 0,
121                col_utf16: 0
122            }
123        );
124        assert_eq!(
125            at(5),
126            LineCol {
127                line: 1,
128                col: 2,
129                col_utf16: 1
130            }
131        );
132        assert_eq!(
133            at(8),
134            LineCol {
135                line: 2,
136                col: 0,
137                col_utf16: 0
138            }
139        );
140    }
141
142    #[test]
143    fn offsets_from_utf16_positions() {
144        let text = "ab\n𝄞d\r\nx";
145        let index = LineIndex::new(text);
146        let at = |line, col| u32::from(index.offset_utf16(text, line, col));
147        assert_eq!(at(0, 1), 1);
148        assert_eq!(at(1, 2), 7);
149        assert_eq!(at(1, 3), 8);
150        assert_eq!(at(1, 9), 8);
151        assert_eq!(at(2, 0), 10);
152        assert_eq!(at(7, 0), 11);
153        for offset in [0u32, 1, 3, 7, 8, 10, 11] {
154            let position = index.line_col(text, TextSize::from(offset));
155            assert_eq!(at(position.line, position.col_utf16), offset);
156        }
157    }
158}