1use 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#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
28pub enum Dot {
29 Cur {
31 c: Cur,
33 },
34 Range {
36 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 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 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 pub fn n_chars(&self) -> usize {
89 let (from, to) = self.as_char_indices();
90
91 to - from + 1
92 }
93
94 pub fn is_cur(&self) -> bool {
96 matches!(self, Dot::Cur { .. })
97 }
98
99 pub fn is_range(&self) -> bool {
101 matches!(self, Dot::Range { .. })
102 }
103
104 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 pub fn contains_range(&self, rng: &Range) -> bool {
114 self.contains(&rng.start) && self.contains(&rng.end)
115 }
116
117 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 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 #[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 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 #[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 #[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 #[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 #[inline]
192 pub fn collapse_to_first_cur(&self) -> Self {
193 Dot::Cur {
194 c: self.first_cur(),
195 }
196 }
197
198 #[inline]
200 pub fn collapse_to_last_cur(&self) -> Self {
201 Dot::Cur { c: self.last_cur() }
202 }
203
204 #[inline]
206 pub fn flip(&mut self) {
207 if let Dot::Range { r } = self {
208 r.flip();
209 }
210 }
211
212 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 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); 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}