use crate::{Position, Viewport};
pub struct Buffer {
lines: Vec<String>,
cursor: Position,
dirty_gen: u64,
pub(crate) folds: Vec<crate::folds::Fold>,
}
impl Default for Buffer {
fn default() -> Self {
Self::new()
}
}
impl Buffer {
pub fn new() -> Self {
Self {
lines: vec![String::new()],
cursor: Position::default(),
dirty_gen: 0,
folds: Vec::new(),
}
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(text: &str) -> Self {
let mut lines: Vec<String> = text.split('\n').map(str::to_owned).collect();
if lines.is_empty() {
lines.push(String::new());
}
Self {
lines,
cursor: Position::default(),
dirty_gen: 0,
folds: Vec::new(),
}
}
pub fn lines(&self) -> &[String] {
&self.lines
}
pub fn line(&self, row: usize) -> Option<&str> {
self.lines.get(row).map(String::as_str)
}
pub fn cursor(&self) -> Position {
self.cursor
}
pub fn dirty_gen(&self) -> u64 {
self.dirty_gen
}
pub fn set_cursor(&mut self, pos: Position) {
let last_row = self.lines.len().saturating_sub(1);
let row = pos.row.min(last_row);
let line_chars = self.lines[row].chars().count();
let col = pos.col.min(line_chars);
self.cursor = Position::new(row, col);
}
pub fn ensure_cursor_visible(&mut self, viewport: &mut Viewport) {
let cursor = self.cursor;
let v = *viewport;
let wrap_active = !matches!(v.wrap, crate::Wrap::None) && v.text_width > 0;
if !wrap_active {
viewport.ensure_visible(cursor);
return;
}
if v.height == 0 {
return;
}
if cursor.row < v.top_row {
viewport.top_row = cursor.row;
viewport.top_col = 0;
return;
}
let height = v.height as usize;
loop {
let csr = self.cursor_screen_row_from(viewport, viewport.top_row);
match csr {
Some(row) if row < height => break,
_ => {}
}
let mut next = viewport.top_row + 1;
while next <= cursor.row && self.folds.iter().any(|f| f.hides(next)) {
next += 1;
}
if next > cursor.row {
viewport.top_row = cursor.row;
break;
}
viewport.top_row = next;
}
viewport.top_col = 0;
}
pub fn cursor_screen_row(&self, viewport: &Viewport) -> Option<usize> {
if matches!(viewport.wrap, crate::Wrap::None) || viewport.text_width == 0 {
return None;
}
self.cursor_screen_row_from(viewport, viewport.top_row)
}
pub fn screen_rows_between(&self, viewport: &Viewport, start: usize, end: usize) -> usize {
if start > end {
return 0;
}
let last = self.lines.len().saturating_sub(1);
let end = end.min(last);
let v = *viewport;
let mut total = 0usize;
for r in start..=end {
if self.folds.iter().any(|f| f.hides(r)) {
continue;
}
if matches!(v.wrap, crate::Wrap::None) || v.text_width == 0 {
total += 1;
} else {
let line = self.lines.get(r).map(String::as_str).unwrap_or("");
total += crate::wrap::wrap_segments(line, v.text_width, v.wrap).len();
}
}
total
}
pub fn max_top_for_height(&self, viewport: &Viewport, height: usize) -> usize {
if height == 0 {
return 0;
}
let last = self.lines.len().saturating_sub(1);
let mut total = 0usize;
let mut row = last;
loop {
if !self.folds.iter().any(|f| f.hides(row)) {
let v = *viewport;
total += if matches!(v.wrap, crate::Wrap::None) || v.text_width == 0 {
1
} else {
let line = self.lines.get(row).map(String::as_str).unwrap_or("");
crate::wrap::wrap_segments(line, v.text_width, v.wrap).len()
};
}
if total >= height {
return row;
}
if row == 0 {
return 0;
}
row -= 1;
}
}
fn cursor_screen_row_from(&self, viewport: &Viewport, top: usize) -> Option<usize> {
let cursor = self.cursor;
if cursor.row < top {
return None;
}
let v = *viewport;
let mut screen = 0usize;
for r in top..=cursor.row {
if self.folds.iter().any(|f| f.hides(r)) {
continue;
}
let line = self.lines.get(r).map(String::as_str).unwrap_or("");
let segs = crate::wrap::wrap_segments(line, v.text_width, v.wrap);
if r == cursor.row {
let seg_idx = crate::wrap::segment_for_col(&segs, cursor.col);
return Some(screen + seg_idx);
}
screen += segs.len();
}
None
}
pub fn clamp_position(&self, pos: Position) -> Position {
let last_row = self.lines.len().saturating_sub(1);
let row = pos.row.min(last_row);
let line_chars = self.lines[row].chars().count();
let col = pos.col.min(line_chars);
Position::new(row, col)
}
pub(crate) fn lines_mut(&mut self) -> &mut Vec<String> {
&mut self.lines
}
pub(crate) fn dirty_gen_bump(&mut self) {
self.dirty_gen = self.dirty_gen.wrapping_add(1);
}
pub fn replace_all(&mut self, text: &str) {
let mut lines: Vec<String> = text.split('\n').map(str::to_owned).collect();
if lines.is_empty() {
lines.push(String::new());
}
self.lines = lines;
let cursor = self.clamp_position(self.cursor);
self.cursor = cursor;
self.dirty_gen_bump();
}
pub fn as_string(&self) -> String {
self.lines.join("\n")
}
pub fn row_count(&self) -> usize {
self.lines.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_has_one_empty_row() {
let b = Buffer::new();
assert_eq!(b.row_count(), 1);
assert_eq!(b.line(0), Some(""));
assert_eq!(b.cursor(), Position::default());
}
#[test]
fn from_str_splits_on_newline() {
let b = Buffer::from_str("foo\nbar\nbaz");
assert_eq!(b.row_count(), 3);
assert_eq!(b.line(0), Some("foo"));
assert_eq!(b.line(2), Some("baz"));
}
#[test]
fn from_str_trailing_newline_keeps_empty_row() {
let b = Buffer::from_str("foo\n");
assert_eq!(b.row_count(), 2);
assert_eq!(b.line(1), Some(""));
}
#[test]
fn from_str_empty_input_keeps_one_row() {
let b = Buffer::from_str("");
assert_eq!(b.row_count(), 1);
assert_eq!(b.line(0), Some(""));
}
#[test]
fn as_string_round_trips() {
let b = Buffer::from_str("a\nb\nc");
assert_eq!(b.as_string(), "a\nb\nc");
}
#[test]
fn dirty_gen_starts_at_zero() {
assert_eq!(Buffer::new().dirty_gen(), 0);
}
fn vp_wrap(width: u16, height: u16) -> Viewport {
Viewport {
top_row: 0,
top_col: 0,
width,
height,
wrap: crate::Wrap::Char,
text_width: width,
}
}
#[test]
fn ensure_cursor_visible_wrap_scrolls_when_cursor_below_screen() {
let mut b = Buffer::from_str("aaaaaaaaaa\nb\nc");
let mut v = vp_wrap(4, 3);
b.set_cursor(Position::new(2, 0));
b.ensure_cursor_visible(&mut v);
assert_eq!(v.top_row, 1);
}
#[test]
fn ensure_cursor_visible_wrap_no_scroll_when_visible() {
let mut b = Buffer::from_str("aaaaaaaaaa\nb");
let mut v = vp_wrap(4, 4);
b.set_cursor(Position::new(0, 5));
b.ensure_cursor_visible(&mut v);
assert_eq!(v.top_row, 0);
}
#[test]
fn ensure_cursor_visible_wrap_snaps_top_when_cursor_above() {
let mut b = Buffer::from_str("a\nb\nc\nd\ne");
let mut v = vp_wrap(4, 2);
v.top_row = 3;
b.set_cursor(Position::new(1, 0));
b.ensure_cursor_visible(&mut v);
assert_eq!(v.top_row, 1);
}
#[test]
fn screen_rows_between_sums_segments_under_wrap() {
let b = Buffer::from_str("aaaaaaaaa\nb\n");
let v = vp_wrap(4, 0);
assert_eq!(b.screen_rows_between(&v, 0, 0), 3);
assert_eq!(b.screen_rows_between(&v, 0, 1), 4);
assert_eq!(b.screen_rows_between(&v, 0, 2), 5);
assert_eq!(b.screen_rows_between(&v, 1, 2), 2);
}
#[test]
fn screen_rows_between_one_per_doc_row_when_wrap_off() {
let b = Buffer::from_str("aaaaa\nb\nc");
let v = Viewport::default();
assert_eq!(b.screen_rows_between(&v, 0, 2), 3);
}
#[test]
fn max_top_for_height_walks_back_until_height_reached() {
let b = Buffer::from_str("a\nb\nc\nd\neeeeeeee");
let v = vp_wrap(4, 0);
assert_eq!(b.max_top_for_height(&v, 4), 2);
assert_eq!(b.max_top_for_height(&v, 99), 0);
}
#[test]
fn cursor_screen_row_returns_none_when_wrap_off() {
let b = Buffer::from_str("a");
let v = Viewport::default();
assert!(b.cursor_screen_row(&v).is_none());
}
#[test]
fn cursor_screen_row_under_wrap() {
let mut b = Buffer::from_str("aaaaaaaaaa\nb");
let v = vp_wrap(4, 0);
b.set_cursor(Position::new(0, 5));
assert_eq!(b.cursor_screen_row(&v), Some(1));
b.set_cursor(Position::new(1, 0));
assert_eq!(b.cursor_screen_row(&v), Some(3));
}
#[test]
fn ensure_cursor_visible_falls_back_when_wrap_disabled() {
let mut b = Buffer::from_str("a\nb\nc\nd\ne");
let mut v = Viewport {
top_row: 0,
top_col: 0,
width: 4,
height: 2,
wrap: crate::Wrap::None,
text_width: 4,
};
b.set_cursor(Position::new(4, 0));
b.ensure_cursor_visible(&mut v);
assert_eq!(v.top_row, 3);
}
}