1use ropey::Rope;
2
3use crate::Position;
4
5#[derive(Debug, Clone)]
7pub struct Buffer {
8 rope: Rope,
9 version: u64,
10}
11
12impl Buffer {
13 pub fn from_text(text: &str) -> Self {
15 Self {
16 rope: Rope::from_str(text),
17 version: 0,
18 }
19 }
20
21 pub fn new() -> Self {
23 Self {
24 rope: Rope::new(),
25 version: 0,
26 }
27 }
28
29 pub fn rope(&self) -> &Rope {
31 &self.rope
32 }
33
34 pub fn version(&self) -> u64 {
36 self.version
37 }
38
39 pub fn len_chars(&self) -> usize {
41 self.rope.len_chars()
42 }
43
44 pub fn len_lines(&self) -> usize {
46 self.rope.len_lines()
47 }
48
49 pub fn is_empty(&self) -> bool {
51 self.rope.len_chars() == 0
52 }
53
54 pub fn text(&self) -> String {
56 self.rope.to_string()
57 }
58
59 pub fn line(&self, line_idx: usize) -> ropey::RopeSlice<'_> {
61 self.rope.line(line_idx)
62 }
63
64 pub fn line_len(&self, line_idx: usize) -> usize {
67 let line = self.rope.line(line_idx);
68 let len = line.len_chars();
69 if len == 0 {
70 return 0;
71 }
72 let len = if line.char(len - 1) == '\n' { len - 1 } else { len };
74 if len > 0 && line.char(len - 1) == '\r' {
76 len - 1
77 } else {
78 len
79 }
80 }
81
82 pub fn len_bytes(&self) -> usize {
84 self.rope.len_bytes()
85 }
86
87 pub fn char_to_byte(&self, char_idx: usize) -> usize {
89 let idx = char_idx.min(self.len_chars());
90 self.rope.char_to_byte(idx)
91 }
92
93 pub fn byte_to_char(&self, byte_idx: usize) -> usize {
95 let idx = byte_idx.min(self.len_bytes());
96 self.rope.byte_to_char(idx)
97 }
98
99 pub fn line_to_char(&self, line_idx: usize) -> usize {
101 let line = line_idx.min(self.len_lines().saturating_sub(1));
102 self.rope.line_to_char(line)
103 }
104
105 pub fn pos_to_char(&self, pos: Position) -> usize {
108 let line = pos.line.min(self.len_lines().saturating_sub(1));
109 let line_start = self.rope.line_to_char(line);
110 let max_col = self.line_len(line);
111 let col = pos.col.min(max_col);
112 line_start + col
113 }
114
115 pub fn char_to_pos(&self, char_idx: usize) -> Position {
117 let idx = char_idx.min(self.len_chars());
118 let line = self.rope.char_to_line(idx);
119 let line_start = self.rope.line_to_char(line);
120 Position::new(line, idx - line_start)
121 }
122
123 pub fn clamp_pos(&self, pos: Position) -> Position {
125 let line = pos.line.min(self.len_lines().saturating_sub(1));
126 let max_col = self.line_len(line);
127 Position::new(line, pos.col.min(max_col))
128 }
129
130 pub(crate) fn insert(&mut self, char_idx: usize, text: &str) -> u64 {
132 let idx = char_idx.min(self.len_chars());
133 self.rope.insert(idx, text);
134 self.version += 1;
135 self.version
136 }
137
138 pub(crate) fn delete(&mut self, start: usize, end: usize) -> (String, u64) {
140 let s = start.min(self.len_chars());
141 let e = end.min(self.len_chars());
142 if s >= e {
143 return (String::new(), self.version);
144 }
145 let deleted: String = self.rope.slice(s..e).to_string();
146 self.rope.remove(s..e);
147 self.version += 1;
148 (deleted, self.version)
149 }
150
151 pub(crate) fn replace(&mut self, start: usize, end: usize, text: &str) -> (String, u64) {
153 let s = start.min(self.len_chars());
154 let e = end.min(self.len_chars());
155 let deleted: String = if s < e {
156 let d = self.rope.slice(s..e).to_string();
157 self.rope.remove(s..e);
158 d
159 } else {
160 String::new()
161 };
162 self.rope.insert(s, text);
163 self.version += 1;
164 (deleted, self.version)
165 }
166}
167
168impl Default for Buffer {
169 fn default() -> Self {
170 Self::new()
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn empty_buffer() {
180 let buf = Buffer::new();
181 assert!(buf.is_empty());
182 assert_eq!(buf.len_chars(), 0);
183 assert_eq!(buf.len_lines(), 1);
184 assert_eq!(buf.text(), "");
185 }
186
187 #[test]
188 fn from_str_basic() {
189 let buf = Buffer::from_text("hello\nworld");
190 assert_eq!(buf.len_lines(), 2);
191 assert_eq!(buf.line_len(0), 5); assert_eq!(buf.line_len(1), 5); assert_eq!(buf.len_chars(), 11);
194 }
195
196 #[test]
197 fn pos_to_char_and_back() {
198 let buf = Buffer::from_text("abc\ndef\nghi");
199 assert_eq!(buf.pos_to_char(Position::new(0, 0)), 0);
200 assert_eq!(buf.pos_to_char(Position::new(0, 3)), 3);
201 assert_eq!(buf.pos_to_char(Position::new(1, 0)), 4);
202 assert_eq!(buf.pos_to_char(Position::new(2, 2)), 10);
203
204 assert_eq!(buf.char_to_pos(0), Position::new(0, 0));
205 assert_eq!(buf.char_to_pos(4), Position::new(1, 0));
206 assert_eq!(buf.char_to_pos(10), Position::new(2, 2));
207 }
208
209 #[test]
210 fn pos_clamping() {
211 let buf = Buffer::from_text("ab\ncd");
212 assert_eq!(buf.clamp_pos(Position::new(99, 0)), Position::new(1, 0));
214 assert_eq!(buf.clamp_pos(Position::new(0, 99)), Position::new(0, 2));
216 }
217
218 #[test]
219 fn insert_and_version() {
220 let mut buf = Buffer::from_text("hello");
221 assert_eq!(buf.version(), 0);
222 buf.insert(5, " world");
223 assert_eq!(buf.version(), 1);
224 assert_eq!(buf.text(), "hello world");
225 }
226
227 #[test]
228 fn delete_range() {
229 let mut buf = Buffer::from_text("hello world");
230 let (deleted, _) = buf.delete(5, 11);
231 assert_eq!(deleted, " world");
232 assert_eq!(buf.text(), "hello");
233 }
234
235 #[test]
236 fn replace_range() {
237 let mut buf = Buffer::from_text("hello world");
238 let (deleted, _) = buf.replace(6, 11, "rust");
239 assert_eq!(deleted, "world");
240 assert_eq!(buf.text(), "hello rust");
241 }
242
243 #[test]
244 fn unicode_positions() {
245 let buf = Buffer::from_text("café\n日本語");
246 assert_eq!(buf.len_lines(), 2);
247 assert_eq!(buf.line_len(0), 4); assert_eq!(buf.line_len(1), 3); assert_eq!(buf.pos_to_char(Position::new(1, 2)), 7); }
251
252 #[test]
253 fn emoji_handling() {
254 let buf = Buffer::from_text("hi 👋🏽 there");
255 let text = buf.text();
257 assert_eq!(text, "hi 👋🏽 there");
258 }
259
260 #[test]
265 fn crlf_line_len_excludes_cr() {
266 let buf = Buffer::from_text("foo\r\nbar");
267 assert_eq!(buf.line_len(0), 3, "CRLF: line_len should be 3, not 4");
268 }
269
270 #[test]
273 fn crlf_pos_to_char_at_line_end_is_before_cr() {
274 let buf = Buffer::from_text("foo\r\nbar");
275 let end_col = buf.line_len(0);
276 let offset = buf.pos_to_char(Position::new(0, end_col));
277 assert_eq!(offset, 3, "CRLF: end-of-line offset should be before \\r");
279 }
280
281 #[test]
284 fn lone_cr_line_len_excludes_cr() {
285 let buf = Buffer::from_text("foo\rbar");
286 assert_eq!(buf.line_len(0), 3, "lone \\r: line_len should be 3, not 4");
287 }
288
289 #[test]
292 fn lone_cr_pos_to_char_stays_on_same_line() {
293 let buf = Buffer::from_text("foo\rbar");
294 let end_col = buf.line_len(0);
295 let offset = buf.pos_to_char(Position::new(0, end_col));
296 assert_eq!(offset, 3, "lone \\r: end-of-line offset must not escape into next line");
298 }
299}