Skip to main content

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}