use core::cmp::{max, min};
use core::fmt;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct CharIdx(pub usize);
impl CharIdx {
#[inline]
pub const fn new(v: usize) -> Self {
Self(v)
}
#[inline]
pub const fn get(self) -> usize {
self.0
}
#[inline]
pub const fn saturating_add(self, delta: usize) -> Self {
Self(self.0.saturating_add(delta))
}
#[inline]
pub const fn saturating_sub(self, delta: usize) -> Self {
Self(self.0.saturating_sub(delta))
}
}
impl fmt::Debug for CharIdx {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("CharIdx").field(&self.0).finish()
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct LineIdx(pub usize);
impl LineIdx {
#[inline]
pub const fn new(v: usize) -> Self {
Self(v)
}
#[inline]
pub const fn get(self) -> usize {
self.0
}
}
impl fmt::Debug for LineIdx {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("LineIdx").field(&self.0).finish()
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct ColIdx(pub usize);
impl ColIdx {
#[inline]
pub const fn new(v: usize) -> Self {
Self(v)
}
#[inline]
pub const fn get(self) -> usize {
self.0
}
}
impl fmt::Debug for ColIdx {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("ColIdx").field(&self.0).finish()
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct LineCol {
pub line: LineIdx,
pub col: ColIdx,
}
impl LineCol {
#[inline]
pub const fn new(line: usize, col: usize) -> Self {
Self {
line: LineIdx(line),
col: ColIdx(col),
}
}
}
impl fmt::Debug for LineCol {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LineCol")
.field("line", &self.line.0)
.field("col", &self.col.0)
.finish()
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct CharRange {
pub start: CharIdx,
pub end: CharIdx,
}
impl CharRange {
#[inline]
pub const fn new(start: CharIdx, end: CharIdx) -> Self {
Self { start, end }
}
#[inline]
pub const fn is_empty(self) -> bool {
self.start.0 >= self.end.0
}
#[inline]
pub const fn len(self) -> usize {
self.end.0.saturating_sub(self.start.0)
}
#[inline]
pub const fn normalized(self) -> Self {
if self.start.0 <= self.end.0 {
self
} else {
Self {
start: self.end,
end: self.start,
}
}
}
#[inline]
pub fn clamp_to_len(self, max_len: usize) -> Self {
let s = min(self.start.0, max_len);
let e = min(self.end.0, max_len);
Self {
start: CharIdx(s),
end: CharIdx(e),
}
.normalized()
}
}
impl fmt::Debug for CharRange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CharRange")
.field("start", &self.start.0)
.field("end", &self.end.0)
.finish()
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default, Debug)]
pub struct GoalCol {
pub goal_col: ColIdx,
}
impl GoalCol {
#[inline]
pub const fn new(goal_col: usize) -> Self {
Self {
goal_col: ColIdx(goal_col),
}
}
}
pub fn char_to_line_col(
char_idx: CharIdx,
line_count: usize,
mut line_to_char: impl FnMut(usize) -> usize,
) -> LineCol {
if line_count == 0 {
return LineCol::new(0, 0);
}
let target = char_idx.0;
let mut lo = 0usize;
let mut hi = line_count - 1;
while lo < hi {
let mid = (lo + hi + 1) / 2;
let mid_start = line_to_char(mid);
if mid_start <= target {
lo = mid;
} else {
hi = mid - 1;
}
}
let line = lo;
let line_start = line_to_char(line);
let col = target.saturating_sub(line_start);
LineCol {
line: LineIdx(line),
col: ColIdx(col),
}
}
pub fn line_col_to_char(
pos: LineCol,
line_count: usize,
mut line_to_char: impl FnMut(usize) -> usize,
mut line_len_chars: impl FnMut(usize) -> usize,
) -> CharIdx {
if line_count == 0 {
return CharIdx(0);
}
let line = min(pos.line.0, line_count - 1);
let line_start = line_to_char(line);
let line_len = line_len_chars(line);
let col = min(pos.col.0, line_len);
CharIdx(line_start.saturating_add(col))
}
#[inline]
pub fn clamp_char(char_idx: CharIdx, len_chars: usize) -> CharIdx {
CharIdx(min(char_idx.0, len_chars))
}
#[inline]
pub fn clamp_range(range: CharRange, len_chars: usize) -> CharRange {
range.normalized().clamp_to_len(len_chars)
}
#[inline]
pub fn clamp_col_to_line(goal: ColIdx, line_len_chars: usize) -> ColIdx {
ColIdx(min(goal.0, line_len_chars))
}
#[inline]
pub fn line_len_without_newline(
line_len_chars_including_newline: usize,
ends_with_newline: bool,
) -> usize {
if ends_with_newline {
line_len_chars_including_newline.saturating_sub(1)
} else {
line_len_chars_including_newline
}
}
#[inline]
pub fn move_char_clamped(current: CharIdx, delta: isize, len_chars: usize) -> CharIdx {
if delta == 0 {
return clamp_char(current, len_chars);
}
if delta > 0 {
let d = delta as usize;
CharIdx(min(current.0.saturating_add(d), len_chars))
} else {
let d = (-delta) as usize;
CharIdx(current.0.saturating_sub(d))
}
}
#[inline]
pub fn ordered_pair(a: CharIdx, b: CharIdx) -> (CharIdx, CharIdx) {
if a.0 <= b.0 { (a, b) } else { (b, a) }
}
#[inline]
pub fn apply_goal_col(goal_col: ColIdx, target_line_len: usize) -> ColIdx {
clamp_col_to_line(goal_col, target_line_len)
}
#[inline]
pub fn line_editable_bounds(
line_start: CharIdx,
line_len_chars_including_newline: usize,
ends_with_newline: bool,
) -> (CharIdx, CharIdx) {
let editable_len =
line_len_without_newline(line_len_chars_including_newline, ends_with_newline);
let start = line_start;
let end = CharIdx(line_start.0.saturating_add(editable_len));
(start, end)
}
#[inline]
pub fn clamp_cursor_to_line_editable(
cursor: CharIdx,
line_start: CharIdx,
editable_end: CharIdx,
) -> CharIdx {
CharIdx(max(line_start.0, min(cursor.0, editable_end.0)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn char_to_line_col_uses_binary_search_correctly() {
let line_starts = [0usize, 4, 6];
let line_count = line_starts.len();
let pos = char_to_line_col(CharIdx(5), line_count, |line| line_starts[line]);
assert_eq!(pos, LineCol::new(1, 1));
}
#[test]
fn line_col_to_char_clamps_line_and_column() {
let line_starts = [0usize, 4, 6];
let line_lens = [3usize, 1, 0];
let line_count = line_starts.len();
let idx = line_col_to_char(
LineCol::new(99, 99),
line_count,
|line| line_starts[line],
|line| line_lens[line],
);
assert_eq!(idx, CharIdx(6));
}
#[test]
fn clamp_helpers_keep_values_in_bounds() {
assert_eq!(clamp_char(CharIdx(9), 4), CharIdx(4));
assert_eq!(clamp_col_to_line(ColIdx(8), 3), ColIdx(3));
assert_eq!(apply_goal_col(ColIdx(8), 3), ColIdx(3));
}
#[test]
fn clamp_range_normalizes_and_clamps() {
let out = clamp_range(CharRange::new(CharIdx(9), CharIdx(2)), 5);
assert_eq!(out.start, CharIdx(2));
assert_eq!(out.end, CharIdx(5));
}
#[test]
fn line_helpers_respect_newline_exclusion() {
assert_eq!(line_len_without_newline(5, true), 4);
assert_eq!(line_len_without_newline(5, false), 5);
let (start, end) = line_editable_bounds(CharIdx(10), 4, true);
assert_eq!(start, CharIdx(10));
assert_eq!(end, CharIdx(13));
assert_eq!(
clamp_cursor_to_line_editable(CharIdx(20), start, end),
CharIdx(13)
);
assert_eq!(
clamp_cursor_to_line_editable(CharIdx(8), start, end),
CharIdx(10)
);
}
#[test]
fn movement_and_ordering_helpers_are_saturating() {
assert_eq!(move_char_clamped(CharIdx(2), -5, 10), CharIdx(0));
assert_eq!(move_char_clamped(CharIdx(2), 99, 10), CharIdx(10));
assert_eq!(
ordered_pair(CharIdx(9), CharIdx(3)),
(CharIdx(3), CharIdx(9))
);
}
}