pub mod cursor;
pub mod line;
pub mod viewport;
use crate::selection::Selection;
use cursor::Cursor;
use line::Line;
use viewport::Viewport;
pub struct Buffer {
pub lines: Vec<Line>,
pub viewport: Viewport,
pub cursor: Cursor,
pub selection: Option<Selection>,
preferred_col: usize,
}
impl Buffer {
pub fn new(lines: Vec<Line>) -> Self {
Self {
lines,
viewport: Viewport::default(),
cursor: Cursor::default(),
selection: None,
preferred_col: 0,
}
}
pub fn line_count(&self) -> usize {
self.lines.len()
}
pub fn get_line(&self, index: usize) -> Option<&Line> {
self.lines.get(index)
}
pub fn current_line(&self) -> Option<&Line> {
self.get_line(self.cursor.row)
}
pub fn visible_lines(&self) -> &[Line] {
let start = self.viewport.offset;
let end = (start + self.viewport.height).min(self.lines.len());
&self.lines[start..end]
}
pub fn scroll_down(&mut self, n: usize) {
let max_offset = self.lines.len().saturating_sub(self.viewport.height);
self.viewport.offset = (self.viewport.offset + n).min(max_offset);
}
pub fn scroll_up(&mut self, n: usize) {
self.viewport.offset = self.viewport.offset.saturating_sub(n);
}
pub fn cursor_down(&mut self, n: usize) {
let max_row = self.lines.len().saturating_sub(1);
self.cursor.row = (self.cursor.row + n).min(max_row);
self.clamp_cursor_col();
self.ensure_cursor_visible();
}
pub fn cursor_up(&mut self, n: usize) {
self.cursor.row = self.cursor.row.saturating_sub(n);
self.clamp_cursor_col();
self.ensure_cursor_visible();
}
pub fn cursor_right(&mut self, n: usize) {
if let Some(line) = self.lines.get(self.cursor.row) {
let max_col = line.display_width().saturating_sub(1);
self.cursor.col = (self.cursor.col + n).min(max_col);
self.preferred_col = self.cursor.col;
}
}
pub fn cursor_left(&mut self, n: usize) {
self.cursor.col = self.cursor.col.saturating_sub(n);
self.preferred_col = self.cursor.col;
}
pub fn cursor_top(&mut self) {
self.cursor.row = 0;
self.cursor.col = 0;
self.preferred_col = 0;
self.ensure_cursor_visible();
}
pub fn cursor_bottom(&mut self) {
self.cursor.row = self.lines.len().saturating_sub(1);
self.clamp_cursor_col();
self.ensure_cursor_visible();
}
pub fn half_page_down(&mut self) {
let half = self.viewport.height / 2;
self.cursor_down(half);
}
pub fn half_page_up(&mut self) {
let half = self.viewport.height / 2;
self.cursor_up(half);
}
pub fn page_down(&mut self) {
let height = self.viewport.height.saturating_sub(1).max(1);
self.cursor_down(height);
}
pub fn page_up(&mut self) {
let height = self.viewport.height.saturating_sub(1).max(1);
self.cursor_up(height);
}
pub fn cursor_line_start(&mut self) {
self.cursor.col = 0;
self.preferred_col = 0;
}
pub fn cursor_line_end(&mut self) {
self.cursor.col = self.max_col_for_row(self.cursor.row);
self.preferred_col = self.cursor.col;
}
pub fn cursor_first_nonblank(&mut self) {
self.cursor.col = self.first_nonblank_col(self.cursor.row);
self.preferred_col = self.cursor.col;
}
pub fn cursor_view_top(&mut self) {
self.cursor.row = self.viewport.offset.min(self.lines.len().saturating_sub(1));
self.clamp_cursor_col();
}
pub fn cursor_view_middle(&mut self) {
let visible_height = self.viewport.height.max(1);
let row = self.viewport.offset + visible_height / 2;
self.cursor.row = row.min(self.lines.len().saturating_sub(1));
self.clamp_cursor_col();
}
pub fn cursor_view_bottom(&mut self) {
let visible_height = self.viewport.height.max(1);
let row = self.viewport.offset + visible_height.saturating_sub(1);
self.cursor.row = row.min(self.lines.len().saturating_sub(1));
self.clamp_cursor_col();
}
pub fn set_cursor(&mut self, row: usize, col: usize) {
self.cursor.row = row.min(self.lines.len().saturating_sub(1));
self.cursor.col = self.clamp_col_for_row(self.cursor.row, col);
self.preferred_col = col;
self.ensure_cursor_visible();
}
fn ensure_cursor_visible(&mut self) {
if self.cursor.row < self.viewport.offset {
self.viewport.offset = self.cursor.row;
} else if self.cursor.row >= self.viewport.offset + self.viewport.height {
self.viewport.offset = self.cursor.row - self.viewport.height + 1;
}
}
pub fn resize(&mut self, width: usize, height: usize) {
self.viewport.width = width;
self.viewport.height = height;
}
pub fn ensure_cursor_visible_pub(&mut self) {
self.ensure_cursor_visible();
}
pub fn preferred_col(&self) -> usize {
self.preferred_col
}
fn clamp_cursor_col(&mut self) {
self.cursor.col = self.clamp_col_for_row(self.cursor.row, self.preferred_col);
}
fn clamp_col_for_row(&self, row: usize, col: usize) -> usize {
col.min(self.max_col_for_row(row))
}
fn max_col_for_row(&self, row: usize) -> usize {
self.lines
.get(row)
.map(|line| line.display_width().saturating_sub(1))
.unwrap_or(0)
}
fn first_nonblank_col(&self, row: usize) -> usize {
self.lines
.get(row)
.map(|line| {
let char_idx = line
.content
.chars()
.position(|ch| !ch.is_whitespace())
.unwrap_or_else(|| line.char_count());
line.display_col_for_char_index(char_idx)
})
.unwrap_or(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_buffer(lines: &[&str]) -> Buffer {
let lines = lines
.iter()
.map(|content| Line {
content: (*content).to_string(),
display_widths: crate::parse::unicode::compute_display_widths(content),
styles: Vec::new(),
})
.collect();
Buffer::new(lines)
}
#[test]
fn vertical_motion_preserves_preferred_col() {
let mut buffer = make_buffer(&["abcd", "", "wxyz"]);
buffer.set_cursor(0, 3);
assert_eq!(buffer.cursor.col, 3);
assert_eq!(buffer.preferred_col(), 3);
buffer.cursor_down(1);
assert_eq!(buffer.cursor.row, 1);
assert_eq!(buffer.cursor.col, 0);
assert_eq!(buffer.preferred_col(), 3);
buffer.cursor_down(1);
assert_eq!(buffer.cursor.row, 2);
assert_eq!(buffer.cursor.col, 3);
assert_eq!(buffer.preferred_col(), 3);
}
}