From 77a266e818bf9d2eded39816b6a77de140234e4f Mon Sep 17 00:00:00 2001
From: Nathan Vegdahl <cessen@cessen.com>
Date: Mon, 28 Jun 2021 07:51:47 -0700
Subject: [PATCH] Better validation method APIs for `Range`.

This way they do less work, are more specific to what we actually
need, and they compose.
---
 helix-core/src/graphemes.rs |   9 ++
 helix-core/src/selection.rs | 245 ++++++++++++++++++++----------------
 2 files changed, 148 insertions(+), 106 deletions(-)

diff --git a/helix-core/src/graphemes.rs b/helix-core/src/graphemes.rs
index f7bf66c0..0465fe51 100644
--- a/helix-core/src/graphemes.rs
+++ b/helix-core/src/graphemes.rs
@@ -71,6 +71,8 @@ pub fn nth_prev_grapheme_boundary(slice: RopeSlice, char_idx: usize, n: usize) -
 }
 
 /// Finds the previous grapheme boundary before the given char position.
+#[must_use]
+#[inline(always)]
 pub fn prev_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> usize {
     nth_prev_grapheme_boundary(slice, char_idx, 1)
 }
@@ -117,12 +119,16 @@ pub fn nth_next_grapheme_boundary(slice: RopeSlice, char_idx: usize, n: usize) -
 }
 
 /// Finds the next grapheme boundary after the given char position.
+#[must_use]
+#[inline(always)]
 pub fn next_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> usize {
     nth_next_grapheme_boundary(slice, char_idx, 1)
 }
 
 /// Returns the passed char index if it's already a grapheme boundary,
 /// or the next grapheme boundary char index if not.
