use unicode_width::UnicodeWidthChar;
pub(crate) fn char_columns(ch: char) -> usize {
UnicodeWidthChar::width(ch).unwrap_or(0)
}
pub(crate) fn tab_advance(column: usize, tab_width: usize) -> usize {
let width = tab_width.max(1);
width - (column % width)
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct LineGrid<'a> {
text: &'a str,
tab_width: usize,
simple: bool,
}
impl<'a> LineGrid<'a> {
pub(crate) fn new(text: &'a str, tab_width: usize) -> Self {
let simple = text.is_ascii() && !text.as_bytes().contains(&b'\t');
Self { text, tab_width: tab_width.max(1), simple }
}
pub(crate) fn char_len(&self) -> usize {
if self.simple {
self.text.len()
} else {
self.text.chars().count()
}
}
pub(crate) fn columns(&self) -> usize {
if self.simple {
self.text.len()
} else {
self.column_of_char(self.char_len())
}
}
pub(crate) fn advance(&self, ch: char, column: usize) -> usize {
if ch == '\t' {
tab_advance(column, self.tab_width)
} else {
char_columns(ch)
}
}
pub(crate) fn column_of_char(&self, index: usize) -> usize {
if self.simple {
return index.min(self.text.len());
}
let mut column = 0usize;
for (taken, ch) in self.text.chars().enumerate() {
if taken == index {
return column;
}
column += self.advance(ch, column);
}
column
}
pub(crate) fn byte_of_char(&self, index: usize) -> usize {
if self.simple {
return index.min(self.text.len());
}
self.text.char_indices().nth(index).map(|(byte, _)| byte).unwrap_or(self.text.len())
}
pub(crate) fn char_of_byte(&self, byte: usize) -> usize {
if self.simple {
return byte.min(self.text.len());
}
self.text[..byte.min(self.text.len())].chars().count()
}
pub(crate) fn char_at_column(&self, column: usize) -> usize {
if self.simple {
return column.min(self.text.len());
}
let mut start = 0usize;
let mut index = 0usize;
for ch in self.text.chars() {
let width = self.advance(ch, start);
if width == 0 {
index += 1;
continue;
}
let half = width.div_ceil(2);
if column < start + half {
return index;
}
if column < start + width {
return index + 1;
}
start += width;
index += 1;
}
index
}
pub(crate) fn char_at_or_after_column(&self, column: usize) -> usize {
if self.simple {
return column.min(self.text.len());
}
let mut start = 0usize;
let mut index = 0usize;
for ch in self.text.chars() {
let width = self.advance(ch, start);
if start >= column {
return index;
}
start += width;
index += 1;
}
index
}
pub(crate) fn segments(&self, columns: usize) -> usize {
let columns = columns.max(1);
let width = self.columns();
if width == 0 {
1
} else {
width.div_ceil(columns)
}
}
pub(crate) fn segment_range(&self, segment: usize, columns: usize) -> (usize, usize) {
let columns = columns.max(1);
if segment == 0 && self.columns() <= columns {
return (0, self.char_len());
}
let start = self.char_at_or_after_column(segment.saturating_mul(columns));
let end = self.char_at_or_after_column(segment.saturating_add(1).saturating_mul(columns));
(start, end.max(start))
}
pub(crate) fn segment_of_char(&self, index: usize, columns: usize) -> (usize, usize) {
let columns = columns.max(1);
let total = self.segments(columns);
let target = index.min(self.char_len());
for segment in 0..total {
let (start, end) = self.segment_range(segment, columns);
if target < end || segment + 1 == total {
return (segment, start);
}
}
(0, 0)
}
pub(crate) fn leading_indent_columns(&self) -> usize {
let mut column = 0usize;
for ch in self.text.chars() {
match ch {
' ' => column += 1,
'\t' => column += tab_advance(column, self.tab_width),
_ => break,
}
}
column
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct RowLayout {
xs: crate::compat::Rc<Vec<f32>>,
}
impl RowLayout {
pub(crate) fn from_boundaries(mut xs: Vec<f32>) -> Self {
if xs.is_empty() {
xs.push(0.0);
}
Self { xs: crate::compat::Rc::new(xs) }
}
pub(crate) fn x_of_char(&self, index: usize) -> Option<f32> {
self.xs.get(index).copied()
}
pub(crate) fn char_at_x(&self, x: f32) -> Option<usize> {
let last = self.xs.len().checked_sub(1)?;
if !x.is_finite() {
return None;
}
if x <= self.xs[0] {
return Some(0);
}
if x >= self.xs[last] {
return Some(last);
}
let mut low = 0usize;
let mut high = last;
while low + 1 < high {
let middle = (low + high) / 2;
if self.xs[middle] <= x {
low = middle;
} else {
high = middle;
}
}
let left = self.xs[low];
let right = self.xs[high];
if x - left < right - x {
Some(low)
} else {
Some(high)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_layout_maps_between_characters_and_pixels() {
let layout = RowLayout::from_boundaries(vec![0.0, 7.0, 15.0, 18.0]);
assert_eq!(layout.x_of_char(2), Some(15.0));
assert_eq!(layout.x_of_char(4), None);
assert_eq!(layout.char_at_x(-1.0), Some(0));
assert_eq!(layout.char_at_x(3.0), Some(0), "the left half of a glyph is its own");
assert_eq!(layout.char_at_x(4.0), Some(1), "the right half belongs to the next one");
assert_eq!(layout.char_at_x(99.0), Some(3));
}
#[test]
fn ascii_is_one_column_per_char() {
let grid = LineGrid::new("let x = 1;", 4);
assert_eq!(grid.columns(), 10);
assert_eq!(grid.column_of_char(4), 4);
assert_eq!(grid.char_at_column(4), 4);
assert_eq!(grid.byte_of_char(4), 4);
}
#[test]
fn cjk_is_two_columns_per_character() {
let grid = LineGrid::new("a中b", 4);
assert_eq!(grid.columns(), 4);
assert_eq!(grid.column_of_char(0), 0);
assert_eq!(grid.column_of_char(1), 1, "the CJK glyph starts after 'a'");
assert_eq!(grid.column_of_char(2), 3, "'b' starts two cells after it");
assert_eq!(grid.char_at_column(1), 1);
assert_eq!(
grid.char_at_column(2),
2,
"the second cell of the glyph is its right half, so the caret goes after it"
);
assert_eq!(grid.char_of_byte(4), 2, "byte offsets and char indices differ now");
}
#[test]
fn a_segment_lookup_is_the_inverse_of_the_range() {
let grid = LineGrid::new("0123456789", 4);
assert_eq!(grid.segment_of_char(0, 4), (0, 0));
assert_eq!(grid.segment_of_char(3, 4), (0, 0));
assert_eq!(grid.segment_of_char(4, 4), (1, 4), "the boundary starts the next row");
assert_eq!(grid.segment_of_char(7, 4), (1, 4));
assert_eq!(grid.segment_of_char(8, 4), (2, 8));
assert_eq!(grid.segment_of_char(10, 4), (2, 8), "the end belongs to the last row");
for index in 0..=10usize {
let (segment, start) = grid.segment_of_char(index, 4);
let (range_start, range_end) = grid.segment_range(segment, 4);
assert_eq!(start, range_start);
assert!(index >= range_start && index <= range_end);
}
}
#[test]
fn leading_whitespace_is_measured_in_columns() {
assert_eq!(LineGrid::new(" fn x()", 4).leading_indent_columns(), 4);
assert_eq!(LineGrid::new("\tx", 4).leading_indent_columns(), 4);
assert_eq!(LineGrid::new(" \tx", 4).leading_indent_columns(), 4);
assert_eq!(LineGrid::new("fn x()", 4).leading_indent_columns(), 0);
assert_eq!(
LineGrid::new(" ", 4).leading_indent_columns(),
4,
"a blank line keeps its indent"
);
}
#[test]
fn a_tab_advances_to_the_next_stop() {
let grid = LineGrid::new("\tx", 4);
assert_eq!(grid.column_of_char(1), 4);
assert_eq!(grid.columns(), 5);
assert_eq!(grid.char_at_column(2), 1, "inside the tab belongs to the next char");
}
#[test]
fn wrapping_counts_cells_not_chars() {
let grid = LineGrid::new("中中中", 4);
assert_eq!(grid.columns(), 6);
assert_eq!(grid.segments(4), 2);
assert_eq!(grid.segment_range(0, 4), (0, 2));
assert_eq!(grid.segment_range(1, 4), (2, 3));
}
#[test]
fn combining_marks_do_not_consume_a_cell() {
let grid = LineGrid::new("e\u{301}x", 4);
assert_eq!(grid.columns(), 2);
assert_eq!(grid.column_of_char(2), 1);
assert_eq!(grid.char_at_column(1), 2);
}
}