use std::cell::RefCell;
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::marks::MarkId;
use std::sync::Arc;
use crate::helix::chars::char_is_line_ending;
use crate::helix::doc_formatter::{DocumentFormatter, TextFormat};
use crate::helix::text_annotations::TextAnnotations;
use crate::helix::transaction::{ChangeSet, Operation};
use crate::helix::{Rope, RopeSlice};
use crate::outline::Outline;
use crate::state::{Config, Document, Follow, Scroll, State, View};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct OutlineLayout {
#[serde(alias = "marks")]
pub gutter: u16,
pub indent: u16,
pub hang: u16,
pub column: u16,
pub min_column: u16,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub extra_rows: BTreeMap<MarkId, u16>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub hang_glyphs: bool,
}
impl Default for OutlineLayout {
fn default() -> Self {
OutlineLayout { gutter: 2, indent: 4, hang: 4, column: 72, min_column: 20, extra_rows: BTreeMap::new(), hang_glyphs: false }
}
}
pub const LONG_LINE_CHARS: usize = 256;
const CACHED_LINES: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct RowStart {
char_idx: usize,
col: usize,
}
type FmtKey = (u16, u16, u16, u16);
fn fmt_key(fmt: &TextFormat) -> FmtKey {
(fmt.viewport_width, fmt.tab_width, fmt.max_wrap, fmt.max_indent_retain)
}
#[derive(Debug, Clone)]
struct LineRows {
fmt: FmtKey,
line: usize,
rows: Vec<RowStart>,
indent: Option<usize>,
complete: bool,
next_line: Option<usize>,
}
impl LineRows {
fn start(&self) -> usize {
self.rows[0].char_idx
}
}
#[derive(Clone, Default)]
pub struct WrapCache(Vec<LineRows>);
impl PartialEq for WrapCache {
fn eq(&self, _: &WrapCache) -> bool {
true
}
}
impl std::fmt::Debug for WrapCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "WrapCache({} lines)", self.0.len())
}
}
impl WrapCache {
pub fn edited(&mut self, changes: &ChangeSet) {
let mut first = 0usize;
let mut changed = false;
for op in changes.changes() {
match op {
Operation::Retain(n) => first += n,
_ => {
changed = true;
break;
}
}
}
if !changed {
return;
}
self.0.retain_mut(|e| {
if e.start() >= first {
return false;
}
if e.complete && e.next_line.is_some_and(|next| first >= next) {
return true;
}
let before = e.rows.iter().take_while(|r| r.char_idx < first).count();
e.rows.truncate(before.saturating_sub(2).max(1));
if e.rows.len() == 1 {
e.indent = None;
}
e.complete = false;
e.next_line = None;
true
});
}
pub fn clear(&mut self) {
self.0.clear();
}
}
#[derive(Debug, Clone, Copy)]
enum Need {
Pos(usize),
Row(usize),
All,
}
impl Need {
fn met(self, e: &LineRows) -> bool {
e.complete
|| match self {
Need::Pos(p) => e.rows.last().is_some_and(|r| r.char_idx > p),
Need::Row(r) => e.rows.len() > r,
Need::All => false,
}
}
fn row(self, e: &LineRows) -> usize {
match self {
Need::Pos(p) => e.rows.iter().rposition(|r| r.char_idx <= p).unwrap_or(0),
Need::Row(r) => r.min(e.rows.len() - 1),
Need::All => e.rows.len() - 1,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct RowPos {
pub line: usize,
pub row: usize,
}
pub fn text_format(config: &Config, width: u16, wrap: bool) -> TextFormat {
let width = width.max(1);
TextFormat {
soft_wrap: wrap && config.soft_wrap && width > 10,
tab_width: config.tab_width.max(1),
max_wrap: 20.min(width / 4),
max_indent_retain: 40.min(width * 2 / 5),
wrap_indicator: Box::from(""),
wrap_indicator_highlight: None,
viewport_width: width,
soft_wrap_at_text_width: false,
hang_spaces: false,
}
}
pub fn prose_format(config: &Config, width: u16, wrap: bool, hang: bool) -> TextFormat {
let mut f = text_format(config, width, wrap);
if hang {
f.max_wrap = f.viewport_width;
}
f.hang_spaces = hang;
f.soft_wrap_at_text_width = hang;
f
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LineFormat {
pub skip: usize,
pub x: usize,
pub hidden: bool,
pub before: usize,
pub after: usize,
fmt: usize,
}
pub struct Layout {
rope: Rope,
pub fmt: TextFormat,
pub annotations: TextAnnotations<'static>,
cache: RefCell<WrapCache>,
outline: Option<Arc<Outline>>,
geometry: Option<OutlineLayout>,
hidden: Vec<(usize, usize)>,
depth_fmts: Vec<TextFormat>,
offset: Option<(usize, usize)>,
width: u16,
}
impl Layout {
pub fn new(state: &State) -> Self {
Layout::of(&state.doc, &state.view)
}
pub fn of(doc: &Document, view: &View) -> Self {
Layout::build(doc, view, true)
}
pub fn unwrapped(state: &State) -> Self {
let mut l = Layout::build(&state.doc, &state.view, false);
l.cache = RefCell::new(WrapCache::default());
l
}
fn build(doc: &Document, view: &View, wrap: bool) -> Self {
let outline = doc.blocks();
let geometry = outline.as_ref().and(view.layout.clone());
let hidden = match &outline {
Some(o) => crate::views::hidden_lines(o, &view.folds),
None => Vec::new(),
};
let width = view.viewport.width;
let mut depth_fmts = Vec::new();
if let (Some(g), Some(o)) = (&geometry, &outline) {
let max_depth = o.blocks.iter().map(|b| b.depth as usize).max().unwrap_or(0);
let cap = (width as usize / g.indent.max(1) as usize) + 1;
for d in 0..=max_depth.min(cap) {
let w = depth_width(g, d, width);
let room = (width as usize).saturating_sub(block_x(g, d, width));
let hang = (w as usize) < room;
depth_fmts.push(prose_format(&doc.config, w, wrap, hang));
depth_fmts.push(prose_format(&doc.config, w, false, hang));
}
}
let mut layout = Layout {
rope: doc.text.clone(),
fmt: text_format(&doc.config, width, wrap),
annotations: TextAnnotations::default(),
cache: RefCell::new(view.wrap.clone()),
outline,
geometry,
hidden,
depth_fmts,
offset: None,
width,
};
if layout.geometry.is_some() {
let caret = view.caret().min(layout.text().len_chars());
let line = layout.text().char_to_line(caret);
let lf = layout.line_format(line);
let fmt = layout.fmt_of(&lf);
if !fmt.soft_wrap {
let col = layout.pos_coords(caret).1 - lf.x;
let w = fmt.viewport_width as usize;
if col >= w {
layout.offset = Some((line, col + 1 - w));
}
}
}
layout
}
pub fn line_format(&self, line: usize) -> LineFormat {
let plain = LineFormat { skip: 0, x: 0, hidden: false, before: 0, after: 0, fmt: 0 };
let Some(o) = &self.outline else { return plain };
let b = o.block_of_line(line);
let first = b.first_line == line;
let hidden = crate::views::hidden_range(&self.hidden, line).is_some();
let before = (first && b.gap) as usize;
let Some(g) = &self.geometry else { return LineFormat { hidden, before, ..plain } };
let d = b.depth as usize;
let after = if line == b.last_line() { g.extra_rows.get(&b.id).copied().unwrap_or(0) as usize } else { 0 };
let level = d.min(self.depth_fmts.len() / 2 - 1);
LineFormat {
skip: if first { b.prefix_len } else { 0 },
x: block_x(g, d, self.width),
hidden,
before,
after,
fmt: 1 + 2 * level + b.fence as usize,
}
}
fn fmt_of(&self, lf: &LineFormat) -> &TextFormat {
match lf.fmt {
0 => &self.fmt,
i => &self.depth_fmts[i - 1],
}
}
pub fn line_text_format(&self, line: usize) -> &TextFormat {
let lf = self.line_format(line);
self.fmt_of(&lf)
}
pub fn geometry(&self) -> Option<&OutlineLayout> {
self.geometry.as_ref()
}
pub fn outline(&self) -> Option<&Arc<Outline>> {
self.outline.as_ref()
}
pub fn line_offset(&self, line: usize) -> usize {
match self.offset {
Some((l, n)) if l == line => n,
_ => 0,
}
}
pub fn content_start(&self, line: usize) -> usize {
self.text().line_to_char(line) + self.line_format(line).skip
}
pub fn gap(&self, line: usize) -> usize {
self.line_format(line).before
}
pub fn is_hidden(&self, line: usize) -> bool {
crate::views::hidden_range(&self.hidden, line).is_some()
}
pub fn visible_at_or_after(&self, line: usize) -> Option<usize> {
let l = match crate::views::hidden_range(&self.hidden, line) {
Some((_, b)) => b,
None => line,
};
(l <= self.last_line()).then_some(l)
}
pub fn visible_at_or_before(&self, line: usize) -> Option<usize> {
match crate::views::hidden_range(&self.hidden, line) {
Some((a, _)) => a.checked_sub(1),
None => Some(line),
}
}
pub fn is_virtual(&self, at: RowPos) -> bool {
let lf = self.line_format(at.line);
at.row < lf.before || (lf.after > 0 && at.row >= lf.before + self.text_rows_of(at.line))
}
pub fn store(self, state: &mut State) {
state.view.wrap = self.cache.into_inner();
}
fn content_range(&self, line: usize, lf: &LineFormat) -> (usize, usize) {
let text = self.text();
let start = text.line_to_char(line);
let end = crate::helix::line_ending::line_end_char_index(&text, line);
((start + lf.skip).min(end), end)
}
fn is_long(&self, line: usize, lf: &LineFormat) -> bool {
if !self.fmt_of(lf).soft_wrap {
return false;
}
let (start, end) = self.content_range(line, lf);
end - start >= LONG_LINE_CHARS
}
fn known_row(&self, line: usize, lf: &LineFormat, need: Need) -> (usize, RowStart, Option<usize>) {
let key = fmt_key(self.fmt_of(lf));
let start = self.content_range(line, lf).0;
let mut cache = self.cache.borrow_mut();
let entries = &mut cache.0;
let text = self.text();
let next_line = (line < self.last_line()).then(|| text.line_to_char(line + 1));
let valid = |e: &LineRows| {
e.fmt == key && e.line == line && e.start() == start && (!e.complete || e.next_line == next_line)
};
let i = match entries.iter().position(valid) {
Some(i) => i,
None => {
entries.retain(|e| !(e.fmt == key && e.line == line));
if entries.len() >= CACHED_LINES {
entries.remove(0);
}
entries.push(LineRows {
fmt: key,
line,
rows: vec![RowStart { char_idx: start, col: 0 }],
indent: None,
complete: false,
next_line: None,
});
entries.len() - 1
}
};
let e = &mut entries[i];
if !need.met(e) {
self.extend(e, lf, need);
}
let row = need.row(e);
(row, e.rows[row], e.indent)
}
fn extend(&self, e: &mut LineRows, lf: &LineFormat, need: Need) {
let last = e.rows.len() - 1;
let mut formatter = self.formatter_from(e.line, lf, last, e.rows[last], e.indent);
while let Some(g) = formatter.next() {
if g.line_idx != e.line {
e.complete = true;
e.next_line = Some(g.char_idx);
return;
}
if e.indent.is_none() {
e.indent = formatter.indent_level();
}
if g.visual_pos.row == e.rows.len() {
e.rows.push(RowStart { char_idx: g.char_idx, col: g.visual_pos.col });
if need.met(e) {
return;
}
}
}
e.complete = true;
e.next_line = None;
}
fn formatter_from(&self, line: usize, lf: &LineFormat, row: usize, at: RowStart, indent: Option<usize>) -> DocumentFormatter<'_> {
let fmt = self.fmt_of(lf);
if row == 0 && lf.skip == 0 {
DocumentFormatter::new_at_prev_checkpoint(self.text(), fmt, &self.annotations, at.char_idx)
} else {
DocumentFormatter::resume_at_row(self.text(), fmt, &self.annotations, at.char_idx, line, row, at.col, indent)
}
}
fn formatter_for(&self, line: usize, lf: &LineFormat, need: Need) -> DocumentFormatter<'_> {
if self.is_long(line, lf) {
let (row, at, indent) = self.known_row(line, lf, need);
self.formatter_from(line, lf, row, at, indent)
} else {
let start = self.content_range(line, lf).0;
self.formatter_from(line, lf, 0, RowStart { char_idx: start, col: 0 }, None)
}
}
pub fn formatter_at_row(&self, at: RowPos) -> DocumentFormatter<'_> {
let lf = self.line_format(at.line);
let row = at.row.saturating_sub(lf.before);
self.formatter_for(at.line, &lf, Need::Row(row))
}
pub fn text(&self) -> RopeSlice<'_> {
self.rope.slice(..)
}
pub fn wraps(&self) -> bool {
self.fmt.soft_wrap || self.geometry.is_some()
}
pub fn last_line(&self) -> usize {
self.text().len_lines().saturating_sub(1)
}
pub fn line_rows(&self, line: usize) -> usize {
let lf = self.line_format(line);
if lf.hidden {
return 0;
}
lf.before + self.text_rows_with(line, &lf) + lf.after
}
fn walk(&self) -> LineWalk<'_> {
LineWalk { layout: self, lines: None }
}
pub fn text_rows_of(&self, line: usize) -> usize {
let lf = self.line_format(line);
if lf.hidden {
return 0;
}
self.text_rows_with(line, &lf)
}
fn text_rows_with(&self, line: usize, lf: &LineFormat) -> usize {
if !self.fmt_of(lf).soft_wrap || self.fits_one_row(line, lf) {
return 1;
}
if self.is_long(line, lf) {
self.known_row(line, lf, Need::All);
let cache = self.cache.borrow();
let key = fmt_key(self.fmt_of(lf));
if let Some(e) = cache.0.iter().find(|e| e.fmt == key && e.line == line) {
return e.rows.len();
}
}
self.formatted_rows_with(line, lf)
}
#[cfg(test)]
fn formatted_rows(&self, line: usize) -> usize {
let lf = self.line_format(line);
self.formatted_rows_with(line, &lf)
}
fn formatted_rows_with(&self, line: usize, lf: &LineFormat) -> usize {
let start = self.content_range(line, lf).0;
let formatter = self.formatter_from(line, lf, 0, RowStart { char_idx: start, col: 0 }, None);
let mut rows = 1;
for g in formatter {
if g.line_idx != line {
break;
}
rows = g.visual_pos.row + 1;
}
rows
}
fn fits_one_row(&self, line: usize, lf: &LineFormat) -> bool {
use unicode_width::UnicodeWidthChar;
let fmt = self.fmt_of(lf);
let width = fmt.viewport_width as usize;
let tab = fmt.tab_width as usize;
let text = self.rope.line(line);
let rest = if lf.skip == 0 { text } else { text.slice(lf.skip.min(text.len_chars())..) };
if surely_fits(rest, width, tab) {
return true;
}
let mut sum = 1;
for c in rest.chars() {
sum += match c {
'\t' => tab,
c if char_is_line_ending(c) => 0,
c if c.is_ascii() => 1,
c => c.width().unwrap_or(0).max(1),
};
if sum >= width {
return false;
}
}
true
}
pub fn pos_coords(&self, pos: usize) -> (RowPos, usize) {
let pos = pos.min(self.text().len_chars());
let line = self.text().char_to_line(pos);
let lf = self.line_format(line);
if lf.hidden {
if let Some(owner) = self.visible_at_or_before(line) {
let end = crate::helix::line_ending::line_end_char_index(&self.text(), owner);
return self.pos_coords(end);
}
}
let pos = pos.max(self.content_range(line, &lf).0);
let mut formatter = self.formatter_for(line, &lf, Need::Pos(pos));
let mut last = crate::helix::Position::default();
while let Some(g) = formatter.next() {
last = g.visual_pos;
if formatter.next_char_pos() > pos {
break;
}
}
(RowPos { line, row: last.row + lf.before }, last.col + lf.x)
}
pub fn pos_at(&self, at: RowPos, col: usize) -> usize {
let lf = self.line_format(at.line);
let (start, end) = self.content_range(at.line, &lf);
if at.row < lf.before {
return start;
}
let row = at.row - lf.before;
if lf.after > 0 && row >= self.text_rows_with(at.line, &lf) {
return end;
}
let col = col.saturating_sub(lf.x).saturating_add(self.line_offset(at.line));
let next = if at.line < self.last_line() { self.text().line_to_char(at.line + 1) } else { self.text().len_chars() };
self.char_at_row_col(at.line, &lf, row, col).min(next).max(start)
}
fn char_at_row_col(&self, line: usize, lf: &LineFormat, row: usize, col: usize) -> usize {
use std::cmp::Ordering;
let mut formatter = self.formatter_for(line, lf, Need::Row(row));
let mut last_char_idx = formatter.next_char_pos();
let mut found_non_virtual_on_row = false;
for g in &mut formatter {
match g.visual_pos.row.cmp(&row) {
Ordering::Equal => {
if g.visual_pos.col + g.width() > col {
if !g.is_virtual() {
return g.char_idx;
} else if found_non_virtual_on_row {
return last_char_idx;
}
} else if !g.is_virtual() {
found_non_virtual_on_row = true;
last_char_idx = g.char_idx;
}
}
Ordering::Greater => return last_char_idx,
Ordering::Less => {
if !g.is_virtual() {
last_char_idx = g.char_idx;
}
}
}
}
formatter.next_char_pos()
}
pub fn step_rows(&self, at: RowPos, n: isize) -> (RowPos, isize) {
let mut walk = self.walk();
let mut pos = at;
let mut moved: isize = 0;
if n >= 0 {
let mut left = n as usize;
while left > 0 {
let rows = walk.rows(pos.line).max(1);
if pos.row + left < rows {
pos.row += left;
moved += left as isize;
left = 0;
} else if let Some(next) = (pos.line < self.last_line()).then(|| self.visible_at_or_after(pos.line + 1)).flatten() {
let step = rows - pos.row;
left -= step;
moved += step as isize;
pos = RowPos { line: next, row: 0 };
} else {
let step = rows - 1 - pos.row;
moved += step as isize;
pos.row = rows - 1;
left = 0;
}
}
} else {
let mut left = n.unsigned_abs();
while left > 0 {
if pos.row >= left {
pos.row -= left;
moved -= left as isize;
left = 0;
} else if let Some(prev) = pos.line.checked_sub(1).and_then(|l| self.visible_at_or_before(l)) {
let step = pos.row + 1;
left -= step;
moved -= step as isize;
pos.line = prev;
pos.row = walk.rows(prev).max(1) - 1;
} else {
moved -= pos.row as isize;
pos.row = 0;
left = 0;
}
}
}
(pos, moved)
}
pub fn rows_between(&self, from: RowPos, to: RowPos, limit: usize) -> isize {
if to < from {
return -1 - self.rows_between(to, from, limit);
}
let mut sum = 0usize;
let mut line = from.line;
let mut row = from.row;
let mut walk = self.walk();
while line < to.line {
sum += walk.rows(line).saturating_sub(row);
row = 0;
line += 1;
if let Some((_, b)) = crate::views::hidden_range(&self.hidden, line) {
line = b.min(to.line);
}
if sum > limit {
return limit as isize + 1;
}
}
(sum + to.row).saturating_sub(row) as isize
}
pub fn top(&self, scroll: &Scroll) -> RowPos {
let line = scroll.line.min(self.last_line());
let line = self.visible_at_or_after(line).or_else(|| self.visible_at_or_before(line)).unwrap_or(0);
let row = scroll.row.min(self.line_rows(line).max(1) - 1);
RowPos { line, row }
}
pub fn end(&self) -> RowPos {
let line = self.visible_at_or_before(self.last_line()).unwrap_or(0);
RowPos { line, row: self.line_rows(line).max(1) - 1 }
}
pub fn pos_at_screen(&self, scroll: &Scroll, col: u16, row: u16) -> usize {
let top = self.top(scroll);
let (at, moved) = self.step_rows(top, row as isize);
if moved < row as isize {
return self.text().len_chars();
}
let col = col as usize + if self.wraps() { 0 } else { scroll.col };
self.pos_at(at, col)
}
}
pub(crate) fn block_x(g: &OutlineLayout, d: usize, width: u16) -> usize {
let x = g.gutter as usize + d * g.indent as usize + g.hang as usize;
let keep = (g.min_column as usize).min(width as usize);
x.min((width as usize).saturating_sub(keep))
}
fn depth_width(g: &OutlineLayout, d: usize, width: u16) -> u16 {
let column = (g.column as usize).saturating_sub(d * g.indent as usize).max(g.min_column as usize);
let room = (width as usize).saturating_sub(block_x(g, d, width));
column.min(room).max(1) as u16
}
pub fn clamp_scroll(state: &mut State) {
let layout = Layout::new(state);
let top = layout.top(&state.view.scroll);
let col = if layout.wraps() { 0 } else { state.view.scroll.col };
state.view.scroll = Scroll { line: top.line, row: top.row, col };
layout.store(state);
}
pub fn ensure_caret_visible(state: &mut State) {
let layout = Layout::new(state);
let h = state.text_rows();
let w = state.view.viewport.width as usize;
let (caret, col) = layout.pos_coords(state.caret());
let mut top = layout.top(&state.view.scroll);
if let (Follow::Typewriter { percent }, true) = (state.view.config.follow, h > 0) {
let row = ((h - 1) * percent.min(100) as usize + 50) / 100;
top = layout.step_rows(caret, -(row as isize)).0;
} else if h > 0 {
let so = (state.view.config.scrolloff as usize).min((h - 1) / 2);
let dist = layout.rows_between(top, caret, h + so);
if dist < so as isize {
top = layout.step_rows(caret, -(so as isize)).0;
} else if dist > (h - 1 - so) as isize {
top = layout.step_rows(caret, -((h - 1 - so) as isize)).0;
}
if layout.last_line().saturating_sub(top.line) < h || !layout.hidden.is_empty() {
let max_top = layout.step_rows(layout.end(), -(h as isize - 1)).0;
if top > max_top && max_top <= caret {
top = max_top;
}
}
} else {
top = caret;
}
let mut scroll_col = state.view.scroll.col;
if layout.wraps() {
scroll_col = 0;
} else if col < scroll_col {
scroll_col = col;
} else if col >= scroll_col + w {
scroll_col = col + 1 - w;
}
state.view.scroll = Scroll {
line: top.line,
row: top.row,
col: scroll_col,
};
layout.store(state);
}
struct LineWalk<'a> {
layout: &'a Layout,
lines: Option<(crate::helix::ropey::iter::Lines<'a>, usize)>,
}
impl LineWalk<'_> {
fn rows(&mut self, line: usize) -> usize {
let l = self.layout;
if l.outline.is_some() || !l.hidden.is_empty() {
return l.line_rows(line);
}
if !l.fmt.soft_wrap {
return 1;
}
let slice = match &mut self.lines {
Some((it, at)) if *at == line => {
*at += 1;
it.next()
}
Some((it, at)) if *at == line + 1 => {
*at = line;
it.prev()
}
_ => {
let mut it = l.rope.lines_at(line);
let s = it.next();
self.lines = Some((it, line + 1));
s
}
};
match slice {
Some(s) if surely_fits(s, l.fmt.viewport_width as usize, l.fmt.tab_width as usize) => 1,
_ => l.line_rows(line),
}
}
}
fn surely_fits(line: RopeSlice<'_>, width: usize, tab: usize) -> bool {
let mut bytes = 0;
let mut tabs = 0;
for chunk in line.chunks() {
bytes += chunk.len();
if chunk.contains('\t') {
tabs += chunk.matches('\t').count();
}
if bytes >= width {
return false;
}
}
1 + bytes + tabs * tab.saturating_sub(1) < width
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::Viewport;
#[test]
fn fits_one_row_agrees_with_the_formatter() {
let pieces = ["a", "word ", "\t", "界", "🙂", "👨👩👧", "🇫🇷", "e\u{301}", "\u{1}", " ", "long-unbroken-token"];
let mut seed = 0x2545_f491_u32;
let mut next = move || {
seed ^= seed << 13;
seed ^= seed >> 17;
seed ^= seed << 5;
seed
};
for _ in 0..3000 {
let mut line = String::new();
for _ in 0..(next() % 40) {
line.push_str(pieces[next() as usize % pieces.len()]);
}
let text = format!("{line}\nnext\n");
let width = 11 + (next() % 60) as u16;
let state = State::new(&text, None, Viewport { width, height: 10 });
let layout = Layout::new(&state);
if layout.fits_one_row(0, &layout.line_format(0)) {
assert_eq!(layout.formatted_rows(0), 1, "{line:?} at width {width}");
}
}
}
#[test]
fn the_line_walk_counts_what_line_rows_counts() {
let pieces = ["a", "word ", "\t", "界", "🙂", "e\u{301}", " ", "long-unbroken-token", "\n"];
let mut next = xorshift(0x9e37_79b9);
for _ in 0..200 {
let mut text = String::new();
for _ in 0..(next() % 300) {
text.push_str(pieces[next() as usize % pieces.len()]);
}
let width = 11 + (next() % 40) as u16;
let state = State::new(&text, None, Viewport { width, height: 10 });
let layout = Layout::new(&state);
let last = layout.last_line();
let rows: Vec<usize> = (0..=last).map(|l| layout.line_rows(l)).collect();
let a = (next() as usize) % (last + 1);
let b = (next() as usize) % (last + 1);
let (a, b) = (a.min(b), a.max(b));
let want: usize = rows[a..b].iter().sum();
let from = RowPos { line: a, row: 0 };
let to = RowPos { line: b, row: 0 };
assert_eq!(layout.rows_between(from, to, usize::MAX / 2), want as isize, "{text:?} at {width}");
assert_eq!(layout.step_rows(from, want as isize), (to, want as isize), "{text:?} at {width}");
assert_eq!(layout.step_rows(to, -(want as isize)), (from, -(want as isize)), "{text:?} at {width}");
}
}
fn xorshift(mut seed: u32) -> impl FnMut() -> u32 {
move || {
seed ^= seed << 13;
seed ^= seed >> 17;
seed ^= seed << 5;
seed
}
}
fn reference_coords(layout: &Layout, pos: usize) -> (RowPos, usize) {
use crate::helix::visual_offset_from_block;
let line = layout.text().char_to_line(pos);
let (p, _) = visual_offset_from_block(layout.text(), pos, pos, &layout.fmt, &layout.annotations);
(RowPos { line, row: p.row }, p.col)
}
fn reference_pos_at(layout: &Layout, at: RowPos, col: usize) -> usize {
use crate::helix::position::char_idx_at_visual_block_offset;
let start = layout.text().line_to_char(at.line);
let end = if at.line < layout.last_line() {
layout.text().line_to_char(at.line + 1)
} else {
layout.text().len_chars()
};
let (pos, _) =
char_idx_at_visual_block_offset(layout.text(), start, at.row, col, &layout.fmt, &layout.annotations);
pos.min(end)
}
fn seeds() -> u32 {
std::env::var("CARETLINE_WRAP_SEEDS").ok().and_then(|s| s.parse().ok()).unwrap_or(6)
}
#[test]
fn wrap_cache_agrees_with_layout_from_scratch() {
use crate::msg::{By, Dir, Msg};
use crate::update::update;
use crate::view::view;
let pieces = [
"a", "word ", "words and more ", "\t", "界", "🙂", "e\u{301}", " ", "long-unbroken-token-that-goes-on",
"x", "x", "x", " ",
];
for seed in 1..=seeds() {
let mut next = xorshift(0x9e37_79b9 ^ seed.wrapping_mul(0x85eb_ca6b));
let mut text = String::new();
for l in 0..(1 + next() % 3) {
if next().is_multiple_of(2) {
text.push_str(["", " ", "\t", " "][next() as usize % 4]);
}
let n = if l == 0 || next().is_multiple_of(2) { 300 + next() % 600 } else { next() % 20 };
for _ in 0..n {
text.push_str(pieces[next() as usize % pieces.len()]);
}
text.push('\n');
}
let width = [11, 17, 40, 80, 8][next() as usize % 5];
let mut warm = State::new(&text, None, Viewport { width, height: 12 });
for step in 0..300u32 {
let msg = match next() % 16 {
0..=4 => Msg::InsertText { text: pieces[next() as usize % pieces.len()].to_string() },
5 => Msg::InsertNewline,
6 | 7 => Msg::DeleteBackward,
8 => Msg::DeleteForward,
9 => Msg::Undo,
10 => Msg::Redo,
11 => Msg::Move {
dir: if next().is_multiple_of(2) { Dir::Forward } else { Dir::Backward },
by: [By::VisualLine, By::Page, By::LineEnd, By::LineStart, By::Word, By::DocEnd][next() as usize % 6],
extend: next().is_multiple_of(4),
},
12 => Msg::Click { col: (next() % 90) as u16, row: (next() % 12) as u16, extend: false },
13 => Msg::Scroll { rows: (next() % 21) as i32 - 10 },
14 => Msg::Resize { width: [11, 17, 40, 80, 8][next() as usize % 5], height: 12 },
_ => Msg::Move { dir: Dir::Backward, by: By::Grapheme, extend: false },
};
let mut cold = warm.clone();
cold.view.wrap.clear();
update(&mut warm, msg.clone());
update(&mut cold, msg.clone());
let ctx = format!("seed {seed} step {step} {msg:?}");
assert_eq!(warm, cold, "{ctx}: state");
assert_eq!(warm.view.scroll, cold.view.scroll, "{ctx}: scroll");
assert_eq!(view(&warm), view(&cold), "{ctx}: frame");
if step.is_multiple_of(8) {
let layout = Layout::new(&warm);
let len = layout.text().len_chars();
for _ in 0..6 {
let pos = next() as usize % (len + 1);
let pos = crate::helix::graphemes::ensure_grapheme_boundary_prev(layout.text(), pos);
let at = layout.pos_coords(pos);
assert_eq!(at, reference_coords(&layout, pos), "{ctx}: coords of {pos}");
let col = next() as usize % 90;
assert_eq!(layout.pos_at(at.0, col), reference_pos_at(&layout, at.0, col), "{ctx}: pos_at");
}
for line in 0..=layout.last_line() {
let rows = if !layout.fmt.soft_wrap { 1 } else { layout.formatted_rows(line) };
assert_eq!(layout.line_rows(line), rows, "{ctx}: rows of line {line}");
}
}
}
}
}
}