mod cursor;
mod history;
mod insert;
mod motion;
mod ops;
mod search;
mod text_object;
mod vcs_link;
pub use search::SearchState;
use std::cell::{Cell, RefCell};
use std::fs;
use std::path::{Path, PathBuf};
use anyhow::Result;
use crate::syntax::Highlighter;
use crate::vcs::{self, LineStatus};
#[derive(Default)]
pub struct Buffer {
pub lines: Vec<String>,
pub cursor: Cursor,
pub extra_cursors: Vec<Cursor>,
pub path: Option<PathBuf>,
pub dirty: bool,
pub yank: String,
pub version: u64,
pub highlighter: Option<Highlighter>,
pub scroll: Cell<usize>,
pub col_scroll: Cell<usize>,
pub viewport_height: Cell<usize>,
pub cursor_visual_y: Cell<u16>,
pub undo_stack: Vec<Snapshot>,
pub redo_stack: Vec<Snapshot>,
pub vcs_base: Option<Vec<String>>,
pub vcs_diff: RefCell<Option<(u64, Vec<Option<LineStatus>>)>>,
}
#[derive(Debug, Clone)]
pub struct Snapshot {
pub lines: Vec<String>,
pub cursor: Cursor,
pub extra_cursors: Vec<Cursor>,
pub dirty: bool,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Cursor {
pub row: usize,
pub col: usize,
}
#[derive(Debug, Clone, Copy)]
pub struct IndentSettings {
pub width: usize,
pub use_tabs: bool,
}
impl Default for IndentSettings {
fn default() -> Self {
Self {
width: 4,
use_tabs: false,
}
}
}
impl Buffer {
pub fn new() -> Self {
Self {
lines: vec![String::new()],
..Default::default()
}
}
pub fn load(path: &Path) -> Result<Self> {
let text = fs::read_to_string(path)?;
let mut lines: Vec<String> = text.split('\n').map(|s| s.to_string()).collect();
if lines.is_empty() {
lines.push(String::new());
}
let vcs_base = vcs::head_blob_lines(path);
Ok(Self {
lines,
cursor: Cursor::default(),
extra_cursors: Vec::new(),
path: Some(path.to_path_buf()),
dirty: false,
yank: String::new(),
version: 0,
highlighter: None,
scroll: Cell::new(0),
col_scroll: Cell::new(0),
viewport_height: Cell::new(0),
cursor_visual_y: Cell::new(0),
undo_stack: Vec::new(),
redo_stack: Vec::new(),
vcs_base,
vcs_diff: RefCell::new(None),
})
}
pub fn save(&mut self) -> Result<()> {
if let Some(p) = &self.path {
fs::write(p, self.lines.join("\n"))?;
self.dirty = false;
}
Ok(())
}
pub fn save_as(&mut self, path: &Path) -> Result<()> {
fs::write(path, self.lines.join("\n"))?;
self.path = Some(path.to_path_buf());
self.dirty = false;
Ok(())
}
fn touch(&mut self) {
self.dirty = true;
self.version = self.version.wrapping_add(1);
}
pub fn bump_version(&mut self) {
self.version = self.version.wrapping_add(1);
}
pub fn refresh_highlights(&mut self) {
let Some(h) = self.highlighter.as_mut() else {
return;
};
let source = self.lines.join("\n");
h.refresh(&source, self.version);
}
}
fn char_to_byte(s: &str, char_idx: usize) -> usize {
s.char_indices()
.nth(char_idx)
.map(|(b, _)| b)
.unwrap_or(s.len())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CharClass {
Word,
Punct,
Space,
}
fn classify(c: char) -> CharClass {
if c.is_whitespace() {
CharClass::Space
} else if c.is_alphanumeric() || c == '_' {
CharClass::Word
} else {
CharClass::Punct
}
}
fn is_blank_line(line: &str) -> bool {
line.chars().all(|c| c.is_whitespace())
}