+#[must_use]
+#[inline]
 pub fn ensure_grapheme_boundary_next(slice: RopeSlice, char_idx: usize) -> usize {
     if char_idx == 0 {
         char_idx
@@ -133,6 +139,8 @@ pub fn ensure_grapheme_boundary_next(slice: RopeSlice, char_idx: usize) -> usize
 
 /// Returns the passed char index if it's already a grapheme boundary,
 /// or the prev grapheme boundary char index if not.
+#[must_use]
+#[inline]
 pub fn ensure_grapheme_boundary_prev(slice: RopeSlice, char_idx: usize) -> usize {
     if char_idx == slice.len_chars() {
         char_idx
@@ -142,6 +150,7 @@ pub fn ensure_grapheme_boundary_prev(slice: RopeSlice, char_idx: usize) -> usize
 }
 
 /// Returns whether the given char position is a grapheme boundary.
+#[must_use]
 pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool {
     // Bounds check
     debug_assert!(char_idx <= slice.len_chars());
diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs
index 906e2e53..4260c002 100644
--- a/helix-core/src/selection.rs
+++ b/helix-core/src/selection.rs
@@ -137,58 +137,61 @@ impl Range {
         }
     }
 
-    /// Compute the ends of the range, shifted (if needed) to align with
-    /// grapheme boundaries.
+    /// Compute a possibly new range from this range, attempting to ensure
+    /// a minimum range width of 1 char by shifting the head in the forward
+    /// direction as needed.
     ///
-    /// This should generally be used for cursor validation.
+    /// This method will never shift the anchor, and will only shift the
+    /// head in the forward direction.  Therefore, this method can fail
+    /// at ensuring the minimum width if and only if the passed range is
+    /// both zero-width and at the end of the `RopeSlice`.
     ///
-    /// Always succeeds.
+    /// If the input range is grapheme-boundary aligned, the returned range
+    /// will also be.  Specifically, if the head needs to shift to achieve
+    /// the minimum width, it will shift to the next grapheme boundary.
     #[must_use]
-    pub fn aligned_range(&self, slice: RopeSlice) -> (usize, usize) {
+    #[inline]
+    pub fn min_width_1(&self, slice: RopeSlice) -> Self {
         if self.anchor == self.head {
-            let pos = ensure_grapheme_boundary_prev(slice, self.anchor);
-            (pos, pos)
+            Range {
+                anchor: self.anchor,
+                head: next_grapheme_boundary(slice, self.head),
+                horiz: self.horiz,
+            }
         } else {
-            (
-                ensure_grapheme_boundary_prev(slice, self.from()),
-                ensure_grapheme_boundary_next(slice, self.to()),
-            )
+            *self
         }
     }
 
-    /// Same as `ensure_grapheme_validity()` + attempts to ensure a minimum
-    /// char width in the direction of the head.
+    /// Compute a possibly new range from this range, with its ends
+    /// shifted as needed to align with grapheme boundaries.
     ///
-    /// This should generally be used as a pre-pass for operations that
-    /// require a minimum selection width to achieve their intended behavior.
-    ///
-    /// This will fail at ensuring the minimum width only if the passed
-    /// `RopeSlice` is too short in the direction of the head, in which
-    /// case the range will fill the available length in that direction.
-    ///
-    /// Ensuring grapheme-boundary alignment always succeeds.
+    /// Zero-width ranges will always stay zero-width, and non-zero-width
+    /// ranges will never collapse to zero-width.
     #[must_use]
-    pub fn min_width_range(&self, slice: RopeSlice, min_char_width: usize) -> (usize, usize) {
-        if min_char_width == 0 {
-            return self.aligned_range(slice);
-        }
-
-        if self.anchor <= self.head {
-            let anchor = ensure_grapheme_boundary_prev(slice, self.anchor);
-            let head = ensure_grapheme_boundary_next(
-                slice,
-                self.head
-                    .max(anchor + min_char_width)
-                    .min(slice.len_chars()),
-            );
-            (anchor, head)
+    pub fn grapheme_aligned(&self, slice: RopeSlice) -> Self {
+        let (new_anchor, new_head) = if self.anchor == self.head {
+            let pos = ensure_grapheme_boundary_prev(slice, self.anchor);
+            (pos, pos)
+        } else if self.anchor < self.head {
+            (
+                ensure_grapheme_boundary_prev(slice, self.anchor),
+                ensure_grapheme_boundary_next(slice, self.head),
+            )
         } else {
-            let anchor = ensure_grapheme_boundary_next(slice, self.anchor);
-            let head = ensure_grapheme_boundary_prev(
-                slice,
-                self.head.min(anchor.saturating_sub(min_char_width)),
-            );
-            (head, anchor)
+            (
+                ensure_grapheme_boundary_next(slice, self.anchor),
+                ensure_grapheme_boundary_prev(slice, self.head),
+            )
+        };
+        Range {
+            anchor: new_anchor,
+            head: new_head,
+            horiz: if new_anchor == self.anchor {
+                self.horiz
+            } else {
+                None
+            },
         }
     }
 
@@ -571,97 +574,127 @@ mod test {
 
     #[test]
     fn test_overlaps() {
+        fn overlaps(a: (usize, usize), b: (usize, usize)) -> bool {
+            Range::new(a.0, a.1).overlaps(&Range::new(b.0, b.1))
+        }
+
         // Two non-zero-width ranges, no overlap.
-        assert!(!Range::new(0, 3).overlaps(&Range::new(3, 6)));
-        assert!(!Range::new(0, 3).overlaps(&Range::new(6, 3)));
-        assert!(!Range::new(3, 0).overlaps(&Range::new(3, 6)));
-        assert!(!Range::new(3, 0).overlaps(&Range::new(6, 3)));
-        assert!(!Range::new(3, 6).overlaps(&Range::new(0, 3)));
-        assert!(!Range::new(3, 6).overlaps(&Range::new(3, 0)));
-        assert!(!Range::new(6, 3).overlaps(&Range::new(0, 3)));
-        assert!(!Range::new(6, 3).overlaps(&Range::new(3, 0)));
+        assert!(!overlaps((0, 3), (3, 6)));
+        assert!(!overlaps((0, 3), (6, 3)));
+        assert!(!overlaps((3, 0), (3, 6)));
+        assert!(!overlaps((3, 0), (6, 3)));
+        assert!(!overlaps((3, 6), (0, 3)));
+        assert!(!overlaps((3, 6), (3, 0)));
+        assert!(!overlaps((6, 3), (0, 3)));
+        assert!(!overlaps((6, 3), (3, 0)));
 
         // Two non-zero-width ranges, overlap.
-        assert!(Range::new(0, 4).overlaps(&Range::new(3, 6)));
-        assert!(Range::new(0, 4).overlaps(&Range::new(6, 3)));
-        assert!(Range::new(4, 0).overlaps(&Range::new(3, 6)));
-        assert!(Range::new(4, 0).overlaps(&Range::new(6, 3)));
-        assert!(Range::new(3, 6).overlaps(&Range::new(0, 4)));
-        assert!(Range::new(3, 6).overlaps(&Range::new(4, 0)));
-        assert!(Range::new(6, 3).overlaps(&Range::new(0, 4)));
-        assert!(Range::new(6, 3).overlaps(&Range::new(4, 0)));
+        assert!(overlaps((0, 4), (3, 6)));
+        assert!(overlaps((0, 4), (6, 3)));
+        assert!(overlaps((4, 0), (3, 6)));
+        assert!(overlaps((4, 0), (6, 3)));
+        assert!(overlaps((3, 6), (0, 4)));
+        assert!(overlaps((3, 6), (4, 0)));
+        assert!(overlaps((6, 3), (0, 4)));
+        assert!(overlaps((6, 3), (4, 0)));
 
         // Zero-width and non-zero-width range, no overlap.
-        assert!(!Range::new(0, 3).overlaps(&Range::new(3, 3)));
-        assert!(!Range::new(3, 0).overlaps(&Range::new(3, 3)));
-        assert!(!Range::new(3, 3).overlaps(&Range::new(0, 3)));
-        assert!(!Range::new(3, 3).overlaps(&Range::new(3, 0)));
+        assert!(!overlaps((0, 3), (3, 3)));
+        assert!(!overlaps((3, 0), (3, 3)));
+        assert!(!overlaps((3, 3), (0, 3)));
+        assert!(!overlaps((3, 3), (3, 0)));
 
         // Zero-width and non-zero-width range, overlap.
-        assert!(Range::new(1, 4).overlaps(&Range::new(1, 1)));
-        assert!(Range::new(4, 1).overlaps(&Range::new(1, 1)));
-        assert!(Range::new(1, 1).overlaps(&Range::new(1, 4)));
-        assert!(Range::new(1, 1).overlaps(&Range::new(4, 1)));
+        assert!(overlaps((1, 4), (1, 1)));
+        assert!(overlaps((4, 1), (1, 1)));
+        assert!(overlaps((1, 1), (1, 4)));
+        assert!(overlaps((1, 1), (4, 1)));
 
-        assert!(Range::new(1, 4).overlaps(&Range::new(3, 3)));
-        assert!(Range::new(4, 1).overlaps(&Range::new(3, 3)));
-        assert!(Range::new(3, 3).overlaps(&Range::new(1, 4)));
-        assert!(Range::new(3, 3).overlaps(&Range::new(4, 1)));
+        assert!(overlaps((1, 4), (3, 3)));
+        assert!(overlaps((4, 1), (3, 3)));
+        assert!(overlaps((3, 3), (1, 4)));
+        assert!(overlaps((3, 3), (4, 1)));
 
         // Two zero-width ranges, no overlap.
-        assert!(!Range::new(0, 0).overlaps(&Range::new(1, 1)));
-        assert!(!Range::new(1, 1).overlaps(&Range::new(0, 0)));
+        assert!(!overlaps((0, 0), (1, 1)));
+        assert!(!overlaps((1, 1), (0, 0)));
 
         // Two zero-width ranges, overlap.
-        assert!(Range::new(1, 1).overlaps(&Range::new(1, 1)));
+        assert!(overlaps((1, 1), (1, 1)));
     }
 
     #[test]
-    fn test_aligned_range() {
+    fn test_graphem_aligned() {
         let r = Rope::from_str("\r\nHi\r\n");
         let s = r.slice(..);
 
-        assert_eq!(Range::new(0, 0).aligned_range(s), (0, 0));
-        assert_eq!(Range::new(0, 1).aligned_range(s), (0, 2));
-        assert_eq!(Range::new(1, 1).aligned_range(s), (0, 0));
-        assert_eq!(Range::new(1, 2).aligned_range(s), (0, 2));
-        assert_eq!(Range::new(2, 2).aligned_range(s), (2, 2));
-        assert_eq!(Range::new(2, 3).aligned_range(s), (2, 3));
-        assert_eq!(Range::new(1, 3).aligned_range(s), (0, 3));
-        assert_eq!(Range::new(3, 5).aligned_range(s), (3, 6));
-        assert_eq!(Range::new(4, 5).aligned_range(s), (4, 6));
-        assert_eq!(Range::new(5, 5).aligned_range(s), (4, 4));
-        assert_eq!(Range::new(6, 6).aligned_range(s), (6, 6));
+        // Zero-width.
+        assert_eq!(Range::new(0, 0).grapheme_aligned(s), Range::new(0, 0));
+        assert_eq!(Range::new(1, 1).grapheme_aligned(s), Range::new(0, 0));
+        assert_eq!(Range::new(2, 2).grapheme_aligned(s), Range::new(2, 2));
+        assert_eq!(Range::new(3, 3).grapheme_aligned(s), Range::new(3, 3));
+        assert_eq!(Range::new(4, 4).grapheme_aligned(s), Range::new(4, 4));
+        assert_eq!(Range::new(5, 5).grapheme_aligned(s), Range::new(4, 4));
+        assert_eq!(Range::new(6, 6).grapheme_aligned(s), Range::new(6, 6));
+
+        // Forward.
+        assert_eq!(Range::new(0, 1).grapheme_aligned(s), Range::new(0, 2));
+        assert_eq!(Range::new(1, 2).grapheme_aligned(s), Range::new(0, 2));
+        assert_eq!(Range::new(2, 3).grapheme_aligned(s), Range::new(2, 3));
+        assert_eq!(Range::new(3, 4).grapheme_aligned(s), Range::new(3, 4));
+        assert_eq!(Range::new(4, 5).grapheme_aligned(s), Range::new(4, 6));
+        assert_eq!(Range::new(5, 6).grapheme_aligned(s), Range::new(4, 6));
+
+        assert_eq!(Range::new(0, 2).grapheme_aligned(s), Range::new(0, 2));
+        assert_eq!(Range::new(1, 3).grapheme_aligned(s), Range::new(0, 3));
+        assert_eq!(Range::new(2, 4).grapheme_aligned(s), Range::new(2, 4));
+        assert_eq!(Range::new(3, 5).grapheme_aligned(s), Range::new(3, 6));
+        assert_eq!(Range::new(4, 6).grapheme_aligned(s), Range::new(4, 6));
+
+        // Reverse.
+        assert_eq!(Range::new(1, 0).grapheme_aligned(s), Range::new(2, 0));
+        assert_eq!(Range::new(2, 1).grapheme_aligned(s), Range::new(2, 0));
+        assert_eq!(Range::new(3, 2).grapheme_aligned(s), Range::new(3, 2));
+        assert_eq!(Range::new(4, 3).grapheme_aligned(s), Range::new(4, 3));
+        assert_eq!(Range::new(5, 4).grapheme_aligned(s), Range::new(6, 4));
+        assert_eq!(Range::new(6, 5).grapheme_aligned(s), Range::new(6, 4));
+
+        assert_eq!(Range::new(2, 0).grapheme_aligned(s), Range::new(2, 0));
+        assert_eq!(Range::new(3, 1).grapheme_aligned(s), Range::new(3, 0));
+        assert_eq!(Range::new(4, 2).grapheme_aligned(s), Range::new(4, 2));
+        assert_eq!(Range::new(5, 3).grapheme_aligned(s), Range::new(6, 3));
+        assert_eq!(Range::new(6, 4).grapheme_aligned(s), Range::new(6, 4));
     }
 
     #[test]
-    fn test_min_width_range() {
+    fn test_min_width_1() {
         let r = Rope::from_str("\r\nHi\r\n");
         let s = r.slice(..);
 
-        assert_eq!(Range::new(0, 0).min_width_range(s, 1), (0, 2));
-        assert_eq!(Range::new(0, 1).min_width_range(s, 1), (0, 2));
-        assert_eq!(Range::new(1, 1).min_width_range(s, 1), (0, 2));
-        assert_eq!(Range::new(1, 2).min_width_range(s, 1), (0, 2));
-        assert_eq!(Range::new(2, 2).min_width_range(s, 1), (2, 3));
-        assert_eq!(Range::new(2, 3).min_width_range(s, 1), (2, 3));
-        assert_eq!(Range::new(1, 3).min_width_range(s, 1), (0, 3));
-        assert_eq!(Range::new(3, 5).min_width_range(s, 1), (3, 6));
-        assert_eq!(Range::new(4, 5).min_width_range(s, 1), (4, 6));
-        assert_eq!(Range::new(5, 5).min_width_range(s, 1), (4, 6));
-        assert_eq!(Range::new(6, 6).min_width_range(s, 1), (6, 6));
+        // Zero-width.
+        assert_eq!(Range::new(0, 0).min_width_1(s), Range::new(0, 2));
+        assert_eq!(Range::new(1, 1).min_width_1(s), Range::new(1, 2));
+        assert_eq!(Range::new(2, 2).min_width_1(s), Range::new(2, 3));
+        assert_eq!(Range::new(3, 3).min_width_1(s), Range::new(3, 4));
+        assert_eq!(Range::new(4, 4).min_width_1(s), Range::new(4, 6));
+        assert_eq!(Range::new(5, 5).min_width_1(s), Range::new(5, 6));
+        assert_eq!(Range::new(6, 6).min_width_1(s), Range::new(6, 6));
 
-        assert_eq!(Range::new(1, 0).min_width_range(s, 1), (0, 2));
-        assert_eq!(Range::new(2, 1).min_width_range(s, 1), (0, 2));
-        assert_eq!(Range::new(3, 2).min_width_range(s, 1), (2, 3));
-        assert_eq!(Range::new(3, 1).min_width_range(s, 1), (0, 3));
-        assert_eq!(Range::new(5, 3).min_width_range(s, 1), (3, 6));
-        assert_eq!(Range::new(5, 4).min_width_range(s, 1), (4, 6));
+        // Forward.
+        assert_eq!(Range::new(0, 1).min_width_1(s), Range::new(0, 1));
+        assert_eq!(Range::new(1, 2).min_width_1(s), Range::new(1, 2));
+        assert_eq!(Range::new(2, 3).min_width_1(s), Range::new(2, 3));
+        assert_eq!(Range::new(3, 4).min_width_1(s), Range::new(3, 4));
+        assert_eq!(Range::new(4, 5).min_width_1(s), Range::new(4, 5));
+        assert_eq!(Range::new(5, 6).min_width_1(s), Range::new(5, 6));
 
-        assert_eq!(Range::new(3, 4).min_width_range(s, 3), (3, 6));
-        assert_eq!(Range::new(4, 3).min_width_range(s, 3), (0, 4));
-        assert_eq!(Range::new(3, 4).min_width_range(s, 20), (3, 6));
-        assert_eq!(Range::new(4, 3).min_width_range(s, 20), (0, 4));
+        // Reverse.
+        assert_eq!(Range::new(1, 0).min_width_1(s), Range::new(1, 0));
+        assert_eq!(Range::new(2, 1).min_width_1(s), Range::new(2, 1));
+        assert_eq!(Range::new(3, 2).min_width_1(s), Range::new(3, 2));
+        assert_eq!(Range::new(4, 3).min_width_1(s), Range::new(4, 3));
+        assert_eq!(Range::new(5, 4).min_width_1(s), Range::new(5, 4));
+        assert_eq!(Range::new(6, 5).min_width_1(s), Range::new(6, 5));
     }
 
     #[test]