1use unicode_width::UnicodeWidthStr;
2
3#[derive(Debug, Clone)]
4pub struct Editor {
5 content: String,
6 cursor: usize,
7 scroll_offset: usize,
8 vertical_scroll: usize,
9 undo_stack: Vec<(String, usize)>,
10 redo_stack: Vec<(String, usize)>,
11}
12
13impl Editor {
14 pub fn new() -> Self {
15 Self {
16 content: String::new(),
17 cursor: 0,
18 scroll_offset: 0,
19 vertical_scroll: 0,
20 undo_stack: Vec::new(),
21 redo_stack: Vec::new(),
22 }
23 }
24
25 pub fn with_content(content: String) -> Self {
26 let cursor = content.len();
27 Self {
28 content,
29 cursor,
30 scroll_offset: 0,
31 vertical_scroll: 0,
32 undo_stack: Vec::new(),
33 redo_stack: Vec::new(),
34 }
35 }
36
37 pub fn content(&self) -> &str {
38 &self.content
39 }
40
41 pub fn cursor(&self) -> usize {
42 self.cursor
43 }
44
45 pub fn scroll_offset(&self) -> usize {
46 self.scroll_offset
47 }
48
49 pub fn vertical_scroll(&self) -> usize {
50 self.vertical_scroll
51 }
52
53 pub fn cursor_line_col(&self) -> (usize, usize) {
55 let before = &self.content[..self.cursor];
56 let line = before.matches('\n').count();
57 let line_start = before.rfind('\n').map(|p| p + 1).unwrap_or(0);
58 let col = UnicodeWidthStr::width(&self.content[line_start..self.cursor]);
59 (line, col)
60 }
61
62 pub fn line_count(&self) -> usize {
63 self.content.matches('\n').count() + 1
64 }
65
66 fn line_start(&self, n: usize) -> usize {
68 if n == 0 {
69 return 0;
70 }
71 let mut count = 0;
72 for (i, c) in self.content.char_indices() {
73 if c == '\n' {
74 count += 1;
75 if count == n {
76 return i + 1;
77 }
78 }
79 }
80 self.content.len()
81 }
82
83 fn line_end(&self, n: usize) -> usize {
85 let start = self.line_start(n);
86 match self.content[start..].find('\n') {
87 Some(pos) => start + pos,
88 None => self.content.len(),
89 }
90 }
91
92 fn line_content(&self, n: usize) -> &str {
94 &self.content[self.line_start(n)..self.line_end(n)]
95 }
96
97 pub fn visual_cursor(&self) -> usize {
99 let (_, col) = self.cursor_line_col();
100 col.saturating_sub(self.scroll_offset)
101 }
102
103 fn push_undo_snapshot(&mut self) {
104 self.undo_stack.push((self.content.clone(), self.cursor));
105 if self.undo_stack.len() > 500 {
106 self.undo_stack.remove(0);
107 }
108 self.redo_stack.clear();
109 }
110
111 pub fn undo(&mut self) -> bool {
112 if let Some((content, cursor)) = self.undo_stack.pop() {
113 self.redo_stack.push((self.content.clone(), self.cursor));
114 self.content = content;
115 self.cursor = cursor;
116 true
117 } else {
118 false
119 }
120 }
121
122 pub fn redo(&mut self) -> bool {
123 if let Some((content, cursor)) = self.redo_stack.pop() {
124 self.undo_stack.push((self.content.clone(), self.cursor));
125 self.content = content;
126 self.cursor = cursor;
127 true
128 } else {
129 false
130 }
131 }
132
133 pub fn insert_char(&mut self, c: char) {
134 self.push_undo_snapshot();
135 self.content.insert(self.cursor, c);
136 self.cursor += c.len_utf8();
137 }
138
139 pub fn insert_newline(&mut self) {
140 self.push_undo_snapshot();
141 self.content.insert(self.cursor, '\n');
142 self.cursor += 1;
143 }
144
145 pub fn delete_back(&mut self) {
146 if self.cursor > 0 {
147 self.push_undo_snapshot();
148 let prev = self.prev_char_boundary();
149 self.content.drain(prev..self.cursor);
150 self.cursor = prev;
151 }
152 }
153
154 pub fn delete_forward(&mut self) {
155 if self.cursor < self.content.len() {
156 self.push_undo_snapshot();
157 let next = self.next_char_boundary();
158 self.content.drain(self.cursor..next);
159 }
160 }
161
162 pub fn move_left(&mut self) {
163 if self.cursor > 0 {
164 self.cursor = self.prev_char_boundary();
165 }
166 }
167
168 pub fn move_right(&mut self) {
169 if self.cursor < self.content.len() {
170 self.cursor = self.next_char_boundary();
171 }
172 }
173
174 pub fn move_up(&mut self) {
175 let (line, col) = self.cursor_line_col();
176 if line > 0 {
177 let target_line = line - 1;
178 let target_start = self.line_start(target_line);
179 let target_content = self.line_content(target_line);
180 self.cursor = target_start + byte_offset_at_width(target_content, col);
181 }
182 }
183
184 pub fn move_down(&mut self) {
185 let (line, col) = self.cursor_line_col();
186 if line + 1 < self.line_count() {
187 let target_line = line + 1;
188 let target_start = self.line_start(target_line);
189 let target_content = self.line_content(target_line);
190 self.cursor = target_start + byte_offset_at_width(target_content, col);
191 }
192 }
193
194 pub fn move_home(&mut self) {
196 let (line, _) = self.cursor_line_col();
197 self.cursor = self.line_start(line);
198 self.scroll_offset = 0;
199 }
200
201 pub fn move_end(&mut self) {
203 let (line, _) = self.cursor_line_col();
204 self.cursor = self.line_end(line);
205 }
206
207 pub fn update_scroll(&mut self, visible_width: usize) {
209 let (_, col) = self.cursor_line_col();
210 if col < self.scroll_offset {
211 self.scroll_offset = col;
212 } else if col >= self.scroll_offset + visible_width {
213 self.scroll_offset = col - visible_width + 1;
214 }
215 }
216
217 pub fn update_vertical_scroll(&mut self, visible_height: usize) {
219 let (line, _) = self.cursor_line_col();
220 if line < self.vertical_scroll {
221 self.vertical_scroll = line;
222 } else if line >= self.vertical_scroll + visible_height {
223 self.vertical_scroll = line - visible_height + 1;
224 }
225 }
226
227 pub fn set_cursor_by_col(&mut self, col: usize) {
229 self.cursor = byte_offset_at_width(&self.content, col);
230 }
231
232 pub fn set_cursor_by_position(&mut self, line: usize, col: usize) {
234 let target_line = line.min(self.line_count().saturating_sub(1));
235 let start = self.line_start(target_line);
236 let line_text = self.line_content(target_line);
237 self.cursor = start + byte_offset_at_width(line_text, col);
238 }
239
240 fn prev_char_boundary(&self) -> usize {
241 let mut pos = self.cursor - 1;
242 while !self.content.is_char_boundary(pos) {
243 pos -= 1;
244 }
245 pos
246 }
247
248 fn next_char_boundary(&self) -> usize {
249 let mut pos = self.cursor + 1;
250 while pos < self.content.len() && !self.content.is_char_boundary(pos) {
251 pos += 1;
252 }
253 pos
254 }
255}
256
257fn byte_offset_at_width(line: &str, target_width: usize) -> usize {
259 let mut width = 0;
260 for (i, c) in line.char_indices() {
261 if width >= target_width {
262 return i;
263 }
264 width += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
265 }
266 line.len()
267}
268
269impl Default for Editor {
270 fn default() -> Self {
271 Self::new()
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
280 fn test_insert_and_content() {
281 let mut editor = Editor::new();
282 editor.insert_char('h');
283 editor.insert_char('i');
284 assert_eq!(editor.content(), "hi");
285 assert_eq!(editor.cursor(), 2);
286 }
287
288 #[test]
289 fn test_delete_back() {
290 let mut editor = Editor::with_content("hello".to_string());
291 editor.delete_back();
292 assert_eq!(editor.content(), "hell");
293 }
294
295 #[test]
296 fn test_cursor_movement() {
297 let mut editor = Editor::with_content("hello".to_string());
298 editor.move_left();
299 assert_eq!(editor.cursor(), 4);
300 editor.move_home();
301 assert_eq!(editor.cursor(), 0);
302 editor.move_end();
303 assert_eq!(editor.cursor(), 5);
304 }
305
306 #[test]
307 fn test_insert_newline() {
308 let mut editor = Editor::new();
309 editor.insert_char('a');
310 editor.insert_newline();
311 editor.insert_char('b');
312 assert_eq!(editor.content(), "a\nb");
313 assert_eq!(editor.cursor(), 3);
314 }
315
316 #[test]
317 fn test_cursor_line_col() {
318 let editor = Editor::with_content("abc\ndef\nghi".to_string());
319 assert_eq!(editor.cursor_line_col(), (2, 3));
321 }
322
323 #[test]
324 fn test_move_up_down() {
325 let mut editor = Editor::with_content("abc\ndef\nghi".to_string());
326 editor.move_up();
328 assert_eq!(editor.cursor_line_col(), (1, 3));
329 assert_eq!(&editor.content()[..editor.cursor()], "abc\ndef");
330 editor.move_up();
331 assert_eq!(editor.cursor_line_col(), (0, 3));
332 assert_eq!(&editor.content()[..editor.cursor()], "abc");
333 editor.move_up();
335 assert_eq!(editor.cursor_line_col(), (0, 3));
336 editor.move_down();
338 assert_eq!(editor.cursor_line_col(), (1, 3));
339 }
340
341 #[test]
342 fn test_move_up_clamps_column() {
343 let mut editor = Editor::with_content("abcdef\nab\nxyz".to_string());
344 editor.move_up();
346 assert_eq!(editor.cursor_line_col(), (1, 2));
348 editor.move_up();
349 assert_eq!(editor.cursor_line_col(), (0, 2));
351 }
352
353 #[test]
354 fn test_line_helpers() {
355 let editor = Editor::with_content("abc\ndef\nghi".to_string());
356 assert_eq!(editor.line_count(), 3);
357 assert_eq!(editor.line_content(0), "abc");
358 assert_eq!(editor.line_content(1), "def");
359 assert_eq!(editor.line_content(2), "ghi");
360 }
361
362 #[test]
363 fn test_home_end_multiline() {
364 let mut editor = Editor::with_content("abc\ndef".to_string());
365 editor.move_home();
367 assert_eq!(editor.cursor(), 4); assert_eq!(editor.cursor_line_col(), (1, 0));
370 editor.move_end();
371 assert_eq!(editor.cursor(), 7); assert_eq!(editor.cursor_line_col(), (1, 3));
373 }
374
375 #[test]
376 fn test_vertical_scroll() {
377 let mut editor = Editor::with_content("a\nb\nc\nd\ne".to_string());
378 editor.update_vertical_scroll(3);
379 assert_eq!(editor.vertical_scroll(), 2);
381 }
382
383 #[test]
384 fn test_undo_insert() {
385 let mut editor = Editor::new();
386 editor.insert_char('a');
387 editor.insert_char('b');
388 assert_eq!(editor.content(), "ab");
389 editor.undo();
390 assert_eq!(editor.content(), "a");
391 editor.undo();
392 assert_eq!(editor.content(), "");
393 assert!(!editor.undo());
395 }
396
397 #[test]
398 fn test_undo_delete() {
399 let mut editor = Editor::with_content("abc".to_string());
400 editor.delete_back();
401 assert_eq!(editor.content(), "ab");
402 editor.undo();
403 assert_eq!(editor.content(), "abc");
404 }
405
406 #[test]
407 fn test_redo() {
408 let mut editor = Editor::new();
409 editor.insert_char('a');
410 editor.insert_char('b');
411 editor.undo();
412 assert_eq!(editor.content(), "a");
413 editor.redo();
414 assert_eq!(editor.content(), "ab");
415 assert!(!editor.redo());
417 }
418
419 #[test]
420 fn test_redo_cleared_on_new_edit() {
421 let mut editor = Editor::new();
422 editor.insert_char('a');
423 editor.insert_char('b');
424 editor.undo();
425 editor.insert_char('c');
427 assert_eq!(editor.content(), "ac");
428 assert!(!editor.redo());
429 }
430
431 #[test]
432 fn test_set_cursor_by_col() {
433 let mut editor = Editor::with_content("hello".to_string());
434 editor.set_cursor_by_col(3);
435 assert_eq!(editor.cursor(), 3);
436 }
437
438 #[test]
439 fn test_set_cursor_by_position() {
440 let mut editor = Editor::with_content("abc\ndef\nghi".to_string());
441 editor.set_cursor_by_position(1, 2);
442 assert_eq!(editor.cursor_line_col(), (1, 2));
443 }
444
445 #[test]
446 fn test_delete_back_across_newline() {
447 let mut editor = Editor::with_content("abc\ndef".to_string());
448 editor.cursor = 4;
450 editor.delete_back();
451 assert_eq!(editor.content(), "abcdef");
452 assert_eq!(editor.cursor(), 3);
453 }
454}