1use hjkl_buffer::{Edit, MotionKind, Position};
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub struct Sel {
65 pub anchor: Position,
67 pub head: Position,
69}
70
71impl Sel {
72 pub fn new(anchor: Position, head: Position) -> Self {
74 Self { anchor, head }
75 }
76
77 pub fn caret(p: Position) -> Self {
79 Self { anchor: p, head: p }
80 }
81
82 pub fn is_caret(&self) -> bool {
84 self.anchor == self.head
85 }
86
87 pub fn start(&self) -> Position {
89 self.anchor.min(self.head)
90 }
91
92 pub fn end(&self) -> Position {
94 self.anchor.max(self.head)
95 }
96}
97
98fn key(p: Position) -> (usize, usize) {
100 (p.row, p.col)
101}
102
103fn after_insert(p: Position, at: Position, text: &str) -> Position {
105 if key(p) < key(at) {
106 return p;
107 }
108 let added_rows = text.matches('\n').count();
109 let tail = text.rsplit('\n').next().unwrap_or("");
112 let tail_len = tail.chars().count();
113
114 if p.row == at.row {
115 if added_rows == 0 {
116 Position::new(p.row, p.col + text.chars().count())
117 } else {
118 Position::new(p.row + added_rows, tail_len + (p.col - at.col))
120 }
121 } else {
122 Position::new(p.row + added_rows, p.col)
124 }
125}
126
127fn after_delete_char(p: Position, start: Position, end: Position) -> Position {
129 if key(p) <= key(start) {
130 return p;
131 }
132 if key(p) < key(end) {
133 return start;
135 }
136 if p.row == end.row {
137 Position::new(start.row, start.col + (p.col - end.col))
139 } else {
140 Position::new(p.row - (end.row - start.row), p.col)
141 }
142}
143
144fn after_delete_lines(p: Position, start_row: usize, end_row: usize) -> Position {
146 if p.row < start_row {
147 p
148 } else if p.row <= end_row {
149 Position::new(start_row, 0)
151 } else {
152 Position::new(p.row - (end_row - start_row + 1), p.col)
153 }
154}
155
156fn after_delete_block(p: Position, start: Position, end: Position) -> Position {
158 let (lo_row, hi_row) = (start.row.min(end.row), start.row.max(end.row));
159 let (lo_col, hi_col) = (start.col.min(end.col), start.col.max(end.col));
160 if p.row < lo_row || p.row > hi_row {
161 return p;
162 }
163 let width = hi_col - lo_col + 1;
164 if p.col > hi_col {
165 Position::new(p.row, p.col - width)
166 } else if p.col >= lo_col {
167 Position::new(p.row, lo_col)
168 } else {
169 p
170 }
171}
172
173fn after_join(
183 p: Position,
184 row: usize,
185 count: usize,
186 with_space: bool,
187 line_len: &impl Fn(usize) -> usize,
188 rows: usize,
189) -> Position {
190 if p.row < row {
191 return p;
192 }
193 let mut cur_len = line_len(row);
196 let mut start_col = Vec::with_capacity(count);
197 let mut joined = 0usize;
198 for k in 1..=count.max(1) {
199 if row + k >= rows {
200 break;
201 }
202 let next_len = line_len(row + k);
203 let space = with_space && cur_len > 0 && next_len > 0;
204 start_col.push(cur_len + usize::from(space));
205 cur_len += usize::from(space) + next_len;
206 joined += 1;
207 }
208 if joined == 0 || p.row == row {
209 return p;
211 }
212 if p.row <= row + joined {
213 let k = p.row - row; Position::new(row, start_col[k - 1] + p.col)
215 } else {
216 Position::new(p.row - joined, p.col)
217 }
218}
219
220fn after_insert_block(p: Position, at: Position, chunks: &[String]) -> Position {
224 if p.row < at.row || p.row >= at.row + chunks.len() || p.col < at.col {
225 return p;
226 }
227 let width = chunks[p.row - at.row].chars().count();
228 Position::new(p.row, p.col + width)
229}
230
231fn after_delete_block_chunks(p: Position, at: Position, widths: &[usize]) -> Position {
234 if p.row < at.row || p.row >= at.row + widths.len() || p.col <= at.col {
235 return p;
236 }
237 let w = widths[p.row - at.row];
238 if p.col >= at.col + w {
239 Position::new(p.row, p.col - w)
240 } else {
241 Position::new(p.row, at.col)
243 }
244}
245
246pub fn shift_position(
262 p: Position,
263 edit: &Edit,
264 line_len: impl Fn(usize) -> usize,
265 rows: usize,
266) -> Option<Position> {
267 match edit {
268 Edit::InsertChar { at, ch } => {
271 let mut buf = [0u8; 4];
272 Some(after_insert(p, *at, ch.encode_utf8(&mut buf)))
273 }
274 Edit::InsertStr { at, text } => Some(after_insert(p, *at, text)),
275 Edit::DeleteRange { start, end, kind } => Some(match kind {
276 MotionKind::Char => after_delete_char(p, *start, *end),
277 MotionKind::Line => after_delete_lines(p, start.row, end.row),
278 MotionKind::Block => after_delete_block(p, *start, *end),
279 }),
280 Edit::Replace { start, end, with } => {
281 let deleted = after_delete_char(p, *start, *end);
283 Some(after_insert(deleted, *start, with))
284 }
285 Edit::JoinLines {
286 row,
287 count,
288 with_space,
289 } => Some(after_join(p, *row, *count, *with_space, &line_len, rows)),
290 Edit::InsertBlock { at, chunks } => Some(after_insert_block(p, *at, chunks)),
291 Edit::DeleteBlockChunks { at, widths } => Some(after_delete_block_chunks(p, *at, widths)),
292 Edit::SplitLines { .. } => None,
297 }
298}
299
300pub fn shift_sel(
311 sel: Sel,
312 edit: &Edit,
313 line_len: impl Fn(usize) -> usize,
314 rows: usize,
315) -> Option<Sel> {
316 let anchor = shift_position(sel.anchor, edit, &line_len, rows)?;
317 let head = shift_position(sel.head, edit, &line_len, rows)?;
318 Some(Sel { anchor, head })
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 fn p(row: usize, col: usize) -> Position {
326 Position::new(row, col)
327 }
328 fn ins(row: usize, col: usize, text: &str) -> Edit {
329 Edit::InsertStr {
330 at: p(row, col),
331 text: text.to_string(),
332 }
333 }
334 fn del(s: (usize, usize), e: (usize, usize), kind: MotionKind) -> Edit {
335 Edit::DeleteRange {
336 start: p(s.0, s.1),
337 end: p(e.0, e.1),
338 kind,
339 }
340 }
341 fn head(row: usize, col: usize, edit: &Edit) -> Option<Position> {
345 shift_position(p(row, col), edit, |_| 0, 0)
346 }
347
348 fn head_in(row: usize, col: usize, edit: &Edit, lens: &[usize]) -> Option<Position> {
350 shift_position(p(row, col), edit, |r| lens[r], lens.len())
351 }
352
353 #[test]
356 fn insert_before_on_same_row_pushes_right() {
357 assert_eq!(head(0, 5, &ins(0, 2, "ab")), Some(p(0, 7)));
358 }
359
360 #[test]
361 fn insert_after_on_same_row_does_not_move() {
362 assert_eq!(head(0, 1, &ins(0, 2, "ab")), Some(p(0, 1)));
363 }
364
365 #[test]
366 fn position_exactly_at_insertion_point_moves_right() {
367 assert_eq!(head(0, 2, &ins(0, 2, "xy")), Some(p(0, 2 + 2)));
369 }
370
371 #[test]
372 fn insert_on_earlier_row_does_not_move_later_col() {
373 assert_eq!(head(3, 4, &ins(1, 0, "abc")), Some(p(3, 4)));
374 }
375
376 #[test]
377 fn multiline_insert_pushes_later_rows_down() {
378 assert_eq!(head(3, 4, &ins(1, 0, "a\nb\n")), Some(p(5, 4)));
379 }
380
381 #[test]
382 fn multiline_insert_relocates_tail_of_the_insert_row() {
383 assert_eq!(head(0, 2, &ins(0, 2, "X\nY")), Some(p(1, 1)));
386 }
387
388 #[test]
389 fn insert_char_newline_restructures_like_a_string() {
390 let e = Edit::InsertChar {
391 at: p(0, 2),
392 ch: '\n',
393 };
394 assert_eq!(head(0, 5, &e), Some(p(1, 3)));
395 }
396
397 #[test]
400 fn delete_before_pulls_left() {
401 assert_eq!(
402 head(0, 9, &del((0, 2), (0, 5), MotionKind::Char)),
403 Some(p(0, 6))
404 );
405 }
406
407 #[test]
408 fn delete_after_does_not_move() {
409 assert_eq!(
410 head(0, 1, &del((0, 2), (0, 5), MotionKind::Char)),
411 Some(p(0, 1))
412 );
413 }
414
415 #[test]
416 fn position_inside_deleted_range_collapses_to_start() {
417 assert_eq!(
418 head(0, 3, &del((0, 2), (0, 5), MotionKind::Char)),
419 Some(p(0, 2))
420 );
421 }
422
423 #[test]
424 fn delete_end_is_exclusive() {
425 assert_eq!(
427 head(0, 5, &del((0, 2), (0, 5), MotionKind::Char)),
428 Some(p(0, 2))
429 );
430 }
431
432 #[test]
433 fn cross_row_delete_folds_tail_onto_start_row() {
434 assert_eq!(
436 head(3, 6, &del((1, 2), (3, 4), MotionKind::Char)),
437 Some(p(1, 4))
438 );
439 }
440
441 #[test]
442 fn row_below_a_cross_row_delete_shifts_up() {
443 assert_eq!(
444 head(7, 3, &del((1, 2), (3, 4), MotionKind::Char)),
445 Some(p(5, 3))
446 );
447 }
448
449 #[test]
452 fn linewise_delete_shifts_rows_below_up() {
453 assert_eq!(
454 head(9, 3, &del((2, 0), (4, 0), MotionKind::Line)),
455 Some(p(6, 3))
456 );
457 }
458
459 #[test]
460 fn linewise_delete_of_the_selections_own_row_collapses_it() {
461 assert_eq!(
462 head(3, 7, &del((2, 0), (4, 0), MotionKind::Line)),
463 Some(p(2, 0))
464 );
465 }
466
467 #[test]
468 fn linewise_delete_above_leaves_earlier_rows_alone() {
469 assert_eq!(
470 head(1, 7, &del((2, 0), (4, 0), MotionKind::Line)),
471 Some(p(1, 7))
472 );
473 }
474
475 #[test]
478 fn block_delete_pulls_columns_right_of_the_rectangle_left() {
479 assert_eq!(
480 head(2, 9, &del((1, 2), (3, 5), MotionKind::Block)),
481 Some(p(2, 5))
482 );
483 }
484
485 #[test]
486 fn block_delete_collapses_columns_inside_the_rectangle() {
487 assert_eq!(
488 head(2, 3, &del((1, 2), (3, 5), MotionKind::Block)),
489 Some(p(2, 2))
490 );
491 }
492
493 #[test]
494 fn block_delete_leaves_rows_outside_the_rectangle_alone() {
495 assert_eq!(
496 head(9, 9, &del((1, 2), (3, 5), MotionKind::Block)),
497 Some(p(9, 9))
498 );
499 }
500
501 #[test]
504 fn replace_shorter_pulls_left() {
505 let e = Edit::Replace {
506 start: p(0, 2),
507 end: p(0, 6),
508 with: "x".to_string(),
509 };
510 assert_eq!(head(0, 8, &e), Some(p(0, 5)));
512 }
513
514 #[test]
515 fn replace_longer_pushes_right() {
516 let e = Edit::Replace {
517 start: p(0, 2),
518 end: p(0, 3),
519 with: "xyz".to_string(),
520 };
521 assert_eq!(head(0, 5, &e), Some(p(0, 7)));
522 }
523
524 #[test]
529 fn join_folds_the_next_row_up_after_the_anchor_plus_a_space() {
530 let e = Edit::JoinLines {
532 row: 0,
533 count: 1,
534 with_space: true,
535 };
536 assert_eq!(head_in(1, 1, &e, &[3, 2]), Some(p(0, 5)));
537 }
538
539 #[test]
540 fn join_without_space_folds_flush() {
541 let e = Edit::JoinLines {
542 row: 0,
543 count: 1,
544 with_space: false,
545 };
546 assert_eq!(head_in(1, 1, &e, &[3, 2]), Some(p(0, 4)));
547 }
548
549 #[test]
550 fn join_inserts_no_space_when_a_side_is_empty() {
551 let e = Edit::JoinLines {
553 row: 0,
554 count: 1,
555 with_space: true,
556 };
557 assert_eq!(
558 head_in(1, 1, &e, &[0, 2]),
559 Some(p(0, 1)),
560 "empty prefix -> no space"
561 );
562 }
563
564 #[test]
565 fn join_leaves_the_anchor_rows_own_columns_alone() {
566 let e = Edit::JoinLines {
567 row: 0,
568 count: 1,
569 with_space: true,
570 };
571 assert_eq!(head_in(0, 2, &e, &[3, 2]), Some(p(0, 2)));
572 }
573
574 #[test]
575 fn join_pulls_rows_below_the_joined_span_up() {
576 let e = Edit::JoinLines {
577 row: 0,
578 count: 1,
579 with_space: true,
580 };
581 assert_eq!(head_in(3, 1, &e, &[3, 2, 4, 4]), Some(p(2, 1)));
582 }
583
584 #[test]
585 fn multi_row_join_accumulates_each_rows_offset() {
586 let e = Edit::JoinLines {
589 row: 0,
590 count: 2,
591 with_space: true,
592 };
593 assert_eq!(head_in(2, 0, &e, &[2, 2, 2]), Some(p(0, 6)));
594 }
595
596 #[test]
599 fn block_insert_pushes_columns_at_or_after_the_block_right() {
600 let e = Edit::InsertBlock {
601 at: p(0, 2),
602 chunks: vec!["xx".into(), "xx".into()],
603 };
604 assert_eq!(head(1, 4, &e), Some(p(1, 6)));
605 }
606
607 #[test]
608 fn block_insert_leaves_columns_before_the_block_alone() {
609 let e = Edit::InsertBlock {
610 at: p(0, 2),
611 chunks: vec!["xx".into(), "xx".into()],
612 };
613 assert_eq!(head(1, 1, &e), Some(p(1, 1)));
614 }
615
616 #[test]
617 fn block_insert_leaves_rows_outside_the_block_alone() {
618 let e = Edit::InsertBlock {
619 at: p(0, 2),
620 chunks: vec!["xx".into()],
621 };
622 assert_eq!(head(5, 4, &e), Some(p(5, 4)));
623 }
624
625 #[test]
626 fn block_chunk_delete_pulls_columns_after_the_chunk_left() {
627 let e = Edit::DeleteBlockChunks {
628 at: p(0, 2),
629 widths: vec![2, 2],
630 };
631 assert_eq!(head(1, 6, &e), Some(p(1, 4)));
632 }
633
634 #[test]
635 fn block_chunk_delete_collapses_columns_inside_the_chunk() {
636 let e = Edit::DeleteBlockChunks {
637 at: p(0, 2),
638 widths: vec![3],
639 };
640 assert_eq!(head(0, 3, &e), Some(p(0, 2)));
641 }
642
643 #[test]
648 fn a_selection_shifts_both_of_its_ends() {
649 let s = Sel::new(p(0, 2), p(0, 5));
651 assert_eq!(
652 shift_sel(s, &ins(0, 0, "XY"), |_| 0, 1),
653 Some(Sel::new(p(0, 4), p(0, 7)))
654 );
655 }
656
657 #[test]
658 fn a_backwards_selection_keeps_its_direction() {
659 let s = Sel::new(p(0, 5), p(0, 2));
660 let out = shift_sel(s, &ins(0, 0, "XY"), |_| 0, 1).unwrap();
661 assert_eq!(out.anchor, p(0, 7));
662 assert_eq!(out.head, p(0, 4));
663 assert!(out.anchor > out.head, "direction must survive the shift");
664 }
665
666 #[test]
667 fn a_selection_whose_text_is_deleted_collapses_to_the_hole() {
668 let s = Sel::new(p(0, 3), p(0, 5));
670 assert_eq!(
671 shift_sel(s, &del((0, 2), (0, 6), MotionKind::Char), |_| 0, 1),
672 Some(Sel::caret(p(0, 2))),
673 "both ends collapse to the deletion start — a caret, not a stale range"
674 );
675 }
676
677 #[test]
678 fn a_selection_is_dropped_whole_when_either_end_is_untrackable() {
679 let e = Edit::SplitLines {
680 row: 0,
681 cols: vec![3],
682 inserted_space: true,
683 };
684 assert_eq!(
685 shift_sel(Sel::new(p(1, 0), p(1, 4)), &e, |_| 0, 0),
686 None,
687 "never half-track: a selection with one guessed end edits the wrong text"
688 );
689 }
690
691 #[test]
692 fn split_lines_drops_rather_than_guessing() {
693 let e = Edit::SplitLines {
697 row: 0,
698 cols: vec![3],
699 inserted_space: true,
700 };
701 assert_eq!(shift_position(p(5, 0), &e, |_| 0, 0), None);
702 }
703}