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(crate) use cur::Cur;
19pub(crate) use range::Range;
20pub(crate) 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    /// The address representation of this dot in the form that is enterable by the user.
118    /// Indices are 1-based rather than their internal 0-based representation.
119    pub fn addr(&self, b: &Buffer) -> String {
120        match self {
121            Self::Cur { c } => c.as_string_addr(b),
122            Self::Range { r } => r.as_string_addr(b),
123        }
124    }
125
126    /// Use this [Dot] to index in to a [Buffer] and extract the range of text it denotes.
127    pub fn content(&self, b: &Buffer) -> String {
128        let len_chars = b.txt.len_chars();
129
130        if len_chars == 0 {
131            return String::new();
132        }
133
134        let (from, to) = self.as_char_indices();
135        b.txt.slice(from, min(to + 1, len_chars)).to_string()
136    }
137
138    /// The active cursor position for this [Dot] which will be manipulated by movement operations
139    #[inline]
140    pub fn active_cur(&self) -> Cur {
141        match self {
142            Self::Cur { c } => *c,
143            Self::Range { r } => r.active_cursor(),
144        }
145    }
146
147    /// Set the active cursor position for this [Dot] directly, replacing the current active
148    /// cursor.
149    pub fn set_active_cur(&mut self, cur: Cur) {
150        match self {
151            Self::Cur { c } => *c = cur,
152            Self::Range { r } => r.set_active_cursor(cur),
153        }
154    }
155
156    /// The cursor position closest to the start of the Buffer.
157    #[inline]
158    pub fn first_cur(&self) -> Cur {
159        match self {
160            Self::Cur { c } => *c,
161            Self::Range { r } => r.start,
162        }
163    }
164
165    /// The cursor position closest to the end of the Buffer.
166    #[inline]
167    pub fn last_cur(&self) -> Cur {
168        match self {
169            Self::Cur { c } => *c,
170            Self::Range { r } => r.end,
171        }
172    }
173
174    /// This [Dot] expressed as a [Range].
175    ///
176    /// For range dots the underlying range is returned directly. For cursor dots a new range is
177    /// constructed where `start` and `end` are equal and the `end` is the active cursor.
178    #[inline]
179    pub fn as_range(&self) -> Range {
180        match self {
181            Self::Cur { c } => Range {
182                start: *c,
183                end: *c,
184                start_active: false,
185            },
186            Self::Range { r } => *r,
187        }
188    }
189
190    /// The [Dot] equivalent of [Dot::first_cur].
191    #[inline]
192    pub fn collapse_to_first_cur(&self) -> Self {
193        Dot::Cur {
194            c: self.first_cur(),
195        }
196    }
197
198    /// The [Dot] equivalent of [Dot::last_cur].
199    #[inline]
200    pub fn collapse_to_last_cur(&self) -> Self {
201        Dot::Cur { c: self.last_cur() }
202    }
203
204    /// Swap the active cursor between `start` and `end` of [Range] dots.
205    #[inline]
206    pub fn flip(&mut self) {
207        if let Dot::Range { r } = self {
208            r.flip();
209        }
210    }
211
212    /// If both ends of a Range match then replace with a single Cur
213    pub(crate) fn collapse_null_range(self) -> Self {
214        match self {
215            Dot::Range {
216                r: Range { start, end, .. },
217            } if start == end => Dot::Cur { c: start },
218            _ => self,
219        }
220    }
221
222    /// Clamp this dot to be valid for the given Buffer
223    pub(crate) fn clamp_idx(&mut self, max_idx: usize) {
224        match self {
225            Dot::Cur { c } => c.clamp_idx(max_idx),
226            Dot::Range { r } => {
227                r.start.clamp_idx(max_idx);
228                r.end.clamp_idx(max_idx);
229            }
230        }
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::{
237        text_object::TextObject::{self, *},
238        *,
239    };
240    use simple_test_case::test_case;
241
242    const EXAMPLE_TEXT: &str = "\
243This is the first line of the file. Followed
244by the second line. Some of the sentences are split
245over multiple lines.
246Others are not.
247
248There is a second paragraph as well. But it
249is quite short when compared to the first.
250
251
252The third paragraph is even shorter.";
253
254    fn cur(y: usize, x: usize) -> Cur {
255        let y = if y == 0 {
256            0
257        } else {
258            EXAMPLE_TEXT
259                .lines()
260                .take(y)
261                .map(|line| line.len() + 1)
262                .sum()
263        };
264
265        Cur { idx: y + x }
266    }
267
268    fn c(y: usize, x: usize) -> Dot {
269        Dot::Cur { c: cur(y, x) }
270    }
271
272    fn r(y: usize, x: usize, y2: usize, x2: usize) -> Dot {
273        Dot::Range {
274            r: Range {
275                start: cur(y, x),
276                end: cur(y2, x2),
277                start_active: false,
278            },
279        }
280    }
281
282    #[test_case(BufferStart, c(0, 0); "buffer start")]
283    #[test_case(BufferEnd, c(9, 36); "buffer end")]
284    #[test_case(Character, c(5, 2); "character")]
285    #[test_case(Line, r(5, 0, 5, 43); "line")]
286    #[test_case(LineEnd, c(5, 43); "line end")]
287    #[test_case(LineStart, c(5, 0); "line start")]
288    #[test]
289    fn set_dot_works(to: TextObject, expected: Dot) {
290        let mut b = Buffer::new_virtual(0, "test".to_string(), EXAMPLE_TEXT.to_string());
291        b.dot = c(5, 1); // Start of paragraph 2
292        to.set_dot(&mut b);
293
294        assert_eq!(b.dot, expected);
295    }
296
297    #[test_case(c(0, 0), "T"; "first character")]
298    #[test_case(r(0, 0, 0, 34), "This is the first line of the file."; "first sentence")]
299    #[test_case(
300        r(0, 0, 1, 18),
301        "This is the first line of the file. Followed\nby the second line.";
302        "spanning a newline"
303    )]
304    #[test]
305    fn dot_content_includes_expected_text(dot: Dot, expected: &str) {
306        let mut b = Buffer::new_virtual(0, "test".to_string(), EXAMPLE_TEXT.to_string());
307        b.dot = dot;
308        let content = b.dot_contents();
309
310        assert_eq!(content, expected);
311    }
312}