Get cursor movement working with byte columns
Introduce `clip_point` and `clip_offset` methods on `Rope`, `Buffer`, and `FoldMap`. Co-Authored-By: Antonio Scandurra <me@as-cii.com>
This commit is contained in:
committed by
Antonio Scandurra
co-authored by
Antonio Scandurra
parent
f3db0dcff6
commit
510f20411a
@@ -11,6 +11,7 @@ pub use selection::*;
|
||||
use similar::{ChangeTag, TextDiff};
|
||||
|
||||
use crate::{
|
||||
editor::Bias,
|
||||
operation_queue::{self, OperationQueue},
|
||||
sum_tree::{self, FilterCursor, SeekBias, SumTree},
|
||||
time::{self, ReplicaId},
|
||||
@@ -1865,12 +1866,12 @@ impl Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_char_boundary(&self, offset: usize) -> usize {
|
||||
self.visible_text.next_char_boundary(offset)
|
||||
pub fn clip_point(&self, point: Point, bias: Bias) -> Point {
|
||||
self.visible_text.clip_point(point, bias)
|
||||
}
|
||||
|
||||
pub fn prev_char_boundary(&self, offset: usize) -> usize {
|
||||
self.visible_text.prev_char_boundary(offset)
|
||||
pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize {
|
||||
self.visible_text.clip_offset(offset, bias)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3232,8 +3233,8 @@ mod tests {
|
||||
|
||||
impl Buffer {
|
||||
fn random_byte_range(&mut self, start_offset: usize, rng: &mut impl Rng) -> Range<usize> {
|
||||
let end = self.next_char_boundary(rng.gen_range(start_offset..=self.len()));
|
||||
let start = self.prev_char_boundary(rng.gen_range(start_offset..=end));
|
||||
let end = self.clip_offset(rng.gen_range(start_offset..=self.len()), Bias::Right);
|
||||
let start = self.clip_offset(rng.gen_range(start_offset..=end), Bias::Left);
|
||||
start..end
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use super::Point;
|
||||
use crate::sum_tree::{self, SeekBias, SumTree};
|
||||
use crate::{
|
||||
editor::Bias,
|
||||
sum_tree::{self, SeekBias, SumTree},
|
||||
};
|
||||
use arrayvec::ArrayString;
|
||||
use smallvec::SmallVec;
|
||||
use std::{cmp, ops::Range, str};
|
||||
@@ -142,32 +145,38 @@ impl Rope {
|
||||
cursor.start().bytes + cursor.item().map_or(0, |chunk| chunk.to_offset(overshoot))
|
||||
}
|
||||
|
||||
pub fn next_char_boundary(&self, mut offset: usize) -> usize {
|
||||
assert!(offset <= self.summary().bytes);
|
||||
pub fn clip_offset(&self, mut offset: usize, bias: Bias) -> usize {
|
||||
let mut cursor = self.chunks.cursor::<usize, usize>();
|
||||
cursor.seek(&offset, SeekBias::Left, &());
|
||||
if let Some(chunk) = cursor.item() {
|
||||
let mut ix = offset - cursor.start();
|
||||
while !chunk.0.is_char_boundary(ix) {
|
||||
ix += 1;
|
||||
offset += 1;
|
||||
match bias {
|
||||
Bias::Left => {
|
||||
ix -= 1;
|
||||
offset -= 1;
|
||||
}
|
||||
Bias::Right => {
|
||||
ix += 1;
|
||||
offset += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
offset
|
||||
} else {
|
||||
self.summary().bytes
|
||||
}
|
||||
offset
|
||||
}
|
||||
|
||||
pub fn prev_char_boundary(&self, mut offset: usize) -> usize {
|
||||
assert!(offset <= self.summary().bytes);
|
||||
let mut cursor = self.chunks.cursor::<usize, usize>();
|
||||
cursor.seek(&offset, SeekBias::Left, &());
|
||||
pub fn clip_point(&self, point: Point, bias: Bias) -> Point {
|
||||
let mut cursor = self.chunks.cursor::<Point, Point>();
|
||||
cursor.seek(&point, SeekBias::Right, &());
|
||||
if let Some(chunk) = cursor.item() {
|
||||
let mut ix = offset - cursor.start();
|
||||
while !chunk.0.is_char_boundary(ix) {
|
||||
ix -= 1;
|
||||
offset -= 1;
|
||||
}
|
||||
let overshoot = point - cursor.start();
|
||||
*cursor.start() + chunk.clip_point(overshoot, bias)
|
||||
} else {
|
||||
self.summary().lines
|
||||
}
|
||||
offset
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,6 +360,22 @@ impl Chunk {
|
||||
}
|
||||
offset
|
||||
}
|
||||
|
||||
fn clip_point(&self, target: Point, bias: Bias) -> Point {
|
||||
for (row, line) in self.0.split('\n').enumerate() {
|
||||
if row == target.row as usize {
|
||||
let mut column = target.column.min(line.len() as u32);
|
||||
while !line.is_char_boundary(column as usize) {
|
||||
match bias {
|
||||
Bias::Left => column -= 1,
|
||||
Bias::Right => column += 1,
|
||||
}
|
||||
}
|
||||
return Point::new(row as u32, column);
|
||||
}
|
||||
}
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Item for Chunk {
|
||||
@@ -466,30 +491,13 @@ fn find_split_ix(text: &str) -> usize {
|
||||
ix
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn assert_char_boundary(s: &str, ix: usize) {
|
||||
if !s.is_char_boundary(ix) {
|
||||
let mut char_start = ix;
|
||||
while !s.is_char_boundary(char_start) {
|
||||
char_start -= 1;
|
||||
}
|
||||
|
||||
let ch = s[char_start..].chars().next().unwrap();
|
||||
let char_range = char_start..char_start + ch.len_utf8();
|
||||
panic!(
|
||||
"byte index {} is not a char boundary; it is inside {:?} (bytes {:?}) of `{}`",
|
||||
ix, ch, char_range, s
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::util::RandomCharIter;
|
||||
|
||||
use super::*;
|
||||
use crate::util::RandomCharIter;
|
||||
use rand::prelude::*;
|
||||
use std::env;
|
||||
use Bias::{Left, Right};
|
||||
|
||||
#[test]
|
||||
fn test_all_4_byte_chars() {
|
||||
@@ -520,8 +528,8 @@ mod tests {
|
||||
let mut expected = String::new();
|
||||
let mut actual = Rope::new();
|
||||
for _ in 0..operations {
|
||||
let end_ix = actual.next_char_boundary(rng.gen_range(0..=expected.len()));
|
||||
let start_ix = actual.prev_char_boundary(rng.gen_range(0..=end_ix));
|
||||
let end_ix = clip_offset(&expected, rng.gen_range(0..=expected.len()), Right);
|
||||
let start_ix = clip_offset(&expected, rng.gen_range(0..=end_ix), Left);
|
||||
let len = rng.gen_range(0..=64);
|
||||
let new_text: String = RandomCharIter::new(&mut rng).take(len).collect();
|
||||
|
||||
@@ -539,8 +547,8 @@ mod tests {
|
||||
log::info!("text: {:?}", expected);
|
||||
|
||||
for _ in 0..5 {
|
||||
let end_ix = actual.next_char_boundary(rng.gen_range(0..=expected.len()));
|
||||
let start_ix = actual.prev_char_boundary(rng.gen_range(0..=end_ix));
|
||||
let end_ix = clip_offset(&expected, rng.gen_range(0..=expected.len()), Right);
|
||||
let start_ix = clip_offset(&expected, rng.gen_range(0..=end_ix), Left);
|
||||
assert_eq!(
|
||||
actual.chunks_in_range(start_ix..end_ix).collect::<String>(),
|
||||
&expected[start_ix..end_ix]
|
||||
@@ -560,8 +568,8 @@ mod tests {
|
||||
}
|
||||
|
||||
for _ in 0..5 {
|
||||
let end_ix = actual.next_char_boundary(rng.gen_range(0..=expected.len()));
|
||||
let start_ix = actual.prev_char_boundary(rng.gen_range(0..=end_ix));
|
||||
let end_ix = clip_offset(&expected, rng.gen_range(0..=expected.len()), Right);
|
||||
let start_ix = clip_offset(&expected, rng.gen_range(0..=end_ix), Left);
|
||||
assert_eq!(
|
||||
actual.cursor(start_ix).summary(end_ix),
|
||||
TextSummary::from(&expected[start_ix..end_ix])
|
||||
@@ -571,6 +579,16 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn clip_offset(text: &str, mut offset: usize, bias: Bias) -> usize {
|
||||
while !text.is_char_boundary(offset) {
|
||||
match bias {
|
||||
Bias::Left => offset -= 1,
|
||||
Bias::Right => offset += 1,
|
||||
}
|
||||
}
|
||||
offset
|
||||
}
|
||||
|
||||
impl Rope {
|
||||
fn text(&self) -> String {
|
||||
let mut text = String::new();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
editor::{
|
||||
buffer::{Anchor, Buffer, Point, ToPoint},
|
||||
display_map::{Bias, DisplayMap},
|
||||
DisplayPoint,
|
||||
display_map::DisplayMap,
|
||||
Bias, DisplayPoint,
|
||||
},
|
||||
time,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user