1#![forbid(unsafe_code)]
4#![no_std]
5
6#[cfg(test)]
7extern crate alloc;
8
9use core::ops::Range;
10
11use codespan_reporting::files::{Error, Files};
12
13use lsp_types::{Position as LspPosition, Range as LspRange};
18
19fn location_to_position(
20 line_str: &str,
21 line: usize,
22 column: usize,
23 byte_index: usize,
24) -> Result<LspPosition, Error> {
25 if column > line_str.len() {
26 let max = line_str.len();
27 let given = column;
28
29 Err(Error::ColumnTooLarge { given, max })
30 } else if !line_str.is_char_boundary(column) {
31 let given = byte_index;
32
33 Err(Error::InvalidCharBoundary { given })
34 } else {
35 let line_utf16 = line_str[..column].encode_utf16();
36 let character = line_utf16.count() as u32;
37 let line = line as u32;
38
39 Ok(LspPosition { line, character })
40 }
41}
42
43pub fn byte_index_to_position<'a, F>(
44 files: &'a F,
45 file_id: F::FileId,
46 byte_index: usize,
47) -> Result<LspPosition, Error>
48where
49 F: Files<'a> + ?Sized,
50{
51 let source = files.source(file_id)?;
52 let source = source.as_ref();
53
54 let line_index = files.line_index(file_id, byte_index)?;
55 let line_span = files.line_range(file_id, line_index).unwrap();
56
57 let line_str = source
58 .get(line_span.clone())
59 .ok_or_else(|| Error::IndexTooLarge {
60 given: if line_span.start >= source.len() {
61 line_span.start
62 } else {
63 line_span.end
64 },
65 max: source.len() - 1,
66 })?;
67 let column = byte_index - line_span.start;
68
69 location_to_position(line_str, line_index, column, byte_index)
70}
71
72pub fn byte_span_to_range<'a, F>(
73 files: &'a F,
74 file_id: F::FileId,
75 span: Range<usize>,
76) -> Result<LspRange, Error>
77where
78 F: Files<'a> + ?Sized,
79{
80 Ok(LspRange {
81 start: byte_index_to_position(files, file_id, span.start)?,
82 end: byte_index_to_position(files, file_id, span.end)?,
83 })
84}
85
86fn character_to_line_offset(line: &str, character: u32) -> Result<usize, Error> {
87 let line_len = line.len();
88 let mut character_offset = 0;
89
90 let mut chars = line.chars();
91 while let Some(ch) = chars.next() {
92 if character_offset == character {
93 let chars_off = chars.as_str().len();
94 let ch_off = ch.len_utf8();
95
96 return Ok(line_len - chars_off - ch_off);
97 }
98
99 character_offset += ch.len_utf16() as u32;
100 }
101
102 if character_offset == character {
104 Ok(line_len)
105 } else {
106 Err(Error::ColumnTooLarge {
107 given: character_offset as usize,
108 max: line.len(),
109 })
110 }
111}
112
113pub fn position_to_byte_index<'a, F>(
114 files: &'a F,
115 file_id: F::FileId,
116 position: &LspPosition,
117) -> Result<usize, Error>
118where
119 F: Files<'a> + ?Sized,
120{
121 let source = files.source(file_id)?;
122 let source = source.as_ref();
123
124 let line_span = files.line_range(file_id, position.line as usize).unwrap();
125 let line_str = source.get(line_span.clone()).unwrap();
126
127 let byte_offset = character_to_line_offset(line_str, position.character)?;
128
129 Ok(line_span.start + byte_offset)
130}
131
132pub fn range_to_byte_span<'a, F>(
133 files: &'a F,
134 file_id: F::FileId,
135 range: &LspRange,
136) -> Result<Range<usize>, Error>
137where
138 F: Files<'a> + ?Sized,
139{
140 Ok(position_to_byte_index(files, file_id, &range.start)?
141 ..position_to_byte_index(files, file_id, &range.end)?)
142}
143
144#[cfg(test)]
145mod tests {
146 use alloc::string::ToString;
147
148 use codespan_reporting::files::{Location, SimpleFiles};
149
150 use super::*;
151
152 #[test]
153 fn position() {
154 let text = r#"
155let test = 2
156let test1 = ""
157test
158"#;
159 let mut files = SimpleFiles::new();
160 let file_id = files.add("test", text);
161 let pos = position_to_byte_index(
162 &files,
163 file_id,
164 &LspPosition {
165 line: 3,
166 character: 2,
167 },
168 )
169 .unwrap();
170 assert_eq!(
171 Location {
172 line_number: 3 + 1,
174 column_number: 2 + 1,
175 },
176 files.location(file_id, pos).unwrap()
177 );
178 }
179
180 const UNICODE: &str = "åä t𐐀b";
183
184 #[test]
185 fn unicode_get_byte_index() {
186 let mut files = SimpleFiles::new();
187 let file_id = files.add("unicode", UNICODE);
188
189 let result = position_to_byte_index(
190 &files,
191 file_id,
192 &LspPosition {
193 line: 0,
194 character: 3,
195 },
196 );
197 assert_eq!(result.unwrap(), 5);
198
199 let result = position_to_byte_index(
200 &files,
201 file_id,
202 &LspPosition {
203 line: 0,
204 character: 6,
205 },
206 );
207 assert_eq!(result.unwrap(), 10);
208 }
209
210 #[test]
211 fn unicode_get_position() {
212 let mut files = SimpleFiles::new();
213 let file_id = files.add("unicode", UNICODE.to_string());
214 let file_id2 = files.add("unicode newline", "\n".to_string() + UNICODE);
215
216 let result = byte_index_to_position(&files, file_id, 5);
217 assert_eq!(
218 result.unwrap(),
219 LspPosition {
220 line: 0,
221 character: 3,
222 }
223 );
224
225 let result = byte_index_to_position(&files, file_id, 10);
226 assert_eq!(
227 result.unwrap(),
228 LspPosition {
229 line: 0,
230 character: 6,
231 }
232 );
233
234 let result = byte_index_to_position(&files, file_id2, 11);
235 assert_eq!(
236 result.unwrap(),
237 LspPosition {
238 line: 1,
239 character: 6,
240 }
241 );
242 }
243}