sql_dialect_fmt_text/
lib.rs1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub struct LineColumn {
10 pub line: usize,
11 pub column: usize,
12}
13
14impl LineColumn {
15 pub const fn new(line: usize, column: usize) -> Self {
16 Self { line, column }
17 }
18}
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub struct Utf16Position {
23 pub line: u32,
24 pub character: u32,
25}
26
27impl Utf16Position {
28 pub const fn new(line: u32, character: u32) -> Self {
29 Self { line, character }
30 }
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub struct Utf8Position {
36 pub line: u32,
37 pub character: u32,
38}
39
40impl Utf8Position {
41 pub const fn new(line: u32, character: u32) -> Self {
42 Self { line, character }
43 }
44}
45
46pub struct LineIndex<'a> {
48 text: &'a str,
49 line_starts: Vec<usize>,
50}
51
52impl<'a> LineIndex<'a> {
53 pub fn new(text: &'a str) -> Self {
54 let mut line_starts = vec![0];
55 line_starts.extend(
56 text.bytes()
57 .enumerate()
58 .filter(|&(_, b)| b == b'\n')
59 .map(|(i, _)| i + 1),
60 );
61 Self { text, line_starts }
62 }
63
64 pub fn line_column(&self, offset: usize) -> LineColumn {
70 let offset = self.clamp_offset(offset);
71 let line = self.line_for_offset(offset);
72 let line_start = self.line_starts[line];
73 let column = self.text[line_start..offset].chars().count() + 1;
74 LineColumn::new(line + 1, column)
75 }
76
77 pub fn utf16_position(&self, offset: usize) -> Utf16Position {
82 let offset = self.clamp_offset(offset);
83 let line = self.line_for_offset(offset);
84 let line_start = self.line_starts[line];
85 let character = utf16_len(&self.text[line_start..offset]);
86 Utf16Position::new(line as u32, character)
87 }
88
89 pub fn utf8_position(&self, offset: usize) -> Utf8Position {
91 let offset = self.clamp_offset(offset);
92 let line = self.line_for_offset(offset);
93 let line_start = self.line_starts[line];
94 Utf8Position::new(line as u32, (offset - line_start) as u32)
95 }
96
97 pub fn end_utf16_position(&self) -> Utf16Position {
99 self.utf16_position(self.text.len())
100 }
101
102 pub fn end_utf8_position(&self) -> Utf8Position {
104 self.utf8_position(self.text.len())
105 }
106
107 pub fn offset_for_utf16_position(&self, position: Utf16Position) -> usize {
112 let line = position.line as usize;
113 let Some(&line_start) = self.line_starts.get(line) else {
114 return self.text.len();
115 };
116 let mut remaining = position.character as usize;
117 let mut offset = line_start;
118 for ch in self.text[line_start..].chars() {
119 let width = ch.len_utf16();
120 if remaining < width || ch == '\n' {
121 break;
122 }
123 remaining -= width;
124 offset += ch.len_utf8();
125 }
126 offset
127 }
128
129 pub fn offset_for_utf8_position(&self, position: Utf8Position) -> usize {
131 let line = position.line as usize;
132 let Some(&line_start) = self.line_starts.get(line) else {
133 return self.text.len();
134 };
135 self.clamp_offset(line_start.saturating_add(position.character as usize))
136 }
137
138 fn line_for_offset(&self, offset: usize) -> usize {
139 match self.line_starts.binary_search(&offset) {
140 Ok(line) => line,
141 Err(next) => next - 1,
142 }
143 }
144
145 fn clamp_offset(&self, offset: usize) -> usize {
146 let mut offset = offset.min(self.text.len());
147 while !self.text.is_char_boundary(offset) {
148 offset -= 1;
149 }
150 offset
151 }
152}
153
154pub fn utf16_len(text: &str) -> u32 {
156 text.chars().map(|ch| ch.len_utf16() as u32).sum()
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn maps_offsets_to_one_based_line_columns() {
165 let text = "abc\ndefg\nhi";
166 let index = LineIndex::new(text);
167 assert_eq!(index.line_column(0), LineColumn::new(1, 1));
168 assert_eq!(index.line_column(4), LineColumn::new(2, 1));
169 assert_eq!(index.line_column(6), LineColumn::new(2, 3));
170 assert_eq!(index.line_column(999), LineColumn::new(3, 3));
171 }
172
173 #[test]
174 fn maps_offsets_to_utf16_positions() {
175 let text = "SELECT a\nFROM 芋;\n";
176 let index = LineIndex::new(text);
177 assert_eq!(index.utf16_position(0), Utf16Position::new(0, 0));
178 assert_eq!(index.utf16_position(7), Utf16Position::new(0, 7));
179 assert_eq!(
180 index.utf16_position(text.find("FROM").unwrap()),
181 Utf16Position::new(1, 0)
182 );
183 assert_eq!(
184 index.utf16_position(text.find(';').unwrap()),
185 Utf16Position::new(1, 6)
186 );
187 }
188
189 #[test]
190 fn maps_offsets_to_utf8_positions() {
191 let text = "SELECT '長芋'\nFROM t";
192 let index = LineIndex::new(text);
193 assert_eq!(index.utf8_position(0), Utf8Position::new(0, 0));
194 assert_eq!(
195 index.utf8_position(text.find("FROM").unwrap()),
196 Utf8Position::new(1, 0)
197 );
198 let newline = text.find('\n').unwrap();
199 assert_eq!(
200 index.utf8_position(newline),
201 Utf8Position::new(0, newline as u32)
202 );
203 }
204
205 #[test]
206 fn maps_utf16_positions_back_to_offsets() {
207 let text = "SELECT a\nFROM 芋;\nSELECT 😀;\n";
208 let index = LineIndex::new(text);
209 for offset in [
210 0usize,
211 7,
212 text.find("FROM").unwrap(),
213 text.find(';').unwrap(),
214 text.find("😀").unwrap(),
215 ] {
216 assert_eq!(
217 index.offset_for_utf16_position(index.utf16_position(offset)),
218 offset
219 );
220 }
221 }
222
223 #[test]
224 fn maps_utf8_positions_back_to_offsets() {
225 let text = "SELECT '長芋'\nFROM t";
226 let index = LineIndex::new(text);
227 for offset in [
228 0usize,
229 7,
230 text.find("FROM").unwrap(),
231 text.find('\n').unwrap(),
232 ] {
233 assert_eq!(
234 index.offset_for_utf8_position(index.utf8_position(offset)),
235 offset
236 );
237 }
238 }
239
240 #[test]
241 fn clamps_mid_character_offsets() {
242 let text = "a😀b";
243 let index = LineIndex::new(text);
244 assert_eq!(index.line_column(2), LineColumn::new(1, 2));
245 assert_eq!(index.utf16_position(2), Utf16Position::new(0, 1));
246 }
247}