use std::ops::Range;
use crate::buffer::Buffer;
use crate::coords::{Bias, Point};
use crate::patch::Patch;
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct DisplayRow(pub(crate) u32);
impl DisplayRow {
#[must_use]
pub const fn index(self) -> u32 {
self.0
}
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct BufferRow(pub u32);
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct DisplayPoint {
row: DisplayRow,
col: u32,
}
impl DisplayPoint {
pub(crate) fn new(row: DisplayRow, col: u32) -> Self {
Self { row, col }
}
#[must_use]
pub fn zero() -> Self {
Self { row: DisplayRow(0), col: 0 }
}
#[must_use]
pub fn row(self) -> DisplayRow {
self.row
}
#[must_use]
pub fn col(self) -> u32 {
self.col
}
}
#[derive(Clone, Debug)]
pub struct DisplayChunk<'a> {
pub text: &'a str,
pub buffer_bytes: Range<u32>,
pub cells: u32,
pub is_tab: bool,
}
#[derive(Clone, Debug)]
pub struct DisplayChunks<'a> {
line: &'a str,
tab_size: u32,
byte: u32,
cell: u32,
}
#[must_use]
pub fn chunks(line: &str, tab_size: u32) -> DisplayChunks<'_> {
DisplayChunks { line, tab_size, byte: 0, cell: 0 }
}
impl<'a> Iterator for DisplayChunks<'a> {
type Item = DisplayChunk<'a>;
fn next(&mut self) -> Option<Self::Item> {
let bytes = self.line.as_bytes();
let len = bytes.len() as u32;
if self.byte >= len {
return None;
}
let start = self.byte;
if bytes[start as usize] == b'\t' {
let cells = tab_width(self.cell, self.tab_size);
self.byte = start + 1;
self.cell += cells;
return Some(DisplayChunk {
text: &self.line[start as usize..start as usize + 1],
buffer_bytes: start..start + 1,
cells,
is_tab: true,
});
}
let mut cells = 0u32;
for ch in self.line[start as usize..].chars() {
if ch == '\t' {
break;
}
self.byte += ch.len_utf8() as u32;
cells += 1;
}
self.cell += cells;
Some(DisplayChunk {
text: &self.line[start as usize..self.byte as usize],
buffer_bytes: start..self.byte,
cells,
is_tab: false,
})
}
}
#[derive(Clone, Debug)]
pub struct DisplayEdit {
pub old: Range<DisplayPoint>,
pub new: Range<DisplayPoint>,
}
#[must_use]
pub fn tab_width(c: u32, tab_size: u32) -> u32 {
tab_size - (c % tab_size)
}
#[must_use]
pub fn expand(line: &str, byte_col: u32, tab_size: u32) -> u32 {
let mut cell = 0u32;
let mut byte = 0u32;
for ch in line.chars() {
if byte >= byte_col {
break; }
cell += if ch == '\t' { tab_width(cell, tab_size) } else { 1 };
byte += ch.len_utf8() as u32;
}
cell
}
#[must_use]
pub fn indent_guide_level(
own: Option<u32>,
above: Option<u32>,
below: Option<u32>,
indent_size: u32,
) -> u32 {
match own {
Some(indent) => indent.div_ceil(indent_size),
None => match (above, below) {
(None, _) | (_, None) => 0,
(Some(a), Some(b)) if a < b => 1 + a / indent_size,
(Some(a), Some(b)) if a == b => b.div_ceil(indent_size),
(Some(_), Some(b)) => 1 + b / indent_size, },
}
}
#[must_use]
pub fn active_indent_guide(
caret_row: u32,
line_count: u32,
rows: Range<u32>,
level_at: impl Fn(u32) -> u32,
) -> Option<(u32, u32, u32)> {
let lo = rows.start;
let hi = rows.end.min(line_count);
if caret_row >= line_count || caret_row < lo || caret_row >= hi {
return None;
}
let initial = level_at(caret_row);
let down = (caret_row + 1 < line_count).then(|| level_at(caret_row + 1));
let up = caret_row.checked_sub(1).map(&level_at);
let (indent, mut start, mut end, go_up, go_down) = match (down, up) {
(Some(d), _) if d > initial => (initial + 1, caret_row + 1, caret_row + 1, false, true),
(_, Some(u)) if u > initial => (initial + 1, caret_row - 1, caret_row - 1, true, false),
_ if initial == 0 => return None,
_ => (initial, caret_row, caret_row, true, true),
};
if go_up {
while start > lo && level_at(start - 1) >= indent {
start -= 1;
}
}
if go_down {
while end + 1 < hi && level_at(end + 1) >= indent {
end += 1;
}
}
Some((indent, start, end))
}
#[must_use]
pub fn collapse(line: &str, cell_col: u32, tab_size: u32, bias: Bias) -> u32 {
let mut cell = 0u32;
let mut byte = 0u32;
for ch in line.chars() {
if cell >= cell_col {
return byte; }
let w = if ch == '\t' { tab_width(cell, tab_size) } else { 1 };
if cell + w > cell_col {
return match bias {
Bias::Left => byte,
Bias::Right => byte + 1,
};
}
cell += w;
byte += ch.len_utf8() as u32;
}
byte }
#[must_use]
pub const fn default_tab_size() -> u32 {
4
}
pub struct TabMap<'a> {
tab_size: u32,
lines: &'a Buffer,
}
impl<'a> TabMap<'a> {
#[must_use]
pub fn new(lines: &'a Buffer, tab_size: u32) -> Self {
Self { tab_size, lines }
}
#[must_use]
pub fn to_display(&self, point: Point, _bias: Bias) -> DisplayPoint {
DisplayPoint::new(
DisplayRow(point.row),
expand(&self.line(point.row), point.col, self.tab_size),
)
}
#[must_use]
pub fn to_buffer(&self, point: DisplayPoint, bias: Bias) -> Point {
Point {
row: point.row().0,
col: collapse(&self.line(point.row().0), point.col(), self.tab_size, bias),
}
}
#[must_use]
pub fn clip(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
let buffer_point = self.lines.clip_point(self.to_buffer(point, bias), bias);
self.to_display(buffer_point, bias)
}
#[must_use]
pub fn max_point(&self) -> DisplayPoint {
let row = self.max_row();
DisplayPoint::new(row, self.line_len(row))
}
#[must_use]
pub fn row_count(&self) -> u32 {
self.lines.line_count()
}
#[must_use]
pub fn line_len(&self, row: DisplayRow) -> u32 {
expand(&self.line(row.0), self.lines.line_len(row.0), self.tab_size)
}
#[must_use]
pub fn row_info(&self, row: DisplayRow) -> Option<BufferRow> {
Some(BufferRow(row.0))
}
pub fn sync(&mut self, _buffer_patch: &Patch) {}
#[must_use]
pub fn max_row(&self) -> DisplayRow {
DisplayRow(self.row_count().saturating_sub(1))
}
fn line(&self, row: u32) -> std::borrow::Cow<'a, str> {
self.lines.line(row)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn buf(s: &str) -> Buffer {
Buffer::new(s).unwrap()
}
fn oracle_expand(line: &str, byte_col: u32, tab_size: u32) -> u32 {
let mut cell = 0u32;
let mut byte = 0u32;
for ch in line.chars() {
if byte >= byte_col {
break;
}
cell += if ch == '\t' {
tab_size - (cell % tab_size)
} else {
1
};
byte += ch.len_utf8() as u32;
}
cell
}
#[test]
fn indent_guide_level_content_rows() {
assert_eq!(indent_guide_level(Some(0), None, None, 4), 0);
assert_eq!(indent_guide_level(Some(4), None, None, 4), 1);
assert_eq!(indent_guide_level(Some(8), None, None, 4), 2);
assert_eq!(indent_guide_level(Some(12), None, None, 4), 3);
assert_eq!(indent_guide_level(Some(6), None, None, 4), 2);
}
#[test]
fn indent_guide_level_blank_rows() {
assert_eq!(indent_guide_level(None, None, Some(4), 4), 0);
assert_eq!(indent_guide_level(None, Some(4), None, 4), 0);
assert_eq!(indent_guide_level(None, Some(12), Some(12), 4), 3);
assert_eq!(indent_guide_level(None, Some(4), Some(8), 4), 2);
assert_eq!(indent_guide_level(None, Some(12), Some(4), 4), 2);
}
#[test]
fn active_indent_guide_scope_cases() {
let levels = [0u32, 1, 2, 3, 3, 3, 2, 1, 0];
let n = levels.len() as u32;
let at = |r: u32| levels[r as usize];
assert_eq!(active_indent_guide(3, n, 0..n, at), Some((3, 3, 5)));
assert_eq!(active_indent_guide(0, n, 0..n, at), Some((1, 1, 7)));
assert_eq!(active_indent_guide(1, n, 0..n, at), Some((2, 2, 6)));
assert_eq!(active_indent_guide(8, n, 0..n, at), Some((1, 1, 7)));
assert_eq!(active_indent_guide(6, n, 0..n, at), Some((3, 3, 5)));
}
#[test]
fn active_indent_guide_over_indented_body() {
let levels = [0u32, 1, 4, 4, 1, 0];
let n = levels.len() as u32;
let at = |r: u32| levels[r as usize];
assert_eq!(active_indent_guide(1, n, 0..n, at), Some((2, 2, 3)));
assert_eq!(active_indent_guide(2, n, 0..n, at), Some((4, 2, 3)));
assert_eq!(active_indent_guide(0, n, 0..n, at), Some((1, 1, 4)));
}
#[test]
fn active_indent_guide_none_at_flat_level_zero() {
let levels = [0u32, 0, 0];
assert_eq!(active_indent_guide(1, 3, 0..3, |r| levels[r as usize]), None);
}
#[test]
fn tab_width_advances_to_next_stop() {
assert_eq!(tab_width(0, 4), 4);
assert_eq!(tab_width(1, 4), 3);
assert_eq!(tab_width(3, 4), 1);
assert_eq!(tab_width(4, 4), 4); assert_eq!(tab_width(7, 4), 1);
}
#[test]
fn expand_no_tabs_is_scalar_count() {
assert_eq!(expand("hello", 5, 4), 5);
assert_eq!(expand("hello", 3, 4), 3);
assert_eq!(expand("hello", 0, 4), 0);
assert_eq!(expand("hi", 99, 4), 2);
}
#[test]
fn expand_counts_scalars_not_bytes() {
let line = "aé b"; assert_eq!(line.len(), 5);
assert_eq!(expand(line, 5, 4), 4); assert_eq!(expand(line, 3, 4), 2); }
#[test]
fn expand_stretches_at_tabs() {
assert_eq!(expand("\t", 1, 4), 4);
assert_eq!(expand("a\t", 2, 4), 4);
assert_eq!(expand("ab\tc", 4, 4), 5); assert_eq!(expand("\t\t", 2, 4), 8); }
#[test]
fn expand_matches_oracle() {
for line in ["", "a", "\t", "a\tb\tc", "\t\ta", "aé\tb", "abcd\te"] {
for byte_col in 0..=line.len() as u32 + 1 {
assert_eq!(
expand(line, byte_col, 4),
oracle_expand(line, byte_col, 4),
"line {line:?} byte_col {byte_col}"
);
}
}
}
#[test]
fn collapse_hits_boundaries_exactly() {
assert_eq!(collapse("hello", 0, 4, Bias::Left), 0);
assert_eq!(collapse("hello", 3, 4, Bias::Left), 3);
assert_eq!(collapse("hello", 5, 4, Bias::Left), 5);
assert_eq!(collapse("hi", 9, 4, Bias::Right), 2);
}
#[test]
fn collapse_snaps_mid_tab_by_bias() {
for c in 1..4 {
assert_eq!(collapse("\t", c, 4, Bias::Left), 0); assert_eq!(collapse("\t", c, 4, Bias::Right), 1); }
assert_eq!(collapse("\t", 0, 4, Bias::Left), 0);
assert_eq!(collapse("\tx", 4, 4, Bias::Left), 1); }
#[test]
fn collapse_lands_on_char_boundaries_after_multibyte() {
let line = "é\t";
assert_eq!(collapse(line, 1, 4, Bias::Left), 2);
assert_eq!(collapse(line, 2, 4, Bias::Left), 2);
assert_eq!(collapse(line, 2, 4, Bias::Right), 3);
}
#[test]
fn expand_collapse_round_trip_on_boundaries() {
let line = "a\tbé\tc";
let mut byte = 0u32;
for ch in line.chars() {
let cell = expand(line, byte, 4);
assert_eq!(collapse(line, cell, 4, Bias::Left), byte, "byte {byte}");
assert_eq!(collapse(line, cell, 4, Bias::Right), byte, "byte {byte}");
byte += ch.len_utf8() as u32;
}
}
#[test]
fn default_tab_size_is_four() {
assert_eq!(default_tab_size(), 4);
}
#[test]
fn display_point_accessors() {
let p = DisplayPoint::new(DisplayRow(2), 7);
assert_eq!(p.row(), DisplayRow(2));
assert_eq!(p.col(), 7);
assert_eq!(DisplayPoint::zero(), DisplayPoint::new(DisplayRow(0), 0));
}
#[test]
fn display_point_orders_row_major() {
assert!(DisplayPoint::new(DisplayRow(0), 9) < DisplayPoint::new(DisplayRow(1), 0));
assert!(DisplayPoint::new(DisplayRow(1), 2) < DisplayPoint::new(DisplayRow(1), 3));
}
#[test]
fn to_display_expands_the_column() {
let b = buf("a\tb\nx");
let m = TabMap::new(&b, 4);
let d = m.to_display(Point::new(0, 2), Bias::Left);
assert_eq!(d.row(), DisplayRow(0));
assert_eq!(d.col(), 4);
assert_eq!(m.to_display(Point::new(1, 1), Bias::Left).row(), DisplayRow(1));
}
#[test]
fn to_buffer_snaps_out_of_the_tab() {
let b = buf("\tx");
let m = TabMap::new(&b, 4);
let mid = DisplayPoint::new(DisplayRow(0), 2);
assert_eq!(m.to_buffer(mid, Bias::Left), Point::new(0, 0));
assert_eq!(m.to_buffer(mid, Bias::Right), Point::new(0, 1));
}
#[test]
fn clip_pulls_a_caret_out_of_a_tab() {
let b = buf("\tab");
let m = TabMap::new(&b, 4);
let inside = DisplayPoint::new(DisplayRow(0), 2);
assert_eq!(m.clip(inside, Bias::Left), DisplayPoint::new(DisplayRow(0), 0));
assert_eq!(m.clip(inside, Bias::Right), DisplayPoint::new(DisplayRow(0), 4));
}
#[test]
fn clip_is_idempotent() {
let b = buf("a\tbc\td");
let m = TabMap::new(&b, 4);
for col in 0..12 {
let once = m.clip(DisplayPoint::new(DisplayRow(0), col), Bias::Left);
let twice = m.clip(once, Bias::Left);
assert_eq!(once, twice, "col {col}");
}
}
#[test]
fn clip_clamps_past_end_of_line() {
let b = buf("hi");
let m = TabMap::new(&b, 4);
let far = DisplayPoint::new(DisplayRow(0), 99);
assert_eq!(m.clip(far, Bias::Left), DisplayPoint::new(DisplayRow(0), 2));
}
#[test]
fn row_count_and_max_row_track_lines() {
let b = buf("a\nb\nc");
let m = TabMap::new(&b, 4);
assert_eq!(m.row_count(), 3);
assert_eq!(m.max_row(), DisplayRow(2));
let e = buf("");
let me = TabMap::new(&e, 4);
assert_eq!(me.row_count(), 1);
assert_eq!(me.max_row(), DisplayRow(0));
}
#[test]
fn line_len_and_max_point_are_cells() {
let b = buf("a\tb\nlong");
let m = TabMap::new(&b, 4);
assert_eq!(m.line_len(DisplayRow(0)), 5); assert_eq!(m.line_len(DisplayRow(1)), 4);
assert_eq!(m.max_point(), DisplayPoint::new(DisplayRow(1), 4));
}
#[test]
fn row_info_is_identity_in_v1() {
let b = buf("x\ny");
let m = TabMap::new(&b, 4);
assert_eq!(m.row_info(DisplayRow(0)), Some(BufferRow(0)));
assert_eq!(m.row_info(DisplayRow(1)), Some(BufferRow(1)));
}
#[test]
fn chunks_split_at_every_tab() {
let b = buf("ab\tc");
let line = b.line(0);
let runs: Vec<_> = chunks(&line, 4).collect();
assert_eq!(runs.len(), 3);
assert_eq!(runs[0].text, "ab");
assert_eq!(runs[0].buffer_bytes, 0..2);
assert_eq!(runs[0].cells, 2);
assert!(!runs[0].is_tab);
assert_eq!(runs[1].text, "\t");
assert_eq!(runs[1].buffer_bytes, 2..3);
assert_eq!(runs[1].cells, 2);
assert!(runs[1].is_tab);
assert_eq!(runs[2].text, "c");
assert_eq!(runs[2].buffer_bytes, 3..4);
assert_eq!(runs[2].cells, 1);
}
#[test]
fn chunks_of_empty_and_tab_only_lines() {
let b = buf("\n\t");
assert_eq!(chunks(&b.line(0), 4).count(), 0);
let line = b.line(1);
let runs: Vec<_> = chunks(&line, 4).collect();
assert_eq!(runs.len(), 1);
assert!(runs[0].is_tab);
assert_eq!(runs[0].cells, 4);
}
#[test]
fn chunk_cells_sum_to_line_len() {
let b = buf("x\t\tyz\tw");
let m = TabMap::new(&b, 4);
let total: u32 = chunks(&b.line(0), 4).map(|c| c.cells).sum();
assert_eq!(total, m.line_len(DisplayRow(0)));
}
#[test]
fn sync_is_a_noop_that_leaves_conversions_current() {
let b = buf("a\tb");
let m0 = TabMap::new(&b, 4);
let before = m0.line_len(DisplayRow(0));
let mut m = TabMap::new(&b, 4);
m.sync(&Patch::new());
assert_eq!(m.line_len(DisplayRow(0)), before);
}
}