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