Skip to main content

lsp_service/
position.rs

1use lsp_types::{ClientCapabilities, Position, PositionEncodingKind};
2use ropey::{Rope, RopeSlice};
3
4/// The unit that LSP [`Position::character`] offsets count, agreed with the client during `initialize`.
5#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
6pub enum PositionEncoding {
7	/// Offsets count UTF-8 bytes.
8	Utf8,
9	/// Offsets count UTF-16 code units. This is the LSP default for clients that do not advertise an encoding.
10	#[default]
11	Utf16,
12}
13
14impl PositionEncoding {
15	/// Picks UTF-8 when the client lists it in `general.positionEncodings`, otherwise UTF-16.
16	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	/// The value to report in `ServerCapabilities::position_encoding`.
26	pub fn kind(self) -> PositionEncodingKind {
27		match self {
28			Self::Utf8 => PositionEncodingKind::UTF8,
29			Self::Utf16 => PositionEncodingKind::UTF16,
30		}
31	}
32
33	/// The length of `text` in this encoding's units.
34	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	/// Converts `position` to a char index into `rope`.
42	///
43	/// A line past the end of the document resolves to the end of the document. A character past the end of a line,
44	/// or inside a multi-unit character, resolves to the end of that line or the start of that character.
45	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	/// Converts a char index into `rope` to a [`Position`], clamping indexes past the end to the end of the document.
61	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}