diff --git a/benches/benchmarks/exec.rs b/benches/benchmarks/exec.rs index 7ff3fb02..e74d66af 100644 --- a/benches/benchmarks/exec.rs +++ b/benches/benchmarks/exec.rs @@ -18,7 +18,7 @@ use std::{ const SCRIPT: &str = include_str!("../../examples/scripts/fancy_impl_blocks.ad"); const ZIPLIST: &str = include_str!("../../src/ziplist.rs"); -const GAP_BUFFER: &str = include_str!("../../src/buffer/internal.rs"); +const GAP_BUFFER: &str = include_str!("../../src/buffer/internal/mod.rs"); fn criterion_benchmark(c: &mut Criterion) { let mut group = c.benchmark_group("Structural regex"); diff --git a/benches/benchmarks/gap_buffer.rs b/benches/benchmarks/gap_buffer.rs index a08e640d..af4f79fc 100644 --- a/benches/benchmarks/gap_buffer.rs +++ b/benches/benchmarks/gap_buffer.rs @@ -1,5 +1,5 @@ //! GapBuffer benchmarks for measuring line-ending tracking performance. -use ad_editor::buffer::GapBuffer; +use ad_editor::buffer::{CharIdx, GapBuffer}; use criterion::{BatchSize, BenchmarkId, Criterion, criterion_group}; use libflate::gzip::Decoder; use serde::Deserialize; @@ -55,9 +55,12 @@ impl From<(usize, usize, String)> for TestPatch { fn apply_patch(patch: &TestPatch, gb: &mut GapBuffer) { if patch.delete_len > 0 { - gb.remove_range(patch.position, patch.position + patch.delete_len); + gb.remove_range( + CharIdx(patch.position), + CharIdx(patch.position + patch.delete_len), + ); } - gb.insert_str(patch.position, &patch.insert_content); + gb.insert_str(CharIdx(patch.position), &patch.insert_content); } fn single_char_insert(c: &mut Criterion) { @@ -82,7 +85,7 @@ fn single_char_insert(c: &mut Criterion) { |b, (content, pos)| { b.iter_batched( || GapBuffer::from(content.as_str()), - |mut gb| gb.insert_char(*pos, 'a'), + |mut gb| gb.insert_char(CharIdx(*pos), 'a'), BatchSize::SmallInput, ); }, diff --git a/benches/benchmarks/ts_update.rs b/benches/benchmarks/ts_update.rs index fe9808e9..afc5c687 100644 --- a/benches/benchmarks/ts_update.rs +++ b/benches/benchmarks/ts_update.rs @@ -1,5 +1,10 @@ // Update times for running TreeSitter syntax highlighting -use ad_editor::{Config, buffer::Buffer, dot::TextObject, editor::BAction}; +use ad_editor::{ + Config, + buffer::{Buffer, LineIdx}, + dot::TextObject, + editor::BAction, +}; use ad_event::Source; use criterion::{Criterion, criterion_group}; use parking_lot::RwLock; @@ -13,22 +18,22 @@ fn criterion_benchmark(c: &mut Criterion) { let config = Arc::new(RwLock::new(Config::try_load().unwrap())); let mut buf = Buffer::new_from_canonical_file_path(0, path.clone(), config.clone()).unwrap(); - buf.update_ts_state(0, 70); + buf.update_ts_state(LineIdx(0), 70); group.bench_function("update without edit", |b| { b.iter(|| { - buf.update_ts_state(0, 70); + buf.update_ts_state(LineIdx(0), 70); }) }); let mut buf = Buffer::new_from_canonical_file_path(0, path.clone(), config.clone()).unwrap(); - buf.update_ts_state(0, 70); + buf.update_ts_state(LineIdx(0), 70); group.bench_function("append newline and update", |b| { b.iter(|| { buf.handle_action(BAction::DotSet(TextObject::BufferEnd, 1), Source::Fsys); buf.handle_action(BAction::InsertChar { c: '\n' }, Source::Fsys); - buf.update_ts_state(0, 70); + buf.update_ts_state(LineIdx(0), 70); }) }); diff --git a/data/plumbing_tests/match-tests/multibyte-narrowing b/data/plumbing_tests/match-tests/multibyte-narrowing new file mode 100644 index 00000000..83f04c28 --- /dev/null +++ b/data/plumbing_tests/match-tests/multibyte-narrowing @@ -0,0 +1,23 @@ +The cursor position is a character offset into the message data, so leading multi-byte characters +must not shift which match the cursor lands in. (Character 15 is in "two.pdf" while byte 15 is in +"one.pdf".) + +-- rules -- +data narrows [a-zA-Z0-9_\-./@]+ +data matches ^[a-zA-Z0-9_\-./@]+\.pdf$ +plumb to edit + +-- initial -- +src: bash +dst: ad +wdir: /home/foo/bar +cur: 15 +ndata: 27 +data: ééééé one.pdf two.pdf + +-- processed -- +src: bash +dst: ad +wdir: /home/foo/bar +ndata: 7 +data: two.pdf diff --git a/fuzz/fuzz_targets/fuzz_gap_buffer.rs b/fuzz/fuzz_targets/fuzz_gap_buffer.rs index c0ede483..9ba7ba1a 100644 --- a/fuzz/fuzz_targets/fuzz_gap_buffer.rs +++ b/fuzz/fuzz_targets/fuzz_gap_buffer.rs @@ -1,6 +1,6 @@ #![no_main] -use ad_editor::buffer::GapBuffer; +use ad_editor::buffer::{CharIdx, GapBuffer}; use arbitrary::Arbitrary; use libfuzzer_sys::fuzz_target; use std::cmp::{max, min}; @@ -9,13 +9,17 @@ fuzz_target!(|data: FuzzData| { let mut gb = GapBuffer::from(data.initial_content); for action in data.actions.into_iter() { match action { - TestAction::InsertChar(idx, ch) if idx < gb.len_chars() => gb.insert_char(idx, ch), - TestAction::InsertString(idx, s) if idx < gb.len_chars() => gb.insert_str(idx, &s), - TestAction::RemoveChar(idx) if idx < gb.len_chars() => gb.remove_char(idx), + TestAction::InsertChar(idx, ch) if idx < gb.len_chars() => { + gb.insert_char(CharIdx(idx), ch) + } + TestAction::InsertString(idx, s) if idx < gb.len_chars() => { + gb.insert_str(CharIdx(idx), &s) + } + TestAction::RemoveChar(idx) if idx < gb.len_chars() => gb.remove_char(CharIdx(idx)), TestAction::RemoveRange(a, b) => { let (from, to) = (min(a, b), max(a, b)); if to < gb.len_chars() { - gb.remove_range(from, to); + gb.remove_range(CharIdx(from), CharIdx(to)); } } diff --git a/reference-tests/src/lib.rs b/reference-tests/src/lib.rs index c51f2319..66fafb34 100644 --- a/reference-tests/src/lib.rs +++ b/reference-tests/src/lib.rs @@ -1,6 +1,10 @@ //! Reference editing tests for the ad Buffer implementation using //! -use ad_editor::{Source, buffer::Buffer, editor::BAction}; +use ad_editor::{ + Source, + buffer::{Buffer, CharIdx}, + editor::BAction, +}; use libflate::gzip::Decoder; use serde::Deserialize; use std::{ @@ -69,11 +73,14 @@ impl TestPatch { // https://github.com/josephg/editing-traces/blob/master/sequential_traces/README.md#data-format pub fn apply(self, b: &mut Buffer) { if self.delete_len > 0 { - b.set_dot_from_range(self.position, self.position + self.delete_len - 1); + b.set_dot_from_range( + CharIdx(self.position), + CharIdx(self.position + self.delete_len - 1), + ); b.handle_action(BAction::Delete, Source::Fsys); } - b.set_dot_from_cursor(self.position); + b.set_dot_from_cursor(CharIdx(self.position)); b.handle_action( BAction::InsertString { s: self.insert_content, diff --git a/src/buffer/buffers.rs b/src/buffer/buffers.rs index 27125f68..737010f2 100644 --- a/src/buffer/buffers.rs +++ b/src/buffer/buffers.rs @@ -1,5 +1,5 @@ use crate::{ - buffer::{Buffer, BufferKind, Cur, ScratchBuf, WELCOME_SQUIRREL}, + buffer::{Buffer, BufferKind, CharIdx, Cur, ScratchBuf, WELCOME_SQUIRREL}, config::Config, dot::TextObject, fsys::InputFilter, @@ -226,8 +226,8 @@ impl Buffers { self.focus_id(id); self.inner.focus.txt = content.into(); let n = self.inner.focus.txt.len_chars(); - self.inner.focus.dot.clamp_idx(n); - self.inner.focus.xdot.clamp_idx(n); + self.inner.focus.dot.clamp_idx(CharIdx(n)); + self.inner.focus.xdot.clamp_idx(CharIdx(n)); return id; } @@ -407,7 +407,7 @@ impl Buffers { if new_id == bufid { // Clamp the cursor to the buffer's valid range before setting it // in case the buffer was edited since this position was recorded - cur.clamp_idx(self.inner.focus.txt.len_chars()); + cur.clamp_idx(CharIdx(self.inner.focus.txt.len_chars())); self.inner.focus.dot = cur.into(); } @@ -583,21 +583,20 @@ mod tests { let initial_content = "This is a test buffer with enough content to demonstrate the bug."; buffers.inner.focus.txt = initial_content.into(); - buffers.inner.focus.set_dot_from_cursor(60); - assert_eq!(buffers.inner.focus.dot.active_cur().idx, 60); + buffers.inner.focus.set_dot_from_cursor(CharIdx(60)); + assert_eq!(buffers.inner.focus.dot.active_cur().idx, CharIdx(60)); buffers.record_jump_position(); assert_eq!(buffers.inner.focus.txt.len_chars(), 65); let new_len = 30; - buffers - .inner - .focus - .txt - .remove_range(new_len, buffers.inner.focus.txt.len_chars()); + buffers.inner.focus.txt.remove_range( + CharIdx(new_len), + CharIdx(buffers.inner.focus.txt.len_chars()), + ); assert_eq!(buffers.inner.focus.txt.len_chars(), new_len); - buffers.inner.focus.set_dot_from_cursor(0); + buffers.inner.focus.set_dot_from_cursor(CharIdx(0)); let result = buffers.jump_list_backward(); assert!(result.is_some()); @@ -606,7 +605,7 @@ mod tests { let (_y, _x) = cur.as_yx(&buffers.inner.focus); assert!( - cur.idx <= buffers.inner.focus.txt.len_chars(), + cur.idx.0 <= buffers.inner.focus.txt.len_chars(), "cursor index {} exceeds buffer length {}", cur.idx, buffers.inner.focus.txt.len_chars() diff --git a/src/buffer/edit.rs b/src/buffer/edit.rs index 1aa4b997..16a21e5f 100644 --- a/src/buffer/edit.rs +++ b/src/buffer/edit.rs @@ -1,4 +1,4 @@ -use crate::buffer::{Buffer, Cur}; +use crate::buffer::{Buffer, CharOffset, Cur}; #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub(super) enum Kind { @@ -95,11 +95,11 @@ impl Edit { None } else { match &self.txt { - Txt::Char(_) if e.cur.idx == self.cur.idx + 1 => { + Txt::Char(_) if e.cur.idx == self.cur.idx.uadd(1) => { self.txt.append(e.txt); None } - Txt::String(s) if e.cur.idx == self.cur.idx + s.len() => { + Txt::String(s) if e.cur.idx == self.cur.idx + CharOffset::from(s.as_str()) => { self.txt.append(e.txt); None } @@ -114,12 +114,12 @@ impl Edit { None } else { match &e.txt { - Txt::Char(_) if e.cur.idx + 1 == self.cur.idx => { + Txt::Char(_) if e.cur.idx.uadd(1) == self.cur.idx => { self.txt.prepend(e.txt); self.cur = e.cur; None } - Txt::String(s) if e.cur.idx + s.len() == self.cur.idx => { + Txt::String(s) if e.cur.idx + CharOffset::from(s.as_str()) == self.cur.idx => { self.txt.prepend(e.txt); self.cur = e.cur; None @@ -262,12 +262,13 @@ impl EditLog { #[cfg(test)] pub(crate) mod tests { use super::*; + use crate::buffer::CharIdx; use simple_test_case::test_case; pub(crate) fn in_c(idx: usize, c: char) -> Edit { Edit { kind: Kind::Insert, - cur: Cur { idx }, + cur: Cur { idx: CharIdx(idx) }, txt: Txt::Char(c), } } @@ -275,7 +276,7 @@ pub(crate) mod tests { pub(crate) fn in_s(idx: usize, s: &str) -> Edit { Edit { kind: Kind::Insert, - cur: Cur { idx }, + cur: Cur { idx: CharIdx(idx) }, txt: Txt::String(s.to_string()), } } @@ -283,7 +284,7 @@ pub(crate) mod tests { pub(crate) fn del_c(idx: usize, c: char) -> Edit { Edit { kind: Kind::Delete, - cur: Cur { idx }, + cur: Cur { idx: CharIdx(idx) }, txt: Txt::Char(c), } } @@ -291,7 +292,7 @@ pub(crate) mod tests { pub(crate) fn del_s(idx: usize, s: &str) -> Edit { Edit { kind: Kind::Delete, - cur: Cur { idx }, + cur: Cur { idx: CharIdx(idx) }, txt: Txt::String(s.to_string()), } } @@ -336,6 +337,16 @@ pub(crate) mod tests { &[in_s(0, "dabc")]; "string then char at same cursor" )] + #[test_case( + vec![in_s(0, "éé"), in_c(2, 'x')], + &[in_s(0, "ééx")]; + "multi-byte string then adjacent char" + )] + #[test_case( + vec![in_s(0, "é"), in_c(2, 'x')], + &[in_s(0, "é"), in_c(2, 'x')]; + "multi-byte string then non-adjacent char" + )] #[test] fn inserts_work(edits: Vec, expected: &[Edit]) { let mut log = EditLog::default(); @@ -386,6 +397,16 @@ pub(crate) mod tests { &[del_s(0, "abcd")]; "string then char at same cursor" )] + #[test_case( + vec![del_c(2, 'x'), del_s(0, "éé")], + &[del_s(0, "ééx")]; + "char then adjacent multi-byte string" + )] + #[test_case( + vec![del_c(4, 'x'), del_s(0, "éé")], + &[del_c(4, 'x'), del_s(0, "éé")]; + "char then non-adjacent multi-byte string" + )] #[test] fn delete_work(edits: Vec, expected: &[Edit]) { let mut log = EditLog::default(); diff --git a/src/buffer/internal/index.rs b/src/buffer/internal/index.rs index 5771e4c3..940c6f07 100644 --- a/src/buffer/internal/index.rs +++ b/src/buffer/internal/index.rs @@ -1,8 +1,271 @@ //! Type-safe indexing into GapBuffers. +//! +//! Mixing up character and byte indices when interacting with the GapBuffer has been one of the +//! main sources of logic bugs within ad. The GapBuffer itself is highly defensive about this but +//! previous versions of the public GapBuffer API made use of bare usize parameters for indexing +//! which allowed callers to mess things up (particularly when multi-byte utf-8 characters were +//! involved). +//! +//! # Indexing into a GapBuffer +//! +//! There are four distinct ways to index into a GapBuffer depending on the operation in question. +//! The first three offer different ways of performing arbitrary indexing into the contents of the +//! buffer: +//! +//! 1. [RawIdx] - A raw byte offset into the memory allocation held by the GapBuffer. This is only +//! permitted within the `buffer::internal` module as an implementation detail of the GapBuffer +//! and Slice types. Indexing in this way is never guaranteed to return valid utf-8 data so the +//! caller must ensure that indexes are calculated correctly and that the data read is valid. +//! 2. [ByteIdx] - A byte offset within the logical contents of the GapBuffer. This is used +//! primarily by the regex and treesitter APIs where a GapBuffer is being treated as a byte +//! slice. The standard pitfalls of indexing into a utf-8 encoded string all apply (i.e. care +//! must be taken when multi-byte characters are present) and it is _not_ guaranteed that an +//! arbitrary index of this type lies on a character boundary. However, this type _does_ +//! guarantee that indexing in this way always returns bytes from the logical contents of the +//! buffer and not the gap region. +//! 3. [CharIdx] - A character offset within the logical contents of the GapBuffer. This is the +//! most expensive index type to work with but it guarantees that indexing in this way always +//! points to a valid utf-8 character boundary. This is used when reading and writing utf-8 data +//! and is efficiently mapped to a [RawIdx] when inserting / deleting at the start of the gap +//! region. +//! +//! The fourth index type is [LineIdx] which is used to quickly jump to the beginning of newline +//! delimited lines within the logical buffer state. GapBuffer maintains a cache of line ending +//! positions within the buffer in order to make this an `O(1)` operation. +//! +//! # Shared API for index types +//! +//! All four index types behave the same way and implement a set of shared helper methods / traits +//! to make them look as much like a bare `usize` as possible while still forcing the user to be +//! explicit about how they are indexing into a GapBuffer. Indices can be freely added and +//! subtracted from one another but offsetting by a `usize` requires calling the `uadd` and `usub` +//! methods in order make it easy to audit for logic errors around mixing indices. +//! +//! The underlying `usize` value of the index is freely accessible as a public field, but +//! conversion between index types should be handled via the methods provided by GapBuffer rather +//! than ad-hoc manual conversion or external helper functions. +use std::{ + fmt, + ops::{Add, AddAssign, Neg, Sub, SubAssign}, +}; -/// A byte offset within the logical buffer state (i.e. ignoring the position of the gap). -pub type ByteIdx = usize; -/// A character offset within the logical buffer state. -pub type CharIdx = usize; -/// A byte offset within the raw buffer state (i.e. accounting for the position of the gap). -pub type RawIdx = usize; +macro_rules! impl_index_types { + ( + $( $(#[$doc_str:meta])* $name:ident; )* + ) => { + $( + $(#[$doc_str])* + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct $name(pub usize); + + impl $name { + #[allow(dead_code)] + /// The largest value that can be represented by this index type. + pub const MAX: Self = Self(usize::MAX); + + pub const fn saturating_sub(self, rhs: Self) -> Self { + Self(self.0.saturating_sub(rhs.0)) + } + + pub const fn uadd(self, rhs: usize) -> Self { + Self(self.0 + rhs) + } + + pub const fn usub(self, rhs: usize) -> Self { + Self(self.0 - rhs) + } + + #[allow(dead_code)] + pub const fn saturating_usub(self, rhs: usize) -> Self { + Self(self.0.saturating_sub(rhs)) + } + + #[allow(dead_code)] + pub fn to(self, end: Self) -> impl Iterator { + (self.0..end.0).map(Self) + } + } + + impl fmt::Display for $name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } + } + + impl Add for $name { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + Self(self.0 + rhs.0) + } + } + + impl Sub for $name { + type Output = Self; + + fn sub(self, rhs: Self) -> Self { + Self(self.0 - rhs.0) + } + } + + impl AddAssign for $name { + fn add_assign(&mut self, rhs: Self) { + self.0 += rhs.0; + } + } + + impl SubAssign for $name { + fn sub_assign(&mut self, rhs: Self) { + self.0 -= rhs.0; + } + } + )* + }; +} + +impl_index_types!( + /// A byte offset within the logical buffer state (ignoring the position of the gap). + ByteIdx; + /// A character offset within the logical buffer state. + CharIdx; + /// A byte offset within the raw buffer state (accounting for the position of the gap). + RawIdx; + /// A line index within the logical buffer state. + LineIdx; +); + +macro_rules! impl_offset_types { + ( + $( $(#[$doc_str:meta])* $name:ident => $idx:ident; )* + ) => { + $( + $(#[$doc_str])* + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct $name(pub isize); + + impl $idx { + pub const fn offset_from(self, origin: Self) -> $name { + $name(self.0 as isize - origin.0 as isize) + } + + pub const fn saturating_add(self, rhs: $name) -> Self { + Self(self.0.saturating_add_signed(rhs.0)) + } + } + + impl From<$idx> for $name { + fn from(idx: $idx) -> Self { + Self(idx.0 as isize) + } + } + + impl fmt::Display for $name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } + } + + impl Add for $name { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + Self(self.0 + rhs.0) + } + } + + impl Sub for $name { + type Output = Self; + + fn sub(self, rhs: Self) -> Self { + Self(self.0 - rhs.0) + } + } + + impl Neg for $name { + type Output = Self; + + fn neg(self) -> Self { + Self(-self.0) + } + } + + impl AddAssign for $name { + fn add_assign(&mut self, rhs: Self) { + self.0 += rhs.0; + } + } + + impl SubAssign for $name { + fn sub_assign(&mut self, rhs: Self) { + self.0 -= rhs.0; + } + } + + impl Add<$name> for $idx { + type Output = Self; + + fn add(self, rhs: $name) -> Self { + match self.0.checked_add_signed(rhs.0) { + Some(idx) => Self(idx), + None => panic!("index overflow: {} + {}", self.0, rhs.0), + } + } + } + + impl Sub<$name> for $idx { + type Output = Self; + + fn sub(self, rhs: $name) -> Self { + self + -rhs + } + } + + impl AddAssign<$name> for $idx { + fn add_assign(&mut self, rhs: $name) { + *self = *self + rhs; + } + } + + impl SubAssign<$name> for $idx { + fn sub_assign(&mut self, rhs: $name) { + *self = *self - rhs; + } + } + )* + }; +} + +impl_offset_types!( + /// A signed offset between two [ByteIdx] values. + ByteOffset => ByteIdx; + /// A signed offset between two [CharIdx] values. + CharOffset => CharIdx; + /// A signed offset between two [LineIdx] values. + LineOffset => LineIdx; +); + +impl From<&str> for ByteOffset { + fn from(s: &str) -> Self { + // Casting a length to isize is lossless as Rust guarantees that allocations are never + // larger than isize::MAX bytes. + Self(s.len() as isize) + } +} + +impl From for ByteOffset { + fn from(ch: char) -> Self { + Self(ch.len_utf8() as isize) + } +} + +impl From<&str> for CharOffset { + fn from(s: &str) -> Self { + Self(s.chars().count() as isize) + } +} + +impl From for CharOffset { + fn from(_: char) -> Self { + Self(1) + } +} diff --git a/src/buffer/internal/mod.rs b/src/buffer/internal/mod.rs index e22e8f6e..17e92a12 100644 --- a/src/buffer/internal/mod.rs +++ b/src/buffer/internal/mod.rs @@ -21,15 +21,18 @@ use std::{ cmp::{Ordering, max, min}, collections::HashMap, fmt, + ops::RangeBounds, }; mod index; mod slice; mod utf8; -pub use index::{ByteIdx, CharIdx, RawIdx}; +pub use index::{ByteIdx, ByteOffset, CharIdx, CharOffset, LineIdx, LineOffset}; pub use slice::{Chars, IdxChars, RevChars, Slice, SliceIter}; -use utf8::{count_chars, decode_char_at, decode_char_ending_at}; + +use index::RawIdx; +use utf8::{decode_char_at, decode_char_ending_at}; // The internal data is `[u8]` so the values here are in terms of bytes @@ -89,7 +92,7 @@ pub struct GapBuffer { /// the raw data being stored (both buffer content and the gap) data: Box<[u8]>, /// current size of the allocation for data - cap: RawIdx, + cap: usize, /// raw byte offset to the first character in the gap gap_start: RawIdx, /// raw byte offset to the last character in the gap @@ -98,7 +101,7 @@ pub struct GapBuffer { next_gap: usize, /// total number of characters in the buffer /// this is != line_endings.last() if there is no trailing newline - n_chars: CharIdx, + n_chars: usize, /// line ending raw byte offset -> char offset line_endings: Vec<(RawIdx, CharIdx)>, /// Simple cache of computed char->byte mappings since the last time the buffer was modified @@ -111,14 +114,16 @@ impl Default for GapBuffer { } } -fn compute_line_endings(s: &str) -> (CharIdx, Vec<(RawIdx, CharIdx)>) { +fn compute_line_endings(s: &str) -> (usize, Vec<(RawIdx, CharIdx)>) { let mut n_chars = 0; let mut line_endings = Vec::new(); for (line_chars, (idx, ch)) in s.char_indices().enumerate() { n_chars += 1; if ch == '\n' { - line_endings.push((idx, line_chars)); + // The gap is placed at the end of the raw buffer on construction so logical and raw + // offsets are equal here. + line_endings.push((RawIdx(idx), CharIdx(line_chars))); } } @@ -137,15 +142,15 @@ impl From for GapBuffer { let mut gb = Self { data: v.into_boxed_slice(), cap, - gap_start, - gap_end: cap, + gap_start: RawIdx(gap_start), + gap_end: RawIdx(cap), next_gap, n_chars, line_endings, char_to_byte_cache: CharToByteCache(UnsafeCell::new(HashMap::new())), }; - gb.move_gap_to(0); + gb.move_gap_to(ByteIdx(0)); gb } } @@ -163,15 +168,15 @@ impl From<&str> for GapBuffer { let mut gb = Self { data: v.into_boxed_slice(), cap, - gap_start, - gap_end: cap, + gap_start: RawIdx(gap_start), + gap_end: RawIdx(cap), next_gap, n_chars, line_endings, char_to_byte_cache: CharToByteCache(UnsafeCell::new(HashMap::new())), }; - gb.move_gap_to(0); + gb.move_gap_to(ByteIdx(0)); gb } } @@ -190,8 +195,8 @@ impl<'a> PartialEq<&'a str> for GapBuffer { return false; } - b[..self.gap_start] == self.data[..self.gap_start] - && b[self.gap_start..] == self.data[self.gap_end..] + b[..self.gap_start.0] == *self.raw_slice(..self.gap_start) + && b[self.gap_start.0..] == *self.raw_slice(self.gap_end..) } } @@ -209,14 +214,16 @@ impl PartialEq for GapBuffer { /// because there is a bug around line endings that the current test suite didn't catch. macro_rules! assert_line_endings { ($self:expr) => {{ - let true_endings: Vec = $self + let true_endings: Vec = $self .data .iter() .enumerate() + .map(|(i, b)| (RawIdx(i), b)) .filter(|&(i, &b)| b == b'\n' && (i < $self.gap_start || i >= $self.gap_end)) .map(|(i, _)| i) .collect(); - let tracked_line_endings: Vec = $self.line_endings.iter().map(|(n, _)| *n).collect(); + let tracked_line_endings: Vec = + $self.line_endings.iter().map(|(n, _)| *n).collect(); assert_eq!( tracked_line_endings, true_endings, @@ -231,7 +238,8 @@ macro_rules! assert_line_endings { .filter(|&(_, c)| c == '\n') .map(|(i, _)| i) .collect(); - let tracked_line_endings: Vec = $self.line_endings.iter().map(|(_, n)| *n).collect(); + let tracked_line_endings: Vec = + $self.line_endings.iter().map(|(_, n)| n.0).collect(); assert_eq!( tracked_line_endings, true_endings, @@ -250,12 +258,25 @@ impl GapBuffer { /// Number of bytes in the gap #[inline] fn gap(&self) -> usize { - self.gap_end - self.gap_start + (self.gap_end - self.gap_start).0 + } + + /// A slice of the raw buffer (including the gap) for the given range of raw byte offsets. + #[inline] + fn raw_slice(&self, r: impl RangeBounds) -> &[u8] { + &self.data[(r.start_bound().map(|i| i.0), r.end_bound().map(|i| i.0))] + } + + /// A mutable slice of the raw buffer (including the gap) for the given range of raw byte + /// offsets. + #[inline] + fn raw_slice_mut(&mut self, r: impl RangeBounds) -> &mut [u8] { + &mut self.data[(r.start_bound().map(|i| i.0), r.end_bound().map(|i| i.0))] } /// The current length of "active" data in the buffer (i.e. not including the gap) #[inline] - pub fn len(&self) -> ByteIdx { + pub fn len(&self) -> usize { self.cap - self.gap() } @@ -269,13 +290,13 @@ impl GapBuffer { /// Rearrange the internal storage of this buffer so that the active data is in a single /// contiguous slice which is then returned. pub fn make_contiguous(&mut self) -> &[u8] { - self.move_gap_to(0); - &self.data[self.gap_end..] + self.move_gap_to(ByteIdx(0)); + self.raw_slice(self.gap_end..) } /// Whether or not the full data within the buffer is contiguous (all on one side of the gap). pub fn is_contiguous(&self) -> bool { - self.gap_start == 0 || self.gap_end == self.cap + self.gap_start == RawIdx(0) || self.gap_end.0 == self.cap } /// The contents of the buffer as a single `&str`. @@ -296,13 +317,14 @@ impl GapBuffer { /// You must call [GapBuffer::make_contiguous] before calling this method. pub unsafe fn substr_from(&self, byte_offset: ByteIdx) -> &str { // SAFETY: See above - let (start, end) = if self.gap_start == 0 { - (self.gap_end + byte_offset, self.cap) + let (start, end) = if self.gap_start == RawIdx(0) { + (self.gap_end.uadd(byte_offset.0), RawIdx(self.cap)) } else { - (byte_offset, self.gap_start) + // The data before the gap has equal logical and raw offsets + (RawIdx(byte_offset.0), self.gap_start) }; // SAFETY: We know that buffer is contiguous and that indices are within bounds - unsafe { std::str::from_utf8_unchecked(&self.data[start..end]) } + unsafe { std::str::from_utf8_unchecked(self.raw_slice(start..end)) } } /// Assume that the gap is at 0 and return the full contents of the inner buffer as a slice of @@ -311,7 +333,7 @@ impl GapBuffer { /// # Safety /// You must call [GapBuffer::make_contiguous] before calling this method. pub unsafe fn assume_contiguous_bytes(&self) -> &[u8] { - &self.data[self.gap_end..] + self.raw_slice(self.gap_end..) } /// Assume that the gap is at 0 and return the full contents of the inner buffer as a &str @@ -320,7 +342,7 @@ impl GapBuffer { /// You must call [GapBuffer::make_contiguous] before calling this method. pub unsafe fn assume_contiguous_str(&self) -> &str { // SAFETY: See above - unsafe { std::str::from_utf8_unchecked(&self.data[self.gap_end..]) } + unsafe { std::str::from_utf8_unchecked(self.raw_slice(self.gap_end..)) } } /// The contents of the buffer either side of the current gap as &str slices. @@ -345,7 +367,10 @@ impl GapBuffer { /// If [make_contiguous][GapBuffer::make_contiguous] was previously called, the first slice /// will be empty and the full content of the buffer will be in the second slice. pub fn as_byte_slices(&self) -> (&[u8], &[u8]) { - (&self.data[0..self.gap_start], &self.data[self.gap_end..]) + ( + self.raw_slice(..self.gap_start), + self.raw_slice(self.gap_end..), + ) } /// Whether or not the contents of the buffer end with a final newline character. @@ -371,14 +396,14 @@ impl GapBuffer { /// Iterate over the lines of the buffer pub fn iter_lines(&self) -> impl Iterator> { - let mut line_idx = 0; + let mut line_idx = LineIdx(0); std::iter::from_fn(move || { - if line_idx == self.len_lines() { + if line_idx.0 == self.len_lines() { return None; } let slice = self.line(line_idx); - line_idx += 1; + line_idx = line_idx.uadd(1); Some(slice) }) @@ -386,26 +411,14 @@ impl GapBuffer { /// The number of lines within the buffer #[inline] - pub fn len_lines(&self) -> usize { - match self.line_endings.last() { - Some(&(raw_idx, _)) => { - let n = self.line_endings.len(); - let byte_idx = if raw_idx > self.gap_start { - raw_idx - self.gap_end - } else { - raw_idx - }; - - if byte_idx < self.len() { n + 1 } else { n } - } - - None => 1, - } + pub const fn len_lines(&self) -> usize { + // The final line is always counted, even when it is empty following a trailing newline + self.line_endings.len() + 1 } /// The number of characters in the buffer #[inline] - pub fn len_chars(&self) -> CharIdx { + pub const fn len_chars(&self) -> usize { self.n_chars } @@ -414,17 +427,17 @@ impl GapBuffer { /// For lines with a trailing newline this is the position of the newline character, /// for the final line (if there is no trailing newline) this is the end of the buffer. #[inline] - pub fn line_end_byte(&self, line_idx: usize) -> ByteIdx { - if line_idx < self.line_endings.len() { - self.raw_byte_to_byte(self.line_endings[line_idx].0) + pub fn line_end_byte(&self, line_idx: LineIdx) -> ByteIdx { + if line_idx.0 < self.line_endings.len() { + self.raw_byte_to_byte(self.line_endings[line_idx.0].0) } else { - self.len() + ByteIdx(self.len()) } } - pub fn lines_before_byte_offset(&self, byte_offset: ByteIdx) -> usize { + pub fn lines_before_byte_offset(&self, byte_offset: ByteIdx) -> LineIdx { let raw_offset = self.byte_to_raw_byte(byte_offset); - self.line_endings.partition_point(|(b, _)| *b < raw_offset) + LineIdx(self.line_endings.partition_point(|(b, _)| *b < raw_offset)) } /// Clear the contents of the buffer. @@ -434,8 +447,8 @@ impl GapBuffer { /// allocation in any way. It simply resets internal state so that it behaves like an empty /// initial buffer. pub fn clear(&mut self) { - self.move_gap_to(0); - self.gap_end = self.cap; + self.move_gap_to(ByteIdx(0)); + self.gap_end = RawIdx(self.cap); self.line_endings.clear(); self.n_chars = 0; @@ -452,13 +465,13 @@ impl GapBuffer { let raw_idx = self.char_to_raw_byte(char_idx); // SAFETY: we know that we have valid utf8 data internally - unsafe { decode_char_at(raw_idx, &self.data) } + unsafe { decode_char_at(raw_idx.0, &self.data) } } /// The character at the specified character index. #[inline] pub fn get_char(&self, char_idx: CharIdx) -> Option { - if char_idx < self.n_chars { + if char_idx.0 < self.n_chars { Some(self.char(char_idx)) } else { None @@ -471,16 +484,21 @@ impl GapBuffer { /// This method will panic if the given byte index is out of bounds #[inline] pub fn char_at(&self, byte_idx: ByteIdx) -> char { - let raw_idx = self.byte_to_raw_byte(byte_idx); + let mut raw_idx = self.byte_to_raw_byte(byte_idx); + // A logical offset at the start of the gap maps to gap_start, so the character we want is + // the one immediately after the gap at gap_end. + if raw_idx == self.gap_start { + raw_idx = self.gap_end; + } // SAFETY: we know that we have valid utf8 data internally - unsafe { decode_char_at(raw_idx, &self.data) } + unsafe { decode_char_at(raw_idx.0, &self.data) } } /// The character at the specified byte index. #[inline] pub fn get_char_at(&self, byte_idx: ByteIdx) -> Option { - if byte_idx < self.len() { + if byte_idx.0 < self.len() { Some(self.char_at(byte_idx)) } else { None @@ -490,7 +508,7 @@ impl GapBuffer { #[inline] fn char_len(&self, raw_idx: RawIdx) -> usize { // SAFETY: we know that we have valid utf8 data internally - unsafe { decode_char_at(raw_idx, &self.data) }.len_utf8() + unsafe { decode_char_at(raw_idx.0, &self.data) }.len_utf8() } /// The requested line as a [Slice]. @@ -498,8 +516,8 @@ impl GapBuffer { /// # Panics /// This method will panic if the given line index is out of bounds #[inline] - pub fn line(&self, line_idx: usize) -> Slice<'_> { - if line_idx >= self.len_lines() { + pub fn line(&self, line_idx: LineIdx) -> Slice<'_> { + if line_idx.0 >= self.len_lines() { assert_line_endings!(self); panic!( "line index was {line_idx} but buffer has {} lines", @@ -507,18 +525,18 @@ impl GapBuffer { ) } - let to = if line_idx < self.line_endings.len() { - let idx = self.line_endings[line_idx].0; - idx + self.char_len(idx) + let to = if line_idx.0 < self.line_endings.len() { + let idx = self.line_endings[line_idx.0].0; + idx.uadd(self.char_len(idx)) } else { - self.cap + RawIdx(self.cap) }; - let from = if line_idx == 0 { - 0 + let from = if line_idx == LineIdx(0) { + RawIdx(0) } else { - let idx = self.line_endings[line_idx - 1].0; - idx + 1 + let idx = self.line_endings[line_idx.0 - 1].0; + idx.uadd(1) }; if from == to { @@ -531,7 +549,7 @@ impl GapBuffer { /// Returns true if the requested line is empty or only contains a single trailing newline. /// /// See [line][Self::line] for panic details. - pub fn line_is_blank(&self, line_idx: usize) -> bool { + pub fn line_is_blank(&self, line_idx: LineIdx) -> bool { matches!( self.line(line_idx).as_strs(), ("", "") | ("", "\n") | ("\n", "") @@ -543,8 +561,8 @@ impl GapBuffer { /// # Panics /// This method will panic if the given line index is out of bounds #[inline] - pub fn line_len_chars(&self, line_idx: usize) -> CharIdx { - if line_idx >= self.len_lines() { + pub fn line_len_chars(&self, line_idx: LineIdx) -> usize { + if line_idx.0 >= self.len_lines() { assert_line_endings!(self); panic!( "line index was {line_idx} but buffer has {} lines", @@ -554,33 +572,33 @@ impl GapBuffer { let chars_to = if self.line_endings.is_empty() { return self.n_chars; - } else if line_idx < self.line_endings.len() { - self.line_endings[line_idx].1 + 1 + } else if line_idx.0 < self.line_endings.len() { + self.line_endings[line_idx.0].1.uadd(1) } else { - self.n_chars + CharIdx(self.n_chars) }; - let chars_from = if line_idx == 0 { - 0 + let chars_from = if line_idx == LineIdx(0) { + CharIdx(0) } else { - self.line_endings[line_idx - 1].1 + 1 + self.line_endings[line_idx.0 - 1].1.uadd(1) }; - chars_to - chars_from + (chars_to - chars_from).0 } /// Primarily intended for supplying contiguous ranges of bytes to tree-sitter when /// parsing. Returns a byte slice from the underlying data buffer without entering /// the gap. pub fn maximal_slice_from_offset(&self, byte_offset: ByteIdx) -> &[u8] { - if byte_offset > self.len() { + if byte_offset.0 > self.len() { &[] } else { let i = self.byte_to_raw_byte(byte_offset); match i.cmp(&self.gap_start) { - Ordering::Less => &self.data[i..self.gap_start], - Ordering::Equal => &self.data[self.gap_end..], - Ordering::Greater => &self.data[i..], + Ordering::Less => self.raw_slice(i..self.gap_start), + Ordering::Equal => self.raw_slice(self.gap_end..), + Ordering::Greater => self.raw_slice(i..), } } } @@ -610,23 +628,14 @@ impl GapBuffer { } pub fn as_slice(&self) -> Slice<'_> { - self.slice(0, self.len_chars()) - } - - pub fn chars_in_raw_range(&self, raw_from: RawIdx, raw_to: RawIdx) -> usize { - if raw_to <= self.gap_start || raw_from >= self.gap_end { - count_chars(&self.data[raw_from..raw_to]) - } else { - count_chars(&self.data[raw_from..self.gap_start]) - + count_chars(&self.data[self.gap_end..raw_to]) - } + self.slice(CharIdx(0), CharIdx(self.len_chars())) } /// Convert a character index to the index of the line containing it /// /// # Panics /// This method will panic if the given char index is out of bounds - pub fn char_to_line(&self, char_idx: CharIdx) -> usize { + pub fn char_to_line(&self, char_idx: CharIdx) -> LineIdx { match self.try_char_to_line(char_idx) { Some(line_idx) => line_idx, None => { @@ -640,18 +649,20 @@ impl GapBuffer { } /// Convert a character index to the index of the line containing it - pub fn try_char_to_line(&self, char_idx: CharIdx) -> Option { - match char_idx.cmp(&self.n_chars) { + pub fn try_char_to_line(&self, char_idx: CharIdx) -> Option { + match char_idx.0.cmp(&self.n_chars) { Ordering::Less => { - if char_idx < self.n_chars { - Some(self.line_endings.partition_point(|(_, i)| *i < char_idx)) + if char_idx.0 < self.n_chars { + Some(LineIdx( + self.line_endings.partition_point(|(_, i)| *i < char_idx), + )) } else { - Some(self.len_lines() - 1) + Some(LineIdx(self.len_lines() - 1)) } } // We allow setting the cursor to the end of the buffer for inserts - Ordering::Equal => Some(self.len_lines() - 1), + Ordering::Equal => Some(LineIdx(self.len_lines() - 1)), Ordering::Greater => None, } @@ -661,7 +672,7 @@ impl GapBuffer { /// /// # Panics /// This method will panic if the given char index is out of bounds - pub fn line_to_char(&self, line_idx: usize) -> CharIdx { + pub fn line_to_char(&self, line_idx: LineIdx) -> CharIdx { match self.try_line_to_char(line_idx) { Some(char_idx) => char_idx, None => { @@ -675,16 +686,16 @@ impl GapBuffer { } /// Convert a line index to the character index of its first character - pub fn try_line_to_char(&self, line_idx: usize) -> Option { - if line_idx > self.len_lines() - 1 { + pub fn try_line_to_char(&self, line_idx: LineIdx) -> Option { + if line_idx.0 > self.len_lines() - 1 { return None; } - if line_idx == 0 { - Some(0) + if line_idx == LineIdx(0) { + Some(CharIdx(0)) } else { - let k = self.line_endings[line_idx - 1].1; - Some(k + 1) + let k = self.line_endings[line_idx.0 - 1].1; + Some(k.uadd(1)) } } @@ -692,11 +703,11 @@ impl GapBuffer { /// /// # Panics /// This method will panic if the given byte index is out of bounds - pub fn line_to_byte(&self, line_idx: usize) -> ByteIdx { - let raw = if line_idx == 0 { - 0 + pub fn line_to_byte(&self, line_idx: LineIdx) -> ByteIdx { + let raw = if line_idx == LineIdx(0) { + RawIdx(0) } else { - self.line_endings[line_idx - 1].0 + 1 + self.line_endings[line_idx.0 - 1].0.uadd(1) }; self.raw_byte_to_byte(raw) @@ -713,15 +724,19 @@ impl GapBuffer { } let idx = self.char_to_byte(char_idx); - self.move_gap_to(idx); + let raw_idx = self.move_gap_to(idx); - ch.encode_utf8(&mut self.data[self.gap_start..]); - self.gap_start += len; + ch.encode_utf8(self.raw_slice_mut(self.gap_start..)); + self.gap_start = self.gap_start.uadd(len); self.n_chars += 1; - let endings: &[(RawIdx, CharIdx)] = if ch == '\n' { &[(idx, char_idx)] } else { &[] }; + let endings: &[(RawIdx, CharIdx)] = if ch == '\n' { + &[(raw_idx, char_idx)] + } else { + &[] + }; - self.add_line_endings(idx, 1, endings); + self.add_line_endings(raw_idx, 1, endings); self.char_to_byte_cache.clear(); #[cfg(test)] @@ -740,20 +755,21 @@ impl GapBuffer { } let idx = self.char_to_byte(char_idx); - self.move_gap_to(idx); + let raw_idx = self.move_gap_to(idx); - self.data[self.gap_start..self.gap_start + len].copy_from_slice(s.as_bytes()); - self.gap_start += len; + self.raw_slice_mut(self.gap_start..self.gap_start.uadd(len)) + .copy_from_slice(s.as_bytes()); + self.gap_start = self.gap_start.uadd(len); self.n_chars += len_chars; let endings: Vec<_> = s .char_indices() .enumerate() .filter(|(_, (_, ch))| *ch == '\n') - .map(|(i, (offset, _))| (idx + offset, char_idx + i)) + .map(|(i, (offset, _))| (raw_idx.uadd(offset), char_idx.uadd(i))) .collect(); - self.add_line_endings(idx, len_chars, &endings); + self.add_line_endings(raw_idx, len_chars, &endings); self.char_to_byte_cache.clear(); #[cfg(test)] @@ -766,16 +782,11 @@ impl GapBuffer { let idx = self.raw_byte_to_byte(raw_idx); let len = self.char_len(raw_idx); - if idx != self.gap_start { - self.move_gap_to(idx); - } else { - self.char_to_byte_cache.clear(); - } - - self.gap_end += len; + let gap_start = self.move_gap_to(idx); + self.gap_end = self.gap_end.uadd(len); self.n_chars -= 1; - self.remove_line_endings(idx, idx + len, 1); + self.remove_line_endings(gap_start, gap_start.uadd(len), 1); self.char_to_byte_cache.clear(); #[cfg(test)] @@ -800,14 +811,14 @@ impl GapBuffer { let from = self.raw_byte_to_byte(raw_from); let to = self.offset_char_to_byte(char_to, raw_from, char_from); debug_assert!(from < to, "invalid byte range: from={from} > to={to}"); - self.move_gap_to(from); + let gap_start = self.move_gap_to(from); let n_bytes = to - from; - let n_chars = char_to - char_from; + let n_chars = (char_to - char_from).0; - self.remove_line_endings(from, self.byte_to_raw_byte(to), n_chars); + self.remove_line_endings(gap_start, self.byte_to_raw_byte(to), n_chars); - self.gap_end += n_bytes; + self.gap_end = self.gap_end.uadd(n_bytes.0); self.n_chars -= n_chars; self.char_to_byte_cache.clear(); @@ -825,7 +836,7 @@ impl GapBuffer { ) { let idx = self.line_endings.partition_point(|(b, _)| *b < raw_from); for (_, c) in &mut self.line_endings[idx..] { - *c += n_chars; + *c = c.uadd(n_chars); } self.line_endings @@ -863,7 +874,7 @@ impl GapBuffer { } for (_, c) in &mut self.line_endings[line_from..] { - *c -= n_chars; + *c = c.usub(n_chars); } } @@ -876,19 +887,19 @@ impl GapBuffer { let cap = self.cap + self.next_gap + n; let mut buf = Vec::with_capacity(cap); - buf.extend_from_slice(&self.data[..self.gap_start]); // data to gap + buf.extend_from_slice(self.raw_slice(..self.gap_start)); // data to gap buf.resize(buf.len() + self.next_gap + self.gap() + n, 0); // the new gap (zeroed) - buf.extend_from_slice(&self.data[self.gap_end..]); // data after gap + buf.extend_from_slice(self.raw_slice(self.gap_end..)); // data after gap let start = self.gap_start; let from = self.line_endings.partition_point(|(b, _)| *b <= start); for (b, _) in &mut self.line_endings[from..] { - *b += gap_increase; + *b = b.uadd(gap_increase); } self.next_gap = clamp_gap_size(self.len(), self.next_gap * 2); self.data = buf.into_boxed_slice(); - self.gap_end += gap_increase; + self.gap_end = self.gap_end.uadd(gap_increase); self.cap = cap; self.char_to_byte_cache.clear(); @@ -896,38 +907,42 @@ impl GapBuffer { assert_line_endings!(self); } - /// The byte_idx argument here is an absolute position within the "live" buffer which will mark - /// the first byte of the gap region following the move. + /// Move the gap region to start at the provided logical [ByteIdx], returning the new [RawIdx] + /// gap_start position. /// /// We do not require that the data within the gap region is valid utf-8 so it is fine for this /// offset to land in the middle of existing multi-byte characters so long as the regions - /// outside of the gap stay valid utf-8. + /// outside of the gap remain valid utf-8. /// /// # Panics /// This method will panic if the given index is out of bounds - fn move_gap_to(&mut self, byte_idx: ByteIdx) { + fn move_gap_to(&mut self, byte_idx: ByteIdx) -> RawIdx { // we need space to fit the current gap size assert!( - byte_idx <= self.len(), + byte_idx.0 <= self.len(), "index out of bounds: {byte_idx} > {}", self.len() ); + // Once we move the gap we are guaranteed that the values of the logical ByteIdx and RawIdx + // positions for the start of the gap are identical so we can safely use the logical index + // as a raw index into the buffer. + let raw_idx = RawIdx(byte_idx.0); let gap = self.gap(); - let (src, dest) = match byte_idx.cmp(&self.gap_start) { - Ordering::Equal => return, + let (src, dest) = match raw_idx.cmp(&self.gap_start) { + Ordering::Equal => return self.gap_start, // Gap moving left Ordering::Less => { let start = self.gap_start; - let from = self.line_endings.partition_point(|(b, _)| *b < byte_idx); + let from = self.line_endings.partition_point(|(b, _)| *b < raw_idx); let to = self.line_endings.partition_point(|(b, _)| *b <= start); for (b, _) in &mut self.line_endings[from..to] { - *b += gap; + *b = b.uadd(gap); } - (byte_idx..self.gap_start, byte_idx + gap) + (raw_idx..self.gap_start, raw_idx.uadd(gap)) } // Gap moving right @@ -936,22 +951,24 @@ impl GapBuffer { let from = self.line_endings.partition_point(|(b, _)| *b < end); let to = self .line_endings - .partition_point(|(b, _)| *b < byte_idx + gap); + .partition_point(|(b, _)| *b < raw_idx.uadd(gap)); for (b, _) in &mut self.line_endings[from..to] { - *b -= gap; + *b = b.usub(gap); } - (self.gap_end..byte_idx + gap, self.gap_start) + (self.gap_end..raw_idx.uadd(gap), self.gap_start) } }; - self.data.copy_within(src, dest); - self.gap_end = byte_idx + gap; - self.gap_start = byte_idx; + self.data.copy_within(src.start.0..src.end.0, dest.0); + self.gap_end = raw_idx.uadd(gap); + self.gap_start = raw_idx; self.char_to_byte_cache.clear(); #[cfg(test)] assert_line_endings!(self); + + self.gap_start } /// Convert a logical byte offset into a character offset within the buffer. @@ -964,9 +981,9 @@ impl GapBuffer { /// general editor functionality so we don't mind being a little slower in order to keep the /// logic easier to reason about pub fn byte_to_char(&self, byte_idx: ByteIdx) -> CharIdx { - let mut to: RawIdx = usize::MAX; + let mut to = RawIdx::MAX; let raw_byte_idx = self.byte_to_raw_byte(byte_idx); - let (mut raw_byte_offset, mut char_offset): (RawIdx, CharIdx) = (0, 0); + let (mut raw_byte_offset, mut char_offset): (RawIdx, CharIdx) = (RawIdx(0), CharIdx(0)); // Determine which line the character lies in based on the byte index, skipping all // lines that are before the byte offset we were given. @@ -991,8 +1008,8 @@ impl GapBuffer { if byte_cur == byte_idx { break; } - byte_cur += ch.len_utf8(); - cur += 1; + byte_cur = byte_cur.uadd(ch.len_utf8()); + cur = cur.uadd(1); } cur @@ -1004,7 +1021,7 @@ impl GapBuffer { /// account for the position of the gap. #[inline] pub fn char_to_byte(&self, char_idx: CharIdx) -> ByteIdx { - self.offset_char_to_byte(char_idx, 0, 0) + self.offset_char_to_byte(char_idx, RawIdx(0), CharIdx(0)) } /// Convert a character range to a byte range @@ -1019,18 +1036,18 @@ impl GapBuffer { #[inline] fn raw_byte_to_byte(&self, raw: RawIdx) -> ByteIdx { if raw > self.gap_start { - raw - self.gap() + ByteIdx(raw.0 - self.gap()) } else { - raw + ByteIdx(raw.0) } } #[inline] - pub fn byte_to_raw_byte(&self, byte: ByteIdx) -> RawIdx { - if byte > self.gap_start { - byte + self.gap() + pub(super) fn byte_to_raw_byte(&self, byte: ByteIdx) -> RawIdx { + if byte.0 > self.gap_start.0 { + RawIdx(byte.0 + self.gap()) } else { - byte + RawIdx(byte.0) } } @@ -1051,7 +1068,7 @@ impl GapBuffer { /// into our allocated array that accounts for the position of the gap. #[inline] fn char_to_raw_byte(&self, char_idx: CharIdx) -> RawIdx { - self.offset_char_to_raw_byte(char_idx, 0, 0) + self.offset_char_to_raw_byte(char_idx, RawIdx(0), CharIdx(0)) } /// Allow for skipping to a given raw byte index before starting the search as an optimisation @@ -1069,7 +1086,7 @@ impl GapBuffer { // Empty buffer is a special case: the only valid char indices are 0 or 1 which maps to gap_start/EOF if self.n_chars == 0 { assert!( - char_idx == 0 || char_idx == 1, + char_idx == CharIdx(0) || char_idx == CharIdx(1), "char index should be 0 or 1 for an empty buffer" ); return self.gap_start; @@ -1078,13 +1095,13 @@ impl GapBuffer { // When looking for the last character of a buffer containing a single line we can decode // backwards from either the start of the gap or the end of the raw buffer depending on // where the gap currently lies. - if self.line_endings.is_empty() && char_idx == self.len_chars().saturating_sub(1) { - if self.gap_end == self.cap && self.gap_start > 0 { - let ch_end = self.gap_start - 1; + if self.line_endings.is_empty() && char_idx.0 == self.len_chars().saturating_sub(1) { + if self.gap_end.0 == self.cap && self.gap_start > RawIdx(0) { + let ch_end = self.gap_start.usub(1); // SAFETY: we know that we have valid utf-8 data immediately before the gap - let ch = unsafe { decode_char_ending_at(ch_end, &self.data) }; + let ch = unsafe { decode_char_ending_at(ch_end.0, &self.data) }; - let i = self.gap_start.saturating_sub(ch.len_utf8()); + let i = self.gap_start.saturating_sub(RawIdx(ch.len_utf8())); self.char_to_byte_cache.insert(char_idx, i); return i; } else { @@ -1092,13 +1109,13 @@ impl GapBuffer { // self.gap_end != self.cap, so decoding the final character is valid let ch = unsafe { decode_char_ending_at(self.cap - 1, &self.data) }; - let i = self.cap.saturating_sub(ch.len_utf8()); + let i = RawIdx(self.cap.saturating_sub(ch.len_utf8())); self.char_to_byte_cache.insert(char_idx, i); return i; }; } - let mut to: RawIdx = usize::MAX; + let mut to = RawIdx::MAX; // Determine which line the character lies in based on the character index, skipping all // lines that are before the byte offset we were given. @@ -1126,15 +1143,15 @@ impl GapBuffer { let slice = Slice::from_raw_offsets(raw_offset, to, self); let mut chars = slice.chars(); let mut cur: RawIdx = raw_offset; - for _ in 0..(char_idx - char_offset) { + for _ in 0..(char_idx - char_offset).0 { chars.next(); - cur = chars.cur + raw_offset; + cur = raw_offset.uadd(chars.cur); } let slice_enclosed_gap = self.gap_start >= raw_offset && self.gap_end <= to; // Cur landed inside the gap or we counted over the gap while iterating the slice above if cur >= self.gap_start && (cur < self.gap_end || slice_enclosed_gap) { - cur += self.gap(); + cur = cur.uadd(self.gap()); } self.char_to_byte_cache.insert(char_idx, cur); @@ -1146,7 +1163,7 @@ impl GapBuffer { /// test. #[cfg(test)] fn shred_gap(&mut self) { - for b in self.data[self.gap_start..self.gap_end].iter_mut() { + for b in self.raw_slice_mut(self.gap_start..self.gap_end).iter_mut() { *b = b'\0'; } } @@ -1155,20 +1172,20 @@ impl GapBuffer { #[cfg(test)] fn debug_buffer_content(gb: &GapBuffer) -> String { let mut v = gb.data.to_vec(); - v[gb.gap_start..gb.gap_end].copy_from_slice("_".repeat(gb.gap()).as_bytes()); + v[gb.gap_start.0..gb.gap_end.0].copy_from_slice("_".repeat(gb.gap()).as_bytes()); String::from_utf8(v).expect("valid utf8") } #[cfg(test)] fn raw_debug_buffer_content(gb: &GapBuffer) -> String { let mut v = gb.data.to_vec(); - for b in v[gb.gap_start..gb.gap_end].iter_mut() { + for b in v[gb.gap_start.0..gb.gap_end.0].iter_mut() { if *b == b'\0' { *b = b'_'; } } - v.insert(gb.gap_end, b']'); - v.insert(gb.gap_start, b'['); + v.insert(gb.gap_end.0, b']'); + v.insert(gb.gap_start.0, b'['); String::from_utf8(v).expect("valid utf8") } @@ -1197,7 +1214,7 @@ mod tests { #[test] fn insert_into_empty_string_initial_gb_works() { let mut gb = GapBuffer::from(String::new()); - gb.insert_char(0, 'a'); + gb.insert_char(CharIdx(0), 'a'); gb.shred_gap(); assert_eq!(gb.to_string(), "a"); @@ -1219,12 +1236,12 @@ mod tests { "char iter len != len_chars" ); assert_eq!( - gb.line(0).chars().count(), - gb.line_len_chars(0), + gb.line(LineIdx(0)).chars().count(), + gb.line_len_chars(LineIdx(0)), "line_len_chars != len_chars" ); - gb.insert_char(5, 'X'); + gb.insert_char(CharIdx(5), 'X'); gb.shred_gap(); println!("after insert X: {:?}", raw_debug_buffer_content(&gb)); @@ -1235,7 +1252,7 @@ mod tests { "char iter len != len_chars" ); - gb.insert_char(3, '界'); + gb.insert_char(CharIdx(3), '界'); gb.shred_gap(); println!("after insert 界: {:?}", raw_debug_buffer_content(&gb)); assert_eq!(gb.len_chars(), len_s + 2); @@ -1245,8 +1262,8 @@ mod tests { "char iter len != len_chars" ); - assert_eq!(gb.char(3), '界'); - gb.remove_char(3); + assert_eq!(gb.char(CharIdx(3)), '界'); + gb.remove_char(CharIdx(3)); gb.shred_gap(); println!("after remove 界: {:?}", raw_debug_buffer_content(&gb)); assert_eq!(gb.len_chars(), len_s + 1); @@ -1256,8 +1273,8 @@ mod tests { "char iter len != len_chars" ); - assert_eq!(gb.char(5), 'X'); - gb.remove_char(5); + assert_eq!(gb.char(CharIdx(5)), 'X'); + gb.remove_char(CharIdx(5)); gb.shred_gap(); println!("after remove X: {:?}", debug_buffer_content(&gb)); assert_eq!(gb.len_chars(), len_s); @@ -1276,7 +1293,7 @@ mod tests { fn line_len_chars_works(line_idx: usize, expected: usize) { let gb = GapBuffer::from("hello, world!\nhow are you?\nthis is a test"); - assert_eq!(gb.line_len_chars(line_idx), expected); + assert_eq!(gb.line_len_chars(LineIdx(line_idx)), expected); } #[test_case("foo│foo│foo"; "interleaved multibyte and ascii")] @@ -1287,15 +1304,15 @@ mod tests { let mut gb = GapBuffer::from(s); for i in 0..gb.len_chars() { - let idx = gb.char_to_byte(i); + let idx = gb.char_to_byte(CharIdx(i)); gb.move_gap_to(idx); gb.shred_gap(); // Splitting into the two sections like this allows us to verify that // we have valid utf-8 encoded text on either side of the gap. let (s1, s2) = ( - std::str::from_utf8(&gb.data[..gb.gap_start]).unwrap(), - std::str::from_utf8(&gb.data[gb.gap_end..]).unwrap(), + std::str::from_utf8(gb.raw_slice(..gb.gap_start)).unwrap(), + std::str::from_utf8(gb.raw_slice(gb.gap_end..)).unwrap(), ); assert_eq!(format!("{s1}{s2}"), s, "idx={idx}"); @@ -1309,14 +1326,43 @@ mod tests { assert_eq!(gb.len_lines(), 3); for i in 0..gb.len_chars() { - let idx = gb.char_to_byte(i); + let idx = gb.char_to_byte(CharIdx(i)); gb.move_gap_to(idx); gb.shred_gap(); assert_eq!(gb.len_lines(), 3); - assert_eq!(gb.line(0).to_string(), "hello, world!\n", "idx={idx}"); - assert_eq!(gb.line(1).to_string(), "how are you?\n", "idx={idx}"); - assert_eq!(gb.line(2).to_string(), "this is a test", "idx={idx}"); + assert_eq!( + gb.line(LineIdx(0)).to_string(), + "hello, world!\n", + "idx={idx}" + ); + assert_eq!( + gb.line(LineIdx(1)).to_string(), + "how are you?\n", + "idx={idx}" + ); + assert_eq!( + gb.line(LineIdx(2)).to_string(), + "this is a test", + "idx={idx}" + ); + } + } + + // Regression test for char_at returning data from within the gap region. + // + // char_at needs to special case byte_idx == gap_start as the logical byte being targeted + // there is actually gap_end rather than the first byte of the gap itself. + #[test] + fn char_at_gap_returns_correct_value() { + let s = "hello"; + let mut gb = GapBuffer::from("hello"); + + for (idx, ch) in s.chars().enumerate() { + gb.move_gap_to(ByteIdx(idx)); + gb.shred_gap(); + + assert_eq!(gb.char_at(ByteIdx(idx)), ch); } } @@ -1334,11 +1380,16 @@ mod tests { let mut gb = GapBuffer::from(s); assert_eq!(s.len(), 27, "EOF case is not 0..s.len()"); assert_eq!("世".len(), 3); - gb.move_gap_to(cur); + gb.move_gap_to(ByteIdx(cur)); gb.shred_gap(); - let byte_idx = gb.char_to_byte(char_idx); - assert_eq!(byte_idx, expected, "{:?}", debug_buffer_content(&gb)); + let byte_idx = gb.char_to_byte(CharIdx(char_idx)); + assert_eq!( + byte_idx, + ByteIdx(expected), + "{:?}", + debug_buffer_content(&gb) + ); } // Regression test for (gh#140) @@ -1349,11 +1400,11 @@ mod tests { #[test] fn char_to_byte_works_with_leading_multibyte_char(s: &str) { let mut gb = GapBuffer::new(); - gb.insert_str(0, s); + gb.insert_str(CharIdx(0), s); gb.shred_gap(); - let byte_idx = gb.char_to_byte(0); - assert_eq!(byte_idx, 0, "{:?}", debug_buffer_content(&gb)); + let byte_idx = gb.char_to_byte(CharIdx(0)); + assert_eq!(byte_idx, ByteIdx(0), "{:?}", debug_buffer_content(&gb)); } // Regression test for (gh#140) @@ -1368,12 +1419,17 @@ mod tests { expected: usize, ) { let mut gb = GapBuffer::new(); - gb.insert_str(0, s); + gb.insert_str(CharIdx(0), s); gb.shred_gap(); let last_char = s.chars().count() - 1; - let byte_idx = gb.char_to_byte(last_char); - assert_eq!(byte_idx, expected, "{:?}", debug_buffer_content(&gb)); + let byte_idx = gb.char_to_byte(CharIdx(last_char)); + assert_eq!( + byte_idx, + ByteIdx(expected), + "{:?}", + debug_buffer_content(&gb) + ); } #[test_case(0, 0, 64, 'h'; "BOF cur at BOF")] @@ -1390,14 +1446,19 @@ mod tests { let mut gb = GapBuffer::from(s); assert_eq!(s.len(), 27, "EOF case is not 0..s.len()"); assert_eq!("世".len(), 3); - gb.move_gap_to(cur); + gb.move_gap_to(ByteIdx(cur)); gb.shred_gap(); - let byte_idx = gb.char_to_raw_byte(char_idx); - assert_eq!(byte_idx, expected, "{:?}", debug_buffer_content(&gb)); + let byte_idx = gb.char_to_raw_byte(CharIdx(char_idx)); + assert_eq!( + byte_idx, + RawIdx(expected), + "{:?}", + debug_buffer_content(&gb) + ); // SAFETY: safe if the test is passing - let ch = unsafe { decode_char_at(byte_idx, &gb.data) }; + let ch = unsafe { decode_char_at(byte_idx.0, &gb.data) }; assert_eq!(ch, expected_ch); } @@ -1410,12 +1471,12 @@ mod tests { let mut gb = GapBuffer::from(s); for pos in 0..gb.len_chars() - 1 { - gb.move_gap_to(gb.char_to_byte(pos)); + gb.move_gap_to(gb.char_to_byte(CharIdx(pos))); gb.shred_gap(); for (ch_idx, (byte_idx, _ch)) in s.char_indices().enumerate() { - let idx = gb.byte_to_char(byte_idx); - assert_eq!(idx, ch_idx, "gap@{pos}"); + let idx = gb.byte_to_char(ByteIdx(byte_idx)); + assert_eq!(idx, CharIdx(ch_idx), "gap"); } } } @@ -1428,9 +1489,12 @@ mod tests { let gb = GapBuffer::from(format!("{l1}{l2}{l3}")); - assert_eq!(gb.line_to_char(0), 0); - assert_eq!(gb.line_to_char(1), l1.chars().count()); - assert_eq!(gb.line_to_char(2), l1.chars().count() + l2.chars().count()); + assert_eq!(gb.line_to_char(LineIdx(0)), CharIdx(0)); + assert_eq!(gb.line_to_char(LineIdx(1)), CharIdx(l1.chars().count())); + assert_eq!( + gb.line_to_char(LineIdx(2)), + CharIdx(l1.chars().count() + l2.chars().count()) + ); } #[test_case(0, 0; "start of first line")] @@ -1450,7 +1514,7 @@ mod tests { let gb = GapBuffer::from(format!("{l1}{l2}{l3}")); - assert_eq!(gb.char_to_line(char_idx), line_idx); + assert_eq!(gb.char_to_line(CharIdx(char_idx)), LineIdx(line_idx)); } #[test_case(&[(0, 'h')], "hello world"; "insert front")] @@ -1465,7 +1529,7 @@ mod tests { let mut gb = GapBuffer::from("ello world"); for &(idx, ch) in inserts { - gb.insert_char(idx, ch); + gb.insert_char(CharIdx(idx), ch); gb.shred_gap(); } @@ -1475,19 +1539,19 @@ mod tests { #[test] fn insert_char_with_moving_cur() { let mut gb = GapBuffer::from("hello "); - gb.insert_char(6, 'w'); + gb.insert_char(CharIdx(6), 'w'); gb.shred_gap(); - gb.insert_char(7, 'o'); + gb.insert_char(CharIdx(7), 'o'); gb.shred_gap(); - gb.insert_char(8, 'r'); + gb.insert_char(CharIdx(8), 'r'); gb.shred_gap(); - gb.insert_char(9, 'l'); + gb.insert_char(CharIdx(9), 'l'); gb.shred_gap(); - gb.insert_char(10, 'd'); + gb.insert_char(CharIdx(10), 'd'); gb.shred_gap(); - gb.insert_char(11, '!'); + gb.insert_char(CharIdx(11), '!'); gb.shred_gap(); - gb.insert_char(5, ','); + gb.insert_char(CharIdx(5), ','); gb.shred_gap(); assert_eq!( @@ -1505,23 +1569,23 @@ mod tests { assert_eq!(gb.len_lines(), 2); println!("initial: {:?}", raw_debug_buffer_content(&gb)); - gb.insert_char(6, '\n'); + gb.insert_char(CharIdx(6), '\n'); gb.shred_gap(); println!("insert: {:?}", raw_debug_buffer_content(&gb)); assert_eq!(gb.len_lines(), 3); - assert_eq!(gb.line(0).to_string(), "hello,\n"); - assert_eq!(gb.line(1).to_string(), " world!\n"); - assert_eq!(gb.line(2).to_string(), "how are you?"); + assert_eq!(gb.line(LineIdx(0)).to_string(), "hello,\n"); + assert_eq!(gb.line(LineIdx(1)).to_string(), " world!\n"); + assert_eq!(gb.line(LineIdx(2)).to_string(), "how are you?"); for idx in 0..=gb.len_chars() { - gb.move_gap_to(idx); + gb.move_gap_to(ByteIdx(idx)); gb.shred_gap(); assert_eq!(gb.len_lines(), 3); - assert_eq!(gb.line(0).to_string(), "hello,\n", "idx={idx}"); - assert_eq!(gb.line(1).to_string(), " world!\n", "idx={idx}"); - assert_eq!(gb.line(2).to_string(), "how are you?", "idx={idx}"); + assert_eq!(gb.line(LineIdx(0)).to_string(), "hello,\n", "idx={idx}"); + assert_eq!(gb.line(LineIdx(1)).to_string(), " world!\n", "idx={idx}"); + assert_eq!(gb.line(LineIdx(2)).to_string(), "how are you?", "idx={idx}"); } } @@ -1534,7 +1598,7 @@ mod tests { fn insert_str(inserts: &[(usize, &str)], expected: &str) { let mut gb = GapBuffer::from("oworl"); for &(idx, s) in inserts { - gb.insert_str(idx, s); + gb.insert_str(CharIdx(idx), s); gb.shred_gap(); } @@ -1548,19 +1612,31 @@ mod tests { assert_eq!(gb.len_lines(), 2); let s2 = " sailor\nisn't this fun?\nwhat a wonderful\n"; - gb.insert_str(6, s2); + gb.insert_str(CharIdx(6), s2); gb.shred_gap(); for idx in 0..=gb.len_chars() { - gb.move_gap_to(idx); + gb.move_gap_to(ByteIdx(idx)); gb.shred_gap(); assert_eq!(gb.len_lines(), 5); - assert_eq!(gb.line(0).to_string(), "hello, sailor\n", "idx={idx}"); - assert_eq!(gb.line(1).to_string(), "isn't this fun?\n", "idx={idx}"); - assert_eq!(gb.line(2).to_string(), "what a wonderful\n", "idx={idx}"); - assert_eq!(gb.line(3).to_string(), " world!\n", "idx={idx}"); - assert_eq!(gb.line(4).to_string(), "how are you?", "idx={idx}"); + assert_eq!( + gb.line(LineIdx(0)).to_string(), + "hello, sailor\n", + "idx={idx}" + ); + assert_eq!( + gb.line(LineIdx(1)).to_string(), + "isn't this fun?\n", + "idx={idx}" + ); + assert_eq!( + gb.line(LineIdx(2)).to_string(), + "what a wonderful\n", + "idx={idx}" + ); + assert_eq!(gb.line(LineIdx(3)).to_string(), " world!\n", "idx={idx}"); + assert_eq!(gb.line(LineIdx(4)).to_string(), "how are you?", "idx={idx}"); } } @@ -1571,9 +1647,9 @@ mod tests { #[test] fn remove_char(idx: usize, expected: &str) { let mut gb = GapBuffer::from("hello, world!"); - gb.move_gap_to(6); // space before world + gb.move_gap_to(ByteIdx(6)); // space before world gb.shred_gap(); - gb.remove_char(idx); + gb.remove_char(CharIdx(idx)); gb.shred_gap(); assert_eq!(gb.to_string(), expected, "{:?}", debug_buffer_content(&gb)) @@ -1585,11 +1661,11 @@ mod tests { let mut gb = GapBuffer::from(s); assert_eq!(gb.len_lines(), 2); - gb.remove_char(13); + gb.remove_char(CharIdx(13)); gb.shred_gap(); assert_eq!(gb.len_lines(), 1); - assert_eq!(gb.line(0).to_string(), "hello, world!how are you?"); + assert_eq!(gb.line(LineIdx(0)).to_string(), "hello, world!how are you?"); } #[test_case(6, 9, "hello,rld!"; "at gap start")] @@ -1603,9 +1679,9 @@ mod tests { assert_eq!(s.len(), 13, "remove all case is not 0..s.len()"); let mut gb = GapBuffer::from(s); - gb.move_gap_to(6); // space before world + gb.move_gap_to(ByteIdx(6)); // space before world gb.shred_gap(); - gb.remove_range(from, to); + gb.remove_range(CharIdx(from), CharIdx(to)); gb.shred_gap(); assert_eq!(gb.to_string(), expected, "{:?}", debug_buffer_content(&gb)) @@ -1616,17 +1692,17 @@ mod tests { let s = "foo│foo│foo"; let mut gb = GapBuffer::from(s); - gb.remove_range(0, 3); + gb.remove_range(CharIdx(0), CharIdx(3)); gb.shred_gap(); assert_eq!(gb.to_string(), "│foo│foo"); assert_eq!(gb.len_chars(), 8); - gb.remove_range(1, 4); + gb.remove_range(CharIdx(1), CharIdx(4)); gb.shred_gap(); assert_eq!(gb.to_string(), "││foo"); assert_eq!(gb.len_chars(), 5); - gb.remove_range(2, 5); + gb.remove_range(CharIdx(2), CharIdx(5)); gb.shred_gap(); assert_eq!(gb.to_string(), "││"); assert_eq!(gb.len_chars(), 2); @@ -1636,7 +1712,7 @@ mod tests { fn remove_range_for_last_line_works() { let s = "hello, world!\nthis is the last line"; let mut gb = GapBuffer::from(s); - gb.remove_range(14, s.len()); + gb.remove_range(CharIdx(14), CharIdx(s.len())); gb.shred_gap(); assert_eq!(gb.to_string(), "hello, world!\n"); assert_eq!(gb.len_lines(), 2); @@ -1651,12 +1727,12 @@ mod tests { let mut gb = GapBuffer::from(s); assert_eq!(gb.len_lines(), 2); - gb.remove_range(from, to); + gb.remove_range(CharIdx(from), CharIdx(to)); gb.shred_gap(); assert_eq!(gb.len_lines(), 1); assert_eq!(gb.to_string(), expected); - assert_eq!(gb.line(0).to_string(), expected); + assert_eq!(gb.line(LineIdx(0)).to_string(), expected); } #[test_case('X'; "ascii")] @@ -1665,9 +1741,9 @@ mod tests { fn insert_remove_char_is_idempotent(ch: char) { let s = "hello, world!"; let mut gb = GapBuffer::from(s); - gb.insert_char(6, ch); + gb.insert_char(CharIdx(6), ch); gb.shred_gap(); - gb.remove_char(6); + gb.remove_char(CharIdx(6)); gb.shred_gap(); assert_eq!(gb.to_string(), s, "{:?}", debug_buffer_content(&gb)) @@ -1682,49 +1758,28 @@ mod tests { let mut gb = GapBuffer::from(s); println!("initial: {:?}", raw_debug_buffer_content(&gb)); - for n in 0..gb.len_lines() { + for n in LineIdx(0).to(LineIdx(gb.len_lines())) { println!("{:?}", gb.line(n).to_string()); } - gb.insert_str(6, edit); + gb.insert_str(CharIdx(6), edit); gb.shred_gap(); assert_eq!(gb.len_lines(), expected_lines); println!("insert: {:?}", raw_debug_buffer_content(&gb)); - for n in 0..gb.len_lines() { + for n in LineIdx(0).to(LineIdx(gb.len_lines())) { println!("{:?}", gb.line(n).to_string()); } - gb.remove_range(6, 6 + edit.len()); + gb.remove_range(CharIdx(6), CharIdx(6 + edit.len())); gb.shred_gap(); println!("remove: {:?}", raw_debug_buffer_content(&gb)); - for n in 0..gb.len_lines() { + for n in LineIdx(0).to(LineIdx(gb.len_lines())) { println!("{:?}", gb.line(n).to_string()); } assert_eq!(gb.to_string(), s); } - #[test] - fn chars_in_raw_range_works() { - let mut gb = GapBuffer::from("hello, world!\nhow are you?"); - let char_from = 7; - let char_to = 12; - - for i in 0..gb.len_chars() { - let idx = gb.char_to_byte(i); - gb.move_gap_to(idx); - gb.shred_gap(); - - let byte_from = gb.char_to_raw_byte(char_from); - let byte_to = gb.char_to_raw_byte(char_to); - let n_chars = gb.chars_in_raw_range(byte_from, byte_to); - assert_eq!(n_chars, char_to - char_from, "gap at {i}"); - - let n_chars = gb.chars_in_raw_range(0, gb.char_to_raw_byte(gb.n_chars)); - assert_eq!(n_chars, gb.n_chars, "gap at {i}"); - } - } - #[test_case(0, "\n", 2; "insert newline at start")] #[test_case(6, "\n", 2; "insert newline at end of first line")] #[test_case(3, "\n", 2; "insert newline in middle of line")] @@ -1739,7 +1794,7 @@ mod tests { let mut gb = GapBuffer::from(s); assert_eq!(gb.len_lines(), 1); - gb.insert_str(at, insert); + gb.insert_str(CharIdx(at), insert); gb.shred_gap(); assert_eq!(gb.len_lines(), expected_lines); @@ -1761,7 +1816,7 @@ mod tests { let mut gb = GapBuffer::from(s); assert_eq!(gb.len_lines(), 3); - gb.remove_range(from, to); + gb.remove_range(CharIdx(from), CharIdx(to)); gb.shred_gap(); assert_eq!(gb.to_string(), expected); @@ -1770,13 +1825,13 @@ mod tests { fn _insert_chars(gb: &mut GapBuffer, s: &str) { for (idx, ch) in s.chars().enumerate() { - gb.insert_char(idx + 4, ch); + gb.insert_char(CharIdx(idx + 4), ch); gb.shred_gap(); } } fn _insert_str(gb: &mut GapBuffer, s: &str) { - gb.insert_str(4, s); + gb.insert_str(CharIdx(4), s); gb.shred_gap(); } @@ -1784,7 +1839,7 @@ mod tests { #[test_case(_insert_str; "whole string")] #[test] fn insert_with_multibyte_chars_preserves_line_endings(insert: fn(&mut GapBuffer, &str)) { - let slice_str = |gb: &GapBuffer| gb.slice(0, gb.len_chars()).to_string(); + let slice_str = |gb: &GapBuffer| gb.slice(CharIdx(0), CharIdx(gb.len_chars())).to_string(); let mut gb = GapBuffer::from("foo\nbar\nbaz\n"); let s = "世\n界 🦊\n "; @@ -1793,8 +1848,8 @@ mod tests { assert_eq!(slice_str(&gb), "foo\n世\n界 🦊\n bar\nbaz\n"); - assert_eq!(gb.char(8), '🦊'); - gb.remove_char(8); + assert_eq!(gb.char(CharIdx(8)), '🦊'); + gb.remove_char(CharIdx(8)); gb.shred_gap(); assert_eq!(slice_str(&gb), "foo\n世\n界 \n bar\nbaz\n"); @@ -1806,7 +1861,7 @@ mod tests { let gb = GapBuffer::from(s); for (idx, ch) in s.chars().enumerate() { - assert_eq!(gb.char(idx), ch); + assert_eq!(gb.char(CharIdx(idx)), ch); } } @@ -1814,16 +1869,16 @@ mod tests { fn char_to_raw_byte_line_end_with_no_newlines() { let mut gb = GapBuffer::from("// does it need to be a doc comment? that is a long enough line to"); - gb.move_gap_to(0); + gb.move_gap_to(ByteIdx(0)); gb.shred_gap(); - assert_eq!(gb.char_to_raw_byte(65), 129); - gb.move_gap_to(10); + assert_eq!(gb.char_to_raw_byte(CharIdx(65)), RawIdx(129)); + gb.move_gap_to(ByteIdx(10)); gb.shred_gap(); - assert_eq!(gb.char_to_raw_byte(65), 129); - gb.move_gap_to(66); + assert_eq!(gb.char_to_raw_byte(CharIdx(65)), RawIdx(129)); + gb.move_gap_to(ByteIdx(66)); gb.shred_gap(); - assert_eq!(gb.char_to_raw_byte(65), 65); - gb.insert_char(66, '\n'); + assert_eq!(gb.char_to_raw_byte(CharIdx(65)), RawIdx(65)); + gb.insert_char(CharIdx(66), '\n'); gb.shred_gap(); assert_eq!( gb.to_string(), @@ -1836,7 +1891,7 @@ mod tests { #[test] fn as_strs_works(byte_idx: usize, left: &str, right: &str) { let mut gb = GapBuffer::from("foo bar"); - gb.move_gap_to(byte_idx); + gb.move_gap_to(ByteIdx(byte_idx)); gb.shred_gap(); let (l, r) = gb.as_strs(); @@ -1849,13 +1904,13 @@ mod tests { #[test] fn as_str_works(byte_idx: usize) { let mut gb = GapBuffer::from("foo bar"); - gb.move_gap_to(byte_idx); + gb.move_gap_to(ByteIdx(byte_idx)); gb.shred_gap(); let s = gb.as_str(); assert_eq!(s, "foo bar"); - assert_eq!(gb.gap_start, 0); + assert_eq!(gb.gap_start, RawIdx(0)); } #[test] @@ -1865,11 +1920,11 @@ mod tests { gb.make_contiguous(); assert!(gb.is_contiguous()); - assert_eq!(gb.gap_start, 0); - assert_ne!(gb.gap_end, gb.cap); + assert_eq!(gb.gap_start, RawIdx(0)); + assert_ne!(gb.gap_end.0, gb.cap); // SAFETY: buffer is contiguous and offset is valid - let result = unsafe { gb.substr_from(6) }; + let result = unsafe { gb.substr_from(ByteIdx(6)) }; assert_eq!(result, "world"); } @@ -1877,14 +1932,14 @@ mod tests { fn substr_from_works_when_gap_at_end() { let mut gb = GapBuffer::from("hello world"); - gb.move_gap_to(gb.len()); + gb.move_gap_to(ByteIdx(gb.len())); assert!(gb.is_contiguous()); - assert_eq!(gb.gap_end, gb.cap); - assert_ne!(gb.gap_start, 0); + assert_eq!(gb.gap_end.0, gb.cap); + assert_ne!(gb.gap_start, RawIdx(0)); // SAFETY: buffer is contiguous and offset is valid - let result = unsafe { gb.substr_from(6) }; + let result = unsafe { gb.substr_from(ByteIdx(6)) }; assert_eq!(result, "world"); } @@ -1892,19 +1947,19 @@ mod tests { fn substr_from_matches_expected_after_appending_to_end() { let mut gb = GapBuffer::from("hello"); - gb.insert_char(5, ' '); - gb.insert_char(6, 'w'); - gb.insert_char(7, 'o'); - gb.insert_char(8, 'r'); - gb.insert_char(9, 'l'); - gb.insert_char(10, 'd'); + gb.insert_char(CharIdx(5), ' '); + gb.insert_char(CharIdx(6), 'w'); + gb.insert_char(CharIdx(7), 'o'); + gb.insert_char(CharIdx(8), 'r'); + gb.insert_char(CharIdx(9), 'l'); + gb.insert_char(CharIdx(10), 'd'); assert!(gb.is_contiguous()); - assert_eq!(gb.gap_end, gb.cap); - assert_ne!(gb.gap_start, 0); + assert_eq!(gb.gap_end.0, gb.cap); + assert_ne!(gb.gap_start, RawIdx(0)); // SAFETY: buffer is contiguous and offset is valid - let result = unsafe { gb.substr_from(6) }; + let result = unsafe { gb.substr_from(ByteIdx(6)) }; assert_eq!(result, "world"); } } diff --git a/src/buffer/internal/slice.rs b/src/buffer/internal/slice.rs index ba248c02..6bf08702 100644 --- a/src/buffer/internal/slice.rs +++ b/src/buffer/internal/slice.rs @@ -32,7 +32,7 @@ impl<'a> From> for Cow<'a, str> { impl<'a> Slice<'a> { pub(super) const NULL: Slice<'a> = Slice { - from: 0, + from: ByteIdx(0), left: &[], right: &[], }; @@ -48,13 +48,13 @@ impl<'a> Slice<'a> { #[inline] pub(super) fn from_raw_offsets(from: RawIdx, to: RawIdx, gb: &'a GapBuffer) -> Slice<'a> { - let to = min(to, gb.data.len()); + let to = min(to, RawIdx(gb.data.len())); let logical_from = gb.raw_byte_to_byte(from); if to <= gb.gap_start || from >= gb.gap_end { return Slice { from: logical_from, - left: &gb.data[from..to], + left: gb.raw_slice(from..to), right: &[], }; } @@ -63,8 +63,8 @@ impl<'a> Slice<'a> { Slice { from: logical_from, - left: &gb.data[from..gb.gap_start], - right: &gb.data[gb.gap_end..to], + left: gb.raw_slice(from..gb.gap_start), + right: gb.raw_slice(gb.gap_end..to), } } @@ -74,7 +74,7 @@ impl<'a> Slice<'a> { } let to = min(to, self.len()); - let logical_from = self.from + from; + let logical_from = self.from.uadd(from); if to <= self.left.len() { Slice { @@ -154,7 +154,7 @@ impl<'a> Slice<'a> { let (cur, idx) = if rev { ( self.left.len() + self.right.len(), - from + count_chars(self.left) + count_chars(self.right), + from.uadd(count_chars(self.left) + count_chars(self.right)), ) } else { (0, from) @@ -303,7 +303,7 @@ impl Iterator for IdxChars<'_> { // SAFETY: we know we are in bounds and that we contain valid utf-8 data let ch = unsafe { decode_char_ending_at(cur, data) }; let len = ch.len_utf8(); - self.idx -= 1; + self.idx = self.idx.usub(1); self.cur -= len; Some((self.idx, ch)) } else { @@ -313,7 +313,7 @@ impl Iterator for IdxChars<'_> { let len = ch.len_utf8(); let res = Some((self.idx, ch)); self.cur += len; - self.idx += 1; + self.idx = self.idx.uadd(1); res } } @@ -322,7 +322,7 @@ impl Iterator for IdxChars<'_> { #[cfg(test)] mod tests { use super::*; - use crate::buffer::internal::raw_debug_buffer_content; + use crate::buffer::internal::{LineIdx, raw_debug_buffer_content}; use simple_test_case::test_case; #[test_case(0, 0, "hello, world!\n"; "first line cur at BOF")] @@ -334,10 +334,10 @@ mod tests { #[test] fn slice_to_string_works(cur: usize, line: usize, expected: &str) { let mut gb = GapBuffer::from("hello, world!\nhow are you?"); - gb.move_gap_to(cur); + gb.move_gap_to(ByteIdx(cur)); gb.shred_gap(); - assert_eq!(gb.line(line).to_string(), expected); + assert_eq!(gb.line(LineIdx(line)).to_string(), expected); } #[test] @@ -346,10 +346,10 @@ mod tests { let s2 = "how are you?"; let gb = GapBuffer::from(format!("{s1}{s2}")); - let l1_chars: String = gb.line(0).chars().collect(); + let l1_chars: String = gb.line(LineIdx(0)).chars().collect(); assert_eq!(l1_chars, s1); - let l2_chars: String = gb.line(1).chars().collect(); + let l2_chars: String = gb.line(LineIdx(1)).chars().collect(); assert_eq!(l2_chars, s2); } @@ -380,15 +380,23 @@ mod tests { #[test] fn indexed_chars_works(s: &str, rev: bool, expected: &[(usize, char)]) { let mut gb = GapBuffer::from(s); - let v: Vec<(usize, char)> = gb.line(0).indexed_chars(0, rev).collect(); + let v: Vec<(usize, char)> = gb + .line(LineIdx(0)) + .indexed_chars(CharIdx(0), rev) + .map(|(i, ch)| (i.0, ch)) + .collect(); assert_eq!(&v, expected); for i in 0..gb.len_chars() { - let idx = gb.char_to_byte(i); + let idx = gb.char_to_byte(CharIdx(i)); gb.move_gap_to(idx); gb.shred_gap(); - let v: Vec<(usize, char)> = gb.line(0).indexed_chars(0, rev).collect(); + let v: Vec<(usize, char)> = gb + .line(LineIdx(0)) + .indexed_chars(CharIdx(0), rev) + .map(|(i, ch)| (i.0, ch)) + .collect(); assert_eq!(&v, expected, "idx={idx}"); } } @@ -398,15 +406,15 @@ mod tests { #[test] fn chars_works(s: &str) { let mut gb = GapBuffer::from(s); - let chars: String = gb.line(0).chars().collect(); + let chars: String = gb.line(LineIdx(0)).chars().collect(); assert_eq!(chars, s); for i in 0..gb.len_chars() { - let idx = gb.char_to_byte(i); + let idx = gb.char_to_byte(CharIdx(i)); gb.move_gap_to(idx); gb.shred_gap(); - let chars: String = gb.line(0).chars().collect(); + let chars: String = gb.line(LineIdx(0)).chars().collect(); assert_eq!(chars, s, "idx={idx}"); } } @@ -414,21 +422,21 @@ mod tests { #[test] fn slice_works() { let mut gb = GapBuffer::from("hello, world!\nhow are you?"); - let slice = Slice::from_raw_offsets(0, gb.cap, &gb); + let slice = Slice::from_raw_offsets(RawIdx(0), RawIdx(gb.cap), &gb); let (s1, s2) = slice.as_strs(); assert_eq!(s1, ""); assert_eq!(s2, "hello, world!\nhow are you?"); - let slice = gb.slice(6, 17); + let slice = gb.slice(CharIdx(6), CharIdx(17)); let (s1, s2) = slice.as_strs(); assert_eq!(s1, " world!\nhow"); assert_eq!(s2, ""); - gb.move_gap_to(12); + gb.move_gap_to(ByteIdx(12)); gb.shred_gap(); println!("after move: {:?}", raw_debug_buffer_content(&gb)); - let slice = gb.slice(6, 17); + let slice = gb.slice(CharIdx(6), CharIdx(17)); let (s1, s2) = slice.as_strs(); assert_eq!(s1, " world"); assert_eq!(s2, "!\nhow"); @@ -439,11 +447,11 @@ mod tests { let mut gb = GapBuffer::from("hello, world!\nhow are you?"); for i in 0..gb.len_chars() { - let idx = gb.char_to_byte(i); + let idx = gb.char_to_byte(CharIdx(i)); gb.move_gap_to(idx); gb.shred_gap(); - let slice = gb.slice(0, 0); + let slice = gb.slice(CharIdx(0), CharIdx(0)); assert!(slice.left.is_empty(), "{i} slice left: {:?}", slice.left); assert!(slice.right.is_empty(), "{i} slice right: {:?}", slice.right); @@ -453,9 +461,9 @@ mod tests { #[test] fn slice_eq_str_works() { let mut gb = GapBuffer::from("hello, world!\nhow are you?"); - gb.move_gap_to(3); + gb.move_gap_to(ByteIdx(3)); gb.shred_gap(); - let slice = gb.slice(0, 5); + let slice = gb.slice(CharIdx(0), CharIdx(5)); assert_eq!(slice, "hello"); } } diff --git a/src/buffer/mod.rs b/src/buffer/mod.rs index d93999e6..96944435 100644 --- a/src/buffer/mod.rs +++ b/src/buffer/mod.rs @@ -35,7 +35,8 @@ use scratch::ScratchBuf; pub use buffers::{BufferId, Buffers}; pub use internal::{ - ByteIdx, CharIdx, Chars, GapBuffer, IdxChars, RawIdx, RevChars, Slice, SliceIter, + ByteIdx, ByteOffset, CharIdx, CharOffset, Chars, GapBuffer, IdxChars, LineIdx, LineOffset, + RevChars, Slice, SliceIter, }; // Welcome splash message for new users to help them get started (rather than just presenting them @@ -416,8 +417,8 @@ impl Buffer { let n_chars = raw.len(); self.txt = GapBuffer::from(raw); - self.dot.clamp_idx(n_chars); - self.xdot.clamp_idx(n_chars); + self.dot.clamp_idx(CharIdx(n_chars)); + self.xdot.clamp_idx(CharIdx(n_chars)); self.edit_log.clear(); self.dirty = false; self.changed_since_last_render = true; @@ -505,7 +506,10 @@ impl Buffer { /// Check the current [Config] to see if this buffer matches a known filetype configuration. pub fn configured_filetype(&self) -> Option { let lang_configs = &self.config.read().filetypes; - let first_line = self.line(0).map(|l| l.to_string()).unwrap_or_default(); + let first_line = self + .line(LineIdx(0)) + .map(|l| l.to_string()) + .unwrap_or_default(); self.path() .and_then(|path| ftype_config_for_path_and_first_line(path, &first_line, lang_configs)) @@ -536,7 +540,7 @@ impl Buffer { .collect() } - pub fn update_ts_state(&mut self, from: usize, n_rows: usize) { + pub fn update_ts_state(&mut self, from: LineIdx, n_rows: usize) { if let Some(ts) = self.syntax_state.as_mut() { ts.update(&self.txt, from, n_rows); } @@ -544,7 +548,7 @@ impl Buffer { pub fn iter_tokenized_lines_from( &self, - line: usize, + line: LineIdx, load_exec_range: Option<(bool, Range)>, ) -> LineIter<'_> { match self.syntax_state.as_ref() { @@ -598,7 +602,7 @@ impl Buffer { /// The number of utf-8 characters currently held in the buffer. #[inline] - pub fn len_chars(&self) -> CharIdx { + pub fn len_chars(&self) -> usize { self.txt.len_chars() } @@ -620,21 +624,21 @@ impl Buffer { Source::Fsys, ); self.handle_action(BAction::Delete, Source::Fsys); - self.xdot.clamp_idx(self.txt.len_chars()); + self.xdot.clamp_idx(CharIdx(self.txt.len_chars())); } pub(crate) fn debug_edit_log(&self) -> Vec { self.edit_log.debug_edits(self) } - pub(crate) fn x_from_rx(&self, y: usize) -> usize { + pub(crate) fn x_from_rx(&self, y: LineIdx) -> CharOffset { self.x_from_provided_rx(y, self.cached_rx) } - pub(crate) fn x_from_provided_rx(&self, y: usize, buf_rx: usize) -> usize { + pub(crate) fn x_from_provided_rx(&self, y: LineIdx, buf_rx: usize) -> CharOffset { let tabstop = self.tabstop(); if self.is_empty() { - return 0; + return CharOffset(0); } let mut rx = 0; @@ -662,12 +666,12 @@ impl Buffer { cx += 1; } - cx + CharOffset(cx) } /// The line at the requested index returned as a Slice. - pub fn line(&self, y: usize) -> Option> { - if y >= self.len_lines() { + pub fn line(&self, y: LineIdx) -> Option> { + if y.0 >= self.len_lines() { None } else { Some(self.txt.line(y)) @@ -710,12 +714,12 @@ impl Buffer { Dot::Range { .. } => return, }; - let prev = if current_index == 0 { + let prev = if current_index == CharIdx(0) { None } else { - self.txt.get_char(current_index - 1) + self.txt.get_char(current_index.usub(1)) }; - let next = self.txt.get_char(current_index + 1); + let next = self.txt.get_char(current_index.uadd(1)); let (l, r) = match (prev, next) { (Some('\n'), _) | (_, Some('\n')) => ('\n', '\n'), @@ -751,14 +755,14 @@ impl Buffer { // Expand until we hit non-alphanumeric characters on each sides let (mut from, mut to) = (current_index, current_index); - for (i, ch) in self.iter_between_chars(current_index, self.txt.len_chars()) { + for (i, ch) in self.iter_between_chars(current_index, CharIdx(self.txt.len_chars())) { if !(ch == '_' || ch == '-' || ch.is_alphanumeric()) { break; } to = i; } - for (i, ch) in self.rev_iter_between_chars(current_index, 0) { + for (i, ch) in self.rev_iter_between_chars(current_index, CharIdx(0)) { if !(ch == '_' || ch == '-' || ch.is_alphanumeric()) { break; } @@ -782,15 +786,17 @@ impl Buffer { let is_addr_char = |ch: char| "+-/$.#,;?".contains(ch); let is_url_char = |ch: char| "?&=".contains(ch); let has_url_prefix = |i: CharIdx| { - let http = i > 4 && i + 3 <= n_chars && self.txt.slice(i - 4, i + 3) == HTTP; - let https = i > 5 && i + 3 <= n_chars && self.txt.slice(i - 5, i + 3) == HTTPS; + let http = + i.0 > 4 && i.0 + 3 <= n_chars && self.txt.slice(i.usub(4), i.uadd(3)) == HTTP; + let https = + i.0 > 5 && i.0 + 3 <= n_chars && self.txt.slice(i.usub(5), i.uadd(3)) == HTTPS; http || https }; // Start by expanding to cover things that are candidates for being file names (optionally // with a following address) or URLs - for (i, ch) in self.iter_between_chars(current_index, self.txt.len_chars()) { + for (i, ch) in self.iter_between_chars(current_index, CharIdx(self.txt.len_chars())) { if !is_file_char(ch) { break; } @@ -801,7 +807,7 @@ impl Buffer { to = i; } - for (i, ch) in self.rev_iter_between_chars(current_index, 0) { + for (i, ch) in self.rev_iter_between_chars(current_index, CharIdx(0)) { if !(is_file_char(ch) || is_url_char(ch) || is_addr_char(ch)) { break; } @@ -814,29 +820,29 @@ impl Buffer { // Now grab the address if we had a trailing colon if let Some(ix) = colon { to = ix; - for (_, ch) in self.iter_between_chars(ix + 1, self.txt.len_chars()) { + for (_, ch) in self.iter_between_chars(ix.uadd(1), CharIdx(self.txt.len_chars())) { if ch.is_whitespace() || "()[]{}<>;".contains(ch) { break; } - to += 1; + to = to.uadd(1); } } - let dot_content = self.txt.slice(from, to + 1).to_string(); + let dot_content = self.txt.slice(from, to.uadd(1)).to_string(); // If dot looks like a URL then expand until whitespace and strip trailing punctuation if dot_content.starts_with(HTTP) || dot_content.starts_with(HTTPS) { - if to < self.txt.len_chars() { - for (_, ch) in self.iter_between_chars(to + 1, self.txt.len_chars()) { + if to.0 < self.txt.len_chars() { + for (_, ch) in self.iter_between_chars(to.uadd(1), CharIdx(self.txt.len_chars())) { if ch.is_whitespace() || "()[]{}<>;".contains(ch) { break; } - to += 1; + to = to.uadd(1); } } if dot_content.ends_with('.') { - to -= 1; + to = to.usub(1); } return Some(Dot::from_char_indices(from, to)); @@ -875,8 +881,8 @@ impl Buffer { self.set_dot(TextObject::BufferEnd, 1); self.handle_action(BAction::InsertString { s }, source); self.dot = dot; - self.dot.clamp_idx(self.txt.len_chars()); - self.xdot.clamp_idx(self.txt.len_chars()); + self.dot.clamp_idx(CharIdx(self.txt.len_chars())); + self.xdot.clamp_idx(CharIdx(self.txt.len_chars())); self.changed_since_last_render = true; } @@ -891,22 +897,22 @@ impl Buffer { BAction::Delete => { let (c, deleted) = self.delete_dot(self.dot, Some(source)); self.dot = Dot::Cur { c }; - self.dot.clamp_idx(self.txt.len_chars()); - self.xdot.clamp_idx(self.txt.len_chars()); + self.dot.clamp_idx(CharIdx(self.txt.len_chars())); + self.xdot.clamp_idx(CharIdx(self.txt.len_chars())); return deleted.map(ActionOutcome::SetClipboard); } BAction::InsertChar { c } => { let (c, _) = self.insert_char(self.dot, c, Some(source)); self.dot = Dot::Cur { c }; - self.dot.clamp_idx(self.txt.len_chars()); - self.xdot.clamp_idx(self.txt.len_chars()); + self.dot.clamp_idx(CharIdx(self.txt.len_chars())); + self.xdot.clamp_idx(CharIdx(self.txt.len_chars())); return None; } BAction::InsertString { s } => { let (c, _) = self.insert_string(self.dot, s, Some(source)); self.dot = Dot::Cur { c }; - self.dot.clamp_idx(self.txt.len_chars()); - self.xdot.clamp_idx(self.txt.len_chars()); + self.dot.clamp_idx(CharIdx(self.txt.len_chars())); + self.xdot.clamp_idx(CharIdx(self.txt.len_chars())); return None; } @@ -916,7 +922,7 @@ impl Buffer { BAction::CurToLine { y } => { self.dot = Dot::Cur { - c: Cur::from_yx(y, 0, self), + c: Cur::from_yx(y, CharOffset(0), self), }; } @@ -981,15 +987,15 @@ impl Buffer { for _ in 0..n { t.set_dot(self); } - self.dot.clamp_idx(self.txt.len_chars()); - self.xdot.clamp_idx(self.txt.len_chars()); + self.dot.clamp_idx(CharIdx(self.txt.len_chars())); + self.xdot.clamp_idx(CharIdx(self.txt.len_chars())); self.changed_since_last_render = true; } /// Set this Buffer's dot to an explicit cursor position, clamping to EOB. pub fn set_dot_from_cursor(&mut self, idx: CharIdx) { self.dot = Cur::new(idx).into(); - self.dot.clamp_idx(self.txt.len_chars()); + self.dot.clamp_idx(CharIdx(self.txt.len_chars())); self.changed_since_last_render = true; } @@ -1005,22 +1011,22 @@ impl Buffer { pub fn set_dot_from_range(&mut self, from: CharIdx, to: CharIdx) { self.dot = Dot::from(Range::from_cursors(Cur::new(from), Cur::new(to), false)) .collapse_null_range(); - self.dot.clamp_idx(self.txt.len_chars()); + self.dot.clamp_idx(CharIdx(self.txt.len_chars())); self.changed_since_last_render = true; } fn set_dot_from_coords(&mut self, coords: Coords) { let addr: Addr = coords.as_addr(self); self.dot = self.map_addr(&addr); - self.dot.clamp_idx(self.txt.len_chars()); - self.xdot.clamp_idx(self.txt.len_chars()); + self.dot.clamp_idx(CharIdx(self.txt.len_chars())); + self.xdot.clamp_idx(CharIdx(self.txt.len_chars())); self.changed_since_last_render = true; } fn set_xdot_from_coords(&mut self, coords: Coords) { let addr: Addr = coords.as_addr(self); self.xdot = self.map_addr(&addr); - self.xdot.clamp_idx(self.txt.len_chars()); + self.xdot.clamp_idx(CharIdx(self.txt.len_chars())); } /// Set this Buffer's xdot from an [Addr] expression. @@ -1035,8 +1041,8 @@ impl Buffer { for _ in 0..n { t.extend_dot_forward(self); } - self.dot.clamp_idx(self.txt.len_chars()); - self.xdot.clamp_idx(self.txt.len_chars()); + self.dot.clamp_idx(CharIdx(self.txt.len_chars())); + self.xdot.clamp_idx(CharIdx(self.txt.len_chars())); self.changed_since_last_render = true; } @@ -1045,8 +1051,8 @@ impl Buffer { for _ in 0..n { t.extend_dot_backward(self); } - self.dot.clamp_idx(self.txt.len_chars()); - self.xdot.clamp_idx(self.txt.len_chars()); + self.dot.clamp_idx(CharIdx(self.txt.len_chars())); + self.xdot.clamp_idx(CharIdx(self.txt.len_chars())); self.changed_since_last_render = true; } @@ -1112,7 +1118,7 @@ impl Buffer { (Kind::Delete, Txt::Char(_)) => self.delete_dot(Dot::Cur { c: cur }, None).0, (Kind::Delete, Txt::String(s)) => { let start_idx = cur.idx; - let end_idx = (start_idx + s.chars().count()).saturating_sub(1); + let end_idx = (start_idx + CharOffset::from(s.as_str())).saturating_usub(1); let end = Cur { idx: end_idx }; self.delete_dot( Dot::Range { @@ -1172,7 +1178,7 @@ impl Buffer { && r.start.idx != r.end.idx && let Some(s) = self.syntax_state.as_mut() { - s.prepare_delete_range(r.start.idx, r.end.idx + 1, &self.txt); + s.prepare_delete_range(r.start.idx, r.end.idx.uadd(1), &self.txt); have_prepared_edit = true; } @@ -1210,7 +1216,7 @@ impl Buffer { self.mark_dirty(); self.changed_since_last_render = true; - (Cur { idx: idx + 1 }, deleted) + (Cur { idx: idx.uadd(1) }, deleted) } fn insert_string( @@ -1220,16 +1226,16 @@ impl Buffer { source: Option, ) -> (Cur, Option) { let s = normalize_line_endings(s); - let len = s.chars().count(); + let len = CharOffset::from(s.as_str()); let mut have_prepared_edit = false; if let Dot::Range { r } = &dot && let Some(ts) = self.syntax_state.as_mut() { if r.start.idx != r.end.idx { - ts.prepare_delete_range(r.start.idx, r.end.idx + 1, &self.txt); + ts.prepare_delete_range(r.start.idx, r.end.idx.uadd(1), &self.txt); have_prepared_edit = true; - } else if r.start.idx < self.txt.len_chars() { + } else if r.start.idx.0 < self.txt.len_chars() { // r.start.idx == r.end.idx, so delete_range below still removes the char at r.start.idx ts.prepare_delete_char(r.start.idx, &self.txt); have_prepared_edit = true; @@ -1289,12 +1295,12 @@ impl Buffer { if let Some(ts) = self.syntax_state.as_mut() { match &dot { - Dot::Cur { c } if c.idx < self.txt.len_chars() => { + Dot::Cur { c } if c.idx.0 < self.txt.len_chars() => { ts.prepare_delete_char(c.idx, &self.txt); have_prepared_edit = true; } Dot::Range { r } if r.start.idx != r.end.idx => { - ts.prepare_delete_range(r.start.idx, r.end.idx + 1, &self.txt); + ts.prepare_delete_range(r.start.idx, r.end.idx.uadd(1), &self.txt); have_prepared_edit = true; } _ => (), @@ -1321,7 +1327,7 @@ impl Buffer { None }; - if idx < self.txt.len_chars() { + if idx.0 < self.txt.len_chars() { let ch = self.txt.char(idx); self.txt.remove_char(idx); @@ -1338,7 +1344,10 @@ impl Buffer { } fn delete_range(&mut self, r: Range, source: Option) -> (Cur, Option) { - let (from, to) = (r.start.idx, min(r.end.idx + 1, self.txt.len_chars())); + let (from, to) = ( + r.start.idx, + min(r.end.idx.uadd(1), CharIdx(self.txt.len_chars())), + ); let byte_from = if self.input_filter.is_some() { Some(self.txt.char_to_byte(from)) } else { @@ -1385,7 +1394,7 @@ impl Buffer { /// We determine the final cursor we collapse to based on the start of xdot /// and the offset coming from the text being inserted. pub(crate) fn insert_xdot(&mut self, s: String) { - let mut offset = s.chars().count() as isize; + let mut offset = CharOffset::from(s.as_str()); if self.dot.as_range().intersects_range(&self.xdot.as_range()) { if self.xdot.first_cur() > self.dot.first_cur() @@ -1394,7 +1403,7 @@ impl Buffer { // The final case shown above: we need to back up a character in // order to land on the last character of the existing dot rather // than the first character of the inserted text. - offset = -1; + offset = CharOffset(-1); } self.dot = self.xdot.collapse_to_first_cur(); @@ -1402,17 +1411,17 @@ impl Buffer { // Nothing to update for dot. // DOT |---| // XDOT |----| - offset = 0; + offset = CharOffset(0); } else { // In this case, the change in buffer length needs to account for // the text being removed as well as what is being inserted. // DOT |---| // XDOT |----| - offset -= self.xdot.n_chars() as isize; + offset -= CharOffset(self.xdot.n_chars() as isize); if self.xdot.is_cur() { // Cursors insert directly into the buffer without removing the character // they are on. - offset += 1; + offset += CharOffset(1); } }; @@ -1420,7 +1429,7 @@ impl Buffer { self.dot = self.xdot; self.handle_action(BAction::InsertString { s }, Source::Fsys); (self.xdot, self.dot) = (self.dot, dot_after_edit); - self.dot.clamp_idx(self.txt.len_chars()); // xdot clamped as part of handling the insert + self.dot.clamp_idx(CharIdx(self.txt.len_chars())); // xdot clamped as part of handling the insert } } @@ -1450,7 +1459,7 @@ pub(crate) mod tests { const LINE_2: &str = "involving multiple lines"; fn c(idx: usize) -> Cur { - Cur { idx } + Cur { idx: CharIdx(idx) } } fn r(from: usize, to: usize, from_active: bool) -> Range { @@ -1486,7 +1495,7 @@ pub(crate) mod tests { #[test] fn simple_insert_works() { let b = simple_initial_buffer(); - let c = Cur::from_yx(1, LINE_2.len(), &b); + let c = Cur::from_yx(LineIdx(1), CharOffset::from(LINE_2), &b); let lines = b.string_lines(); assert_eq!(lines.len(), 2); @@ -1546,7 +1555,7 @@ pub(crate) mod tests { let lines = b.string_lines(); assert_eq!(lines.len(), 2); - let c = Cur::from_yx(1, 1, &b); + let c = Cur::from_yx(LineIdx(1), CharOffset(1), &b); assert_eq!(b.dot, Dot::Cur { c }); assert_eq!(lines[0], LINE_1); @@ -1566,7 +1575,7 @@ pub(crate) mod tests { let mut b = Buffer::new_unnamed(0, "", Default::default()); b.handle_action(BAction::RawArrow(Arrow::Right), Source::Fsys); - let c = Cur { idx: 0 }; + let c = Cur { idx: CharIdx(0) }; assert_eq!(b.dot, Dot::Cur { c }); } @@ -1574,7 +1583,7 @@ pub(crate) mod tests { fn delete_in_empty_buffer_is_fine() { let mut b = Buffer::new_unnamed(0, "", Default::default()); b.handle_action(BAction::Delete, Source::Keyboard); - let c = Cur { idx: 0 }; + let c = Cur { idx: CharIdx(0) }; let lines = b.string_lines(); assert_eq!(b.dot, Dot::Cur { c }); @@ -1592,7 +1601,7 @@ pub(crate) mod tests { ); b.handle_action(BAction::Delete, Source::Keyboard); - let c = Cur::from_yx(1, LINE_2.len() - 1, &b); + let c = Cur::from_yx(LineIdx(1), CharOffset::from(LINE_2) - CharOffset(1), &b); let lines = b.string_lines(); assert_eq!(b.dot, Dot::Cur { c }); @@ -1614,7 +1623,7 @@ pub(crate) mod tests { b.handle_action(BAction::DotSet(TextObject::Line, 1), Source::Keyboard); b.handle_action(BAction::Delete, Source::Keyboard); - let c = Cur::from_yx(1, 0, &b); + let c = Cur::from_yx(LineIdx(1), CharOffset(0), &b); let lines = b.string_lines(); assert_eq!(b.dot, Dot::Cur { c }); @@ -1633,11 +1642,13 @@ pub(crate) mod tests { #[test] fn delete_range_when_start_equals_end_works() { let mut b = Buffer::new_unnamed(0, "foo", Default::default()); - let (cur, deleted) = - b.delete_range(Range::from_cursors(Cur::new(0), Cur::new(0), false), None); + let (cur, deleted) = b.delete_range( + Range::from_cursors(Cur::new(CharIdx(0)), Cur::new(CharIdx(0)), false), + None, + ); assert_eq!(b.str_contents(), "oo"); - assert_eq!(cur, Cur::new(0)); + assert_eq!(cur, Cur::new(CharIdx(0))); assert_eq!(deleted, None); } @@ -1765,7 +1776,7 @@ pub(crate) mod tests { // Check with the initial cursor position being at any offset within the target for i in 0..s.len() { - let dot = b.try_expand_known(9 + i); + let dot = b.try_expand_known(CharIdx(9 + i)); let maybe_content = dot.map(|d| d.content(&b)); assert_eq!( maybe_content.as_deref(), @@ -1821,7 +1832,12 @@ pub(crate) mod tests { ); assert_eq!(b.txt.to_string(), "bar"); - assert_eq!(b.dot, Dot::Cur { c: Cur { idx: 3 } }); + assert_eq!( + b.dot, + Dot::Cur { + c: Cur { idx: CharIdx(3) } + } + ); } #[test] @@ -1838,7 +1854,12 @@ pub(crate) mod tests { b.handle_action(BAction::InsertChar { c: 'a' }, Source::Fsys); assert_eq!(b.txt.to_string(), "a"); - assert_eq!(b.dot, Dot::Cur { c: Cur { idx: 1 } }); + assert_eq!( + b.dot, + Dot::Cur { + c: Cur::new(CharIdx(1)) + } + ); } #[test] diff --git a/src/dot/cur.rs b/src/dot/cur.rs index 8e23b60f..f7f3896b 100644 --- a/src/dot/cur.rs +++ b/src/dot/cur.rs @@ -1,5 +1,5 @@ use crate::{ - buffer::{Buffer, CharIdx}, + buffer::{Buffer, CharIdx, CharOffset, LineIdx}, key::Arrow, }; use std::cmp::min; @@ -10,41 +10,41 @@ pub struct Cur { } impl Cur { - pub fn new(idx: CharIdx) -> Self { + pub const fn new(idx: CharIdx) -> Self { Self { idx } } - pub fn buffer_start() -> Self { - Cur { idx: 0 } + pub const fn buffer_start() -> Self { + Cur { idx: CharIdx(0) } } - pub fn buffer_end(b: &Buffer) -> Self { + pub const fn buffer_end(b: &Buffer) -> Self { Cur { - idx: b.txt.len_chars(), + idx: CharIdx(b.txt.len_chars()), } } pub fn as_string_addr(&self, b: &Buffer) -> String { let (y, x) = self.as_yx(b); - format!("{}:{}", y + 1, x + 1) + format!("{}:{}", y.uadd(1), x + CharOffset(1)) } - pub(crate) fn as_yx(&self, b: &Buffer) -> (usize, usize) { + pub(crate) fn as_yx(&self, b: &Buffer) -> (LineIdx, CharOffset) { let y = b.txt.char_to_line(self.idx); - let x = self.idx - b.txt.line_to_char(y); + let x = self.idx.offset_from(b.txt.line_to_char(y)); (y, x) } - pub(crate) fn from_yx(y_idx: usize, x_idx: usize, b: &Buffer) -> Self { + pub(crate) fn from_yx(y_idx: LineIdx, x_idx: CharOffset, b: &Buffer) -> Self { let line_start = b.txt.line_to_char(y_idx); let mut x_max = b.txt.line_len_chars(y_idx); - if y_idx < b.len_lines() - 1 { + if y_idx.0 < b.len_lines() - 1 { x_max -= 1; } Self { - idx: line_start + min(x_idx, x_max), + idx: line_start + min(x_idx, CharOffset(x_max as isize)), } } @@ -55,7 +55,7 @@ impl Cur { cur = cur.arr(arr, b); } - cur.clamp_idx(b.txt.len_chars()); + cur.clamp_idx(CharIdx(b.txt.len_chars())); cur } @@ -65,20 +65,20 @@ impl Cur { match arr { Arrow::Up => { let (mut y, _) = self.as_yx(b); - if y != 0 { - y -= 1; + if y != LineIdx(0) { + y = y.usub(1); cur.idx = b.txt.line_to_char(y) + b.x_from_rx(y); } } Arrow::Down => { let (mut y, _) = self.as_yx(b); - if !b.is_empty() && y < b.len_lines() - 1 { - y += 1; + if !b.is_empty() && y.0 < b.len_lines() - 1 { + y = y.uadd(1); cur.idx = b.txt.line_to_char(y) + b.x_from_rx(y); } } - Arrow::Left => cur.idx = cur.idx.saturating_sub(1), - Arrow::Right => cur.idx = min(cur.idx + 1, b.txt.len_chars()), + Arrow::Left => cur.idx = cur.idx.saturating_usub(1), + Arrow::Right => cur.idx = min(cur.idx.uadd(1), CharIdx(b.txt.len_chars())), } cur @@ -98,12 +98,12 @@ impl Cur { #[must_use] pub(super) fn move_to_line_end(mut self, b: &Buffer) -> Self { let y = b.txt.char_to_line(self.idx); - self.idx = b.txt.line_to_char(y) + b.txt.line_len_chars(y); + self.idx = b.txt.line_to_char(y).uadd(b.txt.line_len_chars(y)); // On the last line of the file we don't have a trailing newline // so we really do want the final character - if y < b.len_lines().saturating_sub(1) { - self.idx = self.idx.saturating_sub(1); + if y.0 < b.len_lines().saturating_sub(1) { + self.idx = self.idx.saturating_usub(1); } self @@ -121,12 +121,16 @@ mod tests { let b = buffer_from_lines(lines); for (n, s) in lines.iter().enumerate() { - let c = Cur::from_yx(n, 100, &b); + let c = Cur::from_yx(LineIdx(n), CharOffset(100), &b); // Should be focused on the newline character at the end of the line // In the case of the last line we should be focused on the index after // the end of the string so that we are appending to the buffer. - assert_eq!(c.as_yx(&b), (n, s.len()), "line='{s}'"); + assert_eq!( + c.as_yx(&b), + (LineIdx(n), CharOffset::from(*s)), + "line='{s}'" + ); } } @@ -135,10 +139,10 @@ mod tests { let lines = &["a", "ab", "abc", "abcd"]; let b = buffer_from_lines(lines); let c = Cur::buffer_end(&b); - assert_eq!(c.as_yx(&b), (3, 4)); + assert_eq!(c.as_yx(&b), (LineIdx(3), CharOffset(4))); let final_c = c.arr(Arrow::Right, &b); - assert_eq!(final_c.as_yx(&b), (3, 4)); + assert_eq!(final_c.as_yx(&b), (LineIdx(3), CharOffset(4))); } #[test] @@ -146,12 +150,12 @@ mod tests { let lines = &["a", "ab", "abc", "abcd"]; let b = buffer_from_lines(lines); let mut c = Cur::buffer_end(&b); - c.idx -= 1; + c.idx = c.idx.usub(1); - assert_eq!(c.as_yx(&b), (3, 3)); + assert_eq!(c.as_yx(&b), (LineIdx(3), CharOffset(3))); let mut final_c = c.arr(Arrow::Right, &b); - final_c.clamp_idx(b.txt.len_chars()); - assert_eq!(final_c.as_yx(&b), (3, 4)); + final_c.clamp_idx(CharIdx(b.txt.len_chars())); + assert_eq!(final_c.as_yx(&b), (LineIdx(3), CharOffset(4))); } } diff --git a/src/dot/find.rs b/src/dot/find.rs index 416f18dd..3fcf337e 100644 --- a/src/dot/find.rs +++ b/src/dot/find.rs @@ -41,7 +41,7 @@ pub trait Find { } pub fn find_forward(f: &F, cur: Cur, b: &Buffer) -> Option { - find_between(f, cur.idx, b.txt.len_chars(), b) + find_between(f, cur.idx, CharIdx(b.txt.len_chars()), b) } pub fn find_forward_end(f: &F, cur: Cur, b: &Buffer) -> Cur { @@ -51,12 +51,12 @@ pub fn find_forward_end(f: &F, cur: Cur, b: &Buffer) -> Cur { } pub fn find_forward_wrapping(f: &F, b: &Buffer) -> Option { - find_between(f, b.dot.last_cur().idx, b.txt.len_chars(), b) - .or_else(|| find_between(f, 0, b.dot.last_cur().idx, b)) + find_between(f, b.dot.last_cur().idx, CharIdx(b.txt.len_chars()), b) + .or_else(|| find_between(f, CharIdx(0), b.dot.last_cur().idx, b)) } pub fn find_backward(f: &F, cur: Cur, b: &Buffer) -> Option { - rev_find_between(f, cur.idx, 0, b) + rev_find_between(f, cur.idx, CharIdx(0), b) } pub fn find_backward_start(f: &F, cur: Cur, b: &Buffer) -> Cur { @@ -142,11 +142,11 @@ impl Find for &str { let chars: Vec = self.chars().collect(); let last = chars.len().saturating_sub(1); let mut cix = 0; - let mut start = 0; + let mut start = CharIdx(0); for (i, ch) in it { if ch != chars[cix] { - start = 0; + start = CharIdx(0); cix = 0; continue; } diff --git a/src/dot/mod.rs b/src/dot/mod.rs index 2f581d41..d137fe79 100644 --- a/src/dot/mod.rs +++ b/src/dot/mod.rs @@ -7,7 +7,7 @@ //! All indexing is 0-based when working with the contents of a specific buffer. //! Converting to 1-based indices for the terminal is exclusively handled in the //! rendering logic. -use crate::buffer::{Buffer, CharIdx}; +use crate::buffer::{Buffer, CharIdx, CharOffset}; use std::cmp::min; mod cur; @@ -88,7 +88,7 @@ impl Dot { pub fn n_chars(&self) -> usize { let (from, to) = self.as_char_indices(); - to - from + 1 + (to - from).0 + 1 } /// Whether or not this dot is a [Cur]. @@ -118,17 +118,12 @@ impl Dot { /// /// [Dot::clamp_idx] will still need to be called in order to ensure that the result /// is within bounds for the given buffer. - pub fn with_offset_saturating(mut self, offset: isize) -> Self { + pub fn with_offset_saturating(mut self, offset: CharOffset) -> Self { match &mut self { - Dot::Cur { c } if offset >= 0 => c.idx += offset as usize, - Dot::Cur { c } => c.idx = c.idx.saturating_sub(-offset as usize), - Dot::Range { r } if offset >= 0 => { - r.start.idx += offset as usize; - r.end.idx += offset as usize; - } + Dot::Cur { c } => c.idx = c.idx.saturating_add(offset), Dot::Range { r } => { - r.start.idx = r.start.idx.saturating_sub(-offset as usize); - r.end.idx = r.end.idx.saturating_sub(-offset as usize); + r.start.idx = r.start.idx.saturating_add(offset); + r.end.idx = r.end.idx.saturating_add(offset); } } @@ -153,7 +148,9 @@ impl Dot { } let (from, to) = self.as_char_indices(); - b.txt.slice(from, min(to + 1, len_chars)).to_string() + b.txt + .slice(from, min(to.uadd(1), CharIdx(len_chars))) + .to_string() } /// The active cursor position for this [Dot] which will be manipulated by movement operations @@ -291,7 +288,9 @@ The third paragraph is even shorter."; .sum() }; - Cur { idx: y + x } + Cur { + idx: CharIdx(y + x), + } } fn c(y: usize, x: usize) -> Dot { diff --git a/src/dot/range.rs b/src/dot/range.rs index b7af5510..79ee7062 100644 --- a/src/dot/range.rs +++ b/src/dot/range.rs @@ -1,4 +1,7 @@ -use crate::{buffer::Buffer, dot::Cur}; +use crate::{ + buffer::{Buffer, CharIdx}, + dot::Cur, +}; /// A [Range] is a closed interval [start, end] that includes _both_ the starting cursor and the /// ending cursor. @@ -32,8 +35,8 @@ pub struct Range { impl Range { /// Beginning of file pub const BOF: Self = Range { - start: Cur { idx: 0 }, - end: Cur { idx: 0 }, + start: Cur { idx: CharIdx(0) }, + end: Cur { idx: CharIdx(0) }, start_active: false, }; @@ -124,7 +127,7 @@ mod tests { use simple_test_case::test_case; fn c(idx: usize) -> Cur { - Cur { idx } + Cur { idx: CharIdx(idx) } } fn r(start: usize, end: usize, start_active: bool) -> Range { diff --git a/src/dot/text_object.rs b/src/dot/text_object.rs index d4781887..c9a8d845 100644 --- a/src/dot/text_object.rs +++ b/src/dot/text_object.rs @@ -165,7 +165,7 @@ impl Find for FindDelimited { if other.contains(ch) && target != other { skips += 1; } else if skips == 0 && target.contains(ch) { - let ix = if self.rev { i + 1 } else { i - 1 }; + let ix = if self.rev { i.uadd(1) } else { i.usub(1) }; return Some((ix, ix)); } else if target.contains(ch) { skips -= 1; @@ -196,14 +196,14 @@ impl Find for FindParagraph { I: Iterator, { let mut prev_was_newline = false; - let mut pos = 0; + let mut pos = CharIdx(0); for (i, ch) in it { match ch { '\n' if prev_was_newline => { return match self { Self::Fwd => Some((i, i)), - Self::Bck => Some((i + 1, i + 1)), + Self::Bck => Some((i.uadd(1), i.uadd(1))), }; } '\n' => prev_was_newline = true, @@ -253,8 +253,8 @@ impl Find for FindWord { match (prev, kind) { (Word, Punctuation) | (Punctuation, Word) | (Word | Punctuation, Whitespace) => { return match self { - Self::Fwd => Some((i - 1, i - 1)), - Self::Bck => Some((i + 1, i + 1)), + Self::Fwd => Some((i.usub(1), i.usub(1))), + Self::Bck => Some((i.uadd(1), i.uadd(1))), }; } _ => prev = kind, @@ -272,13 +272,13 @@ impl Find for FindWord { mut end, start_active, } = dot.as_range(); - let max_idx = b.txt.len_chars() - 1; + let max_idx = CharIdx(b.txt.len_chars() - 1); start.idx = min(start.idx, max_idx); end.idx = min(end.idx, max_idx); - if start.idx > 0 { + if start.idx > CharIdx(0) { let current = CharKind::from(b.txt.char(start.idx)); - let prev = CharKind::from(b.txt.char(start.idx - 1)); + let prev = CharKind::from(b.txt.char(start.idx.usub(1))); match (prev, current) { // We're at the start of the current word so start.idx is correct @@ -287,7 +287,7 @@ impl Find for FindWord { // We're in whitespace so advance until we hit a word or the end of the buffer (_, Whitespace) if start.idx < max_idx => { while matches!(CharKind::from(b.txt.char(start.idx)), Whitespace) { - start.idx += 1; + start.idx = start.idx.uadd(1); if start.idx == max_idx { end.idx = max_idx; break; @@ -302,7 +302,7 @@ impl Find for FindWord { if end.idx < max_idx { let current = CharKind::from(b.txt.char(end.idx)); - let next = CharKind::from(b.txt.char(end.idx + 1)); + let next = CharKind::from(b.txt.char(end.idx.uadd(1))); match (current, next) { // We're at the end of a word so end.idx is correct @@ -340,6 +340,7 @@ impl From for CharKind { mod tests { use super::*; use simple_test_case::test_case; + use std::assert_matches; #[test_case(FindWord::Fwd, 0, "this"; "forward start of buffer")] #[test_case(FindWord::Fwd, 3, "this"; "forward end of first word")] @@ -351,7 +352,9 @@ mod tests { #[test] fn expand_word(fw: FindWord, idx: usize, expected: &str) { let b = Buffer::new_virtual(0, "test", "this is a test", Default::default()); - let dot = Dot::Cur { c: Cur { idx } }; + let dot = Dot::Cur { + c: Cur { idx: CharIdx(idx) }, + }; let expanded = fw.expand(dot, &b); let content = expanded.content(&b); @@ -361,12 +364,19 @@ mod tests { #[test] fn expand_word_for_buffer_with_trailing_spaces() { let b = Buffer::new_virtual(0, "test", "this is a test ", Default::default()); - let dot = Dot::Cur { c: Cur { idx: 14 } }; + let dot = Dot::Cur { + c: Cur { idx: CharIdx(14) }, + }; let expanded = FindWord::Fwd.expand(dot, &b); let content = expanded.content(&b); // Should have advanced to the end of the buffer and selected that final space character - assert!(matches!(expanded, Dot::Cur { c: Cur { idx: 16 } })); + assert_matches!( + expanded, + Dot::Cur { + c: Cur { idx: CharIdx(16) } + } + ); assert_eq!(content, " "); } @@ -378,7 +388,9 @@ mod tests { "│ (EX:40727d6d-32a6-4ebc-ae4f-4d92e326a8c9) │", Default::default(), ); - let dot = Dot::Cur { c: Cur { idx: 3 } }; + let dot = Dot::Cur { + c: Cur { idx: CharIdx(3) }, + }; let expanded = FindDelimited::new("(", ")").expand(dot, &b); let content = expanded.content(&b); diff --git a/src/editor/actions.rs b/src/editor/actions.rs index 81a45f99..e696f018 100644 --- a/src/editor/actions.rs +++ b/src/editor/actions.rs @@ -1,6 +1,6 @@ //! Editor actions in response to user input use crate::{ - buffer::{BufferId, BufferKind}, + buffer::{BufferId, BufferKind, LineIdx}, config::Config, dot::{Cur, Range, TextObject}, editor::{Editor, MbSelect, MbSelector, MiniBufferSelection, minibuffer::SimpleMbSelect}, @@ -102,7 +102,7 @@ impl From for Action { /// Actions handled by an individual Buffer. #[derive(Debug, Clone, PartialEq, Eq)] pub enum BAction { - CurToLine { y: usize }, + CurToLine { y: LineIdx }, Delete, InsertChar { c: char }, @@ -625,7 +625,7 @@ where let mb = SimpleMbSelect::new("> ", numbered_lines, |selection| match selection { MiniBufferSelection::Line { cy, .. } => Some(Actions::Multi(vec![ - BAction::CurToLine { y: cy }.for_active(), + BAction::CurToLine { y: LineIdx(cy) }.for_active(), BAction::DotSet(TextObject::Line, 1).for_active(), UAction::SetViewPort(ViewPort::Center).into(), ])), diff --git a/src/editor/layout.rs b/src/editor/layout.rs index f38845d3..55ddcc6f 100644 --- a/src/editor/layout.rs +++ b/src/editor/layout.rs @@ -33,7 +33,7 @@ macro_rules! assert_invariants { ); let b = $buffers.with_id(win.view.bufid).unwrap(); assert!( - win.view.row_off < b.len_lines(), + win.view.row_off.0 < b.len_lines(), "col {i} window {j} has an OOB row_off ({} vs {})", win.view.row_off, b.len_lines() @@ -48,7 +48,7 @@ macro_rules! assert_invariants { ); let b = $buffers.with_id(view.bufid).unwrap(); assert!( - view.row_off < b.len_lines(), + view.row_off.0 < b.len_lines(), "stored view for bufid {} has an OOB row_off ({} vs {})", view.bufid, view.row_off, diff --git a/src/editor/minibuffer.rs b/src/editor/minibuffer.rs index df9fba6a..e7ef9ff3 100644 --- a/src/editor/minibuffer.rs +++ b/src/editor/minibuffer.rs @@ -4,7 +4,7 @@ //! Conceptually this is operates as an embedded dmenu. use crate::{ Config, - buffer::{Buffer, Buffers, GapBuffer, Slice}, + buffer::{Buffer, Buffers, CharIdx, GapBuffer, LineIdx, Slice}, dot::TextObject, editor::{Actions, BAction, Editor}, input::Event, @@ -26,12 +26,12 @@ const MINIBUFFER_ID: usize = usize::MAX - 1; pub struct MiniBufferState<'a> { pub(crate) cx: usize, pub(crate) n_visible_lines: usize, - pub(crate) selected_line_idx: usize, + pub(crate) selected_line_idx: LineIdx, pub(crate) prompt: &'a str, pub(crate) input: Slice<'a>, pub(crate) b: Option<&'a Buffer>, - pub(crate) top: usize, - pub(crate) bottom: usize, + pub(crate) top: LineIdx, + pub(crate) bottom: LineIdx, } pub(crate) enum MiniBufferSelection { @@ -65,11 +65,11 @@ pub(crate) struct MiniBuffer { line_indices: Vec, b: Buffer, max_height: usize, - y: usize, - selected_line_idx: usize, + y: LineIdx, + selected_line_idx: LineIdx, n_visible_lines: usize, - top: usize, - bottom: usize, + top: LineIdx, + bottom: LineIdx, show_buffer_content: bool, } @@ -100,11 +100,11 @@ impl MiniBuffer { line_indices, b: Buffer::new_minibuffer(config), max_height, - y: 0, - selected_line_idx: 0, + y: LineIdx(0), + selected_line_idx: LineIdx(0), n_visible_lines: 0, - top: 0, - bottom: 0, + top: LineIdx(0), + bottom: LineIdx(0), show_buffer_content: true, } } @@ -123,7 +123,7 @@ impl MiniBuffer { self.update_state(); MiniBufferState { - cx: self.input.dot.active_cur().idx + self.n_prompt_chars, + cx: self.input.dot.active_cur().idx.0 + self.n_prompt_chars, n_visible_lines: self.n_visible_lines, prompt: &self.prompt, input: self.input.txt.as_slice(), @@ -161,18 +161,18 @@ impl MiniBuffer { } self.b.txt = GapBuffer::from(visible_lines.join("\n")); - self.b.dot.clamp_idx(self.b.txt.len_chars()); + self.b.dot.clamp_idx(CharIdx(self.b.txt.len_chars())); let n_visible_lines = min(visible_lines.len(), self.max_height); let (y, _) = self.b.dot.active_cur().as_yx(&self.b); let (selected_line_idx, top, bottom, show_buffer_content) = if n_visible_lines == 0 { - (0, 0, 0, false) - } else if y >= n_visible_lines { - let lower = y.saturating_sub(n_visible_lines) + 1; + (LineIdx(0), LineIdx(0), LineIdx(0), false) + } else if y >= LineIdx(n_visible_lines) { + let lower = y.saturating_sub(LineIdx(n_visible_lines)).uadd(1); (y, lower, y, true) } else { - (y, 0, n_visible_lines - 1, true) + (y, LineIdx(0), LineIdx(n_visible_lines - 1), true) }; self.show_buffer_content = show_buffer_content; @@ -225,7 +225,7 @@ impl MiniBuffer { input: self.input.txt.to_string(), }, Some(l) => MiniBufferSelection::Line { - cy: self.line_indices[self.y], + cy: self.line_indices[self.y.0], line: l.to_string(), }, None => MiniBufferSelection::UserInput { @@ -249,14 +249,14 @@ impl MiniBuffer { ); } Input::Alt('k') | Input::Arrow(Arrow::Up) => { - if self.selected_line_idx == 0 { + if self.selected_line_idx == LineIdx(0) { self.b.set_dot(TextObject::BufferEnd, 1); } else { self.b.set_dot(TextObject::Arr(Arrow::Up), 1); } } Input::Alt('j') | Input::Arrow(Arrow::Down) => { - if self.selected_line_idx == self.b.len_lines() - 1 { + if self.selected_line_idx.0 == self.b.len_lines() - 1 { self.b.set_dot(TextObject::BufferStart, 1); } else { self.b.set_dot(TextObject::Arr(Arrow::Down), 1); diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 43f333b2..19c0a80d 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -1,7 +1,7 @@ //! The main control flow and functionality of the `ad` editor. use crate::{ LogBuffer, - buffer::{Buffer, BufferId, Buffers, SCRATCH_ID, WELCOME_SQUIRREL}, + buffer::{Buffer, BufferId, Buffers, CharIdx, SCRATCH_ID, WELCOME_SQUIRREL}, config::Config, die, dot::TextObject, @@ -165,7 +165,7 @@ where buffers .active_ignoring_scratch_mut() .txt - .insert_str(0, WELCOME_SQUIRREL); + .insert_str(CharIdx(0), WELCOME_SQUIRREL); } Self { @@ -798,7 +798,7 @@ mod tests { // hack: avoid race between the child thread pushing lines to the output buffer and the // attempt to drain the event queue below - sleep(Duration::from_millis(100)); + sleep(Duration::from_millis(500)); // drain any pending writes from the script while let Ok(evt) = ed.rx_events.try_recv() { diff --git a/src/editor/mouse.rs b/src/editor/mouse.rs index fd0bc585..abe64af4 100644 --- a/src/editor/mouse.rs +++ b/src/editor/mouse.rs @@ -337,6 +337,7 @@ where #[cfg(test)] mod tests { use super::*; + use crate::buffer::CharIdx; use crate::{ dot::{Cur, Range}, editor::EditorMode, @@ -374,8 +375,10 @@ mod tests { fn r(start: usize, end: usize, start_active: bool) -> Range { Range { - start: Cur { idx: start }, - end: Cur { idx: end }, + start: Cur { + idx: CharIdx(start), + }, + end: Cur { idx: CharIdx(end) }, start_active, } } @@ -760,7 +763,9 @@ mod tests { ); ed.update_window_size(100, 80); // Needed in order to keep clicks in bounds ed.open_virtual("test", "some text to test with", false); - ed.buffers.active_mut().dot = Dot::Cur { c: Cur { idx: 5 } }; + ed.buffers.active_mut().dot = Dot::Cur { + c: Cur { idx: CharIdx(5) }, + }; // attach an input filter so we can intercept load and execute events let (tx, rx) = channel(); diff --git a/src/exec/addr.rs b/src/exec/addr.rs index 5e442615..f214bf00 100644 --- a/src/exec/addr.rs +++ b/src/exec/addr.rs @@ -18,7 +18,7 @@ //! e1,e2 => set dot to e1_start..=e2_end //! ``` use crate::{ - buffer::{Buffer, ByteIdx, CharIdx, GapBuffer}, + buffer::{Buffer, CharIdx, CharOffset, GapBuffer, LineIdx, LineOffset}, dot::{Cur, Dot, Range}, parse::{self, ParseInput}, regex::{self, Haystack, Regex, RevRegex}, @@ -137,15 +137,15 @@ pub enum AddrBase { /// $ Eof, /// n - Line(usize), + Line(LineIdx), /// -/+n - RelativeLine(isize), + RelativeLine(LineOffset), /// #n Char(CharIdx), /// -/+#n - RelativeChar(isize), + RelativeChar(CharOffset), /// n:m - LineAndColumn(usize, usize), + LineAndColumn(LineIdx, CharOffset), /// /re/ or +/re/ Regex(Regex), /// -/re/ @@ -304,9 +304,9 @@ impl<'a> Parser<'a> { let ix = self.try_parse_num()?; match dir { - None => Ok(AddrBase::Char(ix)), - Some(Dir::Fwd) => Ok(AddrBase::RelativeChar(ix as isize)), - Some(Dir::Bck) => Ok(AddrBase::RelativeChar(-(ix as isize))), + None => Ok(AddrBase::Char(CharIdx(ix))), + Some(Dir::Fwd) => Ok(AddrBase::RelativeChar(CharOffset(self.try_isize(ix)?))), + Some(Dir::Bck) => Ok(AddrBase::RelativeChar(CharOffset(-self.try_isize(ix)?))), } } @@ -328,14 +328,21 @@ impl<'a> Parser<'a> { } else { match self.try_parse_num()? { 0 => Err(self.error(ErrorKind::ZeroIndexedLineOrColumn)), - col => Ok(AddrBase::LineAndColumn(line - 1, col - 1)), + col => Ok(AddrBase::LineAndColumn( + LineIdx(line - 1), + CharOffset(self.try_isize(col - 1)?), + )), } } } - (_, None) => Ok(AddrBase::Line(line - 1)), - (_, Some(Dir::Fwd)) => Ok(AddrBase::RelativeLine(line as isize)), - (_, Some(Dir::Bck)) => Ok(AddrBase::RelativeLine(-(line as isize))), + (_, None) => Ok(AddrBase::Line(LineIdx(line - 1))), + (_, Some(Dir::Fwd)) => { + Ok(AddrBase::RelativeLine(LineOffset(self.try_isize(line)?))) + } + (_, Some(Dir::Bck)) => { + Ok(AddrBase::RelativeLine(LineOffset(-self.try_isize(line)?))) + } } } @@ -364,6 +371,10 @@ impl<'a> Parser<'a> { s.parse().map_err(|_| self.error(ErrorKind::NotAnAddress)) } + fn try_isize(&self, n: usize) -> Result { + isize::try_from(n).map_err(|_| self.error(ErrorKind::NotAnAddress)) + } + fn parse_delimited_regex(&self, dir: Dir) -> Result { assert_eq!(self.input.char(), '/'); let mut s = String::new(); @@ -400,14 +411,14 @@ pub trait Address: Haystack + Sized { /// so that don't need to special case running programs against an in-editor /// buffer vs stdin or a file read from disk. fn current_dot(&self) -> Dot; - fn len_bytes(&self) -> ByteIdx; - fn len_chars(&self) -> CharIdx; - fn line_to_char(&self, line_idx: usize) -> Option; - fn char_to_line(&self, char_idx: CharIdx) -> Option; + fn len_bytes(&self) -> usize; + fn len_chars(&self) -> usize; + fn line_to_char(&self, line_idx: LineIdx) -> Option; + fn char_to_line(&self, char_idx: CharIdx) -> Option; fn char_to_line_end(&self, char_idx: CharIdx) -> Option; fn char_to_line_start(&self, char_idx: CharIdx) -> Option; - fn max_iter(&self) -> CharIdx { + fn max_iter(&self) -> usize { self.len_chars() } @@ -419,14 +430,14 @@ pub trait Address: Haystack + Sized { }; let mut dot = maybe_dot.unwrap_or_default(); - dot.clamp_idx(self.max_iter()); + dot.clamp_idx(CharIdx(self.max_iter())); dot } - fn full_line(&self, line_idx: usize) -> Option { + fn full_line(&self, line_idx: LineIdx) -> Option { let from = self.line_to_char(line_idx)?; - let to = self.char_to_line_end(from)?.saturating_sub(1); + let to = self.char_to_line_end(from)?.saturating_usub(1); Some(Dot::from_char_indices(from, to)) } @@ -436,8 +447,8 @@ pub trait Address: Haystack + Sized { let dot = match addr_base { Current => cur_dot, - Bof => Cur { idx: 0 }.into(), - Eof => Cur::new(self.max_iter()).into(), + Bof => Cur { idx: CharIdx(0) }.into(), + Eof => Cur::new(CharIdx(self.max_iter())).into(), Bol => { let Range { start, end, .. } = cur_dot.as_range(); @@ -461,14 +472,14 @@ pub trait Address: Haystack + Sized { Line(line_idx) => self.full_line(*line_idx)?, RelativeLine(offset) => { let mut line_idx = self.char_to_line(cur_dot.active_cur().idx)?; - line_idx = (line_idx as isize + *offset) as usize; + line_idx = line_idx.saturating_add(*offset); self.full_line(line_idx)? } Char(idx) => Cur { idx: *idx }.into(), RelativeChar(offset) => { let mut c = cur_dot.active_cur(); - c.idx = (c.idx as isize + *offset) as usize; + c.idx = c.idx.saturating_add(*offset); c.into() } @@ -478,23 +489,23 @@ pub trait Address: Haystack + Sized { } Regex(re) => { - let from = cur_dot.last_cur().idx; + let from = self.char_to_byte(cur_dot.last_cur().idx)?; let m = re.find_from(self, from)?; let (byte_from, byte_to) = m.loc(); let from = self.byte_to_char(byte_from).unwrap(); let to = self.byte_to_char(byte_to).unwrap(); - Dot::from_char_indices(from, to.saturating_sub(1)) + Dot::from_char_indices(from, to.saturating_usub(1)) } RegexBack(re) => { - let from = cur_dot.first_cur().idx; + let from = self.char_to_byte(cur_dot.first_cur().idx)?; let m = re.find_rev_from(self, from)?; let (byte_from, byte_to) = m.loc(); let from = self.byte_to_char(byte_from).unwrap(); let to = self.byte_to_char(byte_to).unwrap(); - Dot::from_char_indices(from, to.saturating_sub(1)) + Dot::from_char_indices(from, to.saturating_usub(1)) } }; @@ -521,29 +532,29 @@ pub trait Address: Haystack + Sized { impl Address for GapBuffer { fn current_dot(&self) -> Dot { - Dot::from_char_indices(0, self.len_chars().saturating_sub(1)) + Dot::from_char_indices(CharIdx(0), CharIdx(self.len_chars().saturating_sub(1))) } - fn len_bytes(&self) -> ByteIdx { + fn len_bytes(&self) -> usize { self.len() } - fn len_chars(&self) -> CharIdx { + fn len_chars(&self) -> usize { self.len_chars() } - fn line_to_char(&self, line_idx: usize) -> Option { + fn line_to_char(&self, line_idx: LineIdx) -> Option { self.try_line_to_char(line_idx) } - fn char_to_line(&self, char_idx: CharIdx) -> Option { + fn char_to_line(&self, char_idx: CharIdx) -> Option { self.try_char_to_line(char_idx) } fn char_to_line_end(&self, char_idx: CharIdx) -> Option { let line_idx = self.try_char_to_line(char_idx)?; - match self.try_line_to_char(line_idx + 1) { - None => Some(self.len_chars() - 1), + match self.try_line_to_char(line_idx.uadd(1)) { + None => Some(CharIdx(self.len_chars() - 1)), Some(idx) => Some(idx), } } @@ -559,26 +570,26 @@ impl Address for Buffer { self.dot } - fn len_bytes(&self) -> ByteIdx { + fn len_bytes(&self) -> usize { self.txt.len() } - fn len_chars(&self) -> CharIdx { + fn len_chars(&self) -> usize { self.txt.len_chars() } - fn line_to_char(&self, line_idx: usize) -> Option { + fn line_to_char(&self, line_idx: LineIdx) -> Option { self.txt.try_line_to_char(line_idx) } - fn char_to_line(&self, char_idx: CharIdx) -> Option { + fn char_to_line(&self, char_idx: CharIdx) -> Option { self.txt.try_char_to_line(char_idx) } fn char_to_line_end(&self, char_idx: CharIdx) -> Option { let line_idx = self.txt.try_char_to_line(char_idx)?; - match self.txt.try_line_to_char(line_idx + 1) { - None => Some(self.txt.len_chars() - 1), + match self.txt.try_line_to_char(line_idx.uadd(1)) { + None => Some(CharIdx(self.txt.len_chars() - 1)), Some(idx) => Some(idx), } } @@ -619,34 +630,34 @@ mod tests { #[test_case("+-", simple(CurrentLine); "current line plus minus")] #[test_case("0", simple(Bof); "beginning of file")] #[test_case("$", simple(Eof); "end of file")] - #[test_case("3", simple(Line(2)); "single line")] - #[test_case("+42", simple(RelativeLine(42)); "relative line forward")] - #[test_case("-12", simple(RelativeLine(-12)); "relative line backward")] - #[test_case("#3", simple(Char(3)); "char")] - #[test_case("+#42", simple(RelativeChar(42)); "relative char forward")] - #[test_case("-#12", simple(RelativeChar(-12)); "relative char backward")] - #[test_case("3:9", simple(LineAndColumn(2, 8)); "line and column cursor")] + #[test_case("3", simple(Line(LineIdx(2))); "single line")] + #[test_case("+42", simple(RelativeLine(LineOffset(42))); "relative line forward")] + #[test_case("-12", simple(RelativeLine(LineOffset(-12))); "relative line backward")] + #[test_case("#3", simple(Char(CharIdx(3))); "char")] + #[test_case("+#42", simple(RelativeChar(CharOffset(42))); "relative char forward")] + #[test_case("-#12", simple(RelativeChar(CharOffset(-12))); "relative char backward")] + #[test_case("3:9", simple(LineAndColumn(LineIdx(2), CharOffset(8))); "line and column cursor")] #[test_case("/foo/", simple(Regex(re("foo"))); "regex")] #[test_case("+/baz/", simple(Regex(re("baz"))); "regex explicit forward")] #[test_case("-/bar/", simple(RegexBack(re_rev("bar"))); "regex back")] // Simple with suffix - #[test_case("#5+", simple(SimpleAddr::new(Char(5), vec![Eol])); "char to eol")] - #[test_case("#5-", simple(SimpleAddr::new(Char(5), vec![Bol])); "char to bol")] - #[test_case("5+#3", simple(SimpleAddr::new(Line(4), vec![RelativeChar(3)])); "line plus char")] - #[test_case("5-#3", simple(SimpleAddr::new(Line(4), vec![RelativeChar(-3)])); "line minus char")] + #[test_case("#5+", simple(SimpleAddr::new(Char(CharIdx(5)), vec![Eol])); "char to eol")] + #[test_case("#5-", simple(SimpleAddr::new(Char(CharIdx(5)), vec![Bol])); "char to bol")] + #[test_case("5+#3", simple(SimpleAddr::new(Line(LineIdx(4)), vec![RelativeChar(CharOffset(3))])); "line plus char")] + #[test_case("5-#3", simple(SimpleAddr::new(Line(LineIdx(4)), vec![RelativeChar(CharOffset(-3))])); "line minus char")] // Compound #[test_case(",", compound(Bof, Eof); "full")] - #[test_case("5,", compound(Line(4), Eof); "from n")] - #[test_case("50,", compound(Line(49), Eof); "from n multi digit")] - #[test_case("5,9", compound(Line(4), Line(8)); "from n to m")] - #[test_case("25,90", compound(Line(24), Line(89)); "from n to m multi digit")] + #[test_case("5,", compound(Line(LineIdx(4)), Eof); "from n")] + #[test_case("50,", compound(Line(LineIdx(49)), Eof); "from n multi digit")] + #[test_case("5,9", compound(Line(LineIdx(4)), Line(LineIdx(8))); "from n to m")] + #[test_case("25,90", compound(Line(LineIdx(24)), Line(LineIdx(89))); "from n to m multi digit")] #[test_case("/foo/,/bar/", compound(Regex(re("foo")), Regex(re("bar"))); "regex range")] // Compound with suffix #[test_case( "-/\\s/+#1,/\\s/-#1", compound( - SimpleAddr::new(RegexBack(re_rev("\\s")), vec![RelativeChar(1)]), - SimpleAddr::new(Regex(re("\\s")), vec![RelativeChar(-1)]), + SimpleAddr::new(RegexBack(re_rev("\\s")), vec![RelativeChar(CharOffset(1))]), + SimpleAddr::new(Regex(re("\\s")), vec![RelativeChar(CharOffset(-1))]), ); "regex range with suffixes" )] @@ -657,15 +668,17 @@ mod tests { } #[test_case("0", Dot::default(), "t"; "bof")] - #[test_case("2", Dot::from_char_indices(15, 26), "and another\n"; "line 2")] - #[test_case("2:1", Cur { idx: 15 }.into(), "a"; "line 2 col 1")] - #[test_case("2:2", Cur { idx: 16 }.into(), "n"; "line 2 col 2")] - #[test_case("-1", Dot::from_char_indices(0, 14), "this is a line\n"; "line 1 relative to 2")] - #[test_case("/something/", Dot::from_char_indices(33, 41), "something"; "regex forward")] - #[test_case("-/line/", Dot::from_char_indices(10, 13), "line"; "regex back")] - #[test_case("-/his/", Dot::from_char_indices(1, 3), "his"; "regex back 2")] - #[test_case("-/a/,/a/", Dot::from_char_indices(15, 19), "and a"; "regex range")] - #[test_case("-/\\s/+#1,/\\s/-#1", Dot::from_char_indices(15, 17), "and"; "regex range boundaries")] + #[test_case("2", Dot::from_char_indices(CharIdx(15), CharIdx(26)), "and another\n"; "line 2")] + #[test_case("2:1", Cur { idx: CharIdx(15) }.into(), "a"; "line 2 col 1")] + #[test_case("2:2", Cur { idx: CharIdx(16) }.into(), "n"; "line 2 col 2")] + #[test_case("-1", Dot::from_char_indices(CharIdx(0), CharIdx(14)), "this is a line\n"; "line 1 relative to 2")] + #[test_case("-5", Dot::from_char_indices(CharIdx(0), CharIdx(14)), "this is a line\n"; "relative line before bof clamps")] + #[test_case("-#20", Cur { idx: CharIdx(0) }.into(), "t"; "relative char before bof clamps")] + #[test_case("/something/", Dot::from_char_indices(CharIdx(33), CharIdx(41)), "something"; "regex forward")] + #[test_case("-/line/", Dot::from_char_indices(CharIdx(10), CharIdx(13)), "line"; "regex back")] + #[test_case("-/his/", Dot::from_char_indices(CharIdx(1), CharIdx(3)), "his"; "regex back 2")] + #[test_case("-/a/,/a/", Dot::from_char_indices(CharIdx(15), CharIdx(19)), "and a"; "regex range")] + #[test_case("-/\\s/+#1,/\\s/-#1", Dot::from_char_indices(CharIdx(15), CharIdx(17)), "and"; "regex range boundaries")] #[test] fn map_addr_works(s: &str, expected: Dot, expected_contents: &str) { let mut b = Buffer::new_unnamed( @@ -673,7 +686,7 @@ mod tests { "this is a line\nand another\n- [ ] something to do\n", Default::default(), ); - b.dot = Cur::new(16).into(); + b.dot = Cur::new(CharIdx(16)).into(); let addr = Addr::parse(s).expect("valid addr"); b.dot = b.map_addr(&addr); @@ -690,6 +703,10 @@ mod tests { #[test_case("99999999999999999999,100"; "range start overflow")] #[test_case("1,99999999999999999999"; "range end overflow")] #[test_case("99999999999999999999:5"; "line in line col overflow")] + #[test_case("+#10000000000000000000"; "relative char forward isize overflow")] + #[test_case("-#10000000000000000000"; "relative char back isize overflow")] + #[test_case("+10000000000000000000"; "relative line forward isize overflow")] + #[test_case("-10000000000000000000"; "relative line back isize overflow")] #[test] fn giant_address_integers_error(s: &str) { let res = Addr::parse(s); diff --git a/src/exec/mod.rs b/src/exec/mod.rs index 13e379a0..399216de 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -1,6 +1,6 @@ //! Sam style language for executing structural regular expressions against ad Buffers use crate::{ - buffer::{Buffer, ByteIdx, CharIdx, GapBuffer}, + buffer::{Buffer, ByteIdx, CharIdx, CharOffset, GapBuffer}, dot::{Cur, Dot}, editor::BAction, parse::ParseInput, @@ -130,7 +130,7 @@ impl Edit for Buffer { if from == to { return; } - self.dot = Dot::from_char_indices(from, to.saturating_sub(1)).collapse_null_range(); + self.dot = Dot::from_char_indices(from, to.saturating_usub(1)).collapse_null_range(); self.handle_action(BAction::Delete, Source::Fsys); } @@ -238,8 +238,8 @@ impl Program { let (char_from, char_to) = dot.as_char_indices(); let byte_from = ed.char_to_byte(char_from).unwrap(); let byte_to = ed - .char_to_byte(char_to.saturating_add(1)) - .unwrap_or_else(|| ed.len_bytes()); + .char_to_byte(char_to.uadd(1)) + .unwrap_or_else(|| ByteIdx(ed.len_bytes())); let mut edit_actions = self.gather_actions(byte_from, byte_to, ed, runner, fname, out)?; @@ -249,7 +249,7 @@ impl Program { // this selection based on how they manipulate the buffer so we need to special case this // final action in order to not double-count the delta it would generate. let last_action = edit_actions.pop(); - let mut delta = 0; + let mut delta = CharOffset(0); if let Some(action) = last_action { dot = action.as_dot(ed); action.apply(ed); @@ -272,10 +272,13 @@ impl Program { // Apply the cumulative delta from all edit actions to the final dot position to account // for changes made to the buffer state. let (from, to) = dot.as_char_indices(); - let from = (from as isize + delta) as usize; - let to = (to as isize + delta) as usize; + let from = from.saturating_add(delta); + let to = to.saturating_add(delta); - Ok(Dot::from_char_indices(min(from, ix_max), min(to, ix_max))) + Ok(Dot::from_char_indices( + min(from, CharIdx(ix_max)), + min(to, CharIdx(ix_max)), + )) } fn gather_actions<'a, E, R, W>( @@ -297,15 +300,15 @@ impl Program { let mut edit_actions = Vec::new(); let mut ctx = Ctx { fname, - byte_from: 0, + byte_from: ByteIdx(0), ed, row_col: RefCell::new(None), }; - for caps in se.iter_tagged_captures_between(byte_from, byte_to, ed) { + for caps in se.iter_tagged_captures_between(byte_from.0, byte_to.0, ed) { let action = caps.action.as_ref().unwrap(); let id = action.id(); - ctx.byte_from = caps.from(); + ctx.byte_from = ByteIdx(caps.from()); ctx.row_col.borrow_mut().take(); match action.tag() { @@ -336,13 +339,15 @@ impl Program { // Edit actions // Delete matched text - 'd' => edit_actions.push(EditAction::Remove(caps.from(), caps.to())), + 'd' => { + edit_actions.push(EditAction::Remove(ByteIdx(caps.from()), ByteIdx(caps.to()))) + } // Change matched text to rendered template 'c' => { edit_actions.push(EditAction::Replace( - caps.from(), - caps.to(), + ByteIdx(caps.from()), + ByteIdx(caps.to()), self.templates[&id].render_with_context(&caps, &ctx)?, )); } @@ -350,7 +355,7 @@ impl Program { // Insert rendered template before match 'i' => { edit_actions.push(EditAction::Insert( - caps.from(), + ByteIdx(caps.from()), self.templates[&id].render_with_context(&caps, &ctx)?, )); } @@ -358,7 +363,7 @@ impl Program { // Append rendered template after match 'a' => { edit_actions.push(EditAction::Insert( - caps.to(), + ByteIdx(caps.to()), self.templates[&id].render_with_context(&caps, &ctx)?, )); } @@ -367,8 +372,8 @@ impl Program { '<' => { let cmd = self.templates[&id].render_with_context(&caps, &ctx)?; edit_actions.push(EditAction::Replace( - caps.from(), - caps.to(), + ByteIdx(caps.from()), + ByteIdx(caps.to()), runner.run_shell_command(&cmd, None)?, )); } @@ -378,8 +383,8 @@ impl Program { let cmd = self.templates[&id].render_with_context(&caps, &ctx)?; let slice = caps.as_slice(); edit_actions.push(EditAction::Replace( - caps.from(), - caps.to(), + ByteIdx(caps.from()), + ByteIdx(caps.to()), runner.run_shell_command(&cmd, Some(slice.into().as_ref()))?, )); } @@ -407,9 +412,9 @@ impl EditAction { match self { Self::Insert(from, s) | Self::Replace(from, _, s) => { let from = ed.byte_to_char(*from).unwrap(); - let n_chars = s.chars().count(); + let n_chars = CharOffset::from(s.as_str()); - Dot::from_char_indices(from, from + n_chars - 1) + Dot::from_char_indices(from, from + n_chars - CharOffset(1)) } Self::Remove(from, _) => { @@ -419,7 +424,7 @@ impl EditAction { } } - fn apply(self, ed: &mut E) -> isize + fn apply(self, ed: &mut E) -> CharOffset where E: Edit, { @@ -427,20 +432,20 @@ impl EditAction { Self::Insert(from, s) => { let from = ed.byte_to_char(from).unwrap(); ed.insert(from, &s); - s.chars().count() as isize + CharOffset::from(s.as_str()) } Self::Remove(from, to) => { let from = ed.byte_to_char(from).unwrap(); let to = ed.byte_to_char(to).unwrap(); ed.remove(from, to); - -((to - from) as isize) + -to.offset_from(from) } Self::Replace(from, to, s) => { - Self::Remove(from, to).apply(ed); - let n_chars = Self::Insert(from, s).apply(ed); - n_chars - (to - from) as isize + let removed = Self::Remove(from, to).apply(ed); + let inserted = Self::Insert(from, s).apply(ed); + inserted + removed } } } @@ -587,7 +592,7 @@ mod tests { let mut runner = SystemRunner::new(env::current_dir().unwrap()); let mut b = Buffer::new_unnamed(0, "foo│foo│foo", Default::default()); - b.dot = Cur::new(idx).into(); + b.dot = Cur::new(CharIdx(idx)).into(); prog.execute(&mut b, &mut runner, "test", &mut vec![]) .unwrap(); @@ -626,7 +631,7 @@ mod tests { let initial_content = "this is a FOO line\nand another"; let mut b = Buffer::new_unnamed(0, initial_content, Default::default()); - b.dot = Dot::from_char_indices(9, 12); + b.dot = Dot::from_char_indices(CharIdx(9), CharIdx(12)); assert_eq!(b.dot_contents(), " FOO"); prog.execute(&mut b, &mut runner, "test", &mut vec![]) @@ -660,7 +665,25 @@ mod tests { assert_eq!(&b.str_contents(), expected_content); assert_eq!(&dot.content(&b), expected_dot_content); - assert_eq!(dot.as_char_indices(), expected_dot); + assert_eq!( + dot.as_char_indices(), + (CharIdx(expected_dot.0), CharIdx(expected_dot.1)) + ); + } + + #[test] + fn returned_dot_is_correct_when_replacing_multibyte_chars() { + let prog = Program::try_parse(", x/é/ c/E/").unwrap(); + let mut runner = SystemRunner::new(env::current_dir().unwrap()); + let mut b = Buffer::new_unnamed(0, "éa éa", Default::default()); + + let dot = prog + .execute(&mut b, &mut runner, "test", &mut vec![]) + .unwrap(); + + assert_eq!(&b.str_contents(), "Ea Ea"); + assert_eq!(&dot.content(&b), "E"); + assert_eq!(dot.as_char_indices(), (CharIdx(3), CharIdx(3))); } #[test_case(", d"; "delete buffer")] @@ -1084,14 +1107,18 @@ mod tests { let prog = Program::try_parse(program).unwrap(); let mut runner = SystemRunner::new(env::current_dir().unwrap()); let mut b = Buffer::new_unnamed(0, initial_content, Default::default()); - b.dot = Cur::new(initial_dot_idx).into(); + b.dot = Cur::new(CharIdx(initial_dot_idx)).into(); let dot = prog .execute(&mut b, &mut runner, "test", &mut vec![]) .unwrap(); assert_eq!(&b.str_contents(), expected_content, "buffer content"); - assert_eq!(dot.as_char_indices(), expected_dot, "returned dot"); + assert_eq!( + dot.as_char_indices(), + (CharIdx(expected_dot.0), CharIdx(expected_dot.1)), + "returned dot" + ); } #[test_case("0,/foo/ d", "start\nfoo bar", " bar"; "bof to regex")] @@ -1216,7 +1243,11 @@ mod tests { expected_content, "buffer should not change" ); - assert_eq!(dot.as_char_indices(), expected_dot, "dot should be set"); + assert_eq!( + dot.as_char_indices(), + (CharIdx(expected_dot.0), CharIdx(expected_dot.1)), + "dot should be set" + ); } #[test_case(", x/./ c/X/", "hello", "XXXXX"; "ascii single char")] @@ -1265,7 +1296,7 @@ mod tests { .unwrap(); assert_eq!(&b.str_contents(), "REPLACEMENT REPLACEMENT REPLACEMENT"); - assert!(dot.as_char_indices().1 > 5); + assert!(dot.as_char_indices().1 > CharIdx(5)); } #[test] @@ -1279,7 +1310,7 @@ mod tests { .unwrap(); assert_eq!(&b.str_contents(), "x x x"); - assert_eq!(dot.as_char_indices(), (4, 4)); + assert_eq!(dot.as_char_indices(), (CharIdx(4), CharIdx(4))); } #[test_case(", x/foo/ x/o/ c/X/", "foo bar", "fXX bar"; "nested x")] diff --git a/src/fsys/event.rs b/src/fsys/event.rs index dedf3652..391dcc6d 100644 --- a/src/fsys/event.rs +++ b/src/fsys/event.rs @@ -44,7 +44,7 @@ impl InputFilter { } else { Kind::InsertBody }; - let evt = FsysEvent::new(source, k, ch_from, byte_from, txt); + let evt = FsysEvent::new(source, k, ch_from.0, byte_from.0, txt); _ = self.tx.send(evt); } @@ -54,7 +54,7 @@ impl InputFilter { } else { Kind::DeleteBody }; - let evt = FsysEvent::new(source, k, ch_from, byte_from, txt); + let evt = FsysEvent::new(source, k, ch_from.0, byte_from.0, txt); _ = self.tx.send(evt); } @@ -64,7 +64,7 @@ impl InputFilter { } else { Kind::LoadBody }; - let evt = FsysEvent::new(source, k, ch_from, byte_from, txt); + let evt = FsysEvent::new(source, k, ch_from.0, byte_from.0, txt); _ = self.tx.send(evt); } @@ -77,7 +77,7 @@ impl InputFilter { arg: Option<(CharIdx, ByteIdx, String)>, ) { if let Some((ch_from, byte_from, arg)) = arg { - let evt = FsysEvent::new(source, Kind::ChordedArgument, ch_from, byte_from, &arg); + let evt = FsysEvent::new(source, Kind::ChordedArgument, ch_from.0, byte_from.0, &arg); _ = self.tx.send(evt); } let k = if self.is_scratch { @@ -86,7 +86,7 @@ impl InputFilter { Kind::ExecuteBody }; - let evt = FsysEvent::new(source, k, ch_from, byte_from, txt); + let evt = FsysEvent::new(source, k, ch_from.0, byte_from.0, txt); _ = self.tx.send(evt); } } diff --git a/src/lsp/capabilities.rs b/src/lsp/capabilities.rs index 97303a15..ab5db6f8 100644 --- a/src/lsp/capabilities.rs +++ b/src/lsp/capabilities.rs @@ -1,6 +1,6 @@ //! Capability based logic use crate::{ - buffer::{Buffer, ByteIdx, CharIdx}, + buffer::{Buffer, ByteIdx, ByteOffset, CharIdx, CharOffset, LineIdx}, exec::{Addr, AddrBase}, lsp::Pos, }; @@ -74,26 +74,30 @@ pub(crate) enum PositionEncoding { } impl PositionEncoding { - pub(crate) fn parse_lsp_position(&self, b: &Buffer, pos: Position) -> (usize, usize) { + pub(crate) fn parse_lsp_position(&self, b: &Buffer, pos: Position) -> (LineIdx, CharOffset) { let pos_line = pos.line as usize; if pos_line > b.len_lines() - 1 { warn!("LSP position out of bounds, clamping to EOF"); - return (b.len_lines().saturating_sub(1), b.len_chars()); + return ( + LineIdx(b.len_lines().saturating_sub(1)), + CharOffset(b.len_chars() as isize), + ); } match self { Self::Utf8 => { - let line_start: ByteIdx = b.txt.line_to_byte(pos.line as usize); - let col = b.txt.chars_in_raw_range( - b.txt.byte_to_raw_byte(line_start), - b.txt.byte_to_raw_byte(line_start + pos.character as usize), - ); - - (pos.line as usize, col) + let line = LineIdx(pos.line as usize); + let line_start: ByteIdx = b.txt.line_to_byte(line); + let col = b + .txt + .byte_to_char(line_start + ByteOffset(pos.character as isize)) + .offset_from(b.txt.line_to_char(line)); + + (line, col) } Self::Utf16 => { - let slice = b.txt.line(pos.line as usize); + let slice = b.txt.line(LineIdx(pos.line as usize)); let mut remaining = pos.character as usize; let mut col = 0; for ch in slice.chars() { @@ -107,10 +111,13 @@ impl PositionEncoding { col = slice.chars().count(); // clamp to EOL } - (pos.line as usize, col) + (LineIdx(pos.line as usize), CharOffset(col as isize)) } - Self::Utf32 => (pos.line as usize, pos.character as usize), + Self::Utf32 => ( + LineIdx(pos.line as usize), + CharOffset(pos.character as isize), + ), } } @@ -122,27 +129,27 @@ impl PositionEncoding { Pos::new(file, line, character) } - fn lsp_position(&self, b: &Buffer, line: usize, col: usize) -> (u32, u32) { + fn lsp_position(&self, b: &Buffer, line: LineIdx, col: CharOffset) -> (u32, u32) { match self { Self::Utf8 => { let line_start: CharIdx = b.txt.line_to_char(line); let start_idx: ByteIdx = b.txt.char_to_byte(line_start); let character = b.txt.char_to_byte(line_start + col) - start_idx; - (line as u32, character as u32) + (line.0 as u32, character.0 as u32) } Self::Utf16 => { let slice = b.txt.line(line); let mut character = 0; - for ch in slice.chars().take(col) { + for ch in slice.chars().take(col.0 as usize) { character += ch.len_utf16(); } - (line as u32, character as u32) + (line.0 as u32, character as u32) } - Self::Utf32 => (line as u32, col as u32), + Self::Utf32 => (line.0 as u32, col.0 as u32), } } } @@ -199,7 +206,7 @@ impl Coords { if (row_start, col_start) == (row_end, col_end) { // LSP insert at this position within the buffer Addr::simple(AddrBase::LineAndColumn(row_start, col_start)) - } else if row_start == row_end && col_end == col_start + 1 { + } else if row_start == row_end && col_end == col_start + CharOffset(1) { // LSP delete of a single character Addr::compound( AddrBase::LineAndColumn(row_start, col_start), @@ -218,14 +225,14 @@ impl Coords { // character of a given line then... // With that in mind, we need to check for this case and filter for when the line is // actually a blank line otherwise removing full lines doesn't work. - if col_end == 0 && !b.txt.line_is_blank(row_end) { - row_end = row_end.saturating_sub(1); - col_end = b.txt.line(row_end).chars().count(); + if col_end == CharOffset(0) && !b.txt.line_is_blank(row_end) { + row_end = row_end.saturating_sub(LineIdx(1)); + col_end = CharOffset(b.txt.line(row_end).chars().count() as isize); } Addr::compound( AddrBase::LineAndColumn(row_start, col_start), - AddrBase::LineAndColumn(row_end, col_end.saturating_sub(1)), + AddrBase::LineAndColumn(row_end, (col_end - CharOffset(1)).max(CharOffset(0))), ) } } @@ -262,7 +269,7 @@ mod tests { let (line, col) = PositionEncoding::Utf16.parse_lsp_position(&b, pos); - assert_eq!(line, 0); - assert_eq!(col, expected_col); + assert_eq!(line, LineIdx(0)); + assert_eq!(col, CharOffset(expected_col as isize)); } } diff --git a/src/lsp/messages/request/completion.rs b/src/lsp/messages/request/completion.rs index 83e90138..0672864b 100644 --- a/src/lsp/messages/request/completion.rs +++ b/src/lsp/messages/request/completion.rs @@ -1,5 +1,5 @@ use crate::{ - buffer::Buffers, + buffer::{Buffers, CharIdx, CharOffset}, die, dot::{Cur, Dot, Range}, editor::{Actions, EAction, MbSelect, MiniBufferSelection}, @@ -133,11 +133,11 @@ impl MbSelect for Completions { // find the start of the "word" we are on let offset = b - .rev_iter_between_chars(cur.idx, 0) + .rev_iter_between_chars(cur.idx, CharIdx(0)) .take_while(|(_, ch)| ch.is_alphanumeric() || *ch == '_') .count(); let start = Cur { - idx: cur.idx - offset, + idx: cur.idx - CharOffset(offset as isize), }; let r = Range::from_cursors(start, cur, false); let input = Dot::from(r).content(b); diff --git a/src/lsp/messages/request/references.rs b/src/lsp/messages/request/references.rs index e2f2da1e..748622c1 100644 --- a/src/lsp/messages/request/references.rs +++ b/src/lsp/messages/request/references.rs @@ -1,5 +1,5 @@ use crate::{ - buffer::Buffers, + buffer::{Buffers, LineIdx}, editor::{Actions, BAction, EAction, MbSelect, MiniBufferSelection, UAction, ViewPort}, lsp::{ LspManager, Pos, PositionEncoding, @@ -82,12 +82,12 @@ impl Reference { } fn mb_line(&self, buffers: &Buffers, width: usize) -> String { - let try_buffer_line = |path: &str, y: usize| -> Option { + let try_buffer_line = |path: &str, y: LineIdx| -> Option { let s = buffers.with_path(path)?.line(y)?.to_string(); Some(s.trim().to_string()) }; - match try_buffer_line(&self.path, self.coords.line() as usize) { + match try_buffer_line(&self.path, LineIdx(self.coords.line() as usize)) { Some(line) => format!("{: self.prefix.clone(), } diff --git a/src/lsp/mod.rs b/src/lsp/mod.rs index cf9a40a9..72921e1a 100644 --- a/src/lsp/mod.rs +++ b/src/lsp/mod.rs @@ -3,7 +3,7 @@ //! See the LSP spec for details of semantics: //! use crate::{ - buffer::{Buffer, Buffers}, + buffer::{Buffer, Buffers, LineIdx}, config::{FtypeConfig, LspConfig, ftype_config_for_path_and_first_line}, die, editor::{Action, EAction, MbSelect}, @@ -129,7 +129,10 @@ impl LspManagerHandle { } fn config_for_buffer(&self, b: &Buffer) -> Option<(&String, &LspConfig)> { - let first_line = b.line(0).map(|l| l.to_string()).unwrap_or_default(); + let first_line = b + .line(LineIdx(0)) + .map(|l| l.to_string()) + .unwrap_or_default(); self.config_for_path_and_first_line(b.path()?, &first_line) } diff --git a/src/plumb.rs b/src/plumb.rs index 590d76c3..2fd5a1fd 100644 --- a/src/plumb.rs +++ b/src/plumb.rs @@ -122,7 +122,7 @@ pub struct PlumbingMessage { pub dst: Option, /// The working directory (used when data is a filename) pub wdir: Option, - /// The offset within 'data' where the user's cursor currently lies (default=0). + /// The character offset within 'data' where the user's cursor currently lies. /// If a rule specifies a 'narrows-to' rule it will be used to narrow initial data /// to only include the specified cursor, otherwise the match will be rejected. pub cur: usize, @@ -434,7 +434,10 @@ impl Pattern { debug!(%msg.cur, "narrowing for provided cur"); for m in re.find_iter(&msg.data.as_str()) { let (from, to) = m.loc(); - if from <= msg.cur && msg.cur <= to { + // The match location is in bytes but cur is a character offset + let ch_from = msg.data[..from.0].chars().count(); + let ch_to = ch_from + msg.data[from.0..to.0].chars().count(); + if ch_from <= msg.cur && msg.cur <= ch_to { debug!(%from, %to, "successfully narrowed"); msg.data = m.match_text(&msg.data.as_str()).into_owned(); msg.cur = 0; // consume the cursor as it is now invalid diff --git a/src/regex/haystack.rs b/src/regex/haystack.rs index f92eb702..927a1c32 100644 --- a/src/regex/haystack.rs +++ b/src/regex/haystack.rs @@ -1,5 +1,5 @@ //! A haystack is something that can be searched over by a Regex -use crate::buffer::{Buffer, ByteIdx, CharIdx, GapBuffer}; +use crate::buffer::{Buffer, ByteIdx, ByteOffset, CharIdx, GapBuffer}; use std::borrow::Cow; /// Something that can be searched over by a [Regex][0]. @@ -12,7 +12,7 @@ use std::borrow::Cow; pub trait Haystack { fn try_make_contiguous(&mut self); fn is_contiguous(&self) -> bool; - fn len(&self) -> ByteIdx; + fn len(&self) -> usize; fn substr_from<'a>(&'a self, offset: ByteIdx) -> Option>; fn substr<'a>(&'a self, from: ByteIdx, to: ByteIdx) -> Cow<'a, str>; @@ -32,52 +32,54 @@ impl Haystack for &str { true } - fn len(&self) -> ByteIdx { + fn len(&self) -> usize { str::len(self) } fn substr_from<'a>(&'a self, offset: ByteIdx) -> Option> { - if offset > self.len() { + if offset.0 > self.len() { None } else { - let raw = &self.as_bytes()[offset..]; + let raw = &self.as_bytes()[offset.0..]; // SAFETY: assumes a valid byte offset Some(Cow::Borrowed(unsafe { std::str::from_utf8_unchecked(raw) })) } } fn substr<'a>(&'a self, from: ByteIdx, to: ByteIdx) -> Cow<'a, str> { - Cow::Borrowed(&self[from..to]) + Cow::Borrowed(&self[from.0..to.0]) } fn byte_to_char(&self, byte_idx: ByteIdx) -> Option { - Some( + Some(CharIdx( self.char_indices() - .take_while(|&(idx, _)| idx < byte_idx) + .take_while(|&(idx, _)| idx < byte_idx.0) .count(), - ) + )) } fn char_to_byte(&self, char_idx: CharIdx) -> Option { - self.char_indices().nth(char_idx).map(|(idx, _)| idx) + self.char_indices() + .nth(char_idx.0) + .map(|(idx, _)| ByteIdx(idx)) } fn iter_from(&self, from: ByteIdx) -> Option> { - if from >= self.len() { + if from.0 >= self.len() { None } else { Some( - self[from..] + self[from.0..] .char_indices() - .map(move |(i, ch)| (i + from, ch)), + .map(move |(i, ch)| (from + ByteOffset(i as isize), ch)), ) } } fn iter_between(&self, from: ByteIdx, to: ByteIdx) -> impl Iterator { - self[from..to] + self[from.0..to.0] .char_indices() - .map(move |(i, ch)| (i + from, ch)) + .map(move |(i, ch)| (from + ByteOffset(i as isize), ch)) } fn rev_iter_between( @@ -85,9 +87,9 @@ impl Haystack for &str { from: ByteIdx, to: ByteIdx, ) -> impl Iterator { - self[from..to] + self[from.0..to.0] .char_indices() - .map(move |(i, ch)| (i + from, ch)) + .map(move |(i, ch)| (from + ByteOffset(i as isize), ch)) .rev() } } @@ -101,12 +103,12 @@ impl Haystack for GapBuffer { self.is_contiguous() } - fn len(&self) -> ByteIdx { + fn len(&self) -> usize { self.len() } fn substr_from<'a>(&'a self, offset: ByteIdx) -> Option> { - if offset > self.len() { + if offset.0 > self.len() { None } else { // SAFETY: assumes make_contiguous was called first @@ -119,7 +121,7 @@ impl Haystack for GapBuffer { } fn byte_to_char(&self, byte_idx: ByteIdx) -> Option { - if byte_idx > self.len() { + if byte_idx.0 > self.len() { None } else { Some(self.byte_to_char(byte_idx)) @@ -127,7 +129,7 @@ impl Haystack for GapBuffer { } fn char_to_byte(&self, char_idx: CharIdx) -> Option { - if char_idx > self.len_chars() { + if char_idx.0 > self.len_chars() { None } else { Some(self.char_to_byte(char_idx)) @@ -135,16 +137,16 @@ impl Haystack for GapBuffer { } fn iter_from(&self, from: ByteIdx) -> Option> { - if from >= self.len() { + if from.0 >= self.len() { None } else { let mut acc = from; Some( - self.slice_from_byte_offsets(from, self.len()) + self.slice_from_byte_offsets(from, ByteIdx(self.len())) .chars() .map(move |ch| { let item = (acc, ch); - acc += ch.len_utf8(); + acc += ByteOffset::from(ch); item }), ) @@ -158,7 +160,7 @@ impl Haystack for GapBuffer { .chars() .map(move |ch| { let item = (acc, ch); - acc += ch.len_utf8(); + acc += ByteOffset::from(ch); item }) } @@ -173,7 +175,7 @@ impl Haystack for GapBuffer { self.slice_from_byte_offsets(from, to) .rev_chars() .map(move |ch| { - acc -= ch.len_utf8(); + acc -= ByteOffset::from(ch); (acc, ch) }) } @@ -188,12 +190,12 @@ impl Haystack for Buffer { self.txt.is_contiguous() } - fn len(&self) -> ByteIdx { + fn len(&self) -> usize { self.txt.len() } fn substr_from<'a>(&'a self, offset: ByteIdx) -> Option> { - if offset > self.txt.len() { + if offset.0 > self.txt.len() { None } else { // SAFETY: assumes make_contiguous was called first @@ -206,7 +208,7 @@ impl Haystack for Buffer { } fn byte_to_char(&self, byte_idx: ByteIdx) -> Option { - if byte_idx > self.txt.len() { + if byte_idx.0 > self.txt.len() { None } else { Some(self.txt.byte_to_char(byte_idx)) @@ -214,7 +216,7 @@ impl Haystack for Buffer { } fn char_to_byte(&self, char_idx: CharIdx) -> Option { - if char_idx > self.txt.len_chars() { + if char_idx.0 > self.txt.len_chars() { None } else { Some(self.txt.char_to_byte(char_idx)) @@ -222,17 +224,17 @@ impl Haystack for Buffer { } fn iter_from(&self, from: ByteIdx) -> Option> { - if from >= self.len() { + if from.0 >= self.len() { None } else { let mut acc = from; Some( self.txt - .slice_from_byte_offsets(from, self.len()) + .slice_from_byte_offsets(from, ByteIdx(self.len())) .chars() .map(move |ch| { let item = (acc, ch); - acc += ch.len_utf8(); + acc += ByteOffset::from(ch); item }), ) @@ -247,7 +249,7 @@ impl Haystack for Buffer { .chars() .map(move |ch| { let item = (acc, ch); - acc += ch.len_utf8(); + acc += ByteOffset::from(ch); item }) } @@ -263,7 +265,7 @@ impl Haystack for Buffer { .slice_from_byte_offsets(from, to) .rev_chars() .map(move |ch| { - acc -= ch.len_utf8(); + acc -= ByteOffset::from(ch); (acc, ch) }) } diff --git a/src/regex/matches.rs b/src/regex/matches.rs index 42753889..29992e80 100644 --- a/src/regex/matches.rs +++ b/src/regex/matches.rs @@ -1,5 +1,5 @@ use crate::{ - buffer::ByteIdx, + buffer::{ByteIdx, ByteOffset}, regex::{Haystack, Regex, vm::N_SLOTS}, }; use std::{borrow::Cow, sync::Arc}; @@ -16,7 +16,7 @@ pub struct Match { impl Match { pub(crate) fn synthetic(from: ByteIdx, to: ByteIdx) -> Self { - let mut sub_matches = [0; N_SLOTS]; + let mut sub_matches = [ByteIdx(0); N_SLOTS]; sub_matches[0] = from; sub_matches[1] = to; @@ -92,7 +92,7 @@ impl Match { return None; } let (start, end) = (self.sub_matches[2 * n], self.sub_matches[2 * n + 1]); - if n > 0 && start == 0 && end == 0 { + if n > 0 && start == ByteIdx(0) && end == ByteIdx(0) { return None; } @@ -119,6 +119,13 @@ impl Match { } }) } + + /// The same as [Match::iter_locs] but returning the raw usize offsets for use with APIs that + /// do not make use of [ByteIdx]. + pub fn iter_raw_locs(&self) -> impl Iterator> { + self.iter_locs() + .map(|loc| loc.map(|(from, to)| (from.0, to.0))) + } } /// An iterator over sequential, non overlapping matches of a Regex @@ -143,14 +150,14 @@ where let m = self.r.find_from(self.haystack, self.from)?; let (_, from) = m.loc(); if from == self.from { - self.from += self + let ch = self .haystack .substr_from(from) .unwrap() .chars() .next() - .unwrap() - .len_utf8(); + .unwrap(); + self.from += ByteOffset::from(ch); } else { self.from = from; } diff --git a/src/regex/mod.rs b/src/regex/mod.rs index 436d7583..d24a5faf 100644 --- a/src/regex/mod.rs +++ b/src/regex/mod.rs @@ -181,7 +181,7 @@ fn next_char(it: &mut Peekable>) -> Result, Error mod impl_structex { use super::*; - use crate::buffer::{Buffer, GapBuffer, Slice}; + use crate::buffer::{Buffer, ByteIdx, GapBuffer, Slice}; use std::{io, ops::Range}; use structex::re::{Haystack, RawCaptures, RegexEngine, Sliceable, Writable}; @@ -195,25 +195,25 @@ mod impl_structex { impl Haystack for str { fn is_match_between(&self, re: &Regex, from: usize, to: usize) -> bool { - re.matches_between(&self, from, to) + re.matches_between(&self, ByteIdx(from), ByteIdx(to)) } fn captures_between(&self, re: &Regex, from: usize, to: usize) -> Option { - let m = re.find_between(&self, from, to)?; + let m = re.find_between(&self, ByteIdx(from), ByteIdx(to))?; - Some(RawCaptures::new(m.iter_locs())) + Some(RawCaptures::new(m.iter_raw_locs())) } } impl Haystack for GapBuffer { fn is_match_between(&self, re: &Regex, from: usize, to: usize) -> bool { - re.matches_between(self, from, to) + re.matches_between(self, ByteIdx(from), ByteIdx(to)) } fn captures_between(&self, re: &Regex, from: usize, to: usize) -> Option { - let m = re.find_between(self, from, to)?; + let m = re.find_between(self, ByteIdx(from), ByteIdx(to))?; - Some(RawCaptures::new(m.iter_locs())) + Some(RawCaptures::new(m.iter_raw_locs())) } } @@ -224,11 +224,11 @@ mod impl_structex { Self: 'h; fn char_at(&self, byte_offset: usize) -> Option { - self.get_char_at(byte_offset) + self.get_char_at(ByteIdx(byte_offset)) } fn slice(&self, range: Range) -> Self::Slice<'_> { - self.slice_from_byte_offsets(range.start, range.end) + self.slice_from_byte_offsets(ByteIdx(range.start), ByteIdx(range.end)) } fn max_len(&self) -> usize { @@ -264,13 +264,13 @@ mod impl_structex { impl Haystack for Buffer { fn is_match_between(&self, re: &Regex, from: usize, to: usize) -> bool { - re.matches_between(self, from, to) + re.matches_between(self, ByteIdx(from), ByteIdx(to)) } fn captures_between(&self, re: &Regex, from: usize, to: usize) -> Option { - let m = re.find_between(self, from, to)?; + let m = re.find_between(self, ByteIdx(from), ByteIdx(to))?; - Some(RawCaptures::new(m.iter_locs())) + Some(RawCaptures::new(m.iter_raw_locs())) } } @@ -281,11 +281,12 @@ mod impl_structex { Self: 'h; fn char_at(&self, byte_offset: usize) -> Option { - self.txt.get_char_at(byte_offset) + self.txt.get_char_at(ByteIdx(byte_offset)) } fn slice(&self, range: Range) -> Self::Slice<'_> { - self.txt.slice_from_byte_offsets(range.start, range.end) + self.txt + .slice_from_byte_offsets(ByteIdx(range.start), ByteIdx(range.end)) } fn max_len(&self) -> usize { diff --git a/src/regex/stream.rs b/src/regex/stream.rs index 215abdc1..fc1a139e 100644 --- a/src/regex/stream.rs +++ b/src/regex/stream.rs @@ -1,6 +1,6 @@ use crate::{ Edit, - buffer::{ByteIdx, CharIdx, GapBuffer}, + buffer::{ByteIdx, ByteOffset, CharIdx, GapBuffer, LineIdx}, dot::Dot, exec::Address, regex::Haystack, @@ -35,8 +35,8 @@ where buf: String::with_capacity(LINE_BUF_LEN), gb: GapBuffer::from(""), closed: false, - cleared_lines: 0, - cleared_bytes: 0, + cleared_lines: LineIdx(0), + cleared_bytes: ByteIdx(0), }), } } @@ -73,7 +73,7 @@ where false } - fn len(&self) -> ByteIdx { + fn len(&self) -> usize { usize::MAX } @@ -106,7 +106,7 @@ where Some(CachingStreamIter { inner: self, from, - to: usize::MAX, + to: ByteIdx::MAX, }) } } @@ -146,10 +146,10 @@ where R: Read, { fn current_dot(&self) -> Dot { - Dot::from_char_indices(0, usize::MAX) + Dot::from_char_indices(CharIdx(0), CharIdx::MAX) } - fn len_bytes(&self) -> ByteIdx { + fn len_bytes(&self) -> usize { if self.is_closed() { self.inner.borrow().gb.len() } else { @@ -157,7 +157,7 @@ where } } - fn len_chars(&self) -> CharIdx { + fn len_chars(&self) -> usize { if self.is_closed() { self.inner.borrow().gb.len_chars() } else { @@ -165,7 +165,7 @@ where } } - fn max_iter(&self) -> CharIdx { + fn max_iter(&self) -> usize { if self.is_closed() { self.inner.borrow().gb.len_chars() } else { @@ -173,11 +173,11 @@ where } } - fn line_to_char(&self, line_idx: usize) -> Option { - let cur_len = self.inner.borrow().gb.len_lines(); + fn line_to_char(&self, line_idx: LineIdx) -> Option { + let cur_len = LineIdx(self.inner.borrow().gb.len_lines()); if line_idx > cur_len { - for _ in cur_len..=line_idx { + for _ in cur_len.to(line_idx.uadd(1)) { self.try_read_next_line(); if self.is_closed() { break; @@ -188,15 +188,15 @@ where self.inner.borrow().gb.try_line_to_char(line_idx) } - fn char_to_line(&self, char_idx: CharIdx) -> Option { + fn char_to_line(&self, char_idx: CharIdx) -> Option { self.inner.borrow().gb.try_char_to_line(char_idx) } fn char_to_line_end(&self, char_idx: CharIdx) -> Option { let gb = &self.inner.borrow().gb; let line_idx = gb.try_char_to_line(char_idx)?; - match gb.try_line_to_char(line_idx + 1) { - None => Some(gb.len_chars() - 1), + match gb.try_line_to_char(line_idx.uadd(1)) { + None => Some(CharIdx(gb.len_chars() - 1)), Some(idx) => Some(idx), } } @@ -217,7 +217,7 @@ where buf: String, gb: GapBuffer, closed: bool, - cleared_lines: usize, + cleared_lines: LineIdx, cleared_bytes: ByteIdx, } @@ -231,7 +231,7 @@ where match self.reader.read_line(&mut self.buf) { Ok(n) => { let len = self.gb.len_chars(); - self.gb.insert_str(len, &self.buf); + self.gb.insert_str(CharIdx(len), &self.buf); self.closed = n == 0; } Err(_) => self.closed = true, @@ -242,7 +242,7 @@ where let offset = logical_offset - self.cleared_bytes; let cleared_lines = self.gb.lines_before_byte_offset(offset); let char_to = self.gb.byte_to_char(offset); - self.gb.remove_range(0, char_to); + self.gb.remove_range(CharIdx(0), char_to); self.cleared_bytes = logical_offset; self.cleared_lines += cleared_lines; @@ -264,7 +264,7 @@ where R: Read, { pub fn len(&self) -> usize { - self.to - self.from + (self.to - self.from).0 } pub fn is_empty(&self) -> bool { @@ -278,8 +278,8 @@ where { fn from(s: StreamSlice<'a, R>) -> Self { let inner = s.inner.borrow(); - let to = if s.to == usize::MAX { - inner.gb.len() + let to = if s.to == ByteIdx::MAX { + ByteIdx(inner.gb.len()) } else { s.to - inner.cleared_bytes }; @@ -319,7 +319,7 @@ where match self.inner.get_char_at(self.from) { Some(ch) => { let res = (self.from, ch); - self.from += ch.len_utf8(); + self.from += ByteOffset::from(ch); return Some(res); } None if self.inner.is_closed() => return None, @@ -342,14 +342,14 @@ mod impl_structex { where R: Read, { - fn is_match_between(&self, re: &Regex, from: ByteIdx, to: ByteIdx) -> bool { - re.matches_between(self, from, to) + fn is_match_between(&self, re: &Regex, from: usize, to: usize) -> bool { + re.matches_between(self, ByteIdx(from), ByteIdx(to)) } - fn captures_between(&self, re: &Regex, from: ByteIdx, to: ByteIdx) -> Option { - let m = re.find_between(self, from, to)?; + fn captures_between(&self, re: &Regex, from: usize, to: usize) -> Option { + let m = re.find_between(self, ByteIdx(from), ByteIdx(to))?; - Some(RawCaptures::new(m.iter_locs())) + Some(RawCaptures::new(m.iter_raw_locs())) } } @@ -362,19 +362,19 @@ mod impl_structex { where Self: 'h; - fn char_at(&self, byte_offset: ByteIdx) -> Option { - self.get_char_at(byte_offset) + fn char_at(&self, byte_offset: usize) -> Option { + self.get_char_at(ByteIdx(byte_offset)) } - fn slice(&self, range: Range) -> Self::Slice<'_> { + fn slice(&self, range: Range) -> Self::Slice<'_> { StreamSlice { inner: &self.inner, - from: range.start, - to: range.end, + from: ByteIdx(range.start), + to: ByteIdx(range.end), } } - fn max_len(&self) -> ByteIdx { + fn max_len(&self) -> usize { usize::MAX } } @@ -405,8 +405,8 @@ mod impl_structex { W: std::io::Write, { let inner = self.inner.borrow(); - let to = if self.to == usize::MAX { - inner.gb.len() + let to = if self.to == ByteIdx::MAX { + ByteIdx(inner.gb.len()) } else { self.to - inner.cleared_bytes }; diff --git a/src/regex/vm.rs b/src/regex/vm.rs index 28d3eeb1..cb5ae443 100644 --- a/src/regex/vm.rs +++ b/src/regex/vm.rs @@ -9,7 +9,7 @@ //! index we are up to per-iteration as this results in roughly a 100x speed //! up from not having to allocate and free inside of the main loop. use crate::{ - buffer::ByteIdx, + buffer::{ByteIdx, ByteOffset}, regex::{ Error, Haystack, ast::{Assertion, parse}, @@ -142,7 +142,7 @@ impl Regex { { let mut inner = self.inner.lock(); inner.track_submatches = false; - inner.match_from_byte_offset(haystack, 0).is_some() + inner.match_from_byte_offset(haystack, ByteIdx(0)).is_some() } /// Determine whether or not this Regex matches the [Haystack] from the given offset without @@ -180,7 +180,7 @@ impl Regex { { let mut inner = self.inner.lock(); inner.track_submatches = true; - inner.match_from_byte_offset(haystack, 0) + inner.match_from_byte_offset(haystack, ByteIdx(0)) } /// Search the given [Haystack] for the leftmost longest match of this [Regex] starting from @@ -224,7 +224,7 @@ impl Regex { MatchIter { haystack, r: self, - from: 0, + from: ByteIdx(0), } } } @@ -265,7 +265,7 @@ impl RevRegex { { let mut inner = self.0.inner.lock(); inner.track_submatches = true; - inner.run_vm(&mut haystack.rev_iter_between(0, offset), offset) + inner.run_vm(&mut haystack.rev_iter_between(ByteIdx(0), offset), offset) } } @@ -322,7 +322,7 @@ impl RegexInner { if let Some(m) = ac.find(haystack.substr_from(byte_offset)?.as_ref()) { // m.start is based on the substr not the full haystack so we need to add the original // offset to get the correct value. - byte_offset += m.start(); + byte_offset += ByteOffset(m.start() as isize); } Some(byte_offset) @@ -378,11 +378,11 @@ impl RegexInner { where I: Iterator, { - let mut sub_matches = [0; N_SLOTS]; + let mut sub_matches = [ByteIdx(0); N_SLOTS]; self.free_sms = (1..self.prog.len()).collect(); self.sms[0] = SubMatches { refs: 1, - inner: [0; N_SLOTS], + inner: [ByteIdx(0); N_SLOTS], }; // We bump the generation to ensure we don't collide with anything from @@ -412,7 +412,7 @@ impl RegexInner { for i in 0..n { if let Some(sm) = self.step_thread(i, sp, ch) { if !self.track_submatches { - return Some(Match::synthetic(0, 0)); + return Some(Match::synthetic(ByteIdx(0), ByteIdx(0))); } matched = true; @@ -538,7 +538,11 @@ impl RegexInner { // If we are saving our initial position from a forward match then we are looking at the // correct character, otherwise the Save op is being processed at the character before the // one we need to save. - let inc_bytes = if !initial && !rev { ch.len_utf8() } else { 0 }; + let inc_bytes = if !initial && !rev { + ByteOffset::from(ch) + } else { + ByteOffset(0) + }; if (!rev && s.is_multiple_of(2)) || (rev && !s.is_multiple_of(2)) { let sm = self.sm_update(t.sm, s, sp + inc_bytes); @@ -740,7 +744,9 @@ mod tests { fn find_rev_works(re: &str, s: &str, expected: Option<&str>) { let r = RevRegex::compile(re).unwrap(); let b = Buffer::new_unnamed(0, s, Default::default()); - let m = r.find_rev_from(&b, s.len()).map(|m| m.match_text(&b)); + let m = r + .find_rev_from(&b, ByteIdx(s.len())) + .map(|m| m.match_text(&b)); assert_eq!(m.as_deref(), expected); } @@ -820,10 +826,10 @@ mod tests { // Just to convince me that the offsets here are exactly as I am expecting assert_eq!(s.chars().skip(7).collect::(), "\na multiline\nfile"); - let m1 = r.find_from(&s, 7).unwrap(); + let m1 = r.find_from(&s, ByteIdx(7)).unwrap(); assert_eq!(m1.match_text(&s), ""); - let m2 = r.find_from(&s, 8).unwrap(); + let m2 = r.find_from(&s, ByteIdx(8)).unwrap(); assert_eq!(m2.match_text(&s), "a multiline"); } diff --git a/src/syntax/mod.rs b/src/syntax/mod.rs index e5d6ec69..a15959d0 100644 --- a/src/syntax/mod.rs +++ b/src/syntax/mod.rs @@ -18,7 +18,7 @@ //! none, part or all of any given token should be rendered. use crate::{ Config, - buffer::{ByteIdx, CharIdx, GapBuffer, Slice}, + buffer::{ByteIdx, CharIdx, GapBuffer, LineIdx, Slice}, dot::Range, }; use std::{ @@ -124,7 +124,7 @@ impl SyntaxState { /// Update internal state for the requested region to prepare for a call to /// [Self::iter_tokenized_lines_from]. - pub fn update(&mut self, gb: &GapBuffer, from: usize, n_rows: usize) { + pub fn update(&mut self, gb: &GapBuffer, from: LineIdx, n_rows: usize) { match &mut self.inner { SyntaxStateInner::Ts(s) => s.update(gb, from, n_rows), SyntaxStateInner::Re(s) => s.update(gb, from, n_rows), @@ -136,7 +136,7 @@ impl SyntaxState { #[inline] pub fn iter_tokenized_lines_from<'a>( &'a self, - line: usize, + line: LineIdx, gb: &'a GapBuffer, dot_range: Range, load_exec_range: Option<(bool, Range)>, @@ -174,7 +174,7 @@ impl ByteRange { // For cursor ranges we don't highlight and for "real" ranges we need to // insert the end of the range _after_ the end index. if end.idx != start.idx { - end.idx += 1; + end.idx = end.idx.uadd(1); } Self { @@ -286,7 +286,7 @@ pub struct LineIter<'a> { /// full set of syntax ranges for the input ranges: &'a [SyntaxRange], /// the next line to yield - line: usize, + line: LineIdx, /// total number of lines (cached from gb.len_lines()) n_lines: usize, dot_range: ByteRange, @@ -295,7 +295,7 @@ pub struct LineIter<'a> { impl<'a> LineIter<'a> { pub(crate) fn new( - line: usize, + line: LineIdx, gb: &'a GapBuffer, dot_range: Range, load_exec_range: Option<(bool, Range)>, @@ -322,14 +322,14 @@ impl<'a> Iterator for LineIter<'a> { type Item = TokenIter<'a>; fn next(&mut self) -> Option { - if self.line == self.n_lines { + if self.line.0 == self.n_lines { return None; } let start_byte = self.gb.line_to_byte(self.line); let end_byte = self.gb.line_end_byte(self.line); - self.line += 1; + self.line = self.line.uadd(1); // Determine tokens required for the next line let held: Option>; @@ -757,35 +757,50 @@ mod tests { fn sr(from: usize, to: usize) -> SyntaxRange { SyntaxRange { cap_idx: Some(0), - r: ByteRange { from, to }, + r: ByteRange { + from: ByteIdx(from), + to: ByteIdx(to), + }, } } fn rt_def(from: usize, to: usize) -> RangeToken<'static> { RangeToken { tag: TK_DEFAULT, - r: ByteRange { from, to }, + r: ByteRange { + from: ByteIdx(from), + to: ByteIdx(to), + }, } } fn rt_dot(from: usize, to: usize) -> RangeToken<'static> { RangeToken { tag: TK_DOT, - r: ByteRange { from, to }, + r: ByteRange { + from: ByteIdx(from), + to: ByteIdx(to), + }, } } fn rt_exe(from: usize, to: usize) -> RangeToken<'static> { RangeToken { tag: TK_EXEC, - r: ByteRange { from, to }, + r: ByteRange { + from: ByteIdx(from), + to: ByteIdx(to), + }, } } fn rt_str(from: usize, to: usize) -> RangeToken<'static> { RangeToken { tag: "string", - r: ByteRange { from, to }, + r: ByteRange { + from: ByteIdx(from), + to: ByteIdx(to), + }, } } @@ -946,8 +961,8 @@ mod tests { expected: Held<'static>, ) { let mut it = TokenIter { - start_byte: 0, - end_byte: 42, + start_byte: ByteIdx(0), + end_byte: ByteIdx(42), names: &["string".to_string()], ranges: ranges.iter().peekable(), held, diff --git a/src/syntax/re.rs b/src/syntax/re.rs index c191a1c4..5557da4b 100644 --- a/src/syntax/re.rs +++ b/src/syntax/re.rs @@ -5,7 +5,7 @@ //! or overlapping regex sets but its better than nothing if all you need is some quick and simple //! rules for highlighting regions of a known file format. use crate::{ - buffer::{CharIdx, GapBuffer}, + buffer::{ByteIdx, GapBuffer, LineIdx}, dot::Range, regex::{Match, Regex}, syntax::{ByteRange, LineIter, SyntaxRange}, @@ -38,28 +38,29 @@ impl ReState { }) } - pub fn update(&mut self, gb: &GapBuffer, from: usize, n_rows: usize) { + pub fn update(&mut self, gb: &GapBuffer, from: LineIdx, n_rows: usize) { self.ranges.clear(); - let pos = gb.line_to_char(from); - let ch_to = if from + n_rows + 1 < gb.len_lines() { - gb.line_to_char(from + n_rows + 1) + let pos = gb.line_to_byte(from); + let to = from.uadd(n_rows + 1); + let byte_to = if to.0 < gb.len_lines() { + gb.line_to_byte(to) } else { - gb.len_chars() + ByteIdx(gb.len()) }; self.ranges.extend(Tokenizer { mr: &mut self.mr, gb, pos, - ch_to, + byte_to, }); } #[inline] pub fn iter_tokenized_lines_from<'a>( &'a self, - line: usize, + line: LineIdx, gb: &'a GapBuffer, dot_range: Range, load_exec_range: Option<(bool, Range)>, @@ -79,24 +80,21 @@ impl ReState { struct Tokenizer<'a> { mr: &'a mut MultiRegex, gb: &'a GapBuffer, - pos: CharIdx, - ch_to: CharIdx, + pos: ByteIdx, + byte_to: ByteIdx, } impl Iterator for Tokenizer<'_> { type Item = SyntaxRange; fn next(&mut self) -> Option { - if self.pos >= self.ch_to { + if self.pos >= self.byte_to { return None; } let (i, m) = self.mr.match_gb_from(self.gb, self.pos)?; - let (mfrom, mto) = m.loc(); - self.pos = mto; - - let from = self.gb.char_to_byte(mfrom); - let to = self.gb.offset_char_to_byte(mto, from, mfrom); + let (from, to) = m.loc(); + self.pos = to; Some(SyntaxRange { cap_idx: Some(i), @@ -117,11 +115,11 @@ impl MultiRegex { // really this should live in the regex module and it should run the VMs in parallel, returning // the first match rather than in series like this - pub fn match_gb_from(&mut self, gb: &GapBuffer, ch_from: CharIdx) -> Option<(usize, Match)> { + pub fn match_gb_from(&mut self, gb: &GapBuffer, byte_from: ByteIdx) -> Option<(usize, Match)> { let mut leftmost: Option<(usize, Match)> = None; for (i, re) in self.re.iter_mut().enumerate() { - let m = re.find_from(gb, ch_from); + let m = re.find_from(gb, byte_from); match (leftmost.as_ref(), m) { (Some((_, prev)), Some(m)) if &m < prev => leftmost = Some((i, m)), (None, Some(m)) => leftmost = Some((i, m)), @@ -180,10 +178,10 @@ plumb to edit"; .collect(); let mut state = ReState::new(&tagged_re).expect("valid regex patterns"); - state.update(&gb, 0, 6); + state.update(&gb, LineIdx(0), 6); let mut toks = Vec::new(); - for line in state.iter_tokenized_lines_from(0, &gb, Range::BOF, None) { + for line in state.iter_tokenized_lines_from(LineIdx(0), &gb, Range::BOF, None) { for tok in line { let s = tok.as_slice(&gb).to_string(); toks.push((tok.tag.to_string(), s)); @@ -192,4 +190,15 @@ plumb to edit"; assert_eq!(toks, expected); } + + #[test] + fn tokenizing_handles_multibyte_chars() { + // "data" is at chars 4..8 but bytes 6..10 + let gb = GapBuffer::from("été data"); + let mut state = ReState::new(&[("keyword", r"\bdata\b")]).expect("valid regex patterns"); + state.update(&gb, LineIdx(0), 1); + + let ranges: Vec<_> = state.ranges.iter().map(|sr| (sr.r.from, sr.r.to)).collect(); + assert_eq!(ranges, vec![(ByteIdx(6), ByteIdx(10))]); + } } diff --git a/src/syntax/ts.rs b/src/syntax/ts.rs index f7628c09..c72930c7 100644 --- a/src/syntax/ts.rs +++ b/src/syntax/ts.rs @@ -3,7 +3,7 @@ //! For a given language the user needs to provide a .so file containing the compiled //! tree-sitter parser and a highlights .scm file for driving the highlighting. use crate::{ - buffer::{ByteIdx, CharIdx, GapBuffer, SliceIter}, + buffer::{ByteIdx, ByteOffset, CharIdx, GapBuffer, LineIdx, SliceIter}, dot::Range, syntax::{ByteRange, LineIter, SyntaxRange}, }; @@ -24,8 +24,8 @@ pub const SUPPORTED_PREDICATES: [&str; 0] = []; impl From for ByteRange { fn from(r: ts::Range) -> Self { Self { - from: r.start_byte, - to: r.end_byte, + from: ByteIdx(r.start_byte), + to: ByteIdx(r.end_byte), } } } @@ -70,7 +70,7 @@ impl TsState { fn try_new_explicit(mut p: Parser, query: &str, gb: &GapBuffer) -> Result { let tree = p.parse_with_options( - &mut |byte_offset: ByteIdx, _| gb.maximal_slice_from_offset(byte_offset), + &mut |byte_offset: usize, _| gb.maximal_slice_from_offset(ByteIdx(byte_offset)), None, None, ); @@ -100,9 +100,9 @@ impl TsState { gb: &GapBuffer, ) { self.tree.edit(&ts::InputEdit { - start_byte, - old_end_byte, - new_end_byte, + start_byte: start_byte.0, + old_end_byte: old_end_byte.0, + new_end_byte: new_end_byte.0, // See https://github.com/tree-sitter/tree-sitter/discussions/1793 for why this OK start_position: ts::Point::new(0, 0), old_end_position: ts::Point::new(0, 0), @@ -110,7 +110,7 @@ impl TsState { }); let new_tree = self.p.parse_with_options( - &mut |byte_offset: ByteIdx, _| gb.maximal_slice_from_offset(byte_offset), + &mut |byte_offset: usize, _| gb.maximal_slice_from_offset(ByteIdx(byte_offset)), Some(&self.tree), None, ); @@ -132,7 +132,7 @@ impl TsState { ) -> (ByteIdx, ByteIdx, ByteIdx) { let start_byte = gb.char_to_byte(ch_idx); - (start_byte, start_byte, start_byte + ch.len_utf8()) + (start_byte, start_byte, start_byte + ByteOffset::from(ch)) } pub(super) fn prepare_insert_string( @@ -143,7 +143,7 @@ impl TsState { ) -> (ByteIdx, ByteIdx, ByteIdx) { let start_byte = gb.char_to_byte(ch_idx); - (start_byte, start_byte, start_byte + s.len()) + (start_byte, start_byte, start_byte + ByteOffset::from(s)) } pub(super) fn prepare_delete_char( @@ -151,7 +151,7 @@ impl TsState { ch_idx: CharIdx, gb: &GapBuffer, ) -> (ByteIdx, ByteIdx, ByteIdx) { - let (start_byte, old_end_byte) = gb.char_range_to_byte_range(ch_idx, ch_idx + 1); + let (start_byte, old_end_byte) = gb.char_range_to_byte_range(ch_idx, ch_idx.uadd(1)); (start_byte, old_end_byte, start_byte) } @@ -167,12 +167,13 @@ impl TsState { (start_byte, old_end_byte, start_byte) } - pub fn update(&mut self, gb: &GapBuffer, from_row: usize, n_rows: usize) { + pub fn update(&mut self, gb: &GapBuffer, from_row: LineIdx, n_rows: usize) { let raw_from = gb.line_to_byte(from_row); - let raw_to = if from_row + n_rows + 1 < gb.len_lines() { - gb.line_to_byte(from_row + n_rows + 1) + let row_to = from_row.uadd(n_rows + 1); + let raw_to = if row_to.0 < gb.len_lines() { + gb.line_to_byte(row_to) } else { - gb.len() + ByteIdx(gb.len()) }; if let Some((a, b)) = self.t.missing_region(raw_from, raw_to) { @@ -181,12 +182,12 @@ impl TsState { // requested one if we have missing tokens in that direction. const PADDING: usize = 512; let byte_from = if b < raw_to { - a.saturating_sub(PADDING) + a.saturating_sub(ByteIdx(PADDING)) } else { a }; let byte_to = if a > raw_from { - min(b + PADDING, gb.len()) + min(b.uadd(PADDING), ByteIdx(gb.len())) } else { b }; @@ -198,7 +199,7 @@ impl TsState { #[inline] pub fn iter_tokenized_lines_from<'a>( &'a self, - line: usize, + line: LineIdx, gb: &'a GapBuffer, dot_range: Range, load_exec_range: Option<(bool, Range)>, @@ -254,7 +255,7 @@ impl<'a> ts::TextProvider<&'a [u8]> for &'a GapBuffer { .. } = node.range(); - self.slice_from_byte_offsets(start_byte, end_byte) + self.slice_from_byte_offsets(ByteIdx(start_byte), ByteIdx(end_byte)) .slice_iter() } } @@ -465,7 +466,7 @@ impl Tokenizer { } pub fn update(&mut self, root: ts::Node<'_>, gb: &GapBuffer, from: ByteIdx, to: ByteIdx) { - self.cur.set_byte_range(from..to); + self.cur.set_byte_range(from.0..to.0); // This is a streaming-iterator not an iterator, hence the odd while-let that follows let mut it = self.cur.captures(&self.q, root, gb); @@ -497,7 +498,7 @@ impl Tokenizer { #[inline] pub fn iter_tokenized_lines_from<'a>( &'a self, - line: usize, + line: LineIdx, gb: &'a GapBuffer, dot_range: Range, load_exec_range: Option<(bool, Range)>, @@ -543,20 +544,23 @@ mod tests { fn rt(tag: &str, from: usize, to: usize) -> RangeToken<'_> { RangeToken { tag, - r: ByteRange { from, to }, + r: ByteRange { + from: ByteIdx(from), + to: ByteIdx(to), + }, } } #[test_case( |b| { - b.dot = Dot::Cur { c: Cur { idx: 9 } }; + b.dot = Dot::Cur { c: Cur { idx: CharIdx(9) } }; b.handle_action(BAction::Delete, Source::Fsys); }; "cur delete" )] #[test_case( |b| { - b.xdot = Dot::from_char_indices(9, 9); + b.xdot = Dot::from_char_indices(CharIdx(9), CharIdx(9)); b.insert_xdot(String::new()); }; "LSP null range empty string insert" @@ -575,7 +579,7 @@ mod tests { let mut ts = TsState::try_new_from_language("rust", tree_sitter_rust::LANGUAGE.into(), query, gb) .unwrap(); - ts.update(gb, 0, gb.len()); + ts.update(gb, LineIdx(0), gb.len()); b.syntax_state = Some(SyntaxState::ts(ts)); assert_eq!(b.str_contents(), "fn main() {}"); @@ -603,7 +607,7 @@ mod tests { b.syntax_state .as_mut() .unwrap() - .update(&b.txt, 0, usize::MAX - 1); + .update(&b.txt, LineIdx(0), usize::MAX - 1); let ranges = match b.syntax_state.as_ref() { Some(SyntaxState { @@ -652,7 +656,7 @@ mod tests { gb, ) .unwrap(); - ts.update(gb, 0, gb.len()); + ts.update(gb, LineIdx(0), gb.len()); assert_eq!( ts.t.range_tokens(), @@ -683,7 +687,7 @@ mod tests { let mut ts = TsState::try_new_from_language("rust", tree_sitter_rust::LANGUAGE.into(), query, gb) .unwrap(); - ts.update(gb, 0, gb.len()); + ts.update(gb, LineIdx(0), gb.len()); assert_eq!( ts.t.range_tokens(), @@ -701,7 +705,10 @@ mod tests { } fn br(from: usize, to: usize) -> ByteRange { - ByteRange { from, to } + ByteRange { + from: ByteIdx(from), + to: ByteIdx(to), + } } #[test_case(vec![], 0, 5, vec![br(0, 5)]; "no initial regions")] @@ -719,7 +726,7 @@ mod tests { to: usize, expected: Vec, ) { - mark_region(&mut regions, from, to); + mark_region(&mut regions, ByteIdx(from), ByteIdx(to)); assert_eq!(regions, expected); } @@ -740,7 +747,7 @@ mod tests { to: usize, expected: Option<(usize, usize)>, ) { - let res = missing_region(®ions, from, to); - assert_eq!(res, expected); + let res = missing_region(®ions, ByteIdx(from), ByteIdx(to)); + assert_eq!(res.map(|(a, b)| (a.0, b.0)), expected); } } diff --git a/src/ui/layout.rs b/src/ui/layout.rs index 565df6a5..dea40fc1 100644 --- a/src/ui/layout.rs +++ b/src/ui/layout.rs @@ -1,6 +1,6 @@ //! Layout of UI windows use crate::{ - buffer::{Buffer, BufferId, Buffers, SCRATCH_ID}, + buffer::{Buffer, BufferId, Buffers, CharIdx, CharOffset, LineIdx, SCRATCH_ID}, config::Config, die, dot::Cur, @@ -817,7 +817,7 @@ impl Layout { } let b = buffers.with_id_mut(win.view.bufid).unwrap(); - let y_max = b.txt.len_lines() - 1; + let y_max = LineIdx(b.txt.len_lines() - 1); win.view.row_off = min(win.view.row_off, y_max); } } @@ -918,16 +918,16 @@ impl Layout { .saturating_sub(x_offset) .saturating_sub(w_sgncol); let y = min( - y.saturating_sub(y_offset).saturating_add(row_off), - b.len_lines(), + row_off.uadd(y.saturating_sub(y_offset)), + LineIdx(b.len_lines()), ) - .saturating_sub(1); + .saturating_sub(LineIdx(1)); win.view.rx = rx; b.cached_rx = rx; let mut cur = Cur::from_yx(y, b.x_from_provided_rx(y, rx), b); - cur.clamp_idx(b.len_chars()); + cur.clamp_idx(CharIdx(b.len_chars())); cur } @@ -1014,7 +1014,7 @@ impl Window { pub(crate) struct View { pub(crate) bufid: BufferId, pub(crate) col_off: usize, - pub(crate) row_off: usize, + pub(crate) row_off: LineIdx, pub(crate) rx: usize, pending_viewport: Option, cur: Cur, @@ -1025,7 +1025,7 @@ impl View { Self { bufid, col_off: 0, - row_off: 0, + row_off: LineIdx(0), rx: 0, cur: Cur::default(), pending_viewport: None, @@ -1037,18 +1037,18 @@ impl View { let (_, w_sgncol) = b.sign_col_dims(); let (y, _) = b.dot.active_cur().as_yx(b); let x = self.rx - self.col_off + w_sgncol; - let y = y - self.row_off; + let y = (y - self.row_off).0; (x, y) } - pub(crate) fn rx_from_x(&self, b: &Buffer, y: usize, x: usize, tabstop: usize) -> usize { - if y >= b.len_lines() { + pub(crate) fn rx_from_x(&self, b: &Buffer, y: LineIdx, x: CharOffset, tabstop: usize) -> usize { + if y.0 >= b.len_lines() { return 0; } let mut rx = 0; - for c in b.txt.line(y).chars().take(x) { + for c in b.txt.line(y).chars().take(x.0 as usize) { if c == '\t' { rx += (tabstop - 1) - (rx % tabstop); } @@ -1072,8 +1072,8 @@ impl View { if let Some(vp) = self.pending_viewport.take() { self.row_off = match vp { ViewPort::Top => y, - ViewPort::Center => y.saturating_sub(rows / 2), - ViewPort::Bottom => y.saturating_sub(rows), + ViewPort::Center => y.saturating_sub(LineIdx(rows / 2)), + ViewPort::Bottom => y.saturating_sub(LineIdx(rows)), }; } @@ -1085,8 +1085,8 @@ impl View { self.row_off = y; } - if y >= self.row_off + rows { - self.row_off = y + 1 - rows; + if y >= self.row_off.uadd(rows) { + self.row_off = y.uadd(1).usub(rows); } if self.rx < self.col_off { @@ -1243,30 +1243,30 @@ fn apply_scroll( view.cur }; let (y, x) = cur.as_yx(b); - let y_max = b.txt.len_lines() - 1; + let y_max = LineIdx(b.txt.len_lines() - 1); let mut need_clamp = false; - if up && view.row_off > 0 && y == view.row_off + n_rows - 1 { - cur = Cur::from_yx(y.saturating_sub(scroll_rows), x, b); + if up && view.row_off > LineIdx(0) && y == view.row_off.uadd(n_rows).usub(1) { + cur = Cur::from_yx(y.saturating_sub(LineIdx(scroll_rows)), x, b); } else if !up && y == view.row_off && view.row_off < y_max { - cur = Cur::from_yx(min(y + scroll_rows, y_max), x, b); + cur = Cur::from_yx(min(y.uadd(scroll_rows), y_max), x, b); need_clamp = true; }; if focused { b.dot.set_active_cur(cur); if need_clamp { - b.dot.clamp_idx(b.txt.len_chars()); - b.xdot.clamp_idx(b.txt.len_chars()); + b.dot.clamp_idx(CharIdx(b.txt.len_chars())); + b.xdot.clamp_idx(CharIdx(b.txt.len_chars())); } } else { view.cur = cur; } view.row_off = if up { - view.row_off.saturating_sub(scroll_rows) + view.row_off.saturating_sub(LineIdx(scroll_rows)) } else { - min(view.row_off + scroll_rows, y_max) + min(view.row_off.uadd(scroll_rows), y_max) }; if focused { @@ -1342,7 +1342,7 @@ mod tests { fn drag_left_works() { let (mut l, _) = test_layout(&[1, 1, 2], 80, 100); let ao = l.next_column(); - assert_eq!(ao, ActionOutcome::SetCursor(1, Cur { idx: 0 })); + assert_eq!(ao, ActionOutcome::SetCursor(1, Cur { idx: CharIdx(0) })); l.drag_left(); assert_eq!(l.cols.len(), 2); @@ -1577,7 +1577,12 @@ mod tests { // sign column offset is 3 for (idx, ch) in s.chars().enumerate() { assert_eq!(b.dot_contents(), ch.to_string()); - assert_eq!(b.dot, Dot::Cur { c: Cur { idx } }); + assert_eq!( + b.dot, + Dot::Cur { + c: Cur::new(CharIdx(idx)) + } + ); assert_eq!( view.ui_xy(&b), (3 + offset, 0), @@ -1884,7 +1889,7 @@ mod tests { for (_, win) in col.wins.iter_mut() { let b = bfs.with_id_mut(win.view.bufid).unwrap(); b.insert_xdot("line1\nline2\nline3".to_string()); - win.view.row_off = 100; + win.view.row_off = LineIdx(100); } } @@ -1893,9 +1898,11 @@ mod tests { for (_, col) in l.cols.iter() { for (_, win) in col.wins.iter() { assert_eq!( - win.view.row_off, 2, + win.view.row_off, + LineIdx(2), "bufid {} had row_off={}", - win.view.bufid, win.view.row_off + win.view.bufid, + win.view.row_off ); } } @@ -1905,11 +1912,11 @@ mod tests { fn apply_scroll_for_unfocused_window_clamps_row_off() { let config = Arc::new(RwLock::new(Config::default())); let mut b = Buffer::new_unnamed(0, "line1\nline2\nline3\nline4\nline5", config); - let y_max = b.txt.len_lines() - 1; - assert_eq!(y_max, 4); + let y_max = LineIdx(b.txt.len_lines() - 1); + assert_eq!(y_max, LineIdx(4)); let mut win = Window::new(0, 3); - win.view.row_off = 3; + win.view.row_off = LineIdx(3); let focused = false; let up = false; diff --git a/src/ui/tui/mod.rs b/src/ui/tui/mod.rs index 8d9a1b56..a885dc43 100644 --- a/src/ui/tui/mod.rs +++ b/src/ui/tui/mod.rs @@ -132,7 +132,7 @@ where let mb_has_lines = mb.b.map(|b| !b.is_empty()).unwrap_or_default(); let offset = if mb_has_lines { - mb.bottom - mb.top + 1 + (mb.bottom - mb.top).uadd(1).0 } else if !w_minibuffer && layout.scratch.is_visible { max_mb_lines } else { @@ -518,7 +518,7 @@ impl Frame { self.mb_lines.clear(); if let Some(b) = mb.b { - for i in mb.top..=mb.bottom { + for i in mb.top.to(mb.bottom.uadd(1)) { let slice = b.line(i).unwrap(); let bg = if i == mb.selected_line_idx { cs.minibuffer_hl @@ -726,7 +726,7 @@ impl<'a> WinRenderer<'a> { return false; } - let file_row = self.y + self.w.view.row_off; + let file_row = self.w.view.row_off.uadd(self.y); self.y += 1; if let Some(pc) = prev_col { @@ -766,7 +766,7 @@ impl<'a> WinRenderer<'a> { "{}{} {:>width$}{V_STR}", AnsiStyle::Fg(self.cs.signcol_fg), AnsiStyle::Bg(self.cs.bg), - file_row + 1, + file_row.uadd(1), width = self.w_lnum ); @@ -1122,7 +1122,10 @@ fn try_read_input(stdin: &mut impl Read) -> Option { #[cfg(test)] mod tests { use super::*; - use crate::syntax::{ByteRange, TK_DEFAULT}; + use crate::{ + buffer::{ByteIdx, LineIdx}, + syntax::{ByteRange, TK_DEFAULT}, + }; use simple_test_case::test_case; use std::{char::REPLACEMENT_CHARACTER, io}; @@ -1145,7 +1148,10 @@ mod tests { fn rt(tag: &'static str, from: usize, to: usize) -> RangeToken<'static> { RangeToken { tag, - r: ByteRange { from, to }, + r: ByteRange { + from: ByteIdx(from), + to: ByteIdx(to), + }, } } @@ -1231,12 +1237,12 @@ mod tests { let mb = MiniBufferState { cx: 0, n_visible_lines: 10, - selected_line_idx: 0, + selected_line_idx: LineIdx(0), prompt: "> ", input: Default::default(), b: Some(&b), - top: 0, - bottom: 0, + top: LineIdx(0), + bottom: LineIdx(0), }; let mut frame = Frame::new();