1use bynk_syntax::span::{LineIndex, Span};
9use tower_lsp::lsp_types::{Position, Range};
10
11pub struct PositionMap<'a> {
20 source: &'a str,
21 index: LineIndex,
22}
23
24impl<'a> PositionMap<'a> {
25 pub fn new(source: &'a str) -> Self {
28 Self {
29 source,
30 index: LineIndex::new(source),
31 }
32 }
33
34 pub fn position(&self, offset: usize) -> Position {
37 let (line, character) = self.index.utf16_line_col(self.source, offset);
38 Position { line, character }
39 }
40
41 pub fn range(&self, span: Span) -> Range {
43 Range {
44 start: self.position(span.start),
45 end: self.position(span.end),
46 }
47 }
48
49 pub fn end(&self) -> Position {
51 self.position(self.source.len())
52 }
53}
54
55pub fn offset_to_position(source: &str, offset: usize) -> Position {
57 let mut line: u32 = 0;
58 let mut column: u32 = 0;
59 let bytes = source.as_bytes();
60 let limit = offset.min(bytes.len());
61 let mut i = 0;
62 while i < limit {
63 let b = bytes[i];
64 if b == b'\n' {
65 line += 1;
66 column = 0;
67 i += 1;
68 continue;
69 }
70 let cp_len = utf8_char_len(b);
72 column += if cp_len == 4 { 2 } else { 1 };
77 i += cp_len;
78 }
79 Position {
80 line,
81 character: column,
82 }
83}
84
85pub fn position_to_offset(source: &str, position: Position) -> Option<usize> {
88 let target_line = position.line;
89 let target_char = position.character;
90 let mut line: u32 = 0;
91 let mut character: u32 = 0;
92 let bytes = source.as_bytes();
93 let mut i = 0;
94 while i < bytes.len() {
95 if line == target_line && character == target_char {
96 return Some(i);
97 }
98 let b = bytes[i];
99 if b == b'\n' {
100 if line == target_line {
101 return Some(i);
103 }
104 line += 1;
105 character = 0;
106 i += 1;
107 continue;
108 }
109 let cp_len = utf8_char_len(b);
110 character += if cp_len == 4 { 2 } else { 1 };
111 i += cp_len;
112 }
113 if line == target_line && character >= target_char {
114 Some(i)
115 } else {
116 None
117 }
118}
119
120fn utf8_char_len(first: u8) -> usize {
121 if first < 0x80 {
122 1
123 } else if first < 0xC0 {
124 1
126 } else if first < 0xE0 {
127 2
128 } else if first < 0xF0 {
129 3
130 } else {
131 4
132 }
133}
134
135pub fn span_to_range(source: &str, span: Span) -> Range {
137 Range {
138 start: offset_to_position(source, span.start),
139 end: offset_to_position(source, span.end),
140 }
141}
142
143pub fn end_position(source: &str) -> Position {
146 offset_to_position(source, source.len())
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn ascii_offsets_match_columns() {
155 let src = "abc\ndef";
156 assert_eq!(offset_to_position(src, 0), Position::new(0, 0));
157 assert_eq!(offset_to_position(src, 2), Position::new(0, 2));
158 assert_eq!(offset_to_position(src, 4), Position::new(1, 0));
159 assert_eq!(offset_to_position(src, 6), Position::new(1, 2));
160 }
161
162 #[test]
163 fn position_round_trip() {
164 let src = "alpha\n beta\ngamma";
165 let p = Position::new(1, 4);
166 let off = position_to_offset(src, p).unwrap();
167 assert_eq!(offset_to_position(src, off), p);
168 }
169
170 #[test]
174 fn non_ascii_offsets_count_utf16_units() {
175 let src = "-- café\nlet x";
177 assert_eq!(position_to_offset(src, Position::new(0, 7)), Some(8));
179 assert_eq!(offset_to_position(src, 8), Position::new(0, 7));
180 assert_eq!(
182 position_to_offset(src, Position::new(1, 3)),
183 Some(src.find("let").unwrap() + 3)
184 );
185
186 let crab = "🦀ab";
188 assert_eq!(position_to_offset(crab, Position::new(0, 2)), Some(4));
189 assert_eq!(position_to_offset(crab, Position::new(0, 3)), Some(5));
190 assert_eq!(offset_to_position(crab, 4), Position::new(0, 2));
191 }
192
193 #[test]
196 fn non_ascii_round_trips_on_char_boundaries() {
197 let src = "π = 3\n-- naïve café €10 🦀\nend";
198 for line in 0..3u32 {
199 for character in 0..24u32 {
200 if let Some(off) = position_to_offset(src, Position::new(line, character)) {
201 assert!(
202 src.is_char_boundary(off),
203 "offset {off} for ({line},{character}) splits a codepoint"
204 );
205 let _ = &src[..off];
206 }
207 }
208 }
209 }
210}