termesh_editor/
position.rs1use ropey::Rope;
8
9pub fn utf16_position(text: &Rope, offset: usize) -> (u32, u32) {
11 let offset = offset.min(text.len_chars());
12 let line = text.char_to_line(offset);
13 let line_start = text.line_to_char(line);
14 let character = text.line(line).char_to_utf16_cu(offset - line_start);
15 (line as u32, character as u32)
16}
17
18pub fn offset_from_utf16(text: &Rope, line: u32, character: u32) -> usize {
21 let line = line as usize;
22 if line >= text.len_lines() {
23 return text.len_chars();
24 }
25
26 let line_slice = text.line(line);
27 let content_len =
28 line_slice.len_chars().saturating_sub(usize::from(line_slice.chars().last() == Some('\n')));
29 let content = line_slice.slice(..content_len);
30 let character = (character as usize).min(content.len_utf16_cu());
31 text.line_to_char(line) + content.utf16_cu_to_char(character)
32}
33
34#[cfg(test)]
35mod tests {
36 use super::*;
37 use ropey::Rope;
38
39 #[test]
40 fn ascii_positions_round_trip() {
41 let text = Rope::from_str("fn main() {\n let x = 1;\n}\n");
42 let offset = 16; let (line, character) = utf16_position(&text, offset);
44 assert_eq!((line, character), (1, 4));
45 assert_eq!(offset_from_utf16(&text, line, character), offset);
46 }
47
48 #[test]
49 fn a_non_bmp_char_counts_as_two_utf16_units() {
50 let text = Rope::from_str("let s = \"🦀\";\n");
53 let after_crab = text.line(0).chars().position(|c| c == '"').unwrap() + 2;
54 let (line, character) = utf16_position(&text, after_crab);
55 assert_eq!(line, 0);
56 assert_eq!(character, 11, "🦀 must count as two UTF-16 units, not one");
57 assert_eq!(offset_from_utf16(&text, line, character), after_crab);
58 }
59
60 #[test]
61 fn multibyte_bmp_chars_count_as_one_unit() {
62 let text = Rope::from_str("// café\n");
63 let end = text.line(0).len_chars() - 1;
64 let (_, character) = utf16_position(&text, end);
65 assert_eq!(character, end as u32);
66 }
67
68 #[test]
69 fn a_position_past_the_line_clamps_to_the_line_end() {
70 let text = Rope::from_str("ab\ncd\n");
71 assert_eq!(offset_from_utf16(&text, 0, 99), 2);
72 }
73
74 #[test]
75 fn a_position_past_the_document_clamps_to_the_end() {
76 let text = Rope::from_str("ab\n");
77 assert_eq!(offset_from_utf16(&text, 99, 0), text.len_chars());
78 }
79
80 #[test]
81 fn crlf_normalised_text_uses_buffer_offsets_not_disk_offsets() {
82 let text = Rope::from_str("a\nb\n");
85 let (line, character) = utf16_position(&text, 2);
86 assert_eq!((line, character), (1, 0));
87 }
88}