|
| 1 | +#![allow(dead_code)] |
| 2 | + |
| 3 | +/// The direction a range extends in. |
| 4 | +/// |
| 5 | +/// `Forward` when `head >= anchor`, `Backward` when `head < anchor`. |
| 6 | +#[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 7 | +pub(super) enum Direction { |
| 8 | + Forward, |
| 9 | + Backward, |
| 10 | +} |
| 11 | + |
| 12 | +/// A selection range. |
| 13 | +/// |
| 14 | +/// Uses gap indexing — `anchor` and `head` represent positions *between* bytes, |
| 15 | +/// not bytes themselves. Ranges are inclusive on the left and exclusive on the |
| 16 | +/// right, regardless of anchor/head ordering. |
| 17 | +#[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 18 | +pub(super) struct HelixRange { |
| 19 | + /// The anchor of the range: the side that doesn't move when extending. |
| 20 | + anchor: usize, |
| 21 | + /// The head of the range, moved when extending. |
| 22 | + head: usize, |
| 23 | +} |
| 24 | + |
| 25 | +impl HelixRange { |
| 26 | + pub(super) fn new(anchor: usize, head: usize) -> Self { |
| 27 | + Self { anchor, head } |
| 28 | + } |
| 29 | + |
| 30 | + /// A zero-width range at `head`. |
| 31 | + pub(super) fn point(head: usize) -> Self { |
| 32 | + Self::new(head, head) |
| 33 | + } |
| 34 | + |
| 35 | + /// Start of the range |
| 36 | + pub(super) fn start(&self) -> usize { |
| 37 | + self.anchor.min(self.head) |
| 38 | + } |
| 39 | + |
| 40 | + /// End of the range |
| 41 | + pub(super) fn end(&self) -> usize { |
| 42 | + self.anchor.max(self.head) |
| 43 | + } |
| 44 | + |
| 45 | + /// Total length of the range. |
| 46 | + pub(super) fn len(&self) -> usize { |
| 47 | + self.end() - self.start() |
| 48 | + } |
| 49 | + |
| 50 | + /// `true` when anchor and head are at the same position. |
| 51 | + pub(super) fn is_empty(&self) -> bool { |
| 52 | + self.anchor == self.head |
| 53 | + } |
| 54 | + |
| 55 | + /// `Forward` when `head >= anchor`, `Backward` otherwise. |
| 56 | + pub(super) fn direction(&self) -> Direction { |
| 57 | + if self.head < self.anchor { |
| 58 | + Direction::Backward |
| 59 | + } else { |
| 60 | + Direction::Forward |
| 61 | + } |
| 62 | + } |
| 63 | + |
| 64 | + /// Swap anchor and head. |
| 65 | + pub(super) fn flip(self) -> Self { |
| 66 | + Self { |
| 67 | + anchor: self.head, |
| 68 | + head: self.anchor, |
| 69 | + } |
| 70 | + } |
| 71 | + |
| 72 | + /// Return the range if it already points in `direction`, otherwise flip it. |
| 73 | + pub(super) fn with_direction(self, direction: Direction) -> Self { |
| 74 | + if self.direction() == direction { |
| 75 | + self |
| 76 | + } else { |
| 77 | + self.flip() |
| 78 | + } |
| 79 | + } |
| 80 | + |
| 81 | + /// Grow the range to cover at least `[from, to]`, preserving anchor/head |
| 82 | + /// ordering. |
| 83 | + /// |
| 84 | + /// If the range is currently `Forward`, the anchor can only move left and |
| 85 | + /// the head can only move right. If `Backward`, the roles are inverted. |
| 86 | + pub(super) fn extend(self, from: usize, to: usize) -> Self { |
| 87 | + debug_assert!(from <= to); |
| 88 | + if self.anchor <= self.head { |
| 89 | + Self { |
| 90 | + anchor: self.anchor.min(from), |
| 91 | + head: self.head.max(to), |
| 92 | + } |
| 93 | + } else { |
| 94 | + Self { |
| 95 | + anchor: self.anchor.max(to), |
| 96 | + head: self.head.min(from), |
| 97 | + } |
| 98 | + } |
| 99 | + } |
| 100 | + |
| 101 | + /// `true` if `pos` lies inside the range (left-inclusive, right-exclusive). |
| 102 | + pub(super) fn contains(&self, pos: usize) -> bool { |
| 103 | + self.start() <= pos && pos < self.end() |
| 104 | + } |
| 105 | +} |
| 106 | + |
| 107 | +#[cfg(test)] |
| 108 | +mod tests { |
| 109 | + use super::*; |
| 110 | + |
| 111 | + #[test] |
| 112 | + fn contains() { |
| 113 | + let range = HelixRange::new(10, 12); |
| 114 | + |
| 115 | + assert!(!range.contains(9)); |
| 116 | + assert!(range.contains(10)); |
| 117 | + assert!(range.contains(11)); |
| 118 | + assert!(!range.contains(12)); |
| 119 | + assert!(!range.contains(13)); |
| 120 | + |
| 121 | + let range = HelixRange::new(9, 6); |
| 122 | + assert!(!range.contains(9)); |
| 123 | + assert!(range.contains(7)); |
| 124 | + assert!(range.contains(6)); |
| 125 | + } |
| 126 | + |
| 127 | + #[test] |
| 128 | + fn point_constructs_empty_range_at_head() { |
| 129 | + let range = HelixRange::point(5); |
| 130 | + assert_eq!(range.start(), 5); |
| 131 | + assert_eq!(range.end(), 5); |
| 132 | + assert!(range.is_empty()); |
| 133 | + } |
| 134 | + |
| 135 | + #[test] |
| 136 | + fn new_preserves_anchor_and_head_order() { |
| 137 | + let forward = HelixRange::new(2, 5); |
| 138 | + assert_eq!(forward.direction(), Direction::Forward); |
| 139 | + |
| 140 | + let backward = HelixRange::new(5, 2); |
| 141 | + assert_eq!(backward.direction(), Direction::Backward); |
| 142 | + } |
| 143 | + |
| 144 | + #[test] |
| 145 | + fn start_returns_lower_of_anchor_and_head() { |
| 146 | + assert_eq!(HelixRange::new(2, 5).start(), 2); |
| 147 | + assert_eq!(HelixRange::new(5, 2).start(), 2); |
| 148 | + } |
| 149 | + |
| 150 | + #[test] |
| 151 | + fn end_returns_higher_of_anchor_and_head() { |
| 152 | + assert_eq!(HelixRange::new(2, 5).end(), 5); |
| 153 | + assert_eq!(HelixRange::new(5, 2).end(), 5); |
| 154 | + } |
| 155 | + |
| 156 | + #[test] |
| 157 | + fn start_and_end_agree_for_empty_range() { |
| 158 | + let range = HelixRange::point(7); |
| 159 | + assert_eq!(range.start(), range.end()); |
| 160 | + } |
| 161 | + |
| 162 | + #[test] |
| 163 | + fn len_is_zero_for_empty_range() { |
| 164 | + assert_eq!(HelixRange::point(7).len(), 0); |
| 165 | + } |
| 166 | + |
| 167 | + #[test] |
| 168 | + fn len_ignores_direction() { |
| 169 | + assert_eq!(HelixRange::new(2, 5).len(), 3); |
| 170 | + assert_eq!(HelixRange::new(5, 2).len(), 3); |
| 171 | + } |
| 172 | + |
| 173 | + #[test] |
| 174 | + fn is_empty_true_when_anchor_equals_head() { |
| 175 | + assert!(HelixRange::new(5, 5).is_empty()); |
| 176 | + assert!(HelixRange::point(0).is_empty()); |
| 177 | + } |
| 178 | + |
| 179 | + #[test] |
| 180 | + fn is_empty_false_for_nonzero_width() { |
| 181 | + assert!(!HelixRange::new(2, 5).is_empty()); |
| 182 | + assert!(!HelixRange::new(5, 2).is_empty()); |
| 183 | + } |
| 184 | + |
| 185 | + #[test] |
| 186 | + fn direction_forward_when_head_greater_than_anchor() { |
| 187 | + assert_eq!(HelixRange::new(2, 5).direction(), Direction::Forward); |
| 188 | + } |
| 189 | + |
| 190 | + #[test] |
| 191 | + fn direction_backward_when_head_less_than_anchor() { |
| 192 | + assert_eq!(HelixRange::new(5, 2).direction(), Direction::Backward); |
| 193 | + } |
| 194 | + |
| 195 | + #[test] |
| 196 | + fn direction_forward_for_empty_range() { |
| 197 | + assert_eq!(HelixRange::point(5).direction(), Direction::Forward); |
| 198 | + } |
| 199 | + |
| 200 | + #[test] |
| 201 | + fn flip_swaps_anchor_and_head() { |
| 202 | + let flipped = HelixRange::new(2, 5).flip(); |
| 203 | + assert_eq!(flipped, HelixRange::new(5, 2)); |
| 204 | + } |
| 205 | + |
| 206 | + #[test] |
| 207 | + fn flip_twice_returns_original() { |
| 208 | + let range = HelixRange::new(2, 5); |
| 209 | + assert_eq!(range.flip().flip(), range); |
| 210 | + } |
| 211 | + |
| 212 | + #[test] |
| 213 | + fn flip_of_empty_range_is_unchanged() { |
| 214 | + let range = HelixRange::point(5); |
| 215 | + assert_eq!(range.flip(), range); |
| 216 | + } |
| 217 | + |
| 218 | + #[test] |
| 219 | + fn with_direction_noop_when_already_forward() { |
| 220 | + let range = HelixRange::new(2, 5); |
| 221 | + assert_eq!(range.with_direction(Direction::Forward), range); |
| 222 | + } |
| 223 | + |
| 224 | + #[test] |
| 225 | + fn with_direction_noop_when_already_backward() { |
| 226 | + let range = HelixRange::new(5, 2); |
| 227 | + assert_eq!(range.with_direction(Direction::Backward), range); |
| 228 | + } |
| 229 | + |
| 230 | + #[test] |
| 231 | + fn with_direction_flips_forward_to_backward() { |
| 232 | + let range = HelixRange::new(2, 5); |
| 233 | + assert_eq!( |
| 234 | + range.with_direction(Direction::Backward), |
| 235 | + HelixRange::new(5, 2) |
| 236 | + ); |
| 237 | + } |
| 238 | + |
| 239 | + #[test] |
| 240 | + fn with_direction_flips_backward_to_forward() { |
| 241 | + let range = HelixRange::new(5, 2); |
| 242 | + assert_eq!( |
| 243 | + range.with_direction(Direction::Forward), |
| 244 | + HelixRange::new(2, 5) |
| 245 | + ); |
| 246 | + } |
| 247 | + |
| 248 | + #[test] |
| 249 | + fn with_direction_on_empty_range_stays_forward() { |
| 250 | + let range = HelixRange::point(5); |
| 251 | + assert_eq!(range.with_direction(Direction::Forward), range); |
| 252 | + // Empty range is already Forward, so asking for Backward flips it — |
| 253 | + // which is still the same point, since anchor == head. |
| 254 | + assert_eq!(range.with_direction(Direction::Backward), range); |
| 255 | + } |
| 256 | + |
| 257 | + #[test] |
| 258 | + fn extend_forward_shrinks_anchor_left() { |
| 259 | + let range = HelixRange::new(5, 8); |
| 260 | + assert_eq!(range.extend(2, 3), HelixRange::new(2, 8)); |
| 261 | + } |
| 262 | + |
| 263 | + #[test] |
| 264 | + fn extend_forward_grows_head_right() { |
| 265 | + let range = HelixRange::new(2, 5); |
| 266 | + assert_eq!(range.extend(6, 8), HelixRange::new(2, 8)); |
| 267 | + } |
| 268 | + |
| 269 | + #[test] |
| 270 | + fn extend_forward_grows_both_sides() { |
| 271 | + let range = HelixRange::new(4, 6); |
| 272 | + assert_eq!(range.extend(2, 8), HelixRange::new(2, 8)); |
| 273 | + } |
| 274 | + |
| 275 | + #[test] |
| 276 | + fn extend_forward_noop_when_range_already_covers() { |
| 277 | + let range = HelixRange::new(1, 9); |
| 278 | + assert_eq!(range.extend(3, 5), range); |
| 279 | + } |
| 280 | + |
| 281 | + #[test] |
| 282 | + fn extend_backward_preserves_direction() { |
| 283 | + let range = HelixRange::new(8, 2); |
| 284 | + let result = range.extend(4, 6); |
| 285 | + assert_eq!(result.direction(), Direction::Backward); |
| 286 | + } |
| 287 | + |
| 288 | + #[test] |
| 289 | + fn extend_backward_grows_head_left() { |
| 290 | + let range = HelixRange::new(8, 5); |
| 291 | + assert_eq!(range.extend(2, 3), HelixRange::new(8, 2)); |
| 292 | + } |
| 293 | + |
| 294 | + #[test] |
| 295 | + fn extend_backward_grows_anchor_right() { |
| 296 | + let range = HelixRange::new(5, 2); |
| 297 | + assert_eq!(range.extend(6, 8), HelixRange::new(8, 2)); |
| 298 | + } |
| 299 | + |
| 300 | + #[test] |
| 301 | + fn extend_from_empty_range_stays_forward() { |
| 302 | + let range = HelixRange::point(5); |
| 303 | + let result = range.extend(3, 7); |
| 304 | + assert_eq!(result.direction(), Direction::Forward); |
| 305 | + assert_eq!(result, HelixRange::new(3, 7)); |
| 306 | + } |
| 307 | + |
| 308 | + #[test] |
| 309 | + fn extend_with_zero_width_target_is_safe() { |
| 310 | + let range = HelixRange::new(2, 5); |
| 311 | + assert_eq!(range.extend(3, 3), range); |
| 312 | + } |
| 313 | + |
| 314 | + #[test] |
| 315 | + fn contains_false_for_empty_range() { |
| 316 | + let range = HelixRange::point(5); |
| 317 | + assert!(!range.contains(5)); |
| 318 | + assert!(!range.contains(4)); |
| 319 | + assert!(!range.contains(6)); |
| 320 | + } |
| 321 | + |
| 322 | + #[test] |
| 323 | + fn contains_is_direction_agnostic() { |
| 324 | + let forward = HelixRange::new(2, 5); |
| 325 | + let backward = HelixRange::new(5, 2); |
| 326 | + for pos in 0..=6 { |
| 327 | + assert_eq!( |
| 328 | + forward.contains(pos), |
| 329 | + backward.contains(pos), |
| 330 | + "mismatch at {pos}" |
| 331 | + ); |
| 332 | + } |
| 333 | + } |
| 334 | +} |
0 commit comments