1use rowan::{TextRange, TextSize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
6pub enum Severity {
7 Error,
8 Warning,
9}
10
11#[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#[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#[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 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 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}