use std::borrow::Cow;
use crate::buffer::Buffer;
use crate::coords::{Bias, Point};
use crate::display_map::{self, BufferRow, DisplayRow};
use crate::fold_map::{FoldMap, InlineFold};
#[cfg(any(test, debug_assertions))]
thread_local! {
pub(crate) static DISPLAY_POSITION_PROBES: std::cell::Cell<u64> =
const { std::cell::Cell::new(0) };
}
pub const INLINE_CHIP_CELLS: u32 = 3;
pub const FOLD_PLACEHOLDER_CELLS: u32 = 4;
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum CaretCell {
Cell(u32),
ChipCenter(f32),
}
impl CaretCell {
#[must_use]
pub fn cells(self) -> f32 {
match self {
Self::Cell(c) => c as f32,
Self::ChipCenter(c) => c,
}
}
}
#[derive(Copy, Clone, PartialEq, Debug)]
pub struct DisplayPosition {
pub row: DisplayRow,
pub x: CaretCell,
}
#[derive(Copy, Clone, PartialEq, Debug)]
pub struct Chip {
pub cell: u32,
pub center: f32,
pub open_col: u32,
pub close_col: u32,
}
#[derive(Copy, Clone, PartialEq, Debug)]
pub struct TailGlyph {
pub col: u32,
pub cell: u32,
pub ch: char,
}
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum HeaderHit {
Head,
Gap,
Tail(u32),
}
#[must_use]
pub fn gap_left_edge(open: u32) -> u32 {
open + 1
}
#[must_use]
pub fn gap_hides_caret(open: u32, close: u32, off: u32) -> bool {
off > gap_left_edge(open) && off < close
}
#[must_use]
pub fn gap_hides_glyph(open: u32, close: u32, off: u32) -> bool {
off > open && off < close
}
#[must_use]
pub fn tail_start_col(line: &str) -> u32 {
(line.len() - line.trim_start().len()) as u32
}
#[must_use]
pub fn pair_has_interior(open: u32, close: u32) -> bool {
close > open + 1
}
#[must_use]
pub fn virtual_cell(cell: f32) -> u32 {
cell.round().max(0.0) as u32
}
#[derive(Copy, Clone, Debug)]
struct InlineSpan {
open_cell: u32,
close_cell: u32,
fold: InlineFold,
}
pub struct RowLayout<'a> {
line: Cow<'a, str>,
row_start: u32,
tab: u32,
spans: Vec<InlineSpan>,
}
impl<'a> RowLayout<'a> {
fn new(fold_map: &FoldMap, buffer: &'a Buffer, row: BufferRow, tab: u32) -> Self {
let line = buffer.line(row.0);
let row_start = buffer.point_to_offset(Point::new(row.0, 0));
let mut spans: Vec<InlineSpan> = fold_map
.inline_folds_on_row(row.0)
.into_iter()
.map(|fold| InlineSpan {
open_cell: display_map::expand(&line, fold.open - row_start, tab),
close_cell: display_map::expand(&line, fold.close - row_start, tab),
fold,
})
.collect();
spans.sort_by_key(|s| s.open_cell);
Self { line, row_start, tab, spans }
}
#[must_use]
pub fn is_plain(&self) -> bool {
self.spans.is_empty()
}
#[must_use]
pub fn row_start(&self) -> u32 {
self.row_start
}
fn shift_at(&self, cell: u32) -> i32 {
self.spans
.iter()
.filter(|s| cell >= s.close_cell)
.map(|s| (s.close_cell as i32 - s.open_cell as i32 - 1) - INLINE_CHIP_CELLS as i32)
.sum()
}
fn cell_of(&self, raw_cell: u32) -> u32 {
(raw_cell as i32 - self.shift_at(raw_cell)).max(0) as u32
}
#[must_use]
pub fn display_cell(&self, col: u32) -> u32 {
self.cell_of(display_map::expand(&self.line, col, self.tab))
}
#[must_use]
pub fn caret_cell(&self, col: u32) -> CaretCell {
let off = self.row_start + col;
match self.spans.iter().find(|s| s.fold.hides_caret_at(off)) {
Some(s) => CaretCell::ChipCenter(
self.cell_of(s.open_cell + 1) as f32 + INLINE_CHIP_CELLS as f32 / 2.0,
),
None => CaretCell::Cell(self.display_cell(col)),
}
}
#[must_use]
pub fn glyph_hidden(&self, col: u32) -> bool {
let off = self.row_start + col;
self.spans.iter().any(|s| s.fold.hides_glyph_at(off))
}
#[must_use]
pub fn hit(&self, cell: f32, bias: Bias) -> u32 {
let dc = cell.round().max(0.0) as u32;
let mut extra = 0i32;
for s in &self.spans {
let d_open = self.cell_of(s.open_cell);
let d_chip_end = d_open + 1 + INLINE_CHIP_CELLS;
if dc >= d_chip_end {
extra += (s.close_cell as i32 - s.open_cell as i32 - 1) - INLINE_CHIP_CELLS as i32;
} else if dc > d_open {
return s.fold.left_edge() - self.row_start; }
}
let raw_cell = (dc as i32 + extra).max(0) as u32;
display_map::collapse(&self.line, raw_cell, self.tab, bias)
}
#[must_use]
pub fn width(&self) -> u32 {
self.display_cell(self.line.len() as u32)
}
pub fn chips(&self) -> impl Iterator<Item = Chip> + '_ {
self.spans.iter().map(|s| {
let cell = self.cell_of(s.open_cell + 1);
Chip {
cell,
center: cell as f32 + INLINE_CHIP_CELLS as f32 / 2.0,
open_col: s.fold.open - self.row_start,
close_col: s.fold.close - self.row_start,
}
})
}
}
pub struct HeaderLayout<'a> {
head: RowLayout<'a>,
last: BufferRow,
tail_line: Cow<'a, str>,
tail_lead: u32,
tab: u32,
}
impl<'a> HeaderLayout<'a> {
#[must_use]
pub fn head(&self) -> &RowLayout<'a> {
&self.head
}
#[must_use]
pub fn head_cells(&self) -> u32 {
self.head.width()
}
#[must_use]
pub fn gap_center(&self) -> f32 {
self.head_cells() as f32 + FOLD_PLACEHOLDER_CELLS as f32 / 2.0
}
#[must_use]
pub fn tail_cell(&self) -> u32 {
self.head_cells() + FOLD_PLACEHOLDER_CELLS
}
#[must_use]
pub fn last_row(&self) -> BufferRow {
self.last
}
#[must_use]
pub fn tail_start_col(&self) -> u32 {
self.tail_lead
}
fn lead_cells(&self) -> u32 {
display_map::expand(&self.tail_line, self.tail_lead, self.tab)
}
#[must_use]
pub fn tail_col_cell(&self, col: u32) -> Option<u32> {
(col >= self.tail_lead)
.then(|| self.tail_cell() + display_map::expand(&self.tail_line, col, self.tab) - self.lead_cells())
}
#[must_use]
pub fn tail_cells(&self) -> u32 {
display_map::expand(&self.tail_line, self.tail_line.len() as u32, self.tab) - self.lead_cells()
}
#[must_use]
pub fn width(&self) -> u32 {
self.tail_cell() + self.tail_cells()
}
pub fn tail_glyphs(&self) -> impl Iterator<Item = TailGlyph> + '_ {
self.tail_line[self.tail_lead as usize..].char_indices().map(move |(i, ch)| {
let col = self.tail_lead + i as u32;
TailGlyph {
col,
cell: self.tail_col_cell(col).expect("tail glyph is at/after the lead"),
ch,
}
})
}
#[must_use]
pub fn hit(&self, cell: f32, bias: Bias) -> HeaderHit {
if cell >= self.tail_cell() as f32 - 0.5 {
let cell_in_tail = (cell - self.tail_cell() as f32).round().max(0.0) as u32;
let col = display_map::collapse(&self.tail_line, self.lead_cells() + cell_in_tail, self.tab, bias);
HeaderHit::Tail(col)
} else if cell > self.head_cells() as f32 + 0.5 {
HeaderHit::Gap
} else {
HeaderHit::Head
}
}
}
impl FoldMap {
#[must_use]
pub fn row_layout<'a>(&self, buffer: &'a Buffer, row: BufferRow, tab: u32) -> RowLayout<'a> {
RowLayout::new(self, buffer, row, tab)
}
#[must_use]
pub fn header_layout<'a>(&self, buffer: &'a Buffer, row: BufferRow, tab: u32) -> Option<HeaderLayout<'a>> {
let last = self.fold_at_header(row)?;
let head = self.row_layout(buffer, row, tab);
let tail_line = buffer.line(last.0);
let tail_lead = tail_start_col(&tail_line);
Some(HeaderLayout { head, last, tail_line, tail_lead, tab })
}
#[must_use]
pub fn display_position(&self, buffer: &Buffer, offset: u32, tab: u32) -> Option<DisplayPosition> {
#[cfg(any(test, debug_assertions))]
DISPLAY_POSITION_PROBES.with(|c| c.set(c.get() + 1));
crate::perf::charge(1); let p = buffer.offset_to_point(offset);
let row = BufferRow(p.row);
if !self.is_folded(row) {
let layout = self.row_layout(buffer, row, tab);
return Some(DisplayPosition { row: self.to_display_row(row), x: layout.caret_cell(p.col) });
}
let hdr = self.header_of_tail(row)?;
let layout = self.header_layout(buffer, hdr, tab)?;
let cell = layout.tail_col_cell(p.col)?;
Some(DisplayPosition { row: self.to_display_row(hdr), x: CaretCell::Cell(cell) })
}
#[must_use]
pub fn hit_row(&self, buffer: &Buffer, row: BufferRow, cell: f32, bias: Bias, tab: u32) -> u32 {
if let Some(layout) = self.header_layout(buffer, row, tab) {
match layout.hit(cell, bias) {
HeaderHit::Tail(col) => return buffer.point_to_offset(Point::new(layout.last_row().0, col)),
HeaderHit::Gap => return buffer.point_to_offset(Point::new(row.0, buffer.line_len(row.0))),
HeaderHit::Head => {}
}
}
let layout = self.row_layout(buffer, row, tab);
buffer.point_to_offset(Point::new(row.0, layout.hit(cell, bias)))
}
#[must_use]
pub fn display_row_at(&self, rows_from_top: f64) -> DisplayRow {
DisplayRow((rows_from_top.floor().max(0.0) as u32).min(self.display_row_count().saturating_sub(1)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::document::Document;
fn doc_with_folds(text: &str, fold_openers: &[u32]) -> Document {
let mut doc = Document::new(text).expect("test doc fits");
for &o in fold_openers {
assert!(doc.toggle_fold_opener(o), "opener {o} must be foldable");
}
doc
}
fn fold_map(doc: &Document) -> FoldMap {
FoldMap::new(doc.folds(), doc.brackets(), doc.buffer())
}
#[test]
fn caret_cell_lands_on_chip_edge_not_center() {
let doc = doc_with_folds("a[bcdef]g", &[1]);
let fm = fold_map(&doc);
let rl = fm.row_layout(doc.buffer(), BufferRow(0), 4);
assert_eq!(rl.caret_cell(2), CaretCell::Cell(2), "open+1 is landable at the chip's left edge");
assert_eq!(rl.caret_cell(3), CaretCell::ChipCenter(2.0 + INLINE_CHIP_CELLS as f32 / 2.0));
assert_eq!(rl.caret_cell(7), CaretCell::Cell(rl.display_cell(7)));
assert!(rl.glyph_hidden(2));
assert!(!rl.glyph_hidden(7), "the closing bracket stays visible");
assert!(!rl.glyph_hidden(1), "the opening bracket stays visible");
}
#[test]
fn hit_round_trips_display_cell_on_multi_chip_rows() {
let text = "\tf([aa], [bb]) é";
let open1 = text.find("[aa").unwrap() as u32;
let open2 = text.find("[bb").unwrap() as u32;
let doc = doc_with_folds(text, &[open1, open2]);
let fm = fold_map(&doc);
let rl = fm.row_layout(doc.buffer(), BufferRow(0), 4);
assert_eq!(rl.chips().count(), 2);
let line = doc.buffer().line(0);
for (i, _) in line.char_indices() {
let col = i as u32;
if rl.glyph_hidden(col) {
continue; }
assert_eq!(rl.hit(rl.display_cell(col) as f32, Bias::Left), col, "round-trip col {col}");
}
for chip in rl.chips().collect::<Vec<_>>() {
for dc in chip.cell..chip.cell + INLINE_CHIP_CELLS {
assert_eq!(rl.hit(dc as f32, Bias::Left), chip.open_col + 1, "chip cell {dc}");
}
}
}
#[test]
fn header_layout_shrinks_with_preceding_inline_fold() {
let text = "\tcall([a, b, c]) {\n\tbody\n\t}\n";
let inline_open = text.find('[').unwrap() as u32;
let block_open = text.find('{').unwrap() as u32;
let doc = doc_with_folds(text, &[inline_open, block_open]);
let fm = fold_map(&doc);
let hl = fm.header_layout(doc.buffer(), BufferRow(0), 4).expect("row 0 is a collapsed header");
let raw = display_map::expand(&doc.buffer().line(0), doc.buffer().line(0).len() as u32, 4);
assert!(hl.head_cells() < raw, "head {} must be < raw {raw}", hl.head_cells());
assert_eq!(hl.head_cells(), fm.row_layout(doc.buffer(), BufferRow(0), 4).width());
assert_eq!(hl.tail_cell(), hl.head_cells() + FOLD_PLACEHOLDER_CELLS);
let g: Vec<TailGlyph> = hl.tail_glyphs().collect();
assert_eq!(g.len(), 1);
assert_eq!(g[0].ch, '}');
assert_eq!(Some(g[0].cell), hl.tail_col_cell(g[0].col));
}
#[test]
fn tail_glyph_cells_match_tail_col_cell_with_tab_in_tail() {
let text = "f() {\nbody\n}\tx\n";
let block_open = text.find('{').unwrap() as u32;
let doc = doc_with_folds(text, &[block_open]);
let fm = fold_map(&doc);
let hl = fm.header_layout(doc.buffer(), BufferRow(0), 4).expect("collapsed header");
for g in hl.tail_glyphs() {
assert_eq!(Some(g.cell), hl.tail_col_cell(g.col), "glyph at col {} agrees with the col map", g.col);
}
for g in hl.tail_glyphs() {
assert_eq!(hl.hit(g.cell as f32, Bias::Left), HeaderHit::Tail(g.col));
}
}
#[test]
fn display_position_follows_tail_and_hides_gap() {
let text = "a {\nhidden\n} tail\nafter\n";
let block_open = text.find('{').unwrap() as u32;
let doc = doc_with_folds(text, &[block_open]);
let fm = fold_map(&doc);
let buffer = doc.buffer();
let hidden = buffer.point_to_offset(Point::new(1, 2));
assert_eq!(fm.display_position(buffer, hidden, 4), None);
let tail = buffer.point_to_offset(Point::new(2, 0));
let p = fm.display_position(buffer, tail, 4).expect("tail is visible");
assert_eq!(p.row, DisplayRow(0));
let hl = fm.header_layout(buffer, BufferRow(0), 4).unwrap();
assert_eq!(p.x, CaretCell::Cell(hl.tail_cell()));
let after = buffer.point_to_offset(Point::new(3, 0));
let p = fm.display_position(buffer, after, 4).expect("visible");
assert_eq!(p.row, DisplayRow(1), "rows 1..=2 hidden ⇒ row 3 displays at 1");
}
#[test]
fn hit_row_resolves_head_gap_and_tail() {
let text = "ab {\nhidden\n}\n";
let block_open = text.find('{').unwrap() as u32;
let doc = doc_with_folds(text, &[block_open]);
let fm = fold_map(&doc);
let buffer = doc.buffer();
let hl = fm.header_layout(buffer, BufferRow(0), 4).unwrap();
assert_eq!(fm.hit_row(buffer, BufferRow(0), 0.0, Bias::Left, 4), 0);
let gap_cell = hl.head_cells() as f32 + FOLD_PLACEHOLDER_CELLS as f32 / 2.0;
assert_eq!(fm.hit_row(buffer, BufferRow(0), gap_cell, Bias::Left, 4), buffer.line_len(0));
let tail_off = buffer.point_to_offset(Point::new(2, 0));
assert_eq!(fm.hit_row(buffer, BufferRow(0), hl.tail_cell() as f32, Bias::Left, 4), tail_off);
}
#[test]
fn display_row_at_floors_and_clamps() {
let doc = doc_with_folds("a\nb\nc\n", &[]);
let fm = fold_map(&doc);
assert_eq!(fm.display_row_at(-2.0), DisplayRow(0));
assert_eq!(fm.display_row_at(0.9), DisplayRow(0));
assert_eq!(fm.display_row_at(1.0), DisplayRow(1));
assert_eq!(fm.display_row_at(99.0), fm.max_display_row());
}
#[test]
fn plain_row_layout_is_tab_expansion() {
let doc = doc_with_folds("\tx = 1\n", &[]);
let fm = fold_map(&doc);
let rl = fm.row_layout(doc.buffer(), BufferRow(0), 4);
assert!(rl.is_plain());
assert_eq!(rl.display_cell(0), 0);
assert_eq!(rl.display_cell(1), 4, "tab expands to the stop");
assert_eq!(rl.width(), 4 + "x = 1".len() as u32);
assert_eq!(rl.hit(4.0, Bias::Left), 1);
}
}