1use ropey::Rope;
8
9use crate::encoding::PositionEncoding;
10
11#[non_exhaustive]
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct Position {
18 pub line: u32,
20 pub character: u32,
22}
23
24impl Position {
25 #[must_use]
27 pub const fn new(line: u32, character: u32) -> Self {
28 Self { line, character }
29 }
30}
31
32#[non_exhaustive]
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub struct Range {
36 pub start: Position,
38 pub end: Position,
40}
41
42impl Range {
43 #[must_use]
45 pub const fn new(start: Position, end: Position) -> Self {
46 Self { start, end }
47 }
48}
49
50#[non_exhaustive]
52#[derive(Debug, Clone)]
53pub struct TextEdit {
54 pub range: Range,
56 pub text: String,
58}
59
60impl TextEdit {
61 #[must_use]
63 pub const fn new(range: Range, text: String) -> Self {
64 Self { range, text }
65 }
66}
67
68#[non_exhaustive]
70#[derive(Debug, thiserror::Error)]
71pub enum EditError {
72 #[error("position out of bounds: line {line}, character {character}")]
74 OutOfBounds {
75 line: u32,
77 character: u32,
79 },
80}
81
82pub(crate) fn apply(
83 rope: &mut Rope,
84 edits: &[TextEdit],
85 encoding: PositionEncoding,
86) -> Result<(), EditError> {
87 for edit in edits {
88 let start = position_to_byte(rope, edit.range.start, encoding)?;
89 let end = position_to_byte(rope, edit.range.end, encoding)?;
90 let start_char = rope.byte_to_char(start);
91 let end_char = rope.byte_to_char(end);
92 rope.remove(start_char..end_char);
93 rope.insert(start_char, &edit.text);
94 }
95 Ok(())
96}
97
98fn position_to_byte(
99 rope: &Rope,
100 pos: Position,
101 encoding: PositionEncoding,
102) -> Result<usize, EditError> {
103 let line_idx = pos.line as usize;
104 if line_idx > rope.len_lines() {
105 return Err(EditError::OutOfBounds {
106 line: pos.line,
107 character: pos.character,
108 });
109 }
110 let line_byte_start = rope.line_to_byte(line_idx.min(rope.len_lines()));
111 let line = rope.line(line_idx.min(rope.len_lines().saturating_sub(1)));
112 let line_str = line.to_string();
113 let char_offset = match encoding {
114 PositionEncoding::Utf8 => pos.character as usize,
115 PositionEncoding::Utf16 => utf16_to_utf8_offset(&line_str, pos.character as usize)?,
116 PositionEncoding::Utf32 => utf32_to_utf8_offset(&line_str, pos.character as usize)?,
117 };
118 Ok(line_byte_start + char_offset)
119}
120
121fn utf16_to_utf8_offset(line: &str, utf16_units: usize) -> Result<usize, EditError> {
122 let mut counted = 0usize;
123 let mut byte_offset = 0usize;
124 for ch in line.chars() {
125 if counted >= utf16_units {
126 break;
127 }
128 counted += ch.len_utf16();
129 byte_offset += ch.len_utf8();
130 }
131 if counted < utf16_units {
132 return Err(EditError::OutOfBounds {
133 line: u32::MAX,
134 character: u32::try_from(utf16_units).unwrap_or(u32::MAX),
135 });
136 }
137 Ok(byte_offset)
138}
139
140fn utf32_to_utf8_offset(line: &str, code_points: usize) -> Result<usize, EditError> {
141 line.char_indices().nth(code_points).map_or_else(
142 || {
143 if line.chars().count() == code_points {
144 Ok(line.len())
145 } else {
146 Err(EditError::OutOfBounds {
147 line: u32::MAX,
148 character: u32::try_from(code_points).unwrap_or(u32::MAX),
149 })
150 }
151 },
152 |(byte_idx, _)| Ok(byte_idx),
153 )
154}