Skip to main content

kimun_notes/ropetext/
position.rs

1//! Addresses into a [`Text`](crate::ropetext::Text): revisions, columns, positions, spans.
2
3use std::num::NonZeroU64;
4use std::sync::atomic::{AtomicU64, Ordering};
5
6/// Which state of a text a value refers to.
7///
8/// Revisions are unique across the whole process, not per text. Per-text
9/// counters would collide in the case this type exists to catch: an editor's
10/// buffer and a preview clone of it both advance from the same value, so a
11/// [`Position`] made against one would be accepted by the other and read as a
12/// different place in a different buffer.
13///
14/// Cloning a text does not mint a revision — a clone *is* the same text. Only a
15/// committed change does.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct Revision(NonZeroU64);
18
19static NEXT_REVISION: AtomicU64 = AtomicU64::new(1);
20
21impl Revision {
22    /// Mint a revision no other text has held.
23    pub(crate) fn fresh() -> Self {
24        let n = NEXT_REVISION.fetch_add(1, Ordering::Relaxed);
25        // Exhausting u64 would wrap to zero. At a billion revisions a second
26        // that takes about 584 years, so this is a statement of intent rather
27        // than a case to handle.
28        Self(NonZeroU64::new(n).expect("revision counter wrapped"))
29    }
30
31    pub fn get(self) -> NonZeroU64 {
32        self.0
33    }
34}
35
36/// A column within one row, counted in Unicode scalars.
37///
38/// Characters, not grapheme clusters. The cursor *steps* by cluster — motions
39/// and cell mapping only ever land on cluster boundaries — but indexing by
40/// cluster would mean segmenting a row from its start on every lookup, where a
41/// char index is a rope operation. Nothing wants cluster ordinals: vim's `|`
42/// counts characters, an external editor's cursor arrives in bytes, and a
43/// markdown parser reports byte offsets.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
45pub struct Column(usize);
46
47impl Column {
48    pub const ZERO: Column = Column(0);
49
50    pub fn new(chars: usize) -> Self {
51        Self(chars)
52    }
53
54    pub fn get(self) -> usize {
55        self.0
56    }
57}
58
59impl From<usize> for Column {
60    fn from(chars: usize) -> Self {
61        Self(chars)
62    }
63}
64
65/// A place in a text.
66///
67/// Built only by [`Text::position`](crate::ropetext::Text::position) and its siblings, so
68/// a position that exists is one the text could address when it was made. Row,
69/// column and byte offset are all resolved at construction and the type is
70/// `Copy`, so reading them is free while *making* one costs a rope lookup —
71/// loops should carry a position forward rather than rebuild it from `(row,
72/// col)` each time round.
73///
74/// Deliberately not `Ord`. Comparing positions from two revisions is
75/// meaningless, and a comparison operator that quietly answers `false` for
76/// incomparable operands is the kind of wrong answer this crate exists to avoid.
77/// Use [`Text::span`](crate::ropetext::Text::span), which checks both operands and orders
78/// them.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
80pub struct Position {
81    byte: usize,
82    row: usize,
83    column: Column,
84    revision: Revision,
85}
86
87impl Position {
88    pub(crate) fn new(byte: usize, row: usize, column: Column, revision: Revision) -> Self {
89        Self {
90            byte,
91            row,
92            column,
93            revision,
94        }
95    }
96
97    /// Byte offset from the start of the text.
98    pub fn byte(self) -> usize {
99        self.byte
100    }
101
102    /// Zero-based logical row.
103    pub fn row(self) -> usize {
104        self.row
105    }
106
107    /// Column within [`Self::row`], in Unicode scalars.
108    pub fn column(self) -> Column {
109        self.column
110    }
111
112    /// The text state this position addresses.
113    pub fn revision(self) -> Revision {
114        self.revision
115    }
116}
117
118/// An ordered, single-revision range between two [`Position`]s.
119///
120/// The ordering and the revision agreement are established once, when the span
121/// is built, so nothing downstream has to re-establish either.
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
123pub struct Span {
124    start: Position,
125    end: Position,
126}
127
128impl Span {
129    /// `start` and `end` must share a revision, and `start` must not be after
130    /// `end`. Both are guaranteed by [`Text::span`](crate::ropetext::Text::span), the only
131    /// caller.
132    pub(crate) fn new(start: Position, end: Position) -> Self {
133        debug_assert_eq!(start.revision(), end.revision());
134        debug_assert!(start.byte() <= end.byte());
135        Self { start, end }
136    }
137
138    pub fn start(self) -> Position {
139        self.start
140    }
141
142    pub fn end(self) -> Position {
143        self.end
144    }
145
146    pub fn revision(self) -> Revision {
147        self.start.revision()
148    }
149
150    pub fn is_empty(self) -> bool {
151        self.start.byte() == self.end.byte()
152    }
153
154    pub fn byte_range(self) -> std::ops::Range<usize> {
155        self.start.byte()..self.end.byte()
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn fresh_revisions_never_repeat() {
165        let a = Revision::fresh();
166        let b = Revision::fresh();
167        assert_ne!(a, b);
168    }
169
170    #[test]
171    fn column_round_trips() {
172        assert_eq!(Column::from(7).get(), 7);
173        assert_eq!(Column::ZERO.get(), 0);
174    }
175}