codespan_lsp/
lib.rs

1//! Utilities for translating from codespan types into Language Server Protocol (LSP) types
2
3#![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
13// WARNING: Be extremely careful when adding new imports here, as it could break
14// the compatible version range that we claim in our `Cargo.toml`. This could
15// potentially break down-stream builds on a `cargo update`. This is an
16// absolute no-no, breaking much of what we enjoy about Cargo!
17use 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    // Handle positions after the last character on the line
103    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                // One-based
173                line_number: 3 + 1,
174                column_number: 2 + 1,
175            },
176            files.location(file_id, pos).unwrap()
177        );
178    }
179
180    // The protocol specifies that each `character` in position is a UTF-16 character.
181    // This means that `å` and `ä` here counts as 1 while `𐐀` counts as 2.
182    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}