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_word_left(&mut self) {
176 if self.cursor == 0 {
177 return;
178 }
179 let before = &self.content[..self.cursor];
180 let mut chars = before.char_indices().rev();
181 let mut last_idx = self.cursor;
183 for (i, c) in &mut chars {
184 if c.is_alphanumeric() || c == '_' {
185 last_idx = i;
186 break;
187 }
188 last_idx = i;
189 }
190 if last_idx < self.cursor {
192 let before_word = &self.content[..last_idx];
193 for (i, c) in before_word.char_indices().rev() {
194 if !(c.is_alphanumeric() || c == '_') {
195 self.cursor = i + c.len_utf8();
196 return;
197 }
198 }
199 self.cursor = 0;
201 } else {
202 self.cursor = 0;
203 }
204 }
205
206 pub fn move_word_right(&mut self) {
208 if self.cursor >= self.content.len() {
209 return;
210 }
211 let after = &self.content[self.cursor..];
212 let mut chars = after.char_indices();
213 let mut advanced = false;
215 for (i, c) in &mut chars {
216 if !(c.is_alphanumeric() || c == '_') {
217 if advanced {
218 self.cursor += i;
219 let remaining = &self.content[self.cursor..];
221 for (j, c2) in remaining.char_indices() {
222 if c2.is_alphanumeric() || c2 == '_' {
223 self.cursor += j;
224 return;
225 }
226 }
227 self.cursor = self.content.len();
228 return;
229 }
230 let remaining = &self.content[self.cursor + i + c.len_utf8()..];
232 for (j, c2) in remaining.char_indices() {
233 if c2.is_alphanumeric() || c2 == '_' {
234 self.cursor = self.cursor + i + c.len_utf8() + j;
235 return;
236 }
237 }
238 self.cursor = self.content.len();
239 return;
240 }
241 advanced = true;
242 }
243 self.cursor = self.content.len();
244 }
245
246 pub fn move_up(&mut self) {
247 let (line, col) = self.cursor_line_col();
248 if line > 0 {
249 let target_line = line - 1;
250 let target_start = self.line_start(target_line);
251 let target_content = self.line_content(target_line);
252 self.cursor = target_start + byte_offset_at_width(target_content, col);
253 }
254 }
255
256 pub fn move_down(&mut self) {
257 let (line, col) = self.cursor_line_col();
258 if line + 1 < self.line_count() {
259 let target_line = line + 1;
260 let target_start = self.line_start(target_line);
261 let target_content = self.line_content(target_line);
262 self.cursor = target_start + byte_offset_at_width(target_content, col);
263 }
264 }
265
266 pub fn move_home(&mut self) {
268 let (line, _) = self.cursor_line_col();
269 self.cursor = self.line_start(line);
270 self.scroll_offset = 0;
271 }
272
273 pub fn move_end(&mut self) {
275 let (line, _) = self.cursor_line_col();
276 self.cursor = self.line_end(line);
277 }
278
279 pub fn update_scroll(&mut self, visible_width: usize) {
281 let (_, col) = self.cursor_line_col();
282 if col < self.scroll_offset {
283 self.scroll_offset = col;
284 } else if col >= self.scroll_offset + visible_width {
285 self.scroll_offset = col - visible_width + 1;
286 }
287 }
288
289 pub fn update_vertical_scroll(&mut self, visible_height: usize) {
291 let (line, _) = self.cursor_line_col();
292 if line < self.vertical_scroll {
293 self.vertical_scroll = line;
294 } else if line >= self.vertical_scroll + visible_height {
295 self.vertical_scroll = line - visible_height + 1;
296 }
297 }
298
299 pub fn set_cursor_by_col(&mut self, col: usize) {
301 self.cursor = byte_offset_at_width(&self.content, col);
302 }
303
304 pub fn set_cursor_by_position(&mut self, line: usize, col: usize) {
306 let target_line = line.min(self.line_count().saturating_sub(1));
307 let start = self.line_start(target_line);
308 let line_text = self.line_content(target_line);
309 self.cursor = start + byte_offset_at_width(line_text, col);
310 }
311
312 fn prev_char_boundary(&self) -> usize {
313 let mut pos = self.cursor - 1;
314 while !self.content.is_char_boundary(pos) {
315 pos -= 1;
316 }
317 pos
318 }
319
320 fn next_char_boundary(&self) -> usize {
321 let mut pos = self.cursor + 1;
322 while pos < self.content.len() && !self.content.is_char_boundary(pos) {
323 pos += 1;
324 }
325 pos
326 }
327}
328
329fn byte_offset_at_width(line: &str, target_width: usize) -> usize {
331 let mut width = 0;
332 for (i, c) in line.char_indices() {
333 if width >= target_width {
334 return i;
335 }
336 width += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
337 }
338 line.len()
339}
340
341impl Default for Editor {
342 fn default() -> Self {
343 Self::new()
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350
351 #[test]
352 fn test_insert_and_content() {
353 let mut editor = Editor::new();
354 editor.insert_char('h');
355 editor.insert_char('i');
356 assert_eq!(editor.content(), "hi");
357 assert_eq!(editor.cursor(), 2);
358 }
359
360 #[test]
361 fn test_delete_back() {
362 let mut editor = Editor::with_content("hello".to_string());
363 editor.delete_back();
364 assert_eq!(editor.content(), "hell");
365 }
366
367 #[test]
368 fn test_cursor_movement() {
369 let mut editor = Editor::with_content("hello".to_string());
370 editor.move_left();
371 assert_eq!(editor.cursor(), 4);
372 editor.move_home();
373 assert_eq!(editor.cursor(), 0);
374 editor.move_end();
375 assert_eq!(editor.cursor(), 5);
376 }
377
378 #[test]
379 fn test_insert_newline() {
380 let mut editor = Editor::new();
381 editor.insert_char('a');
382 editor.insert_newline();
383 editor.insert_char('b');
384 assert_eq!(editor.content(), "a\nb");
385 assert_eq!(editor.cursor(), 3);
386 }
387
388 #[test]
389 fn test_cursor_line_col() {
390 let editor = Editor::with_content("abc\ndef\nghi".to_string());
391 assert_eq!(editor.cursor_line_col(), (2, 3));
393 }
394
395 #[test]
396 fn test_move_up_down() {
397 let mut editor = Editor::with_content("abc\ndef\nghi".to_string());
398 editor.move_up();
400 assert_eq!(editor.cursor_line_col(), (1, 3));
401 assert_eq!(&editor.content()[..editor.cursor()], "abc\ndef");
402 editor.move_up();
403 assert_eq!(editor.cursor_line_col(), (0, 3));
404 assert_eq!(&editor.content()[..editor.cursor()], "abc");
405 editor.move_up();
407 assert_eq!(editor.cursor_line_col(), (0, 3));
408 editor.move_down();
410 assert_eq!(editor.cursor_line_col(), (1, 3));
411 }
412
413 #[test]
414 fn test_move_up_clamps_column() {
415 let mut editor = Editor::with_content("abcdef\nab\nxyz".to_string());
416 editor.move_up();
418 assert_eq!(editor.cursor_line_col(), (1, 2));
420 editor.move_up();
421 assert_eq!(editor.cursor_line_col(), (0, 2));
423 }
424
425 #[test]
426 fn test_line_helpers() {
427 let editor = Editor::with_content("abc\ndef\nghi".to_string());
428 assert_eq!(editor.line_count(), 3);
429 assert_eq!(editor.line_content(0), "abc");
430 assert_eq!(editor.line_content(1), "def");
431 assert_eq!(editor.line_content(2), "ghi");
432 }
433
434 #[test]
435 fn test_home_end_multiline() {
436 let mut editor = Editor::with_content("abc\ndef".to_string());
437 editor.move_home();
439 assert_eq!(editor.cursor(), 4); assert_eq!(editor.cursor_line_col(), (1, 0));
442 editor.move_end();
443 assert_eq!(editor.cursor(), 7); assert_eq!(editor.cursor_line_col(), (1, 3));
445 }
446
447 #[test]
448 fn test_vertical_scroll() {
449 let mut editor = Editor::with_content("a\nb\nc\nd\ne".to_string());
450 editor.update_vertical_scroll(3);
451 assert_eq!(editor.vertical_scroll(), 2);
453 }
454
455 #[test]
456 fn test_undo_insert() {
457 let mut editor = Editor::new();
458 editor.insert_char('a');
459 editor.insert_char('b');
460 assert_eq!(editor.content(), "ab");
461 editor.undo();
462 assert_eq!(editor.content(), "a");
463 editor.undo();
464 assert_eq!(editor.content(), "");
465 assert!(!editor.undo());
467 }
468
469 #[test]
470 fn test_undo_delete() {
471 let mut editor = Editor::with_content("abc".to_string());
472 editor.delete_back();
473 assert_eq!(editor.content(), "ab");
474 editor.undo();
475 assert_eq!(editor.content(), "abc");
476 }
477
478 #[test]
479 fn test_redo() {
480 let mut editor = Editor::new();
481 editor.insert_char('a');
482 editor.insert_char('b');
483 editor.undo();
484 assert_eq!(editor.content(), "a");
485 editor.redo();
486 assert_eq!(editor.content(), "ab");
487 assert!(!editor.redo());
489 }
490
491 #[test]
492 fn test_redo_cleared_on_new_edit() {
493 let mut editor = Editor::new();
494 editor.insert_char('a');
495 editor.insert_char('b');
496 editor.undo();
497 editor.insert_char('c');
499 assert_eq!(editor.content(), "ac");
500 assert!(!editor.redo());
501 }
502
503 #[test]
504 fn test_set_cursor_by_col() {
505 let mut editor = Editor::with_content("hello".to_string());
506 editor.set_cursor_by_col(3);
507 assert_eq!(editor.cursor(), 3);
508 }
509
510 #[test]
511 fn test_set_cursor_by_position() {
512 let mut editor = Editor::with_content("abc\ndef\nghi".to_string());
513 editor.set_cursor_by_position(1, 2);
514 assert_eq!(editor.cursor_line_col(), (1, 2));
515 }
516
517 #[test]
518 fn test_move_word_right() {
519 let mut editor = Editor::with_content("hello world foo".to_string());
520 editor.cursor = 0;
521 editor.move_word_right();
522 assert_eq!(editor.cursor(), 6);
524 editor.move_word_right();
525 assert_eq!(editor.cursor(), 12);
526 editor.move_word_right();
527 assert_eq!(editor.cursor(), 15); }
529
530 #[test]
531 fn test_move_word_left() {
532 let mut editor = Editor::with_content("hello world foo".to_string());
533 editor.move_word_left();
535 assert_eq!(editor.cursor(), 12); editor.move_word_left();
537 assert_eq!(editor.cursor(), 6); editor.move_word_left();
539 assert_eq!(editor.cursor(), 0); }
541
542 #[test]
543 fn test_delete_back_across_newline() {
544 let mut editor = Editor::with_content("abc\ndef".to_string());
545 editor.cursor = 4;
547 editor.delete_back();
548 assert_eq!(editor.content(), "abcdef");
549 assert_eq!(editor.cursor(), 3);
550 }
551}