1use crate::{Buffer, Position};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum MotionKind {
15 Char,
17 Line,
20 Block,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
38pub enum Edit {
39 InsertChar { at: Position, ch: char },
44 InsertStr { at: Position, text: String },
51 DeleteRange {
62 start: Position,
63 end: Position,
64 kind: MotionKind,
65 },
66 JoinLines {
71 row: usize,
72 count: usize,
73 with_space: bool,
74 },
75 SplitLines {
79 row: usize,
80 cols: Vec<usize>,
81 inserted_space: bool,
82 },
83 Replace {
89 start: Position,
90 end: Position,
91 with: String,
92 },
93 InsertBlock { at: Position, chunks: Vec<String> },
97 DeleteBlockChunks { at: Position, widths: Vec<usize> },
102}
103
104impl Buffer {
105 pub fn apply_edit(&mut self, edit: Edit) -> Edit {
123 match edit {
124 Edit::InsertChar { at, ch } => self.do_insert_str(at, ch.to_string()),
125 Edit::InsertStr { at, text } => self.do_insert_str(at, text),
126 Edit::DeleteRange { start, end, kind } => self.do_delete_range(start, end, kind),
127 Edit::JoinLines {
128 row,
129 count,
130 with_space,
131 } => self.do_join_lines(row, count, with_space),
132 Edit::SplitLines {
133 row,
134 cols,
135 inserted_space,
136 } => self.do_split_lines(row, cols, inserted_space),
137 Edit::Replace { start, end, with } => self.do_replace(start, end, with),
138 Edit::InsertBlock { at, chunks } => self.do_insert_block(at, chunks),
139 Edit::DeleteBlockChunks { at, widths } => self.do_delete_block_chunks(at, widths),
140 }
141 }
142
143 fn do_insert_block(&mut self, at: Position, chunks: Vec<String>) -> Edit {
144 let mut widths: Vec<usize> = Vec::with_capacity(chunks.len());
145 for (i, chunk) in chunks.into_iter().enumerate() {
146 let row = at.row + i;
147 let line_chars = self.lines_mut()[row].chars().count();
150 if line_chars < at.col {
151 let pad = at.col - line_chars;
152 self.lines_mut()[row].push_str(&" ".repeat(pad));
153 }
154 widths.push(chunk.chars().count());
155 self.splice_at(Position::new(row, at.col), &chunk);
156 }
157 self.dirty_gen_bump();
158 self.set_cursor(at);
159 Edit::DeleteBlockChunks { at, widths }
160 }
161
162 fn do_delete_block_chunks(&mut self, at: Position, widths: Vec<usize>) -> Edit {
163 let mut chunks: Vec<String> = Vec::with_capacity(widths.len());
164 for (i, w) in widths.into_iter().enumerate() {
165 let row = at.row + i;
166 let removed =
167 self.cut_chars(Position::new(row, at.col), Position::new(row, at.col + w));
168 chunks.push(removed);
169 }
170 self.dirty_gen_bump();
171 self.set_cursor(at);
172 Edit::InsertBlock { at, chunks }
173 }
174
175 fn do_insert_str(&mut self, at: Position, text: String) -> Edit {
176 let normalised = self.clamp_position(at);
177 let inserted_chars = text.chars().count();
178 let inserted_lines = text.split('\n').count();
179 let end = if inserted_lines > 1 {
180 let last_chars = text.rsplit('\n').next().unwrap_or("").chars().count();
181 Position::new(normalised.row + inserted_lines - 1, last_chars)
182 } else {
183 Position::new(normalised.row, normalised.col + inserted_chars)
184 };
185 self.splice_at(normalised, &text);
186 self.dirty_gen_bump();
187 self.set_cursor(end);
188 Edit::DeleteRange {
189 start: normalised,
190 end,
191 kind: MotionKind::Char,
192 }
193 }
194
195 fn do_delete_range(&mut self, start: Position, end: Position, kind: MotionKind) -> Edit {
196 let (start, end) = order(start, end);
197 match kind {
198 MotionKind::Char => {
199 let removed = self.cut_chars(start, end);
200 self.dirty_gen_bump();
201 self.set_cursor(start);
202 Edit::InsertStr {
203 at: start,
204 text: removed,
205 }
206 }
207 MotionKind::Line => {
208 let lo = start.row;
209 let hi = end.row.min(self.row_count().saturating_sub(1));
210 let removed_lines: Vec<String> = self.lines_mut().drain(lo..=hi).collect();
211 if self.lines_mut().is_empty() {
212 self.lines_mut().push(String::new());
213 }
214 self.dirty_gen_bump();
215 let target_row = lo.min(self.row_count().saturating_sub(1));
216 self.set_cursor(Position::new(target_row, 0));
217 let mut text = removed_lines.join("\n");
218 text.push('\n');
222 Edit::InsertStr {
223 at: Position::new(lo, 0),
224 text,
225 }
226 }
227 MotionKind::Block => {
228 let (left, right) = (start.col.min(end.col), start.col.max(end.col));
229 let mut chunks: Vec<String> = Vec::with_capacity(end.row - start.row + 1);
230 for row in start.row..=end.row {
231 let row_left = Position::new(row, left);
232 let row_right = Position::new(row, right + 1);
233 let removed = self.cut_chars(row_left, row_right);
234 chunks.push(removed);
235 }
236 self.dirty_gen_bump();
237 self.set_cursor(Position::new(start.row, left));
238 Edit::InsertBlock {
242 at: Position::new(start.row, left),
243 chunks,
244 }
245 }
246 }
247 }
248
249 fn do_join_lines(&mut self, row: usize, count: usize, with_space: bool) -> Edit {
250 let count = count.max(1);
251 let row = row.min(self.row_count().saturating_sub(1));
252 let mut split_cols: Vec<usize> = Vec::with_capacity(count);
253 let mut joined = std::mem::take(&mut self.lines_mut()[row]);
254 for _ in 0..count {
255 if row + 1 >= self.row_count() {
256 break;
257 }
258 let next = self.lines_mut().remove(row + 1);
259 let join_col = joined.chars().count();
260 split_cols.push(join_col);
261 if with_space && !joined.is_empty() && !next.is_empty() {
262 joined.push(' ');
263 }
264 joined.push_str(&next);
265 }
266 self.lines_mut()[row] = joined;
267 self.dirty_gen_bump();
268 self.set_cursor(Position::new(row, 0));
269 Edit::SplitLines {
270 row,
271 cols: split_cols,
272 inserted_space: with_space,
273 }
274 }
275
276 fn do_split_lines(&mut self, row: usize, cols: Vec<usize>, inserted_space: bool) -> Edit {
277 let row = row.min(self.row_count().saturating_sub(1));
278 let mut working = std::mem::take(&mut self.lines_mut()[row]);
279 let mut tails: Vec<String> = Vec::with_capacity(cols.len());
282 for &c in cols.iter().rev() {
283 let byte = Position::new(0, c).byte_offset(&working);
284 let mut tail = working.split_off(byte);
285 if inserted_space && tail.starts_with(' ') {
286 tail.remove(0);
287 }
288 tails.push(tail);
289 }
290 self.lines_mut()[row] = working;
292 for (i, tail) in tails.into_iter().rev().enumerate() {
293 self.lines_mut().insert(row + 1 + i, tail);
294 }
295 self.dirty_gen_bump();
296 self.set_cursor(Position::new(row, 0));
297 Edit::JoinLines {
298 row,
299 count: cols.len(),
300 with_space: inserted_space,
301 }
302 }
303
304 fn do_replace(&mut self, start: Position, end: Position, with: String) -> Edit {
305 let (start, end) = order(start, end);
306 let removed = self.cut_chars(start, end);
307 let normalised = self.clamp_position(start);
308 let inserted_chars = with.chars().count();
309 let inserted_lines = with.split('\n').count();
310 let new_end = if inserted_lines > 1 {
311 let last_chars = with.rsplit('\n').next().unwrap_or("").chars().count();
312 Position::new(normalised.row + inserted_lines - 1, last_chars)
313 } else {
314 Position::new(normalised.row, normalised.col + inserted_chars)
315 };
316 self.splice_at(normalised, &with);
317 self.dirty_gen_bump();
318 self.set_cursor(new_end);
319 Edit::Replace {
320 start: normalised,
321 end: new_end,
322 with: removed,
323 }
324 }
325}
326
327impl Buffer {
330 fn splice_at(&mut self, at: Position, text: &str) {
334 let pieces: Vec<&str> = text.split('\n').collect();
335 let row = at.row;
336 let line = &mut self.lines_mut()[row];
337 let byte = at.byte_offset(line);
338 let suffix = line.split_off(byte);
339 if pieces.len() == 1 {
340 line.push_str(pieces[0]);
341 line.push_str(&suffix);
342 return;
343 }
344 line.push_str(pieces[0]);
345 let mut new_rows: Vec<String> = pieces[1..pieces.len() - 1]
346 .iter()
347 .map(|s| (*s).to_string())
348 .collect();
349 let mut last = pieces.last().copied().unwrap_or("").to_string();
350 last.push_str(&suffix);
351 new_rows.push(last);
352 let insert_at = row + 1;
353 for (i, l) in new_rows.into_iter().enumerate() {
354 self.lines_mut().insert(insert_at + i, l);
355 }
356 }
357
358 fn cut_chars(&mut self, start: Position, end: Position) -> String {
361 let (start, end) = order(start, end);
362 if start.row == end.row {
363 let line = &mut self.lines_mut()[start.row];
364 let lo = start.byte_offset(line).min(line.len());
365 let hi = end.byte_offset(line).min(line.len());
366 return line.drain(lo..hi).collect();
367 }
368 let mut out = String::new();
369 {
371 let line = &mut self.lines_mut()[start.row];
372 let byte = start.byte_offset(line).min(line.len());
373 let suffix: String = line.drain(byte..).collect();
374 out.push_str(&suffix);
375 }
376 out.push('\n');
377 let mid_lo = start.row + 1;
379 let mid_hi = end.row.saturating_sub(1);
380 if mid_hi >= mid_lo {
381 let drained: Vec<String> = self.lines_mut().drain(mid_lo..=mid_hi).collect();
382 for l in drained {
383 out.push_str(&l);
384 out.push('\n');
385 }
386 }
387 let end_line_idx = start.row + 1;
389 {
390 let line = &mut self.lines_mut()[end_line_idx];
391 let byte = end.byte_offset(line).min(line.len());
392 let prefix: String = line.drain(..byte).collect();
393 out.push_str(&prefix);
394 }
395 let merged = self.lines_mut().remove(end_line_idx);
397 self.lines_mut()[start.row].push_str(&merged);
398 out
399 }
400}
401
402fn order(a: Position, b: Position) -> (Position, Position) {
403 if a <= b { (a, b) } else { (b, a) }
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409
410 fn round_trip_check(initial: &str, edit: Edit) {
411 let mut b = Buffer::from_str(initial);
412 let snapshot_before = b.as_string();
413 let inverse = b.apply_edit(edit);
414 b.apply_edit(inverse);
415 assert_eq!(b.as_string(), snapshot_before);
416 }
417
418 #[test]
419 fn insert_char_round_trip() {
420 round_trip_check(
421 "abc",
422 Edit::InsertChar {
423 at: Position::new(0, 1),
424 ch: 'X',
425 },
426 );
427 }
428
429 #[test]
430 fn insert_str_multiline_round_trip() {
431 round_trip_check(
432 "abc\ndef",
433 Edit::InsertStr {
434 at: Position::new(0, 2),
435 text: "X\nY\nZ".into(),
436 },
437 );
438 }
439
440 #[test]
441 fn delete_charwise_single_row_round_trip() {
442 round_trip_check(
443 "alpha bravo charlie",
444 Edit::DeleteRange {
445 start: Position::new(0, 6),
446 end: Position::new(0, 11),
447 kind: MotionKind::Char,
448 },
449 );
450 }
451
452 #[test]
453 fn delete_charwise_multi_row_round_trip() {
454 round_trip_check(
455 "row0\nrow1\nrow2",
456 Edit::DeleteRange {
457 start: Position::new(0, 2),
458 end: Position::new(2, 2),
459 kind: MotionKind::Char,
460 },
461 );
462 }
463
464 #[test]
465 fn delete_linewise_round_trip() {
466 round_trip_check(
467 "a\nb\nc\nd",
468 Edit::DeleteRange {
469 start: Position::new(1, 0),
470 end: Position::new(2, 0),
471 kind: MotionKind::Line,
472 },
473 );
474 }
475
476 #[test]
477 fn delete_blockwise_round_trip() {
478 round_trip_check(
479 "abcdef\nghijkl\nmnopqr",
480 Edit::DeleteRange {
481 start: Position::new(0, 1),
482 end: Position::new(2, 3),
483 kind: MotionKind::Block,
484 },
485 );
486 }
487
488 #[test]
489 fn join_lines_with_space_round_trip() {
490 round_trip_check(
491 "first\nsecond\nthird",
492 Edit::JoinLines {
493 row: 0,
494 count: 2,
495 with_space: true,
496 },
497 );
498 }
499
500 #[test]
501 fn join_lines_no_space_round_trip() {
502 round_trip_check(
503 "first\nsecond",
504 Edit::JoinLines {
505 row: 0,
506 count: 1,
507 with_space: false,
508 },
509 );
510 }
511
512 #[test]
513 fn replace_round_trip() {
514 round_trip_check(
515 "foo bar baz",
516 Edit::Replace {
517 start: Position::new(0, 4),
518 end: Position::new(0, 7),
519 with: "QUUX".into(),
520 },
521 );
522 }
523
524 #[test]
525 fn delete_clearing_buffer_keeps_one_empty_row() {
526 let mut b = Buffer::from_str("only");
527 b.apply_edit(Edit::DeleteRange {
528 start: Position::new(0, 0),
529 end: Position::new(0, 0),
530 kind: MotionKind::Line,
531 });
532 assert_eq!(b.row_count(), 1);
533 assert_eq!(b.line(0), Some(""));
534 }
535
536 #[test]
537 fn insert_char_lands_cursor_after() {
538 let mut b = Buffer::from_str("abc");
539 b.set_cursor(Position::new(0, 1));
540 b.apply_edit(Edit::InsertChar {
541 at: Position::new(0, 1),
542 ch: 'X',
543 });
544 assert_eq!(b.cursor(), Position::new(0, 2));
545 assert_eq!(b.line(0), Some("aXbc"));
546 }
547
548 #[test]
549 fn block_delete_on_ragged_rows_handles_short_lines() {
550 let mut b = Buffer::from_str("longline\nhi\nthird row");
553 let inv = b.apply_edit(Edit::DeleteRange {
554 start: Position::new(0, 2),
555 end: Position::new(2, 5),
556 kind: MotionKind::Block,
557 });
558 b.apply_edit(inv);
559 assert_eq!(b.as_string(), "longline\nhi\nthird row");
560 }
561
562 #[test]
563 fn dirty_gen_bumps_per_edit() {
564 let mut b = Buffer::from_str("abc");
565 let g0 = b.dirty_gen();
566 b.apply_edit(Edit::InsertChar {
567 at: Position::new(0, 0),
568 ch: 'X',
569 });
570 assert_eq!(b.dirty_gen(), g0 + 1);
571 b.apply_edit(Edit::DeleteRange {
572 start: Position::new(0, 0),
573 end: Position::new(0, 1),
574 kind: MotionKind::Char,
575 });
576 assert_eq!(b.dirty_gen(), g0 + 2);
577 }
578}