1 //! Utilities for translating from codespan types into Language Server Protocol (LSP) types
2
3 use std::ops::Range;
4
5 use codespan_reporting::files::{Error, Files};
6
7 // WARNING: Be extremely careful when adding new imports here, as it could break
8 // the compatible version range that we claim in our `Cargo.toml`. This could
9 // potentially break down-stream builds on a `cargo update`. This is an
10 // absolute no-no, breaking much of what we enjoy about Cargo!
11 use lsp_types::{Position as LspPosition, Range as LspRange};
12
location_to_position( line_str: &str, line: usize, column: usize, byte_index: usize, ) -> Result<LspPosition, Error>13 fn location_to_position(
14 line_str: &str,
15 line: usize,
16 column: usize,
17 byte_index: usize,
18 ) -> Result<LspPosition, Error> {
19 if column > line_str.len() {
20 let max = line_str.len();
21 let given = column;
22
23 Err(Error::ColumnTooLarge { given, max })
24 } else if !line_str.is_char_boundary(column) {
25 let given = byte_index;
26
27 Err(Error::InvalidCharBoundary { given })
28 } else {
29 let line_utf16 = line_str[..column].encode_utf16();
30 let character = line_utf16.count() as u32;
31 let line = line as u32;
32
33 Ok(LspPosition { line, character })
34 }
35 }
36
byte_index_to_position<'a, F>( files: &'a F, file_id: F::FileId, byte_index: usize, ) -> Result<LspPosition, Error> where F: Files<'a> + ?Sized,37 pub fn byte_index_to_position<'a, F>(
38 files: &'a F,
39 file_id: F::FileId,
40 byte_index: usize,
41 ) -> Result<LspPosition, Error>
42 where
43 F: Files<'a> + ?Sized,
44 {
45 let source = files.source(file_id)?;
46 let source = source.as_ref();
47
48 let line_index = files.line_index(file_id, byte_index)?;
49 let line_span = files.line_range(file_id, line_index).unwrap();
50
51 let line_str = source
52 .get(line_span.clone())
53 .ok_or_else(|| Error::IndexTooLarge {
54 given: if line_span.start >= source.len() {
55 line_span.start
56 } else {
57 line_span.end
58 },
59 max: source.len() - 1,
60 })?;
61 let column = byte_index - line_span.start;
62
63 location_to_position(line_str, line_index, column, byte_index)
64 }
65
byte_span_to_range<'a, F>( files: &'a F, file_id: F::FileId, span: Range<usize>, ) -> Result<LspRange, Error> where F: Files<'a> + ?Sized,66 pub fn byte_span_to_range<'a, F>(
67 files: &'a F,
68 file_id: F::FileId,
69 span: Range<usize>,
70 ) -> Result<LspRange, Error>
71 where
72 F: Files<'a> + ?Sized,
73 {
74 Ok(LspRange {
75 start: byte_index_to_position(files, file_id, span.start)?,
76 end: byte_index_to_position(files, file_id, span.end)?,
77 })
78 }
79
character_to_line_offset(line: &str, character: u32) -> Result<usize, Error>80 fn character_to_line_offset(line: &str, character: u32) -> Result<usize, Error> {
81 let line_len = line.len();
82 let mut character_offset = 0;
83
84 let mut chars = line.chars();
85 while let Some(ch) = chars.next() {
86 if character_offset == character {
87 let chars_off = chars.as_str().len();
88 let ch_off = ch.len_utf8();
89
90 return Ok(line_len - chars_off - ch_off);
91 }
92
93 character_offset += ch.len_utf16() as u32;
94 }
95
96 // Handle positions after the last character on the line
97 if character_offset == character {
98 Ok(line_len)
99 } else {
100 Err(Error::ColumnTooLarge {
101 given: character_offset as usize,
102 max: line.len(),
103 })
104 }
105 }
106
position_to_byte_index<'a, F>( files: &'a F, file_id: F::FileId, position: &LspPosition, ) -> Result<usize, Error> where F: Files<'a> + ?Sized,107 pub fn position_to_byte_index<'a, F>(
108 files: &'a F,
109 file_id: F::FileId,
110 position: &LspPosition,
111 ) -> Result<usize, Error>
112 where
113 F: Files<'a> + ?Sized,
114 {
115 let source = files.source(file_id)?;
116 let source = source.as_ref();
117
118 let line_span = files.line_range(file_id, position.line as usize).unwrap();
119 let line_str = source.get(line_span.clone()).unwrap();
120
121 let byte_offset = character_to_line_offset(line_str, position.character)?;
122
123 Ok(line_span.start + byte_offset)
124 }
125
range_to_byte_span<'a, F>( files: &'a F, file_id: F::FileId, range: &LspRange, ) -> Result<Range<usize>, Error> where F: Files<'a> + ?Sized,126 pub fn range_to_byte_span<'a, F>(
127 files: &'a F,
128 file_id: F::FileId,
129 range: &LspRange,
130 ) -> Result<Range<usize>, Error>
131 where
132 F: Files<'a> + ?Sized,
133 {
134 Ok(position_to_byte_index(files, file_id, &range.start)?
135 ..position_to_byte_index(files, file_id, &range.end)?)
136 }
137
138 #[cfg(test)]
139 mod tests {
140 use codespan_reporting::files::{Location, SimpleFiles};
141
142 use super::*;
143
144 #[test]
position()145 fn position() {
146 let text = r#"
147 let test = 2
148 let test1 = ""
149 test
150 "#;
151 let mut files = SimpleFiles::new();
152 let file_id = files.add("test", text);
153 let pos = position_to_byte_index(
154 &files,
155 file_id,
156 &LspPosition {
157 line: 3,
158 character: 2,
159 },
160 )
161 .unwrap();
162 assert_eq!(
163 Location {
164 // One-based
165 line_number: 3 + 1,
166 column_number: 2 + 1,
167 },
168 files.location(file_id, pos).unwrap()
169 );
170 }
171
172 // The protocol specifies that each `character` in position is a UTF-16 character.
173 // This means that `å` and `ä` here counts as 1 while `` counts as 2.
174 const UNICODE: &str = "åä tb";
175
176 #[test]
unicode_get_byte_index()177 fn unicode_get_byte_index() {
178 let mut files = SimpleFiles::new();
179 let file_id = files.add("unicode", UNICODE);
180
181 let result = position_to_byte_index(
182 &files,
183 file_id,
184 &LspPosition {
185 line: 0,
186 character: 3,
187 },
188 );
189 assert_eq!(result.unwrap(), 5);
190
191 let result = position_to_byte_index(
192 &files,
193 file_id,
194 &LspPosition {
195 line: 0,
196 character: 6,
197 },
198 );
199 assert_eq!(result.unwrap(), 10);
200 }
201
202 #[test]
unicode_get_position()203 fn unicode_get_position() {
204 let mut files = SimpleFiles::new();
205 let file_id = files.add("unicode", UNICODE.to_string());
206 let file_id2 = files.add("unicode newline", "\n".to_string() + UNICODE);
207
208 let result = byte_index_to_position(&files, file_id, 5);
209 assert_eq!(
210 result.unwrap(),
211 LspPosition {
212 line: 0,
213 character: 3,
214 }
215 );
216
217 let result = byte_index_to_position(&files, file_id, 10);
218 assert_eq!(
219 result.unwrap(),
220 LspPosition {
221 line: 0,
222 character: 6,
223 }
224 );
225
226 let result = byte_index_to_position(&files, file_id2, 11);
227 assert_eq!(
228 result.unwrap(),
229 LspPosition {
230 line: 1,
231 character: 6,
232 }
233 );
234 }
235 }
236