Skip to content

Commit a23e5e8

Browse files
helix-mode: introduce HelixRange selection type (no wiring) (#1061)
1 parent 7f99c6b commit a23e5e8

2 files changed

Lines changed: 335 additions & 0 deletions

File tree

src/edit_mode/helix/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ mod bindings;
33
mod event;
44
mod key;
55
mod mode;
6+
mod range;
67

78
use crate::{
89
edit_mode::EditMode,

src/edit_mode/helix/range.rs

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
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

Comments
 (0)