From d07074740bc44b71de83cf23dd692fa90c2854a9 Mon Sep 17 00:00:00 2001 From: Nathan Vegdahl Date: Sat, 26 Jun 2021 15:37:32 -0700 Subject: [PATCH] Add `Range` methods for various kinds of validation. --- helix-core/src/graphemes.rs | 14 ++++- helix-core/src/selection.rs | 113 +++++++++++++++++++++++++++++++++++- helix-term/src/ui/editor.rs | 6 +- 3 files changed, 127 insertions(+), 6 deletions(-) diff --git a/helix-core/src/graphemes.rs b/helix-core/src/graphemes.rs index f71b6d5f..f7bf66c0 100644 --- a/helix-core/src/graphemes.rs +++ b/helix-core/src/graphemes.rs @@ -123,14 +123,24 @@ pub fn next_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> usize { /// Returns the passed char index if it's already a grapheme boundary, /// or the next grapheme boundary char index if not. -pub fn ensure_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> usize { +pub fn ensure_grapheme_boundary_next(slice: RopeSlice, char_idx: usize) -> usize { if char_idx == 0 { - 0 + char_idx } else { next_grapheme_boundary(slice, char_idx - 1) } } +/// Returns the passed char index if it's already a grapheme boundary, +/// or the prev grapheme boundary char index if not. +pub fn ensure_grapheme_boundary_prev(slice: RopeSlice, char_idx: usize) -> usize { + if char_idx == slice.len_chars() { + char_idx + } else { + prev_grapheme_boundary(slice, char_idx + 1) + } +} + /// Returns whether the given char position is a grapheme boundary. pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool { // Bounds check diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 35ad9845..906e2e53 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -2,7 +2,12 @@ //! defined as a single empty or 1-wide selection range. //! //! All positioning is done via `char` offsets into the buffer. -use crate::{Assoc, ChangeSet, Rope, RopeSlice}; +use crate::{ + graphemes::{ + ensure_grapheme_boundary_next, ensure_grapheme_boundary_prev, next_grapheme_boundary, + }, + Assoc, ChangeSet, Rope, RopeSlice, +}; use smallvec::{smallvec, SmallVec}; use std::borrow::Cow; @@ -132,6 +137,61 @@ impl Range { } } + /// Compute the ends of the range, shifted (if needed) to align with + /// grapheme boundaries. + /// + /// This should generally be used for cursor validation. + /// + /// Always succeeds. + #[must_use] + pub fn aligned_range(&self, slice: RopeSlice) -> (usize, usize) { + if self.anchor == self.head { + let pos = ensure_grapheme_boundary_prev(slice, self.anchor); + (pos, pos) + } else { + ( + ensure_grapheme_boundary_prev(slice, self.from()), + ensure_grapheme_boundary_next(slice, self.to()), + ) + } + } + + /// Same as `ensure_grapheme_validity()` + attempts to ensure a minimum + /// char width in the direction of the head. + /// + /// 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. + #[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) + } 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) + } + } + // groupAt #[inline] @@ -556,6 +616,54 @@ mod test { assert!(Range::new(1, 1).overlaps(&Range::new(1, 1))); } + #[test] + fn test_aligned_range() { + 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)); + } + + #[test] + fn test_min_width_range() { + 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)); + + 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)); + + 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)); + } + #[test] fn test_split_on_matches() { use crate::regex::Regex; @@ -569,6 +677,9 @@ mod test { assert_eq!( result.ranges(), &[ + // TODO: rather than this behavior, maybe we want it + // to be based on which side is the anchor? + // // We get a leading zero-width range when there's // a leading match because ranges are inclusive on // the left. Imagine, for example, if the entire diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index b55a830e..d2925e35 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -8,7 +8,7 @@ use crate::{ use helix_core::{ coords_at_pos, - graphemes::ensure_grapheme_boundary, + graphemes::ensure_grapheme_boundary_next, syntax::{self, Highlight, HighlightEvent}, LineEnding, Position, Range, }; @@ -144,8 +144,8 @@ impl EditorView { let highlights = highlights.into_iter().map(|event| match event.unwrap() { // convert byte offsets to char offset HighlightEvent::Source { start, end } => { - let start = ensure_grapheme_boundary(text, text.byte_to_char(start)); - let end = ensure_grapheme_boundary(text, text.byte_to_char(end)); + let start = ensure_grapheme_boundary_next(text, text.byte_to_char(start)); + let end = ensure_grapheme_boundary_next(text, text.byte_to_char(end)); HighlightEvent::Source { start, end } } event => event,