Skip to main content

tiptap_rusty_parser/
pos_map.rs

1//! Flat-position mapping: carry a position or range through a batch of edits.
2//!
3//! A [`PosMap`] is a ProseMirror-style position map built from the replaced
4//! spans of a disjoint edit batch (the `(from, old_len, new_len)` deltas a
5//! [`PosEdit`](crate::PosEdit) batch produces). [`PosMap::map`] carries a flat
6//! position from the **pre-edit** coordinate system to the **post-edit** one —
7//! so a cursor, selection, decoration, or streamed-suggestion range stays
8//! anchored across an [`apply_pos_edits`](crate::Node::apply_pos_edits) call.
9//!
10//! ```
11//! use tiptap_rusty_parser::{Assoc, Node, PosContent, PosEdit};
12//!
13//! // doc > paragraph("hello world"); insert "big " before "world" (pos 7).
14//! let mut doc = Node::element("doc")
15//!     .with_child(Node::element("paragraph").with_child(Node::text("hello world")));
16//! let (_patch, map) = doc
17//!     .apply_pos_edits_mapped(&[PosEdit::Insert {
18//!         pos: 7,
19//!         content: PosContent::Text { text: "big ".into(), marks: None },
20//!     }])
21//!     .unwrap();
22//!
23//! assert_eq!(map.map(3, Assoc::Left), 3);   // before the edit: unchanged
24//! assert_eq!(map.map(7, Assoc::Right), 11); // at the edit, after inserted text
25//! assert_eq!(map.map(13, Assoc::Left), 17); // after the edit: shifted by +4
26//! ```
27
28use crate::pos::PosRange;
29use crate::pos_edit::PosEdit;
30use serde::{Deserialize, Serialize};
31
32/// Which edge a position lying inside a replaced span maps to.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
34#[serde(rename_all = "camelCase")]
35pub enum Assoc {
36    /// Bias toward the **start** of the replacement (left edge). The default —
37    /// a position stays before content inserted at it.
38    #[default]
39    Left,
40    /// Bias toward the **end** of the replacement (right edge) — a position
41    /// moves past content inserted at it.
42    Right,
43}
44
45/// One replaced span in **old** coordinates: `[start, start + old_len)` became
46/// `new_len` units wide.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
48struct Step {
49    start: usize,
50    old_len: usize,
51    new_len: usize,
52}
53
54/// A flat position map over a batch of **disjoint** replacements, mapping
55/// pre-edit positions to post-edit ones. See the [module docs](self).
56#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
57pub struct PosMap {
58    /// Steps in old coordinates, kept sorted ascending by `start`. Callers are
59    /// expected to keep them disjoint (`push` does not enforce it); overlapping
60    /// steps still map safely but to unspecified edges.
61    steps: Vec<Step>,
62}
63
64impl PosMap {
65    /// An empty map (the identity: every position maps to itself).
66    pub fn new() -> Self {
67        Self::default()
68    }
69
70    /// Whether this map is the identity — it holds no steps, so every position
71    /// maps to itself. (A same-length step still counts: it maps interior
72    /// positions to an edge, so it is *not* empty.)
73    pub fn is_empty(&self) -> bool {
74        self.steps.is_empty()
75    }
76
77    /// Record a replacement: `[start, start + old_len)` (old coords) became
78    /// `new_len` units. A no-op span (`old_len == new_len == 0`) is ignored.
79    /// Steps are expected disjoint; they're kept sorted by `start`.
80    pub fn push(&mut self, start: usize, old_len: usize, new_len: usize) {
81        if old_len == 0 && new_len == 0 {
82            return;
83        }
84        let step = Step {
85            start,
86            old_len,
87            new_len,
88        };
89        let i = self.steps.partition_point(|s| s.start < start);
90        self.steps.insert(i, step);
91    }
92
93    /// Map a flat position from pre-edit to post-edit coordinates. `assoc`
94    /// decides which edge a position inside a replaced span lands on.
95    pub fn map(&self, pos: usize, assoc: Assoc) -> usize {
96        let mut diff: i64 = 0;
97        for s in &self.steps {
98            if pos < s.start {
99                break; // sorted: this step and all later ones start after `pos`
100            }
101            let old_end = s.start + s.old_len;
102            if pos > old_end {
103                diff += s.new_len as i64 - s.old_len as i64;
104                continue;
105            }
106            // `s.start <= pos <= old_end`: inside (or at a boundary of) the span.
107            let into = match assoc {
108                Assoc::Left => 0,
109                Assoc::Right => s.new_len as i64,
110            };
111            return (s.start as i64 + diff + into) as usize;
112        }
113        (pos as i64 + diff) as usize
114    }
115
116    /// Map both endpoints of a [`PosRange`] with the same `assoc` (so a
117    /// collapsed range stays collapsed).
118    pub fn map_range(&self, range: PosRange, assoc: Assoc) -> PosRange {
119        PosRange::new(self.map(range.from, assoc), self.map(range.to, assoc))
120    }
121
122    /// Build a map from a **disjoint** batch of edits (the same batch passed to
123    /// [`apply_pos_edits`](crate::Node::apply_pos_edits)). Positions are taken in
124    /// the pre-edit coordinate system; mark/attr edits contribute no step.
125    pub fn from_pos_edits(edits: &[PosEdit]) -> Self {
126        let mut map = PosMap::new();
127        for e in edits {
128            match e {
129                PosEdit::Insert { pos, content } => map.push(*pos, 0, content.flat_len()),
130                PosEdit::Delete { from, to } => map.push(*from, to.saturating_sub(*from), 0),
131                PosEdit::Replace { from, to, content } => {
132                    map.push(*from, to.saturating_sub(*from), content.flat_len())
133                }
134                // Marks and attrs don't change flat sizes.
135                PosEdit::AddMark { .. }
136                | PosEdit::RemoveMark { .. }
137                | PosEdit::SetBlockAttrs { .. } => {}
138            }
139        }
140        map
141    }
142}