ad_editor/dot/
mod.rs

1//! Sam style dot manipulation
2//!
3//! See <http://sam.cat-v.org/> for details on how sam works, particularly
4//! <http://doc.cat-v.org/plan_9/4th_edition/papers/sam/> which is the original paper
5//! where Rob Pike lays out how the editor works.
6//!
7//! All indexing is 0-based when working with the contents of a specific buffer.
8//! Converting to 1-based indices for the terminal is exclusively handled in the
9//! rendering logic.
10use crate::buffer::Buffer;
11use std::cmp::min;
12
13mod cur;
14pub(crate) mod find;
15mod range;
16mod text_object;
17
18pub use cur::Cur;
19pub use range::Range;
20pub use text_object::TextObject;
21
22/// A Dot represents the currently selected contents of a Buffer.
23///
24/// Most of the editing commands available in ad which manipulate the buffer contents
25/// do so via setting and manipulating the current dot. The name comes from the fact
26/// that the representation of the current Dot in the editing language is `.`
27#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
28pub enum Dot {
29    /// A single character [Cur]
30    Cur {
31        /// The cursor
32        c: Cur,
33    },
34    /// A [Range] between two cursors
35    Range {
36        /// The range
37        r: Range,
38    },
39}
40
41impl Default for Dot {
42    fn default() -> Self {
43        Self::Cur { c: Cur::default() }
44    }
45}
46
47impl From<Cur> for Dot {
48    fn from(c: Cur) -> Self {
49        Self::Cur { c }
50    }
51}
52
53impl From<Range> for Dot {
54    fn from(r: Range) -> Self {
55        Self::Range { r }
56    }
57}
58
59impl Dot {
60    /// Construct a new [Range] dot from two cursor indices.
61    ///
62    /// `to` will be used as the active cursor position.
63    pub fn from_char_indices(from: usize, to: usize) -> Self {
64        Self::Range {
65            r: Range::from_cursors(Cur { idx: from }, Cur { idx: to }, false),
66        }
67    }
68
69    /// Convert a [Dot] into character offsets within the buffer idendifying its start and end
70    /// positions.
71    ///
72    /// For a [Cur] dot the start and end are equal
73    pub fn as_char_indices(&self) -> (usize, usize) {
74        match *self {
75            Self::Cur { c: Cur { idx } } => (idx, idx),
76            Self::Range {
77                r:
78                    Range {
79                        start: Cur { idx: from },
80                        end: Cur { idx: to },
81                        ..
82                    },
83            } => (from, to),
84        }
85    }
86
87    /// The number of characters contained within this Dot.
88    pub fn n_chars(&self) -> usize {
89        let (from, to) = self.as_char_indices();
90
91        to - from + 1
92    }
93
94    /// Whether or not this dot is a [Cur].
95    pub fn is_cur(&self) -> bool {
96        matches!(self, Dot::Cur { .. })
97    }
98
99    /// Whether or not this dot is a [Range].
100    pub fn is_range(&self) -> bool {
101        matches!(self, Dot::Range { .. })
102    }
103
104    /// Whether or not this dot contains `cur` within it.
105    pub fn contains(&self, cur: &Cur) -> bool {
106        match self {
107            Dot::Cur { c } => cur == c,
108            Dot::Range { r } => r.contains(cur),
109        }
110    }
111
112    /// Whether or not this dot contains all of `rng` within it.
113    pub fn contains_range(&self, rng: &Range) -> bool {
114        self.contains(&rng.start) && self.contains(&rng.end)
115    }
116
117    /// Apply an offset to the cursors within this [Dot] using saturating subtraction.
118    ///
119    /// [Dot::clamp_idx] will still need to be called in order to ensure that the result
120    /// is within bounds for the given buffer.
121    pub fn with_offset_saturating(mut self, offset: isize) -> Self {
122        match &mut self {
123            Dot::Cur { c } if offset >= 0 => c.idx += offset as usize,
124            Dot::Cur { c } => c.idx = c.idx.saturating_sub(-offset as usize),
125            Dot::Range { r } if offset >= 0 => {
126                r.start.idx += offset as usize;
127                r.end.idx += offset as usize;
128            }
129            Dot::Range { r } => {
130                r.start.idx = r.start.idx.saturating_sub(-offset as usize);
131                r.end.idx = r.end.idx.saturating_sub(-offset as usize);
132            }
133        }
134
135        self
136    }
137
138    /// The address representation of this dot in the form that is enterable by the user.
139    /// Indices are 1-based rather than their internal 0-based representation.
140    pub fn addr(&self, b: &Buffer) -> String {
141        match self {
142            Self::Cur { c } => c.as_string_addr(b),
143            Self::Range { r } => r.as_string_addr(b),
144        }
145    }
146
147    /// Use this [Dot] to index in to a [Buffer] and extract the range of text it denotes.
148    pub fn content(&self, b: &Buffer) -> String {
149        let len_chars = b.txt.len_chars();
150
151        if len_chars == 0 {
152            return String::new();
153        }
154
155        let (from, to) = self.as_char_indices();
156        b.txt.slice(from, min(to + 1, len_chars)).to_string()
157    }
158
159    /// The active cursor position for this [Dot] which will be manipulated by movement operations
160    #[inline]
161    pub fn active_cur(&self) -> Cur {
162        match self {
163            Self::Cur { c } => *c,
164            Self::Range { r } => r.active_cursor(),
165        }
166    }
167
168    /// Set the active cursor position for this [Dot] directly, replacing the current active
169    /// cursor.
170    pub fn set_active_cur(&mut self, cur: Cur) {
171        match self {
172            Self::Cur { c } => *c = cur,
173            Self::Range { r } => r.set_active_cursor(cur),
174        }
175    }
176
177    /// The cursor position closest to the start of the Buffer.
178    #[inline]
179    pub fn first_cur(&self) -> Cur {
180        match self {
181            Self::Cur { c } => *c,
182            Self::Range { r } => r.start,
183        }
184    }
185
186    /// The cursor position closest to the end of the Buffer.
187    #[inline]
188    pub fn last_cur(&self) -> Cur {
189        match self {
190            Self::Cur { c } => *c,
191            Self::Range { r } => r.end,
192        }
193    }
194
195    /// This [Dot] expressed as a [Range].
196    ///
197    /// For range dots the underlying range is returned directly. For cursor dots a new range is
198    /// constructed where `start` and `end` are equal and the `end` is the active cursor.
199    #[inline]
200    pub fn as_range(&self) -> Range {
201        match self {
202            Self::Cur { c } => Range {
203                start: *c,
204                end: *c,
205                start_active: false,
206            },
207            Self::Range { r } => *r,
208        }
209    }
210
211    /// The [Dot] equivalent of [Dot::first_cur].
212    #[inline]
213    pub fn collapse_to_first_cur(&self) -> Self {
214        Dot::Cur {
215            c: self.first_cur(),
216        }
217    }
218
219    /// The [Dot] equivalent of [Dot::last_cur].
220    #[inline]
221    pub fn collapse_to_last_cur(&self) -> Self {
222        Dot::Cur { c: self.last_cur() }
223    }
224
225    /// The [Dot] equivalent of [Dot::active_cur].
226    #[inline]
227    pub fn collapse_to_active_cur(&self) -> Self {
228        Dot::Cur {
229            c: self.active_cur(),
230        }
231    }
232
233    /// Swap the active cursor between `start` and `end` of [Range] dots.
234    #[inline]
235    pub fn flip(&mut self) {
236        if let Dot::Range { r } = self {
237            r.flip();
238        }
239    }
240
241    /// If both ends of a Range match then replace with a single Cur
242    pub fn collapse_null_range(self) -> Self {
243        match self {
244            Dot::Range {
245                r: Range { start, end, .. },
246            } if start == end => Dot::Cur { c: start },
247            _ => self,
248        }
249    }
250
251    /// Clamp this dot to be valid for the given Buffer
252    pub fn clamp_idx(&mut self, max_idx: usize) {
253        match self {
254            Dot::Cur { c } => c.clamp_idx(max_idx),
255            Dot::Range { r } => {
256                r.start.clamp_idx(max_idx);
257                r.end.clamp_idx(max_idx);
258            }
259        }
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::{
266        text_object::TextObject::{self, *},
267        *,
268    };
269    use simple_test_case::test_case;
270
271    const EXAMPLE_TEXT: &str = "\
272This is the first line of the file. Followed
273by the second line. Some of the sentences are split
274over multiple lines.
275Others are not.
276
277There is a second paragraph as well. But it
278is quite short when compared to the first.
279
280
281The third paragraph is even shorter.";
282
283    fn cur(y: usize, x: usize) -> Cur {
284        let y = if y == 0 {
285            0
286        } else {
287            EXAMPLE_TEXT
288                .lines()
289                .take(y)
290                .map(|line| line.len() + 1)
291                .sum()
292        };
293
294        Cur { idx: y + x }
295    }
296
297    fn c(y: usize, x: usize) -> Dot {
298        Dot::Cur { c: cur(y, x) }
299    }
300
301    fn r(y: usize, x: usize, y2: usize, x2: usize) -> Dot {
302        Dot::Range {
303            r: Range {
304                start: cur(y, x),
305                end: cur(y2, x2),
306                start_active: false,
307            },
308        }
309    }
310
311    #[test_case(BufferStart, c(0, 0); "buffer start")]
312    #[test_case(BufferEnd, c(9, 36); "buffer end")]
313    #[test_case(Character, c(5, 2); "character")]
314    #[test_case(Line, r(5, 0, 5, 43); "line")]
315    #[test_case(LineEnd, c(5, 43); "line end")]
316    #[test_case(LineStart, c(5, 0); "line start")]
317    #[test]
318    fn set_dot_works(to: TextObject, expected: Dot) {
319        let mut b = Buffer::new_virtual(0, "test", EXAMPLE_TEXT, Default::default());
320        b.dot = c(5, 1); // Start of paragraph 2
321        to.set_dot(&mut b);
322
323        assert_eq!(b.dot, expected);
324    }
325
326    #[test_case(c(0, 0), "T"; "first character")]
327    #[test_case(r(0, 0, 0, 34), "This is the first line of the file."; "first sentence")]
328    #[test_case(
329        r(0, 0, 1, 18),
330        "This is the first line of the file. Followed\nby the second line.";
331        "spanning a newline"
332    )]
333    #[test]
334    fn dot_content_includes_expected_text(dot: Dot, expected: &str) {
335        let mut b = Buffer::new_virtual(0, "test", EXAMPLE_TEXT, Default::default());
336        b.dot = dot;
337        let content = b.dot_contents();
338
339        assert_eq!(content, expected);
340    }
341}