on master help sign in
Add `Range` methods for various kinds of validation.
commitd07074740bc44b71de83cf23dd692fa90c2854a9 authorNathan Vegdahl <cessen@cessen.com> dateSat 26 Jun 2021, 15:37 (5 years ago) parentc1b0a71
3 files changed+127−6
helix-core/src/graphemes.rs+12−2 helix-core/src/selection.rs+112−1 helix-term/src/ui/editor.rs+3−3
helix-core/src/graphemes.rs+12−2
@@ -123,14 +123,24 @@ pub fn next_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> usize {
123123
124124/// Returns the passed char index if it's already a grapheme boundary,
125125/// or the next grapheme boundary char index if not.
126−pub fn ensure_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> usize {
126+pub fn ensure_grapheme_boundary_next(slice: RopeSlice, char_idx: usize) -> usize {
127127 if char_idx == 0 {
128− 0
128+ char_idx
129129 } else {
130130 next_grapheme_boundary(slice, char_idx - 1)
131131 }
132132}
133133
134+/// Returns the passed char index if it's already a grapheme boundary,
135+/// or the prev grapheme boundary char index if not.
136+pub fn ensure_grapheme_boundary_prev(slice: RopeSlice, char_idx: usize) -> usize {
137+ if char_idx == slice.len_chars() {
138+ char_idx
139+ } else {
140+ prev_grapheme_boundary(slice, char_idx + 1)
141+ }
142+}
143+
134144/// Returns whether the given char position is a grapheme boundary.
135145pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool {
136146 // Bounds check
helix-core/src/selection.rs+112−1
@@ -2,7 +2,12 @@
22//! defined as a single empty or 1-wide selection range.
33//!
44//! All positioning is done via `char` offsets into the buffer.
5−use crate::{Assoc, ChangeSet, Rope, RopeSlice};
5+use crate::{
6+ graphemes::{
7+ ensure_grapheme_boundary_next, ensure_grapheme_boundary_prev, next_grapheme_boundary,
8+ },
9+ Assoc, ChangeSet, Rope, RopeSlice,
10+};
611use smallvec::{smallvec, SmallVec};
712use std::borrow::Cow;
813
@@ -132,6 +137,61 @@ impl Range {
132137 }
133138 }
134139
140+ /// Compute the ends of the range, shifted (if needed) to align with
141+ /// grapheme boundaries.
142+ ///
143+ /// This should generally be used for cursor validation.
144+ ///
145+ /// Always succeeds.
146+ #[must_use]
147+ pub fn aligned_range(&self, slice: RopeSlice) -> (usize, usize) {
148+ if self.anchor == self.head {
149+ let pos = ensure_grapheme_boundary_prev(slice, self.anchor);
150+ (pos, pos)
151+ } else {
152+ (
153+ ensure_grapheme_boundary_prev(slice, self.from()),
154+ ensure_grapheme_boundary_next(slice, self.to()),
155+ )
156+ }
157+ }
158+
159+ /// Same as `ensure_grapheme_validity()` + attempts to ensure a minimum
160+ /// char width in the direction of the head.
161+ ///
162+ /// This should generally be used as a pre-pass for operations that
163+ /// require a minimum selection width to achieve their intended behavior.
164+ ///
165+ /// This will fail at ensuring the minimum width only if the passed
166+ /// `RopeSlice` is too short in the direction of the head, in which
167+ /// case the range will fill the available length in that direction.
168+ ///
169+ /// Ensuring grapheme-boundary alignment always succeeds.
170+ #[must_use]
171+ pub fn min_width_range(&self, slice: RopeSlice, min_char_width: usize) -> (usize, usize) {
172+ if min_char_width == 0 {
173+ return self.aligned_range(slice);
174+ }
175+
176+ if self.anchor <= self.head {
177+ let anchor = ensure_grapheme_boundary_prev(slice, self.anchor);
178+ let head = ensure_grapheme_boundary_next(
179+ slice,
180+ self.head
181+ .max(anchor + min_char_width)
182+ .min(slice.len_chars()),
183+ );
184+ (anchor, head)
185+ } else {
186+ let anchor = ensure_grapheme_boundary_next(slice, self.anchor);
187+ let head = ensure_grapheme_boundary_prev(
188+ slice,
189+ self.head.min(anchor.saturating_sub(min_char_width)),
190+ );
191+ (head, anchor)
192+ }
193+ }
194+
135195 // groupAt
136196
137197 #[inline]
@@ -556,6 +616,54 @@ mod test {
556616 assert!(Range::new(1, 1).overlaps(&Range::new(1, 1)));
557617 }
558618
619+ #[test]
620+ fn test_aligned_range() {
621+ let r = Rope::from_str("\r\nHi\r\n");
622+ let s = r.slice(..);
623+
624+ assert_eq!(Range::new(0, 0).aligned_range(s), (0, 0));
625+ assert_eq!(Range::new(0, 1).aligned_range(s), (0, 2));
626+ assert_eq!(Range::new(1, 1).aligned_range(s), (0, 0));
627+ assert_eq!(Range::new(1, 2).aligned_range(s), (0, 2));
628+ assert_eq!(Range::new(2, 2).aligned_range(s), (2, 2));
629+ assert_eq!(Range::new(2, 3).aligned_range(s), (2, 3));
630+ assert_eq!(Range::new(1, 3).aligned_range(s), (0, 3));
631+ assert_eq!(Range::new(3, 5).aligned_range(s), (3, 6));
632+ assert_eq!(Range::new(4, 5).aligned_range(s), (4, 6));
633+ assert_eq!(Range::new(5, 5).aligned_range(s), (4, 4));
634+ assert_eq!(Range::new(6, 6).aligned_range(s), (6, 6));
635+ }
636+
637+ #[test]
638+ fn test_min_width_range() {
639+ let r = Rope::from_str("\r\nHi\r\n");
640+ let s = r.slice(..);
641+
642+ assert_eq!(Range::new(0, 0).min_width_range(s, 1), (0, 2));
643+ assert_eq!(Range::new(0, 1).min_width_range(s, 1), (0, 2));
644+ assert_eq!(Range::new(1, 1).min_width_range(s, 1), (0, 2));
645+ assert_eq!(Range::new(1, 2).min_width_range(s, 1), (0, 2));
646+ assert_eq!(Range::new(2, 2).min_width_range(s, 1), (2, 3));
647+ assert_eq!(Range::new(2, 3).min_width_range(s, 1), (2, 3));
648+ assert_eq!(Range::new(1, 3).min_width_range(s, 1), (0, 3));
649+ assert_eq!(Range::new(3, 5).min_width_range(s, 1), (3, 6));
650+ assert_eq!(Range::new(4, 5).min_width_range(s, 1), (4, 6));
651+ assert_eq!(Range::new(5, 5).min_width_range(s, 1), (4, 6));
652+ assert_eq!(Range::new(6, 6).min_width_range(s, 1), (6, 6));
653+
654+ assert_eq!(Range::new(1, 0).min_width_range(s, 1), (0, 2));
655+ assert_eq!(Range::new(2, 1).min_width_range(s, 1), (0, 2));
656+ assert_eq!(Range::new(3, 2).min_width_range(s, 1), (2, 3));
657+ assert_eq!(Range::new(3, 1).min_width_range(s, 1), (0, 3));
658+ assert_eq!(Range::new(5, 3).min_width_range(s, 1), (3, 6));
659+ assert_eq!(Range::new(5, 4).min_width_range(s, 1), (4, 6));
660+
661+ assert_eq!(Range::new(3, 4).min_width_range(s, 3), (3, 6));
662+ assert_eq!(Range::new(4, 3).min_width_range(s, 3), (0, 4));
663+ assert_eq!(Range::new(3, 4).min_width_range(s, 20), (3, 6));
664+ assert_eq!(Range::new(4, 3).min_width_range(s, 20), (0, 4));
665+ }
666+
559667 #[test]
560668 fn test_split_on_matches() {
561669 use crate::regex::Regex;
@@ -569,6 +677,9 @@ mod test {
569677 assert_eq!(
570678 result.ranges(),
571679 &[
680+ // TODO: rather than this behavior, maybe we want it
681+ // to be based on which side is the anchor?
682+ //
572683 // We get a leading zero-width range when there's
573684 // a leading match because ranges are inclusive on
574685 // the left. Imagine, for example, if the entire
helix-term/src/ui/editor.rs+3−3
@@ -8,7 +8,7 @@ use crate::{
88
99use helix_core::{
1010 coords_at_pos,
11− graphemes::ensure_grapheme_boundary,
11+ graphemes::ensure_grapheme_boundary_next,
1212 syntax::{self, Highlight, HighlightEvent},
1313 LineEnding, Position, Range,
1414};
@@ -144,8 +144,8 @@ impl EditorView {
144144 let highlights = highlights.into_iter().map(|event| match event.unwrap() {
145145 // convert byte offsets to char offset
146146 HighlightEvent::Source { start, end } => {
147− let start = ensure_grapheme_boundary(text, text.byte_to_char(start));
148− let end = ensure_grapheme_boundary(text, text.byte_to_char(end));
147+ let start = ensure_grapheme_boundary_next(text, text.byte_to_char(start));
148+ let end = ensure_grapheme_boundary_next(text, text.byte_to_char(end));
149149 HighlightEvent::Source { start, end }
150150 }
151151 event => event,