1use crate::buffer::{pos_to_char_idx, rope_line_char_count};
10use crate::{Buffer, Position};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum MotionKind {
16 Char,
18 Line,
21 Block,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum Edit {
40 InsertChar { at: Position, ch: char },
45 InsertStr { at: Position, text: String },
52 DeleteRange {
63 start: Position,
64 end: Position,
65 kind: MotionKind,
66 },
67 JoinLines {
72 row: usize,
73 count: usize,
74 with_space: bool,
75 },
76 SplitLines {
80 row: usize,
81 cols: Vec<usize>,
82 inserted_space: bool,
83 },
84 Replace {
90 start: Position,
91 end: Position,
92 with: String,
93 },
94 InsertBlock { at: Position, chunks: Vec<String> },
98 DeleteBlockChunks { at: Position, widths: Vec<usize> },
103}
104
105impl Buffer {
106 pub fn apply_edit(&mut self, edit: Edit) -> Edit {
124 match edit {
125 Edit::InsertChar { at, ch } => self.do_insert_str(at, ch.to_string()),
126 Edit::InsertStr { at, text } => self.do_insert_str(at, text),
127 Edit::DeleteRange { start, end, kind } => self.do_delete_range(start, end, kind),
128 Edit::JoinLines {
129 row,
130 count,
131 with_space,
132 } => self.do_join_lines(row, count, with_space),
133 Edit::SplitLines {
134 row,
135 cols,
136 inserted_space,
137 } => self.do_split_lines(row, cols, inserted_space),
138 Edit::Replace { start, end, with } => self.do_replace(start, end, with),
139 Edit::InsertBlock { at, chunks } => self.do_insert_block(at, chunks),
140 Edit::DeleteBlockChunks { at, widths } => self.do_delete_block_chunks(at, widths),
141 }
142 }
143
144 fn do_insert_block(&mut self, at: Position, chunks: Vec<String>) -> Edit {
145 let mut widths: Vec<usize> = Vec::with_capacity(chunks.len());
146 for (i, chunk) in chunks.into_iter().enumerate() {
147 let row = at.row + i;
148 {
151 let mut c = self.content.lock().unwrap();
152 let n = c.text.len_lines();
153 if row < n {
154 let lc = rope_line_char_count(&c.text, row);
155 if lc < at.col {
156 let pad = at.col - lc;
157 let insert_char_idx = pos_to_char_idx(&c.text, row, lc);
158 c.text.insert(insert_char_idx, &" ".repeat(pad));
159 }
160 }
161 }
162 widths.push(chunk.chars().count());
163 {
165 let mut c = self.content.lock().unwrap();
166 let n = c.text.len_lines();
167 if row < n {
168 let char_idx = pos_to_char_idx(&c.text, row, at.col);
169 c.text.insert(char_idx, &chunk);
170 }
171 }
172 }
173 self.dirty_gen_bump();
174 self.set_cursor(at);
175 Edit::DeleteBlockChunks { at, widths }
176 }
177
178 fn do_delete_block_chunks(&mut self, at: Position, widths: Vec<usize>) -> Edit {
179 let mut chunks: Vec<String> = Vec::with_capacity(widths.len());
180 for (i, w) in widths.into_iter().enumerate() {
181 let row = at.row + i;
182 let removed = {
183 let mut c = self.content.lock().unwrap();
184 let n = c.text.len_lines();
185 if row >= n {
186 String::new()
187 } else {
188 let lc = rope_line_char_count(&c.text, row);
189 let col_start = at.col.min(lc);
190 let col_end = (at.col + w).min(lc);
191 if col_start >= col_end {
192 String::new()
193 } else {
194 let char_start = pos_to_char_idx(&c.text, row, col_start);
195 let char_end = pos_to_char_idx(&c.text, row, col_end);
196 let removed: String = c.text.slice(char_start..char_end).to_string();
197 c.text.remove(char_start..char_end);
198 removed
199 }
200 }
201 };
202 chunks.push(removed);
203 }
204 self.dirty_gen_bump();
205 self.set_cursor(at);
206 Edit::InsertBlock { at, chunks }
207 }
208
209 fn do_insert_str(&mut self, at: Position, text: String) -> Edit {
210 let normalised = self.clamp_position(at);
211 let inserted_chars = text.chars().count();
212 let inserted_lines = text.split('\n').count();
213 let end = if inserted_lines > 1 {
214 let last_chars = text.rsplit('\n').next().unwrap_or("").chars().count();
215 Position::new(normalised.row + inserted_lines - 1, last_chars)
216 } else {
217 Position::new(normalised.row, normalised.col + inserted_chars)
218 };
219 {
220 let mut c = self.content.lock().unwrap();
221 let char_idx = pos_to_char_idx(&c.text, normalised.row, normalised.col);
222 c.text.insert(char_idx, &text);
223 }
224 self.dirty_gen_bump();
225 self.set_cursor(end);
226 Edit::DeleteRange {
227 start: normalised,
228 end,
229 kind: MotionKind::Char,
230 }
231 }
232
233 fn do_delete_range(&mut self, start: Position, end: Position, kind: MotionKind) -> Edit {
234 let (start, end) = order(start, end);
235 match kind {
236 MotionKind::Char => {
237 let removed = {
238 let mut c = self.content.lock().unwrap();
239 rope_cut_chars(&mut c.text, start, end)
240 };
241 self.dirty_gen_bump();
242 self.set_cursor(start);
243 Edit::InsertStr {
244 at: start,
245 text: removed,
246 }
247 }
248 MotionKind::Line => {
249 let lo = start.row;
250 let (removed_text, new_cursor) = {
251 let mut c = self.content.lock().unwrap();
252 let n = c.text.len_lines();
253 let hi = end.row.min(n.saturating_sub(1));
254
255 let mut removed_lines: Vec<String> = Vec::with_capacity(hi - lo + 1);
257 for r in lo..=hi {
258 removed_lines.push(rope_line_str_locked(&c.text, r));
259 }
260
261 let (remove_start, remove_end) = if hi + 1 < n {
267 (c.text.line_to_char(lo), c.text.line_to_char(hi + 1))
270 } else if lo > 0 {
271 (c.text.line_to_char(lo) - 1, c.text.len_chars())
274 } else {
275 (0, c.text.len_chars())
277 };
278
279 c.text.remove(remove_start..remove_end);
280 let n2 = c.text.len_lines();
283 let target_row = lo.min(n2.saturating_sub(1));
284 let removed_joined = {
285 let mut s = removed_lines.join("\n");
286 s.push('\n');
289 s
290 };
291 (removed_joined, Position::new(target_row, 0))
292 };
293 self.dirty_gen_bump();
294 self.set_cursor(new_cursor);
295 Edit::InsertStr {
296 at: Position::new(lo, 0),
297 text: removed_text,
298 }
299 }
300 MotionKind::Block => {
301 let (left, right) = (start.col.min(end.col), start.col.max(end.col));
302 let mut chunks: Vec<String> = Vec::with_capacity(end.row - start.row + 1);
303 for row in start.row..=end.row {
304 let removed = {
305 let mut c = self.content.lock().unwrap();
306 let n = c.text.len_lines();
307 if row >= n {
308 String::new()
309 } else {
310 let row_start_pos = Position::new(row, left);
311 let row_end_pos = Position::new(row, right + 1);
312 rope_cut_chars(&mut c.text, row_start_pos, row_end_pos)
313 }
314 };
315 chunks.push(removed);
316 }
317 self.dirty_gen_bump();
318 self.set_cursor(Position::new(start.row, left));
319 Edit::InsertBlock {
320 at: Position::new(start.row, left),
321 chunks,
322 }
323 }
324 }
325 }
326
327 fn do_join_lines(&mut self, row: usize, count: usize, with_space: bool) -> Edit {
328 let count = count.max(1);
329 let (actual_row, split_cols) = {
330 let mut c = self.content.lock().unwrap();
331 let n = c.text.len_lines();
332 let row = row.min(n.saturating_sub(1));
333 let mut split_cols: Vec<usize> = Vec::with_capacity(count);
334
335 for _ in 0..count {
336 let n2 = c.text.len_lines();
337 if row + 1 >= n2 {
338 break;
339 }
340 let join_col = rope_line_char_count(&c.text, row);
342 split_cols.push(join_col);
343
344 let newline_char = c.text.line_to_char(row) + join_col;
346 c.text.remove(newline_char..newline_char + 1);
348
349 if with_space {
351 let n3 = c.text.len_lines();
355 let merged_len = rope_line_char_count(&c.text, row);
356 let prefix_empty = join_col == 0;
357 let suffix_empty = join_col >= merged_len;
358 if !prefix_empty && !suffix_empty {
359 c.text.insert_char(newline_char, ' ');
361 }
367 let _ = n3;
368 }
369 }
370 (row, split_cols)
371 };
372 self.dirty_gen_bump();
373 self.set_cursor(Position::new(actual_row, 0));
374 Edit::SplitLines {
375 row: actual_row,
376 cols: split_cols,
377 inserted_space: with_space,
378 }
379 }
380
381 fn do_split_lines(&mut self, row: usize, cols: Vec<usize>, inserted_space: bool) -> Edit {
382 let actual_row = {
383 let mut c = self.content.lock().unwrap();
384 let n = c.text.len_lines();
385 let row = row.min(n.saturating_sub(1));
386
387 for &col in cols.iter().rev() {
390 let mut split_col = col;
391 if inserted_space {
392 let lc = rope_line_char_count(&c.text, row);
396 if split_col < lc {
397 let space_char_idx = c.text.line_to_char(row) + split_col;
398 let ch = c.text.char(space_char_idx);
400 if ch == ' ' {
401 c.text.remove(space_char_idx..space_char_idx + 1);
402 }
403 }
404 } else {
407 let lc = rope_line_char_count(&c.text, row);
408 split_col = split_col.min(lc);
409 }
410
411 let char_idx = c.text.line_to_char(row) + split_col;
413 c.text.insert_char(char_idx, '\n');
414 }
415
416 row
417 };
418 self.dirty_gen_bump();
419 self.set_cursor(Position::new(actual_row, 0));
420 Edit::JoinLines {
421 row: actual_row,
422 count: cols.len(),
423 with_space: inserted_space,
424 }
425 }
426
427 fn do_replace(&mut self, start: Position, end: Position, with: String) -> Edit {
428 let (start, end) = order(start, end);
429 let removed = {
430 let mut c = self.content.lock().unwrap();
431 rope_cut_chars(&mut c.text, start, end)
432 };
433 let normalised = self.clamp_position(start);
434 let inserted_chars = with.chars().count();
435 let inserted_lines = with.split('\n').count();
436 let new_end = if inserted_lines > 1 {
437 let last_chars = with.rsplit('\n').next().unwrap_or("").chars().count();
438 Position::new(normalised.row + inserted_lines - 1, last_chars)
439 } else {
440 Position::new(normalised.row, normalised.col + inserted_chars)
441 };
442 {
443 let mut c = self.content.lock().unwrap();
444 let char_idx = pos_to_char_idx(&c.text, normalised.row, normalised.col);
445 c.text.insert(char_idx, &with);
446 }
447 self.dirty_gen_bump();
448 self.set_cursor(new_end);
449 Edit::Replace {
450 start: normalised,
451 end: new_end,
452 with: removed,
453 }
454 }
455}
456
457fn rope_line_str_locked(rope: &ropey::Rope, row: usize) -> String {
463 let slice = rope.line(row);
464 let s = slice.to_string();
465 if s.ends_with('\n') {
466 s[..s.len() - 1].to_string()
467 } else {
468 s
469 }
470}
471
472fn rope_cut_chars(rope: &mut ropey::Rope, start: Position, end: Position) -> String {
479 let (start, end) = order(start, end);
480 let n = rope.len_lines();
481
482 let start_row = start.row.min(n.saturating_sub(1));
484 let start_col = {
485 let lc = crate::buffer::rope_line_char_count(rope, start_row);
486 start.col.min(lc)
487 };
488 let end_row = end.row.min(n.saturating_sub(1));
489 let end_col = {
490 let lc = crate::buffer::rope_line_char_count(rope, end_row);
491 end.col.min(lc)
492 };
493
494 let char_start = rope.line_to_char(start_row) + start_col;
495 let char_end = rope.line_to_char(end_row) + end_col;
496
497 if char_start >= char_end {
498 return String::new();
499 }
500
501 let removed: String = rope.slice(char_start..char_end).to_string();
502 rope.remove(char_start..char_end);
503 removed
504}
505
506fn order(a: Position, b: Position) -> (Position, Position) {
507 if a <= b { (a, b) } else { (b, a) }
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513 use crate::buffer::rope_line_str;
514
515 fn round_trip_check(initial: &str, edit: Edit) {
516 let mut b = Buffer::from_str(initial);
517 let snapshot_before = b.as_string();
518 let inverse = b.apply_edit(edit);
519 b.apply_edit(inverse);
520 assert_eq!(b.as_string(), snapshot_before);
521 }
522
523 #[test]
524 fn insert_char_round_trip() {
525 round_trip_check(
526 "abc",
527 Edit::InsertChar {
528 at: Position::new(0, 1),
529 ch: 'X',
530 },
531 );
532 }
533
534 #[test]
535 fn insert_str_multiline_round_trip() {
536 round_trip_check(
537 "abc\ndef",
538 Edit::InsertStr {
539 at: Position::new(0, 2),
540 text: "X\nY\nZ".into(),
541 },
542 );
543 }
544
545 #[test]
546 fn delete_charwise_single_row_round_trip() {
547 round_trip_check(
548 "alpha bravo charlie",
549 Edit::DeleteRange {
550 start: Position::new(0, 6),
551 end: Position::new(0, 11),
552 kind: MotionKind::Char,
553 },
554 );
555 }
556
557 #[test]
558 fn delete_charwise_multi_row_round_trip() {
559 round_trip_check(
560 "row0\nrow1\nrow2",
561 Edit::DeleteRange {
562 start: Position::new(0, 2),
563 end: Position::new(2, 2),
564 kind: MotionKind::Char,
565 },
566 );
567 }
568
569 #[test]
570 fn delete_linewise_round_trip() {
571 round_trip_check(
572 "a\nb\nc\nd",
573 Edit::DeleteRange {
574 start: Position::new(1, 0),
575 end: Position::new(2, 0),
576 kind: MotionKind::Line,
577 },
578 );
579 }
580
581 #[test]
582 fn delete_blockwise_round_trip() {
583 round_trip_check(
584 "abcdef\nghijkl\nmnopqr",
585 Edit::DeleteRange {
586 start: Position::new(0, 1),
587 end: Position::new(2, 3),
588 kind: MotionKind::Block,
589 },
590 );
591 }
592
593 #[test]
594 fn join_lines_with_space_round_trip() {
595 round_trip_check(
596 "first\nsecond\nthird",
597 Edit::JoinLines {
598 row: 0,
599 count: 2,
600 with_space: true,
601 },
602 );
603 }
604
605 #[test]
606 fn join_lines_no_space_round_trip() {
607 round_trip_check(
608 "first\nsecond",
609 Edit::JoinLines {
610 row: 0,
611 count: 1,
612 with_space: false,
613 },
614 );
615 }
616
617 #[test]
618 fn replace_round_trip() {
619 round_trip_check(
620 "foo bar baz",
621 Edit::Replace {
622 start: Position::new(0, 4),
623 end: Position::new(0, 7),
624 with: "QUUX".into(),
625 },
626 );
627 }
628
629 #[test]
630 fn delete_clearing_buffer_keeps_one_empty_row() {
631 let mut b = Buffer::from_str("only");
632 b.apply_edit(Edit::DeleteRange {
633 start: Position::new(0, 0),
634 end: Position::new(0, 0),
635 kind: MotionKind::Line,
636 });
637 assert_eq!(b.row_count(), 1);
638 assert_eq!(rope_line_str(&b.rope(), 0), "");
639 }
640
641 #[test]
642 fn insert_char_lands_cursor_after() {
643 let mut b = Buffer::from_str("abc");
644 b.set_cursor(Position::new(0, 1));
645 b.apply_edit(Edit::InsertChar {
646 at: Position::new(0, 1),
647 ch: 'X',
648 });
649 assert_eq!(b.cursor(), Position::new(0, 2));
650 assert_eq!(rope_line_str(&b.rope(), 0), "aXbc");
651 }
652
653 #[test]
654 fn block_delete_on_ragged_rows_handles_short_lines() {
655 let mut b = Buffer::from_str("longline\nhi\nthird row");
658 let inv = b.apply_edit(Edit::DeleteRange {
659 start: Position::new(0, 2),
660 end: Position::new(2, 5),
661 kind: MotionKind::Block,
662 });
663 b.apply_edit(inv);
664 assert_eq!(b.as_string(), "longline\nhi\nthird row");
665 }
666
667 #[test]
668 fn dirty_gen_bumps_per_edit() {
669 let mut b = Buffer::from_str("abc");
670 let g0 = b.dirty_gen();
671 b.apply_edit(Edit::InsertChar {
672 at: Position::new(0, 0),
673 ch: 'X',
674 });
675 assert_eq!(b.dirty_gen(), g0 + 1);
676 b.apply_edit(Edit::DeleteRange {
677 start: Position::new(0, 0),
678 end: Position::new(0, 1),
679 kind: MotionKind::Char,
680 });
681 assert_eq!(b.dirty_gen(), g0 + 2);
682 }
683
684 #[test]
689 fn splice_at_60k_paste_at_row_zero_is_under_200ms() {
690 let initial = "\n".repeat(60_000);
692 let mut b = Buffer::from_str(&initial);
693 let payload = vec!["x"; 60_000].join("\n");
695 let t = std::time::Instant::now();
696 b.apply_edit(Edit::InsertStr {
697 at: Position::new(0, 0),
698 text: payload,
699 });
700 let elapsed = t.elapsed();
701 assert!(
702 elapsed.as_millis() < 200,
703 "60k-row InsertStr took {elapsed:?}; budget 200 ms"
704 );
705 }
706}