1use lsp_types::{ClientCapabilities, Position, PositionEncodingKind};
2use ropey::{Rope, RopeSlice};
3
4#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
6pub enum PositionEncoding {
7 Utf8,
9 #[default]
11 Utf16,
12}
13
14impl PositionEncoding {
15 pub fn negotiate(capabilities: &ClientCapabilities) -> Self {
17 let offers_utf8 = capabilities
18 .general
19 .as_ref()
20 .and_then(|general| general.position_encodings.as_ref())
21 .is_some_and(|encodings| encodings.contains(&PositionEncodingKind::UTF8));
22 if offers_utf8 { Self::Utf8 } else { Self::Utf16 }
23 }
24
25 pub fn kind(self) -> PositionEncodingKind {
27 match self {
28 Self::Utf8 => PositionEncodingKind::UTF8,
29 Self::Utf16 => PositionEncodingKind::UTF16,
30 }
31 }
32
33 pub fn len_of(self, text: &str) -> u32 {
35 match self {
36 Self::Utf8 => text.len() as u32,
37 Self::Utf16 => text.encode_utf16().count() as u32,
38 }
39 }
40
41 pub fn char_index(self, rope: &Rope, position: Position) -> usize {
46 let line_idx = position.line as usize;
47 if line_idx >= rope.len_lines() {
48 return rope.len_chars();
49 }
50 let line = without_line_break(rope.line(line_idx));
51 let character = position.character as usize;
52 let offset = match self {
53 Self::Utf8 if character < line.len_bytes() => line.byte_to_char(character),
54 Self::Utf16 if character < line.len_utf16_cu() => line.utf16_cu_to_char(character),
55 _ => line.len_chars(),
56 };
57 rope.line_to_char(line_idx) + offset
58 }
59
60 pub fn position(self, rope: &Rope, char_idx: usize) -> Position {
62 let char_idx = char_idx.min(rope.len_chars());
63 let line_idx = rope.char_to_line(char_idx);
64 let before = rope.slice(rope.line_to_char(line_idx)..char_idx);
65 let character = match self {
66 Self::Utf8 => before.len_bytes(),
67 Self::Utf16 => before.len_utf16_cu(),
68 };
69 Position::new(line_idx as u32, character as u32)
70 }
71}
72
73fn without_line_break(line: RopeSlice<'_>) -> RopeSlice<'_> {
74 let mut end = line.len_chars();
75 if end > 0 && line.char(end - 1) == '\n' {
76 end -= 1;
77 }
78 if end > 0 && line.char(end - 1) == '\r' {
79 end -= 1;
80 }
81 line.slice(..end)
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use lsp_types::GeneralClientCapabilities;
88
89 const ASTRAL: &str = "a🎨b\nc😀d\r\ne";
90
91 fn caps(encodings: Option<Vec<PositionEncodingKind>>) -> ClientCapabilities {
92 ClientCapabilities {
93 general: Some(GeneralClientCapabilities { position_encodings: encodings, ..Default::default() }),
94 ..Default::default()
95 }
96 }
97
98 #[test]
99 fn negotiates_utf8_only_when_offered() {
100 assert_eq!(PositionEncoding::negotiate(&ClientCapabilities::default()), PositionEncoding::Utf16);
101 assert_eq!(PositionEncoding::negotiate(&caps(None)), PositionEncoding::Utf16);
102 assert_eq!(
103 PositionEncoding::negotiate(&caps(Some(vec![PositionEncodingKind::UTF32, PositionEncodingKind::UTF16]))),
104 PositionEncoding::Utf16
105 );
106 assert_eq!(
107 PositionEncoding::negotiate(&caps(Some(vec![PositionEncodingKind::UTF16, PositionEncodingKind::UTF8]))),
108 PositionEncoding::Utf8
109 );
110 }
111
112 #[test]
113 fn len_of_counts_units() {
114 assert_eq!(PositionEncoding::Utf8.len_of("a🎨b"), 6);
115 assert_eq!(PositionEncoding::Utf16.len_of("a🎨b"), 4);
116 assert_eq!(PositionEncoding::Utf16.len_of("é"), 1);
117 }
118
119 #[test]
120 fn utf16_char_index_after_astral() {
121 let rope = Rope::from_str(ASTRAL);
122 let enc = PositionEncoding::Utf16;
123 assert_eq!(rope.char(enc.char_index(&rope, Position::new(0, 3))), 'b');
124 assert_eq!(rope.char(enc.char_index(&rope, Position::new(1, 3))), 'd');
125 assert_eq!(rope.char(enc.char_index(&rope, Position::new(2, 0))), 'e');
126 }
127
128 #[test]
129 fn utf8_char_index_after_astral() {
130 let rope = Rope::from_str(ASTRAL);
131 let enc = PositionEncoding::Utf8;
132 assert_eq!(rope.char(enc.char_index(&rope, Position::new(0, 5))), 'b');
133 assert_eq!(rope.char(enc.char_index(&rope, Position::new(1, 5))), 'd');
134 }
135
136 #[test]
137 fn char_index_inside_astral_resolves_to_its_start() {
138 let rope = Rope::from_str(ASTRAL);
139 assert_eq!(PositionEncoding::Utf16.char_index(&rope, Position::new(0, 2)), 1);
140 assert_eq!(PositionEncoding::Utf8.char_index(&rope, Position::new(0, 3)), 1);
141 }
142
143 #[test]
144 fn char_index_clamps_to_line_and_document_end() {
145 let rope = Rope::from_str(ASTRAL);
146 for enc in [PositionEncoding::Utf8, PositionEncoding::Utf16] {
147 assert_eq!(enc.char_index(&rope, Position::new(0, 99)), 3);
148 assert_eq!(enc.char_index(&rope, Position::new(1, 99)), 7);
149 assert_eq!(enc.char_index(&rope, Position::new(9, 0)), rope.len_chars());
150 }
151 }
152
153 #[test]
154 fn unicode_line_separators_are_not_line_breaks() {
155 let rope = Rope::from_str("a\u{2028}b\nc");
156 assert_eq!(rope.char(PositionEncoding::Utf16.char_index(&rope, Position::new(1, 0))), 'c');
157 }
158
159 #[test]
160 fn position_round_trips() {
161 let rope = Rope::from_str(ASTRAL);
162 for enc in [PositionEncoding::Utf8, PositionEncoding::Utf16] {
163 let inside_crlf = |i: usize| i > 0 && rope.get_char(i - 1) == Some('\r') && rope.get_char(i) == Some('\n');
164 for char_idx in (0..=rope.len_chars()).filter(|&i| !inside_crlf(i)) {
165 let position = enc.position(&rope, char_idx);
166 assert_eq!(enc.char_index(&rope, position), char_idx, "{enc:?} {char_idx} {position:?}");
167 }
168 }
169 }
170
171 #[test]
172 fn position_counts_encoding_units() {
173 let rope = Rope::from_str(ASTRAL);
174 assert_eq!(PositionEncoding::Utf16.position(&rope, 2), Position::new(0, 3));
175 assert_eq!(PositionEncoding::Utf8.position(&rope, 2), Position::new(0, 5));
176 assert_eq!(PositionEncoding::Utf16.position(&rope, 99), Position::new(2, 1));
177 }
178}