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::{LineRange, 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    pub(crate) fn line_range(&self, y: usize, b: &Buffer) -> Option<LineRange> {
213        match self {
214            Dot::Cur { .. } => None,
215            Dot::Range { r } => r.line_range(y, b),
216        }
217    }
218
219    /// If both ends of a Range match then replace with a single Cur
220    pub(crate) fn collapse_null_range(self) -> Self {
221        match self {
222            Dot::Range {
223                r: Range { start, end, .. },
224            } if start == end => Dot::Cur { c: start },
225            _ => self,
226        }
227    }
228
229    /// Clamp this dot to be valid for the given Buffer
230    pub(crate) fn clamp_idx(&mut self, max_idx: usize) {
231        match self {
232            Dot::Cur { c } => c.clamp_idx(max_idx),
233            Dot::Range { r } => {
234                r.start.clamp_idx(max_idx);
235                r.end.clamp_idx(max_idx);
236            }
237        }
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::{
244        text_object::TextObject::{self, *},
245        *,
246    };
247    use simple_test_case::test_case;
248
249    const EXAMPLE_TEXT: &str = "\
250This is the first line of the file. Followed
251by the second line. Some of the sentences are split
252over multiple lines.
253Others are not.
254
255There is a second paragraph as well. But it
256is quite short when compared to the first.
257
258
259The third paragraph is even shorter.";
260
261    fn cur(y: usize, x: usize) -> Cur {
262        let y = if y == 0 {
263            0
264        } else {
265            EXAMPLE_TEXT
266                .lines()
267                .take(y)
268                .map(|line| line.len() + 1)
269                .sum()
270        };
271
272        Cur { idx: y + x }
273    }
274
275    fn c(y: usize, x: usize) -> Dot {
276        Dot::Cur { c: cur(y, x) }
277    }
278
279    fn r(y: usize, x: usize, y2: usize, x2: usize) -> Dot {
280        Dot::Range {
281            r: Range {
282                start: cur(y, x),
283                end: cur(y2, x2),
284                start_active: false,
285            },
286        }
287    }
288
289    #[test_case(BufferStart, c(0, 0); "buffer start")]
290    #[test_case(BufferEnd, c(9, 36); "buffer end")]
291    #[test_case(Character, c(5, 2); "character")]
292    #[test_case(Line, r(5, 0, 5, 43); "line")]
293    #[test_case(LineEnd, c(5, 43); "line end")]
294    #[test_case(LineStart, c(5, 0); "line start")]
295    #[test]
296    fn set_dot_works(to: TextObject, expected: Dot) {
297        let mut b = Buffer::new_virtual(0, "test".to_string(), EXAMPLE_TEXT.to_string());
298        b.dot = c(5, 1); // Start of paragraph 2
299        to.set_dot(&mut b);
300
301        assert_eq!(b.dot, expected);
302    }
303
304    #[test_case(c(0, 0), "T"; "first character")]
305    #[test_case(r(0, 0, 0, 34), "This is the first line of the file."; "first sentence")]
306    #[test_case(
307        r(0, 0, 1, 18),
308        "This is the first line of the file. Followed\nby the second line.";
309        "spanning a newline"
310    )]
311    #[test]
312    fn dot_content_includes_expected_text(dot: Dot, expected: &str) {
313        let mut b = Buffer::new_virtual(0, "test".to_string(), EXAMPLE_TEXT.to_string());
314        b.dot = dot;
315        let content = b.dot_contents();
316
317        assert_eq!(content, expected);
318    }
319}