1use crate::WirePosition as Position;
10use ropey::Rope;
11use serde_json::Value;
12
13pub struct PositionMapper {
37 rope: Rope,
39 line_ending: LineEnding,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum LineEnding {
46 Lf,
48 CrLf,
50 Cr,
52 Mixed,
54}
55
56impl PositionMapper {
57 pub fn new(text: &str) -> Self {
73 let rope = Rope::from_str(text);
74 let line_ending = detect_line_ending(text);
75 Self { rope, line_ending }
76 }
77
78 pub fn update(&mut self, text: &str) {
80 self.rope = Rope::from_str(text);
81 self.line_ending = detect_line_ending(text);
82 }
83
84 pub fn apply_edit(&mut self, start_byte: usize, end_byte: usize, new_text: &str) {
86 let start_byte = start_byte.min(self.rope.len_bytes());
88 let end_byte = end_byte.min(self.rope.len_bytes());
89
90 let start_char = self.rope.byte_to_char(start_byte);
92 let end_char = self.rope.byte_to_char(end_byte);
93
94 if end_char > start_char {
96 self.rope.remove(start_char..end_char);
97 }
98
99 if !new_text.is_empty() {
101 self.rope.insert(start_char, new_text);
102 }
103
104 self.line_ending = detect_line_ending(&self.rope.to_string());
106 }
107
108 pub fn lsp_pos_to_byte(&self, pos: Position) -> Option<usize> {
124 let line_idx = pos.line as usize;
125 if line_idx >= self.rope.len_lines() {
126 return None;
127 }
128
129 let line_start_byte = self.rope.line_to_byte(line_idx);
130 let line = self.rope.line(line_idx);
131
132 let mut utf16_offset = 0u32;
134 let mut byte_offset = 0;
135
136 for ch in line.chars() {
137 if utf16_offset >= pos.character {
138 break;
139 }
140
141 let ch_utf16_len = if ch as u32 > 0xFFFF { 2 } else { 1 };
142 let next_utf16 = utf16_offset + ch_utf16_len;
143
144 if next_utf16 > pos.character {
147 break;
148 }
149
150 utf16_offset = next_utf16;
151 byte_offset += ch.len_utf8();
152 }
153
154 Some(line_start_byte + byte_offset)
155 }
156
157 pub fn byte_to_lsp_pos(&self, byte_offset: usize) -> Position {
172 let byte_offset = byte_offset.min(self.rope.len_bytes());
173
174 let line_idx = self.rope.byte_to_line(byte_offset);
175 let line_start_byte = self.rope.line_to_byte(line_idx);
176 let byte_in_line = byte_offset - line_start_byte;
177
178 let line = self.rope.line(line_idx);
180 let mut utf16_offset = 0u32;
181 let mut current_byte = 0;
182
183 for ch in line.chars() {
184 if current_byte >= byte_in_line {
185 break;
186 }
187 let ch_len = ch.len_utf8();
188 if current_byte + ch_len > byte_in_line {
189 break;
191 }
192 current_byte += ch_len;
193 let ch_utf16_len = if ch as u32 > 0xFFFF { 2 } else { 1 };
194 utf16_offset += ch_utf16_len;
195 }
196
197 Position { line: line_idx as u32, character: utf16_offset }
198 }
199
200 pub fn text(&self) -> String {
202 self.rope.to_string()
203 }
204
205 pub fn slice(&self, start_byte: usize, end_byte: usize) -> String {
207 let start = start_byte.min(self.rope.len_bytes());
208 let end = end_byte.min(self.rope.len_bytes());
209 self.rope.slice(self.rope.byte_to_char(start)..self.rope.byte_to_char(end)).to_string()
210 }
211
212 pub fn len_bytes(&self) -> usize {
214 self.rope.len_bytes()
215 }
216
217 pub fn len_lines(&self) -> usize {
219 self.rope.len_lines()
220 }
221
222 pub fn lsp_pos_to_char(&self, pos: Position) -> Option<usize> {
224 self.lsp_pos_to_byte(pos).map(|byte| self.rope.byte_to_char(byte))
225 }
226
227 pub fn char_to_lsp_pos(&self, char_idx: usize) -> Position {
229 let byte_offset = self.rope.char_to_byte(char_idx);
230 self.byte_to_lsp_pos(byte_offset)
231 }
232
233 pub fn is_empty(&self) -> bool {
235 self.rope.len_bytes() == 0
236 }
237
238 pub fn line_ending(&self) -> LineEnding {
240 self.line_ending
241 }
242}
243
244pub fn json_to_position(pos: &Value) -> Option<Position> {
248 Some(Position {
252 line: u32::try_from(pos["line"].as_u64()?).ok()?,
253 character: u32::try_from(pos["character"].as_u64()?).ok()?,
254 })
255}
256
257pub fn position_to_json(pos: Position) -> Value {
261 serde_json::json!({
262 "line": pos.line,
263 "character": pos.character
264 })
265}
266
267fn detect_line_ending(text: &str) -> LineEnding {
269 let mut crlf_count = 0;
270 let mut lf_count = 0;
271 let mut cr_count = 0;
272
273 let bytes = text.as_bytes();
274 let mut i = 0;
275 while i < bytes.len() {
276 if i + 1 < bytes.len() && bytes[i] == b'\r' && bytes[i + 1] == b'\n' {
277 crlf_count += 1;
278 i += 2;
279 } else if bytes[i] == b'\n' {
280 lf_count += 1;
281 i += 1;
282 } else if bytes[i] == b'\r' {
283 cr_count += 1;
284 i += 1;
285 } else {
286 i += 1;
287 }
288 }
289
290 if crlf_count > 0 && lf_count == 0 && cr_count == 0 {
292 LineEnding::CrLf
293 } else if lf_count > 0 && crlf_count == 0 && cr_count == 0 {
294 LineEnding::Lf
295 } else if cr_count > 0 && crlf_count == 0 && lf_count == 0 {
296 LineEnding::Cr
297 } else if crlf_count > 0 || lf_count > 0 || cr_count > 0 {
298 LineEnding::Mixed
299 } else {
300 LineEnding::Lf }
302}
303
304pub fn apply_edit_utf8(
308 text: &mut String,
309 start_byte: usize,
310 old_end_byte: usize,
311 replacement: &str,
312) {
313 if !text.is_char_boundary(start_byte) || !text.is_char_boundary(old_end_byte) {
314 return;
316 }
317 text.replace_range(start_byte..old_end_byte, replacement);
318}
319
320pub fn newline_count(text: &str) -> usize {
324 text.chars().filter(|&c| c == '\n').count()
325}
326
327pub fn last_line_column_utf8(text: &str) -> u32 {
331 let column = if let Some(last_newline) = text.rfind('\n') {
335 text.len() - last_newline - 1
336 } else {
337 text.len()
338 };
339 u32::try_from(column).unwrap_or(u32::MAX)
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345 use perl_tdd_support::must_some;
346
347 #[test]
348 fn test_lf_positions() {
349 let text = "line 1\nline 2\nline 3";
350 let mapper = PositionMapper::new(text);
351
352 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 0 }), Some(0));
354 assert_eq!(mapper.byte_to_lsp_pos(0), Position { line: 0, character: 0 });
355
356 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 3 }), Some(3));
358 assert_eq!(mapper.byte_to_lsp_pos(3), Position { line: 0, character: 3 });
359
360 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 1, character: 0 }), Some(7));
362 assert_eq!(mapper.byte_to_lsp_pos(7), Position { line: 1, character: 0 });
363 }
364
365 #[test]
366 fn test_crlf_positions() {
367 let text = "line 1\r\nline 2\r\nline 3";
368 let mapper = PositionMapper::new(text);
369
370 assert_eq!(mapper.line_ending(), LineEnding::CrLf);
371
372 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 1, character: 0 }), Some(8));
374 assert_eq!(mapper.byte_to_lsp_pos(8), Position { line: 1, character: 0 });
375
376 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 2, character: 0 }), Some(16));
378 assert_eq!(mapper.byte_to_lsp_pos(16), Position { line: 2, character: 0 });
379 }
380
381 #[test]
382 fn test_utf16_positions() {
383 let text = "hello 😀 world"; let mapper = PositionMapper::new(text);
385
386 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 6 }), Some(6));
388
389 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 8 }), Some(10)); assert_eq!(mapper.byte_to_lsp_pos(10), Position { line: 0, character: 8 });
394 }
395
396 #[test]
397 fn test_utf16_positions_clamp_mid_surrogate_to_char_start() {
398 let text = "a😀b";
399 let mapper = PositionMapper::new(text);
400
401 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 2 }), Some(1));
403 }
404
405 #[test]
406 fn test_utf16_surrogate_pair_boundaries() {
407 let text = "x💖y";
411 let mapper = PositionMapper::new(text);
412
413 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 0 }), Some(0));
415 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 1 }), Some(1));
416
417 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 2 }), Some(1));
420
421 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 3 }), Some(5));
423
424 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 4 }), Some(6));
426 }
427
428 #[test]
429 fn test_utf16_max_code_point() {
430 let max_char = '\u{10FFFF}';
433 let text = format!("a{max_char}b");
434 let mapper = PositionMapper::new(&text);
435
436 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 0 }), Some(0));
438 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 1 }), Some(1));
439 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 2 }), Some(1));
441 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 3 }), Some(5));
442 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 4 }), Some(6));
443
444 assert_eq!(mapper.byte_to_lsp_pos(0), Position { line: 0, character: 0 });
446 assert_eq!(mapper.byte_to_lsp_pos(1), Position { line: 0, character: 1 });
447 assert_eq!(mapper.byte_to_lsp_pos(5), Position { line: 0, character: 3 });
448 assert_eq!(mapper.byte_to_lsp_pos(6), Position { line: 0, character: 4 });
449 }
450
451 #[test]
452 fn test_utf16_mixed_bmp_and_supplementary_plane() {
453 let text = "aé💖ñ🎉b";
458 let mapper = PositionMapper::new(text);
459
460 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 0 }), Some(0)); assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 1 }), Some(1)); assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 2 }), Some(3)); assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 3 }), Some(3)); assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 4 }), Some(7)); assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 5 }), Some(9)); assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 6 }), Some(9)); assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 7 }), Some(13)); assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 8 }), Some(14)); }
478
479 #[test]
480 fn test_utf16_zero_length_input() {
481 let text = "";
482 let mapper = PositionMapper::new(text);
483
484 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 0 }), Some(0));
487 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 5 }), Some(0));
489
490 assert!(mapper.lsp_pos_to_byte(Position { line: 1, character: 0 }).is_none());
492
493 assert_eq!(mapper.byte_to_lsp_pos(0), Position { line: 0, character: 0 });
495 }
496
497 #[test]
498 fn test_utf16_consecutive_surrogate_pairs() {
499 let text = "💖💖";
502 let mapper = PositionMapper::new(text);
503
504 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 0 }), Some(0));
507 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 1 }), Some(0));
509 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 2 }), Some(4));
510 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 3 }), Some(4));
512 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 4 }), Some(8));
513 }
514
515 #[test]
516 fn test_utf16_clamp_matches_convert_helper() {
517 use crate::convert::utf16_line_col_to_offset;
522
523 let text = "a😀b💖c\nx💡y";
524 let mapper = PositionMapper::new(text);
525
526 for col in 0..=7 {
529 let mapper_byte =
530 mapper.lsp_pos_to_byte(Position { line: 0, character: col }).unwrap_or(usize::MAX);
531 let helper_byte = utf16_line_col_to_offset(text, 0, col);
532 assert_eq!(
533 mapper_byte, helper_byte,
534 "disagreement at line 0 col {col}: mapper={mapper_byte} helper={helper_byte}"
535 );
536 }
537 }
538
539 #[test]
540 fn test_mixed_line_endings() {
541 let text = "line 1\r\nline 2\nline 3\rline 4";
542 let mapper = PositionMapper::new(text);
543
544 assert_eq!(mapper.line_ending(), LineEnding::Mixed);
545
546 assert_eq!(mapper.byte_to_lsp_pos(0), Position { line: 0, character: 0 });
548 assert_eq!(mapper.byte_to_lsp_pos(8), Position { line: 1, character: 0 });
549 assert_eq!(mapper.byte_to_lsp_pos(15), Position { line: 2, character: 0 });
550 assert_eq!(mapper.byte_to_lsp_pos(22), Position { line: 3, character: 0 });
551 }
552
553 #[test]
554 fn test_incremental_edit() {
555 let mut mapper = PositionMapper::new("hello world");
556
557 mapper.apply_edit(6, 11, "Rust");
559 assert_eq!(mapper.text(), "hello Rust");
560
561 mapper.apply_edit(5, 5, " beautiful");
563 assert_eq!(mapper.text(), "hello beautiful Rust");
564
565 mapper.apply_edit(5, 16, " ");
567 assert_eq!(mapper.text(), "hello Rust");
568 }
569
570 #[test]
577 fn test_multibyte_utf8_round_trip_byte_to_lsp_pos_to_byte() {
578 let text = "aé🦀b";
581 let mapper = PositionMapper::new(text);
582
583 assert_eq!(mapper.byte_to_lsp_pos(0), Position { line: 0, character: 0 }); assert_eq!(mapper.byte_to_lsp_pos(1), Position { line: 0, character: 1 }); assert_eq!(mapper.byte_to_lsp_pos(3), Position { line: 0, character: 2 }); assert_eq!(mapper.byte_to_lsp_pos(7), Position { line: 0, character: 4 }); for (byte_offset, col) in [(0u32, 0u32), (1, 1), (3, 2), (7, 4)] {
592 let pos = Position { line: 0, character: col };
593 let got_byte = must_some(mapper.lsp_pos_to_byte(pos));
594 assert_eq!(
595 got_byte, byte_offset as usize,
596 "lsp_pos_to_byte for col {col} should be byte {byte_offset}"
597 );
598 assert_eq!(
600 mapper.byte_to_lsp_pos(got_byte),
601 pos,
602 "byte_to_lsp_pos should round-trip for col {col}"
603 );
604 }
605 }
606
607 #[test]
609 fn test_lsp_pos_to_char_and_char_to_lsp_pos_round_trip() {
610 let text = "aéb";
612 let mapper = PositionMapper::new(text);
613
614 let char_idx = must_some(mapper.lsp_pos_to_char(Position { line: 0, character: 1 }));
616 assert_eq!(char_idx, 1, "char index of 'é' is 1");
617
618 let pos = mapper.char_to_lsp_pos(char_idx);
620 assert_eq!(pos, Position { line: 0, character: 1 });
621
622 let char_b = must_some(mapper.lsp_pos_to_char(Position { line: 0, character: 2 }));
624 assert_eq!(char_b, 2);
625 assert_eq!(mapper.char_to_lsp_pos(char_b), Position { line: 0, character: 2 });
626 }
627
628 #[test]
631 fn test_crlf_lsp_pos_to_byte_per_line() {
632 let text = "abc\r\ndef\r\nghi";
635 let mapper = PositionMapper::new(text);
636 assert_eq!(mapper.line_ending(), LineEnding::CrLf);
637
638 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 0 }), Some(0));
640 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 0, character: 2 }), Some(2));
642 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 1, character: 0 }), Some(5));
644 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 1, character: 2 }), Some(7));
646 assert_eq!(mapper.lsp_pos_to_byte(Position { line: 2, character: 0 }), Some(10));
648 }
649
650 #[test]
652 fn test_out_of_bounds_line_returns_none() {
653 let text = "one\ntwo\n";
654 let mapper = PositionMapper::new(text);
655
656 assert!(
659 mapper.lsp_pos_to_byte(Position { line: 3, character: 0 }).is_none(),
660 "line past end of document should return None"
661 );
662 assert!(
663 mapper.lsp_pos_to_char(Position { line: 3, character: 0 }).is_none(),
664 "line past end of document should return None for lsp_pos_to_char"
665 );
666 }
667
668 #[test]
671 fn test_out_of_bounds_column_clamps_to_line_end() {
672 let text = "hello\nworld\n";
673 let mapper = PositionMapper::new(text);
674
675 let clamped = must_some(mapper.lsp_pos_to_byte(Position { line: 0, character: 9999 }));
678
679 assert!(clamped <= 6, "clamped byte {clamped} should not exceed end of line 0 (byte 6)");
682 }
683
684 #[test]
688 fn test_json_to_position_line_above_u32_max_returns_none() {
689 let over = u64::from(u32::MAX) + 1;
691 let json = serde_json::json!({ "line": over, "character": 0 });
692 assert!(
693 json_to_position(&json).is_none(),
694 "line value above u32::MAX must yield None, not a truncated position"
695 );
696
697 let json = serde_json::json!({ "line": 0, "character": over });
699 assert!(
700 json_to_position(&json).is_none(),
701 "character value above u32::MAX must yield None, not a truncated position"
702 );
703
704 let json = serde_json::json!({ "line": u32::MAX, "character": u32::MAX });
706 let pos = must_some(json_to_position(&json));
707 assert_eq!(pos.line, u32::MAX);
708 assert_eq!(pos.character, u32::MAX);
709 }
710
711 #[test]
720 fn test_last_line_column_saturating_cast_no_low_bit_truncation() {
721 let saturating = |column: usize| -> u32 { u32::try_from(column).unwrap_or(u32::MAX) };
723
724 let over_by_one = u32::MAX as usize + 1;
726 assert_eq!(
727 saturating(over_by_one),
728 u32::MAX,
729 "a >u32::MAX-length last line must saturate to u32::MAX, not truncate to 0"
730 );
731
732 assert_eq!(saturating(0), 0);
734 assert_eq!(saturating(42), 42);
735 assert_eq!(saturating(u32::MAX as usize), u32::MAX);
736
737 assert_eq!(last_line_column_utf8(""), 0);
739 assert_eq!(last_line_column_utf8("abc"), 3);
740 assert_eq!(last_line_column_utf8("a\nbc"), 2);
741 }
742}