typ_buffer/change.rs
1//! Mapping a position forward through edits that have already been applied.
2//!
3//! This is the one thing in the tree that knows how a position moves when the
4//! text before it changes. It lived inside the editor panel, private, which
5//! meant every consumer that holds a position across an edit — search results,
6//! diagnostics, git hunks — would have had to rediscover it.
7//!
8//! **This is a shift map, not an anchor system.** It maps positions forward
9//! through one batch of edits, in the order those edits were applied, and is
10//! then discarded. Zed's `Anchor` and Neovim's extmarks survive arbitrary later
11//! edits because the buffer tracks them; nothing here does. That is enough for
12//! every consumer named above, and it is what the code already did correctly.
13//!
14//! ponytail: when something needs a position to survive an arbitrary edit
15//! sequence rather than one batch, that is anchors, and anchors are a separate
16//! decision — not an extension of this.
17
18use crate::position::Position;
19
20/// The accumulated effect of edits already applied, in original coordinates.
21///
22/// Column shifts apply only to positions on the line where the last edit ended;
23/// line shifts apply to everything after it. Tracking both is what lets several
24/// cursors edit the same line without the later ones landing in the wrong place.
25#[derive(Debug, Default, Clone, Copy)]
26pub struct Shift {
27 lines: isize,
28 cols: isize,
29 /// Original line index the column shift belongs to.
30 col_line: Option<usize>,
31}
32
33impl Shift {
34 /// Where `pos` — stated in the coordinates that existed before this batch —
35 /// sits now.
36 pub fn apply(&self, pos: Position) -> Position {
37 let col = if self.col_line == Some(pos.line) {
38 (pos.col as isize + self.cols).max(0) as usize
39 } else {
40 pos.col
41 };
42 Position {
43 line: (pos.line as isize + self.lines).max(0) as usize,
44 col,
45 }
46 }
47
48 /// Record what an edit did.
49 ///
50 /// `original_end_line` is in original coordinates; `applied_end` and `after`
51 /// are in current ones — `applied_end` is where the edit's range ended once
52 /// the shift so far was applied, and `after` is where the replacement text
53 /// left the position.
54 pub fn record(&mut self, original_end_line: usize, applied_end: Position, after: Position) {
55 let col_delta = after.col as isize - applied_end.col as isize;
56 if self.col_line == Some(original_end_line) {
57 self.cols += col_delta;
58 } else {
59 self.cols = col_delta;
60 self.col_line = Some(original_end_line);
61 }
62 self.lines += after.line as isize - applied_end.line as isize;
63 }
64}