textpos/
range.rs

1use std::{
2    fmt::{Debug, Display},
3    str::FromStr
4};
5
6use crate::{InsertPosition, TextPosition};
7
8/// Defines a range in a text.
9///
10/// A range is defined by two [`InsertPosition`]s, `start` and `end`. All the
11/// text between the two positions is included in the range.
12#[derive(Default, Clone, PartialEq, Eq)]
13pub struct TextRange {
14    start: InsertPosition,
15    end: InsertPosition,
16}
17
18impl TextRange {
19    /// Create a new text range containing everything between `start` and `end`.
20    ///
21    /// They may point to the same location (in which case the range is empty),
22    /// but `end` may not be before `start`.
23    pub fn new(start: InsertPosition, end: InsertPosition) -> Self {
24        assert!(start <= end);
25        TextRange { start, end }
26    }
27    /// Create a new empty range at `pos`.
28    pub fn new_empty(pos: InsertPosition) -> Self {
29        Self::new(pos.clone(), pos)
30    }
31
32    /// Append a non-newline character to the range.
33    pub fn inc_col(&mut self) {
34        self.end.inc_col();
35    }
36    /// Append a newline character to the range.
37    pub fn inc_line(&mut self) {
38        self.end.inc_line();
39    }
40    /// Append one character to the range.
41    /// If the character `c` is a newline, it behaves like [`inc_line()`],
42    /// otherwhise it behaves like [`inc_col()`].
43    pub fn inc(&mut self, c: char) {
44        if c == '\n' {
45            self.inc_line();
46        } else {
47            self.inc_col();
48        }
49    }
50
51    /// Merge `other` into `self`.
52    ///
53    /// `self.end()` must line up (ie. be equal to) `other.start()`. This means
54    /// there may be no characters between the two ranges.
55    pub fn merge(&mut self, other: TextRange) {
56        assert!(self.end() == other.start());
57
58        self.end = other.end;
59    }
60
61    /// Create a new empty range starting at the end of `self`
62    pub fn begin_next_range(&self) -> Self {
63        TextRange::new_empty(self.end().clone())
64    }
65
66    /// Get the start insert position
67    pub fn start(&self) -> &InsertPosition { &self.start }
68    /// Get the end insert position
69    pub fn end(&self) -> &InsertPosition { &self.end }
70}
71
72impl Display for TextRange {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        if self.start() == self.end() {
75            return write!(f, "empty after {}", self.start());
76        }
77
78        let start = self.start().text_pos_right();
79        let end = self.end().text_pos_left();
80
81        if start == end {
82            write!(f, "{}", start)
83        } else {
84            write!(f, "{} - {}", start, end)
85        }
86    }
87}
88impl Debug for TextRange {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        write!(f, "{}", self)
91    }
92}
93
94impl FromStr for TextRange {
95    type Err = ();
96
97    fn from_str(s: &str) -> Result<Self, Self::Err> {
98        match s.split_once('-') {
99            None => {
100                // Only one position -> one textposition
101                let pos: TextPosition = s.parse()?;
102                Ok(TextRange::new(pos.insert_pos_left(), pos.insert_pos_right()))
103            }
104            Some((start, end)) => {
105                // Two positions -> start and end textpositions
106                let start = start.trim().parse::<TextPosition>()?.insert_pos_left();
107                let end = end.trim().parse::<TextPosition>()?.insert_pos_right();
108
109                // Validate
110                if end < start { return Err(()); }
111
112                Ok(TextRange::new(start, end))
113            }
114        }
115    }
116}
117
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    #[should_panic]
125    fn disallow_reverse_base_next() {
126        let mut pos1 = InsertPosition::default();
127        let pos2 = InsertPosition::default();
128
129        pos1.inc_col();
130
131        let _ = TextRange::new(pos1, pos2);
132    }
133
134
135    #[test]
136    fn allow_creaton_empty_range() {
137        let pos1 = InsertPosition::default();
138
139        let empty = TextRange::new_empty(pos1);
140        assert_eq!(empty, TextRange::default());
141    }
142
143    #[test]
144    fn merge() {
145        let mut ran1 = TextRange::default();
146        ran1.inc_col();
147
148        let mut pos1 = InsertPosition::default();
149        pos1.inc_col();
150        let mut pos2 = pos1.clone();
151        pos2.inc_col();
152        let ran2 = TextRange::new(pos1, pos2.clone());
153
154        let ran3 = TextRange::new_empty(pos2);
155
156        ran1.merge(ran2);
157        ran1.merge(ran3);
158    }
159
160    #[test]
161    #[should_panic]
162    fn merge_should_fail() {
163        let mut ran1 = TextRange::default();
164
165        let mut pos1 = InsertPosition::default();
166        pos1.inc_col();
167        pos1.inc_col();
168        let ran2 = TextRange::new_empty(pos1);
169
170        ran1.merge(ran2);
171    }
172
173    #[test]
174    fn begin_next_range() {
175        let mut ran1 = TextRange::default();
176        ran1.inc_line();
177        ran1.inc_col();
178        ran1.inc_col();
179        let ran2 = ran1.begin_next_range();
180
181        assert_eq!(ran2, TextRange::new_empty(InsertPosition::new(2, 2)));
182    }
183
184    #[test]
185    fn display_print() {
186        let mut ran1 = TextRange::default();
187        assert_eq!(ran1.to_string(), "empty after 1:0");
188
189        ran1.inc_line();
190        ran1.inc_line();
191        ran1.inc_col();
192        ran1.inc_col();
193        ran1.inc_col();
194        assert_eq!(ran1.to_string(), "1:1 - 3:3");
195
196        // Test zeroth-column char
197        ran1.inc_line();
198        assert_eq!(ran1.to_string(), "1:1 - 4:0");
199    }
200
201    #[test]
202    fn debug_print() {
203        let mut ran1 = TextRange::default();
204        assert_eq!(format!("{:?}", ran1), "empty after 1:0");
205
206        ran1.inc_line();
207        ran1.inc_line();
208        ran1.inc_col();
209        ran1.inc_col();
210        ran1.inc_col();
211        assert_eq!(format!("{:?}", ran1), "1:1 - 3:3");
212
213        // Test zeroth-column char
214        ran1.inc_line();
215        assert_eq!(format!("{:?}", ran1), "1:1 - 4:0");
216    }
217
218    #[test]
219    fn parsing() {
220        assert_eq!("4:7".parse(), Ok(TextRange::new(
221                    InsertPosition::new(4, 6),
222                    InsertPosition::new(4, 7))));
223        assert_eq!("1:1 - 4:3".parse(), Ok(TextRange::new(
224                    InsertPosition::new(1, 0),
225                    InsertPosition::new(4, 3))));
226        assert_eq!("3:7-3:7".parse(), Ok(TextRange::new(
227                    InsertPosition::new(3, 6),
228                    InsertPosition::new(3, 7))));
229        assert_eq!("7:3 - 2:1".parse::<TextRange>(), Err(()));
230        assert_eq!("7:3:2".parse::<TextRange>(), Err(()));
231    }
232}