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)]
27pub enum Edit {
28 InsertChar { at: Position, ch: char },
30 InsertStr { at: Position, text: String },
33 DeleteRange {
35 start: Position,
36 end: Position,
37 kind: MotionKind,
38 },
39 JoinLines {
42 row: usize,
43 count: usize,
44 with_space: bool,
45 },
46 SplitLines {
50 row: usize,
51 cols: Vec<usize>,
52 inserted_space: bool,
53 },
54 Replace {
56 start: Position,
57 end: Position,
58 with: String,
59 },
60 InsertBlock { at: Position, chunks: Vec<String> },
64 DeleteBlockChunks { at: Position, widths: Vec<usize> },
69}
70
71impl Buffer {
72 pub fn apply_edit(&mut self, edit: Edit) -> Edit {
75 match edit {
76 Edit::InsertChar { at, ch } => self.do_insert_str(at, ch.to_string()),
77 Edit::InsertStr { at, text } => self.do_insert_str(at, text),
78 Edit::DeleteRange { start, end, kind } => self.do_delete_range(start, end, kind),
79 Edit::JoinLines {
80 row,
81 count,
82 with_space,
83 } => self.do_join_lines(row, count, with_space),
84 Edit::SplitLines {
85 row,
86 cols,
87 inserted_space,
88 } => self.do_split_lines(row, cols, inserted_space),
89 Edit::Replace { start, end, with } => self.do_replace(start, end, with),
90 Edit::InsertBlock { at, chunks } => self.do_insert_block(at, chunks),
91 Edit::DeleteBlockChunks { at, widths } => self.do_delete_block_chunks(at, widths),
92 }
93 }
94
95 fn do_insert_block(&mut self, at: Position, chunks: Vec<String>) -> Edit {
96 let mut widths: Vec<usize> = Vec::with_capacity(chunks.len());
97 for (i, chunk) in chunks.into_iter().enumerate() {
98 let row = at.row + i;
99 let line_chars = self.lines_mut()[row].chars().count();
102 if line_chars < at.col {
103 let pad = at.col - line_chars;
104 self.lines_mut()[row].push_str(&" ".repeat(pad));
105 }
106 widths.push(chunk.chars().count());
107 self.splice_at(Position::new(row, at.col), &chunk);
108 }
109 self.dirty_gen_bump();
110 self.set_cursor(at);
111 Edit::DeleteBlockChunks { at, widths }
112 }
113
114 fn do_delete_block_chunks(&mut self, at: Position, widths: Vec<usize>) -> Edit {
115 let mut chunks: Vec<String> = Vec::with_capacity(widths.len());
116 for (i, w) in widths.into_iter().enumerate() {
117 let row = at.row + i;
118 let removed =
119 self.cut_chars(Position::new(row, at.col), Position::new(row, at.col + w));
120 chunks.push(removed);
121 }
122 self.dirty_gen_bump();
123 self.set_cursor(at);
124 Edit::InsertBlock { at, chunks }
125 }
126
127 fn do_insert_str(&mut self, at: Position, text: String) -> Edit {
128 let normalised = self.clamp_position(at);
129 let inserted_chars = text.chars().count();
130 let inserted_lines = text.split('\n').count();
131 let end = if inserted_lines > 1 {
132 let last_chars = text.rsplit('\n').next().unwrap_or("").chars().count();
133 Position::new(normalised.row + inserted_lines - 1, last_chars)
134 } else {
135 Position::new(normalised.row, normalised.col + inserted_chars)
136 };
137 self.splice_at(normalised, &text);
138 self.dirty_gen_bump();
139 self.set_cursor(end);
140 Edit::DeleteRange {
141 start: normalised,
142 end,
143 kind: MotionKind::Char,
144 }
145 }
146
147 fn do_delete_range(&mut self, start: Position, end: Position, kind: MotionKind) -> Edit {
148 let (start, end) = order(start, end);
149 match kind {
150 MotionKind::Char => {
151 let removed = self.cut_chars(start, end);
152 self.dirty_gen_bump();
153 self.set_cursor(start);
154 Edit::InsertStr {
155 at: start,
156 text: removed,
157 }
158 }
159 MotionKind::Line => {
160 let lo = start.row;
161 let hi = end.row.min(self.row_count().saturating_sub(1));
162 let removed_lines: Vec<String> = self.lines_mut().drain(lo..=hi).collect();
163 if self.lines_mut().is_empty() {
164 self.lines_mut().push(String::new());
165 }
166 self.dirty_gen_bump();
167 let target_row = lo.min(self.row_count().saturating_sub(1));
168 self.set_cursor(Position::new(target_row, 0));
169 let mut text = removed_lines.join("\n");
170 text.push('\n');
174 Edit::InsertStr {
175 at: Position::new(lo, 0),
176 text,
177 }
178 }
179 MotionKind::Block => {
180 let (left, right) = (start.col.min(end.col), start.col.max(end.col));
181 let mut chunks: Vec<String> = Vec::with_capacity(end.row - start.row + 1);
182 for row in start.row..=end.row {
183 let row_left = Position::new(row, left);
184 let row_right = Position::new(row, right + 1);
185 let removed = self.cut_chars(row_left, row_right);
186 chunks.push(removed);
187 }
188 self.dirty_gen_bump();
189 self.set_cursor(Position::new(start.row, left));
190 Edit::InsertBlock {
194 at: Position::new(start.row, left),
195 chunks,
196 }
197 }
198 }
199 }
200
201 fn do_join_lines(&mut self, row: usize, count: usize, with_space: bool) -> Edit {
202 let count = count.max(1);
203 let row = row.min(self.row_count().saturating_sub(1));
204 let mut split_cols: Vec<usize> = Vec::with_capacity(count);
205 let mut joined = std::mem::take(&mut self.lines_mut()[row]);
206 for _ in 0..count {
207 if row + 1 >= self.row_count() {
208 break;
209 }
210 let next = self.lines_mut().remove(row + 1);
211 let join_col = joined.chars().count();
212 split_cols.push(join_col);
213 if with_space && !joined.is_empty() && !next.is_empty() {
214 joined.push(' ');
215 }
216 joined.push_str(&next);
217 }
218 self.lines_mut()[row] = joined;
219 self.dirty_gen_bump();
220 self.set_cursor(Position::new(row, 0));
221 Edit::SplitLines {
222 row,
223 cols: split_cols,
224 inserted_space: with_space,
225 }
226 }
227
228 fn do_split_lines(&mut self, row: usize, cols: Vec<usize>, inserted_space: bool) -> Edit {
229 let row = row.min(self.row_count().saturating_sub(1));
230 let mut working = std::mem::take(&mut self.lines_mut()[row]);
231 let mut tails: Vec<String> = Vec::with_capacity(cols.len());
234 for &c in cols.iter().rev() {
235 let byte = Position::new(0, c).byte_offset(&working);
236 let mut tail = working.split_off(byte);
237 if inserted_space && tail.starts_with(' ') {
238 tail.remove(0);
239 }
240 tails.push(tail);
241 }
242 self.lines_mut()[row] = working;
244 for (i, tail) in tails.into_iter().rev().enumerate() {
245 self.lines_mut().insert(row + 1 + i, tail);
246 }
247 self.dirty_gen_bump();
248 self.set_cursor(Position::new(row, 0));
249 Edit::JoinLines {
250 row,
251 count: cols.len(),
252 with_space: inserted_space,
253 }
254 }
255
256 fn do_replace(&mut self, start: Position, end: Position, with: String) -> Edit {
257 let (start, end) = order(start, end);
258 let removed = self.cut_chars(start, end);
259 let normalised = self.clamp_position(start);
260 let inserted_chars = with.chars().count();
261 let inserted_lines = with.split('\n').count();
262 let new_end = if inserted_lines > 1 {
263 let last_chars = with.rsplit('\n').next().unwrap_or("").chars().count();
264 Position::new(normalised.row + inserted_lines - 1, last_chars)
265 } else {
266 Position::new(normalised.row, normalised.col + inserted_chars)
267 };
268 self.splice_at(normalised, &with);
269 self.dirty_gen_bump();
270 self.set_cursor(new_end);
271 Edit::Replace {
272 start: normalised,
273 end: new_end,
274 with: removed,
275 }
276 }
277}
278
279impl Buffer {
282 fn splice_at(&mut self, at: Position, text: &str) {
286 let pieces: Vec<&str> = text.split('\n').collect();
287 let row = at.row;
288 let line = &mut self.lines_mut()[row];
289 let byte = at.byte_offset(line);
290 let suffix = line.split_off(byte);
291 if pieces.len() == 1 {
292 line.push_str(pieces[0]);
293 line.push_str(&suffix);
294 return;
295 }
296 line.push_str(pieces[0]);
297 let mut new_rows: Vec<String> = pieces[1..pieces.len() - 1]
298 .iter()
299 .map(|s| (*s).to_string())
300 .collect();
301 let mut last = pieces.last().copied().unwrap_or("").to_string();
302 last.push_str(&suffix);
303 new_rows.push(last);
304 let insert_at = row + 1;
305 for (i, l) in new_rows.into_iter().enumerate() {
306 self.lines_mut().insert(insert_at + i, l);
307 }
308 }
309
310 fn cut_chars(&mut self, start: Position, end: Position) -> String {
313 let (start, end) = order(start, end);
314 if start.row == end.row {
315 let line = &mut self.lines_mut()[start.row];
316 let lo = start.byte_offset(line).min(line.len());
317 let hi = end.byte_offset(line).min(line.len());
318 return line.drain(lo..hi).collect();
319 }
320 let mut out = String::new();
321 {
323 let line = &mut self.lines_mut()[start.row];
324 let byte = start.byte_offset(line).min(line.len());
325 let suffix: String = line.drain(byte..).collect();
326 out.push_str(&suffix);
327 }
328 out.push('\n');
329 let mid_lo = start.row + 1;
331 let mid_hi = end.row.saturating_sub(1);
332 if mid_hi >= mid_lo {
333 let drained: Vec<String> = self.lines_mut().drain(mid_lo..=mid_hi).collect();
334 for l in drained {
335 out.push_str(&l);
336 out.push('\n');
337 }
338 }
339 let end_line_idx = start.row + 1;
341 {
342 let line = &mut self.lines_mut()[end_line_idx];
343 let byte = end.byte_offset(line).min(line.len());
344 let prefix: String = line.drain(..byte).collect();
345 out.push_str(&prefix);
346 }
347 let merged = self.lines_mut().remove(end_line_idx);
349 self.lines_mut()[start.row].push_str(&merged);
350 out
351 }
352}
353
354fn order(a: Position, b: Position) -> (Position, Position) {
355 if a <= b { (a, b) } else { (b, a) }
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361
362 fn round_trip_check(initial: &str, edit: Edit) {
363 let mut b = Buffer::from_str(initial);
364 let snapshot_before = b.as_string();
365 let inverse = b.apply_edit(edit);
366 b.apply_edit(inverse);
367 assert_eq!(b.as_string(), snapshot_before);
368 }
369
370 #[test]
371 fn insert_char_round_trip() {
372 round_trip_check(
373 "abc",
374 Edit::InsertChar {
375 at: Position::new(0, 1),
376 ch: 'X',
377 },
378 );
379 }
380
381 #[test]
382 fn insert_str_multiline_round_trip() {
383 round_trip_check(
384 "abc\ndef",
385 Edit::InsertStr {
386 at: Position::new(0, 2),
387 text: "X\nY\nZ".into(),
388 },
389 );
390 }
391
392 #[test]
393 fn delete_charwise_single_row_round_trip() {
394 round_trip_check(
395 "alpha bravo charlie",
396 Edit::DeleteRange {
397 start: Position::new(0, 6),
398 end: Position::new(0, 11),
399 kind: MotionKind::Char,
400 },
401 );
402 }
403
404 #[test]
405 fn delete_charwise_multi_row_round_trip() {
406 round_trip_check(
407 "row0\nrow1\nrow2",
408 Edit::DeleteRange {
409 start: Position::new(0, 2),
410 end: Position::new(2, 2),
411 kind: MotionKind::Char,
412 },
413 );
414 }
415
416 #[test]
417 fn delete_linewise_round_trip() {
418 round_trip_check(
419 "a\nb\nc\nd",
420 Edit::DeleteRange {
421 start: Position::new(1, 0),
422 end: Position::new(2, 0),
423 kind: MotionKind::Line,
424 },
425 );
426 }
427
428 #[test]
429 fn delete_blockwise_round_trip() {
430 round_trip_check(
431 "abcdef\nghijkl\nmnopqr",
432 Edit::DeleteRange {
433 start: Position::new(0, 1),
434 end: Position::new(2, 3),
435 kind: MotionKind::Block,
436 },
437 );
438 }
439
440 #[test]
441 fn join_lines_with_space_round_trip() {
442 round_trip_check(
443 "first\nsecond\nthird",
444 Edit::JoinLines {
445 row: 0,
446 count: 2,
447 with_space: true,
448 },
449 );
450 }
451
452 #[test]
453 fn join_lines_no_space_round_trip() {
454 round_trip_check(
455 "first\nsecond",
456 Edit::JoinLines {
457 row: 0,
458 count: 1,
459 with_space: false,
460 },
461 );
462 }
463
464 #[test]
465 fn replace_round_trip() {
466 round_trip_check(
467 "foo bar baz",
468 Edit::Replace {
469 start: Position::new(0, 4),
470 end: Position::new(0, 7),
471 with: "QUUX".into(),
472 },
473 );
474 }
475
476 #[test]
477 fn delete_clearing_buffer_keeps_one_empty_row() {
478 let mut b = Buffer::from_str("only");
479 b.apply_edit(Edit::DeleteRange {
480 start: Position::new(0, 0),
481 end: Position::new(0, 0),
482 kind: MotionKind::Line,
483 });
484 assert_eq!(b.row_count(), 1);
485 assert_eq!(b.line(0), Some(""));
486 }
487
488 #[test]
489 fn insert_char_lands_cursor_after() {
490 let mut b = Buffer::from_str("abc");
491 b.set_cursor(Position::new(0, 1));
492 b.apply_edit(Edit::InsertChar {
493 at: Position::new(0, 1),
494 ch: 'X',
495 });
496 assert_eq!(b.cursor(), Position::new(0, 2));
497 assert_eq!(b.line(0), Some("aXbc"));
498 }
499
500 #[test]
501 fn block_delete_on_ragged_rows_handles_short_lines() {
502 let mut b = Buffer::from_str("longline\nhi\nthird row");
505 let inv = b.apply_edit(Edit::DeleteRange {
506 start: Position::new(0, 2),
507 end: Position::new(2, 5),
508 kind: MotionKind::Block,
509 });
510 b.apply_edit(inv);
511 assert_eq!(b.as_string(), "longline\nhi\nthird row");
512 }
513
514 #[test]
515 fn dirty_gen_bumps_per_edit() {
516 let mut b = Buffer::from_str("abc");
517 let g0 = b.dirty_gen();
518 b.apply_edit(Edit::InsertChar {
519 at: Position::new(0, 0),
520 ch: 'X',
521 });
522 assert_eq!(b.dirty_gen(), g0 + 1);
523 b.apply_edit(Edit::DeleteRange {
524 start: Position::new(0, 0),
525 end: Position::new(0, 1),
526 kind: MotionKind::Char,
527 });
528 assert_eq!(b.dirty_gen(), g0 + 2);
529 }
530}