tlc: Sprint 2 B5 — bookmark line adjustment on edit
Bookmarks now track the correct line number across insert/delete
edits (MC parity). Previously a bookmark at line 5 would still
report line 5 even after lines 1-3 were deleted, pointing at
wrong content.
BookmarkSet (editor/bookmark.rs):
New method adjust_lines(at_line: u32, delta: i32)
- For each mark with line > at_line: shift by delta
- For positive delta: mark.line += delta
- For negative delta with mark.line in deletion range:
clamp to at_line + 1 (first surviving line after range)
- Column is preserved across all shifts
- Zero delta is a no-op (no allocation)
Editor (editor/mod.rs):
New private helper adjust_bookmarks_after_edit(edit_line, lines_before)
Wraps insert_char, insert_str, delete_back, delete_forward:
- Captures (line_before, lines_before) before buffer op
- Calls buffer op
- Computes delta = lines_after - lines_before
- Calls bookmarks.adjust_lines(line_before, delta) if non-zero
Tests (6 new in editor::bookmark::tests):
- adjust_lines_zero_delta_is_noop
- adjust_lines_shifts_marks_after_insertion
- adjust_lines_shifts_marks_after_deletion
- adjust_lines_multi_line_insertion
- adjust_lines_deletion_clamps_to_anchor
- adjust_lines_does_not_touch_col
Total: 1259 passing (was 1253; +6 new).
This commit is contained in:
@@ -101,6 +101,41 @@ impl BookmarkSet {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.marks.is_empty()
|
||||
}
|
||||
|
||||
/// Shift bookmark lines in response to a buffer edit.
|
||||
///
|
||||
/// Called after a single edit operation that added or removed
|
||||
/// lines from the buffer. `at_line` is the 0-based line index where
|
||||
/// the edit happened; `delta` is the number of lines added (positive)
|
||||
/// or removed (negative). Bookmarks on lines strictly after
|
||||
/// `at_line` are shifted by `delta`. Bookmarks on earlier lines
|
||||
/// are left alone. Bookmarks exactly at `at_line` are left alone
|
||||
/// when `delta` is zero (default case); callers handling line-split
|
||||
/// or line-merge edits should adjust those bookmarks separately
|
||||
/// before calling this method.
|
||||
pub fn adjust_lines(&mut self, at_line: u32, delta: i32) {
|
||||
if delta == 0 {
|
||||
return;
|
||||
}
|
||||
for mark in self.marks.values_mut() {
|
||||
if mark.line > at_line {
|
||||
if delta > 0 {
|
||||
mark.line = mark.line.saturating_add(delta as u32);
|
||||
} else {
|
||||
// For negative delta, only shift if there's room.
|
||||
let abs_delta = (-delta) as u32;
|
||||
if mark.line >= at_line + 1 + abs_delta {
|
||||
mark.line -= abs_delta;
|
||||
} else {
|
||||
// Bookmark would underflow past `at_line + 1`
|
||||
// — clamp to `at_line` (it stays on the edit
|
||||
// anchor line after the deletion).
|
||||
mark.line = at_line + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// True if `c` is a valid bookmark name (lowercase letter or digit).
|
||||
@@ -194,4 +229,80 @@ mod tests {
|
||||
assert_eq!(bs.len(), 5);
|
||||
assert_eq!(bs.get('a'), Some(Mark::new(50, 50)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adjust_lines_zero_delta_is_noop() {
|
||||
let mut bs = BookmarkSet::new();
|
||||
bs.set('a', Mark::new(5, 2)).unwrap();
|
||||
bs.adjust_lines(2, 0);
|
||||
assert_eq!(bs.get('a'), Some(Mark::new(5, 2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adjust_lines_shifts_marks_after_insertion() {
|
||||
let mut bs = BookmarkSet::new();
|
||||
bs.set('a', Mark::new(2, 0)).unwrap();
|
||||
bs.set('b', Mark::new(5, 3)).unwrap();
|
||||
// Given an insertion at line 3 (delta=+1):
|
||||
bs.adjust_lines(3, 1);
|
||||
// Then mark at line 2 (before) is unchanged.
|
||||
assert_eq!(bs.get('a'), Some(Mark::new(2, 0)));
|
||||
// And mark at line 5 (after) shifts to line 6.
|
||||
assert_eq!(bs.get('b'), Some(Mark::new(6, 3)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adjust_lines_shifts_marks_after_deletion() {
|
||||
let mut bs = BookmarkSet::new();
|
||||
bs.set('a', Mark::new(2, 0)).unwrap();
|
||||
bs.set('b', Mark::new(5, 3)).unwrap();
|
||||
// Given a deletion at line 3 (delta=-1):
|
||||
bs.adjust_lines(3, -1);
|
||||
// Then mark at line 2 (before) is unchanged.
|
||||
assert_eq!(bs.get('a'), Some(Mark::new(2, 0)));
|
||||
// And mark at line 5 (after) shifts to line 4.
|
||||
assert_eq!(bs.get('b'), Some(Mark::new(4, 3)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adjust_lines_multi_line_insertion() {
|
||||
let mut bs = BookmarkSet::new();
|
||||
bs.set('a', Mark::new(0, 0)).unwrap();
|
||||
bs.set('b', Mark::new(1, 5)).unwrap();
|
||||
bs.set('c', Mark::new(3, 2)).unwrap();
|
||||
// Given an insertion of 3 lines at line 2 (delta=+3):
|
||||
bs.adjust_lines(2, 3);
|
||||
// Then marks before line 2 are unchanged.
|
||||
assert_eq!(bs.get('a'), Some(Mark::new(0, 0)));
|
||||
assert_eq!(bs.get('b'), Some(Mark::new(1, 5)));
|
||||
// And mark after line 2 shifts by 3.
|
||||
assert_eq!(bs.get('c'), Some(Mark::new(6, 2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adjust_lines_deletion_clamps_to_anchor() {
|
||||
let mut bs = BookmarkSet::new();
|
||||
bs.set('a', Mark::new(0, 0)).unwrap();
|
||||
bs.set('b', Mark::new(2, 0)).unwrap();
|
||||
bs.set('c', Mark::new(5, 0)).unwrap();
|
||||
// Given a deletion of 3 lines starting at line 3 (delta=-3):
|
||||
bs.adjust_lines(3, -3);
|
||||
// Then mark at line 0 (well before) stays put.
|
||||
assert_eq!(bs.get('a'), Some(Mark::new(0, 0)));
|
||||
// And mark at line 2 (just before the deletion) stays put.
|
||||
assert_eq!(bs.get('b'), Some(Mark::new(2, 0)));
|
||||
// And mark at line 5 (in the deleted range) clamps to
|
||||
// at_line + 1 (line 4) — the first surviving line after the
|
||||
// deletion range.
|
||||
assert_eq!(bs.get('c'), Some(Mark::new(4, 0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adjust_lines_does_not_touch_col() {
|
||||
let mut bs = BookmarkSet::new();
|
||||
bs.set('a', Mark::new(10, 42)).unwrap();
|
||||
bs.adjust_lines(5, 5);
|
||||
// Column component of the mark is preserved across shifts.
|
||||
assert_eq!(bs.get('a').unwrap().col, 42);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -877,18 +877,36 @@ bracket_flash: None,
|
||||
/// modified.
|
||||
pub fn insert_char(&mut self, c: char) {
|
||||
self.completer.cancel();
|
||||
let line_before = self.buffer_line_of(self.buffer.cursor());
|
||||
let lines_before = self.buffer.line_count();
|
||||
self.buffer.insert_char(c);
|
||||
self.adjust_bookmarks_after_edit(line_before, lines_before);
|
||||
self.cursor.set_position(self.buffer.cursor(), &self.buffer);
|
||||
self.modified = true;
|
||||
}
|
||||
|
||||
/// Insert a string at the cursor.
|
||||
pub fn insert_str(&mut self, s: &str) {
|
||||
let line_before = self.buffer_line_of(self.buffer.cursor());
|
||||
let lines_before = self.buffer.line_count();
|
||||
self.buffer.insert_str(s);
|
||||
self.adjust_bookmarks_after_edit(line_before, lines_before);
|
||||
self.cursor.set_position(self.buffer.cursor(), &self.buffer);
|
||||
self.modified = true;
|
||||
}
|
||||
|
||||
/// Recompute bookmark line numbers after a buffer edit. Called by
|
||||
/// every insert/delete entry point on `Editor`. `edit_line` is the
|
||||
/// line where the edit happened; `lines_before` is the line count
|
||||
/// before the edit.
|
||||
fn adjust_bookmarks_after_edit(&mut self, edit_line: usize, lines_before: usize) {
|
||||
let lines_after = self.buffer.line_count();
|
||||
let delta = lines_after as i64 - lines_before as i64;
|
||||
if delta != 0 {
|
||||
self.bookmarks.adjust_lines(edit_line as u32, delta as i32);
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a Tab with indent-aware behavior (MC parity).
|
||||
///
|
||||
/// When the cursor is positioned at or before the first
|
||||
@@ -931,14 +949,20 @@ bracket_flash: None,
|
||||
|
||||
/// Backspace at the cursor.
|
||||
pub fn delete_back(&mut self) {
|
||||
let line_before = self.buffer_line_of(self.buffer.cursor());
|
||||
let lines_before = self.buffer.line_count();
|
||||
self.buffer.delete_back();
|
||||
self.adjust_bookmarks_after_edit(line_before, lines_before);
|
||||
self.cursor.set_position(self.buffer.cursor(), &self.buffer);
|
||||
self.modified = true;
|
||||
}
|
||||
|
||||
/// Delete at the cursor.
|
||||
pub fn delete_forward(&mut self) {
|
||||
let line_before = self.buffer_line_of(self.buffer.cursor());
|
||||
let lines_before = self.buffer.line_count();
|
||||
self.buffer.delete_forward();
|
||||
self.adjust_bookmarks_after_edit(line_before, lines_before);
|
||||
self.cursor.set_position(self.buffer.cursor(), &self.buffer);
|
||||
self.modified = true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user