484 lines
13 KiB
Rust
484 lines
13 KiB
Rust
|
use std::{
|
||
|
ffi::OsStr,
|
||
|
fs::File,
|
||
|
io::{self, BufRead, BufReader, BufWriter, Write},
|
||
|
path::{Path, PathBuf},
|
||
|
};
|
||
|
|
||
|
use unicode_width::UnicodeWidthChar;
|
||
|
|
||
|
use crate::{
|
||
|
config::Config,
|
||
|
error::Error,
|
||
|
line::{Line, TextLike},
|
||
|
term::{Color, CursorStyle, Term, Terminal},
|
||
|
};
|
||
|
|
||
|
#[derive(Default)]
|
||
|
pub struct View {
|
||
|
cursor_column: usize,
|
||
|
cursor_row: usize,
|
||
|
column_offset: usize,
|
||
|
row_offset: usize,
|
||
|
width: usize,
|
||
|
height: usize,
|
||
|
offset_x: usize,
|
||
|
}
|
||
|
|
||
|
#[derive(Clone, Copy, PartialEq)]
|
||
|
pub enum Mode {
|
||
|
Normal,
|
||
|
Insert,
|
||
|
}
|
||
|
|
||
|
pub enum SetMode {
|
||
|
Normal,
|
||
|
InsertBefore,
|
||
|
InsertAfter,
|
||
|
}
|
||
|
|
||
|
pub struct Buffer {
|
||
|
lines: Vec<Line>,
|
||
|
dirty: bool,
|
||
|
mode_dirty: bool,
|
||
|
mode: Mode,
|
||
|
view: View,
|
||
|
name: Option<String>,
|
||
|
path: Option<PathBuf>,
|
||
|
modified: bool,
|
||
|
}
|
||
|
|
||
|
impl Mode {
|
||
|
pub fn as_str(&self) -> &str {
|
||
|
match self {
|
||
|
Self::Normal => "NORMAL",
|
||
|
Self::Insert => "INSERT",
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl View {
|
||
|
pub fn set_row(&mut self, row: usize) {
|
||
|
self.cursor_row = row;
|
||
|
|
||
|
if self.cursor_row < self.row_offset {
|
||
|
self.row_offset = self.cursor_row;
|
||
|
} else if self.cursor_row >= self.row_offset + self.height {
|
||
|
self.row_offset = self.cursor_row - self.height + 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pub fn set_column(&mut self, config: &Config, col: usize, line: Option<&Line>) {
|
||
|
let Some(line) = line else {
|
||
|
self.column_offset = 0;
|
||
|
self.cursor_column = 0;
|
||
|
return;
|
||
|
};
|
||
|
|
||
|
self.cursor_column = col;
|
||
|
|
||
|
if line.display_width(config.tab_width) + 1 <= self.width {
|
||
|
self.column_offset = 0;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let width_to_cursor = line
|
||
|
.span(..self.cursor_column)
|
||
|
.display_width(config.tab_width);
|
||
|
|
||
|
if width_to_cursor < self.column_offset {
|
||
|
self.column_offset = width_to_cursor;
|
||
|
} else if width_to_cursor >= self.column_offset + self.width {
|
||
|
self.column_offset = width_to_cursor - self.width + 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pub fn reset(&mut self) {
|
||
|
self.column_offset = 0;
|
||
|
self.row_offset = 0;
|
||
|
self.cursor_row = 0;
|
||
|
self.cursor_column = 0;
|
||
|
}
|
||
|
|
||
|
// pub fn resize(&mut self, width: usize, height: usize) {
|
||
|
// self.width = width;
|
||
|
// self.height = height;
|
||
|
|
||
|
// self.column_offset = 0;
|
||
|
// self.row_offset = 0;
|
||
|
// }
|
||
|
}
|
||
|
|
||
|
impl Buffer {
|
||
|
pub fn empty() -> Self {
|
||
|
Self {
|
||
|
lines: vec![],
|
||
|
dirty: true,
|
||
|
mode_dirty: true,
|
||
|
view: View::default(),
|
||
|
mode: Mode::Normal,
|
||
|
name: None,
|
||
|
path: None,
|
||
|
modified: false,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
fn read_lines<P: AsRef<Path>>(path: P) -> io::Result<Vec<Line>> {
|
||
|
let input = BufReader::new(File::open(path)?);
|
||
|
let lines = input.lines().collect::<Result<Vec<_>, _>>()?;
|
||
|
let lines = lines
|
||
|
.into_iter()
|
||
|
.map(|line| Line::from_str(line.trim_end_matches('\n')))
|
||
|
.collect();
|
||
|
Ok(lines)
|
||
|
}
|
||
|
|
||
|
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
|
||
|
let path = path.as_ref();
|
||
|
let name = path.file_name().and_then(OsStr::to_str).map(String::from);
|
||
|
let lines = if path.exists() {
|
||
|
Self::read_lines(path)?
|
||
|
} else {
|
||
|
vec![]
|
||
|
};
|
||
|
|
||
|
Ok(Self {
|
||
|
lines,
|
||
|
name,
|
||
|
path: Some(path.into()),
|
||
|
mode: Mode::Normal,
|
||
|
dirty: true,
|
||
|
mode_dirty: true,
|
||
|
view: View::default(),
|
||
|
modified: false,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
pub fn reopen<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
||
|
let path = path.as_ref();
|
||
|
let name = path.file_name().and_then(OsStr::to_str).map(String::from);
|
||
|
let lines = if path.exists() {
|
||
|
Self::read_lines(path)?
|
||
|
} else {
|
||
|
vec![]
|
||
|
};
|
||
|
|
||
|
self.lines = lines;
|
||
|
self.modified = false;
|
||
|
self.mode = Mode::Normal;
|
||
|
self.dirty = true;
|
||
|
self.mode_dirty = true;
|
||
|
self.view.reset();
|
||
|
|
||
|
self.path = Some(path.into());
|
||
|
self.name = name;
|
||
|
|
||
|
Ok(())
|
||
|
}
|
||
|
|
||
|
pub fn save(&mut self) -> Result<(), Error> {
|
||
|
let path = self.path.as_ref().ok_or(Error::NoPath)?;
|
||
|
let mut writer = BufWriter::new(File::create(path).map_err(Error::WriteError)?);
|
||
|
|
||
|
for line in self.lines.iter() {
|
||
|
writer
|
||
|
.write_all(line.to_string().as_ref())
|
||
|
.map_err(Error::WriteError)?;
|
||
|
writer.write_all(b"\n").map_err(Error::WriteError)?;
|
||
|
}
|
||
|
|
||
|
self.modified = false;
|
||
|
|
||
|
Ok(())
|
||
|
}
|
||
|
|
||
|
pub fn set_path<P: AsRef<Path>>(&mut self, path: P) {
|
||
|
let path = PathBuf::from(path.as_ref());
|
||
|
let name = path.file_name().and_then(OsStr::to_str).map(String::from);
|
||
|
|
||
|
self.path = Some(path);
|
||
|
self.name = name;
|
||
|
}
|
||
|
|
||
|
pub fn set_mode(&mut self, config: &Config, mode: SetMode) {
|
||
|
let dst_mode = match mode {
|
||
|
SetMode::Normal => Mode::Normal,
|
||
|
SetMode::InsertAfter | SetMode::InsertBefore => Mode::Insert,
|
||
|
};
|
||
|
|
||
|
if dst_mode == self.mode {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
self.mode = dst_mode;
|
||
|
self.mode_dirty = true;
|
||
|
match mode {
|
||
|
SetMode::Normal => self.move_cursor(config, -1, 0),
|
||
|
SetMode::InsertBefore => self.move_cursor(config, 0, 0),
|
||
|
SetMode::InsertAfter => self.move_cursor(config, 1, 0),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pub fn mode(&self) -> Mode {
|
||
|
self.mode
|
||
|
}
|
||
|
|
||
|
pub fn name(&self) -> Option<&String> {
|
||
|
self.name.as_ref()
|
||
|
}
|
||
|
|
||
|
pub fn path(&self) -> Option<&PathBuf> {
|
||
|
self.path.as_ref()
|
||
|
}
|
||
|
|
||
|
pub fn row_offset(&self) -> usize {
|
||
|
self.view.row_offset
|
||
|
}
|
||
|
|
||
|
pub fn len(&self) -> usize {
|
||
|
self.lines.len()
|
||
|
}
|
||
|
|
||
|
pub fn cursor_row(&self) -> usize {
|
||
|
self.view.cursor_row
|
||
|
}
|
||
|
|
||
|
pub fn set_position(&mut self, config: &Config, px: usize, py: usize) {
|
||
|
self.dirty = true;
|
||
|
|
||
|
if self.lines.is_empty() {
|
||
|
self.view.reset();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Move to row
|
||
|
self.view.set_row(py.min(self.lines.len() - 1));
|
||
|
|
||
|
// Set mode- and line-len-adjusted column
|
||
|
if let Some(line) = self.lines.get(self.view.cursor_row) && !line.is_empty() {
|
||
|
match self.mode {
|
||
|
// Limited by line.len()
|
||
|
Mode::Normal => self.view.set_column(config, px.min(line.len() - 1), Some(line)),
|
||
|
Mode::Insert => self.view.set_column(config, px.min(line.len()), Some(line)),
|
||
|
}
|
||
|
} else {
|
||
|
self.view.set_column(config, 0, None);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pub fn to_line_end(&mut self, config: &Config) {
|
||
|
let len = self
|
||
|
.lines
|
||
|
.get(self.view.cursor_row)
|
||
|
.map(Line::len)
|
||
|
.unwrap_or(0);
|
||
|
|
||
|
self.set_position(config, len, self.view.cursor_row);
|
||
|
}
|
||
|
|
||
|
pub fn set_column(&mut self, config: &Config, x: usize) {
|
||
|
self.set_position(config, x, self.view.cursor_row);
|
||
|
}
|
||
|
|
||
|
pub fn move_cursor(&mut self, config: &Config, dx: isize, dy: isize) {
|
||
|
let px = (self.view.cursor_column as isize + dx).max(0) as usize;
|
||
|
let py = (self.view.cursor_row as isize + dy).max(0) as usize;
|
||
|
|
||
|
self.set_position(config, px, py);
|
||
|
}
|
||
|
|
||
|
pub fn resize(&mut self, config: &Config, offset_x: usize, width: usize, height: usize) {
|
||
|
self.dirty = true;
|
||
|
self.view.height = height;
|
||
|
self.view.width = width;
|
||
|
self.view.offset_x = offset_x;
|
||
|
|
||
|
self.view.set_row(self.view.cursor_row);
|
||
|
self.view.set_column(
|
||
|
config,
|
||
|
self.view.cursor_column,
|
||
|
self.lines.get(self.view.cursor_row),
|
||
|
);
|
||
|
}
|
||
|
|
||
|
pub fn display_cursor(&self, config: &Config) -> (usize, usize) {
|
||
|
if self.lines.is_empty() {
|
||
|
return (0, 0);
|
||
|
}
|
||
|
|
||
|
// assert!(self.view.column_offset <= self.view.cursor_column);
|
||
|
assert!(self.view.row_offset <= self.view.cursor_row);
|
||
|
|
||
|
let line = &self.lines[self.view.cursor_row];
|
||
|
assert!(self.view.cursor_column <= line.len());
|
||
|
|
||
|
let column = line
|
||
|
.span(..self.view.cursor_column)
|
||
|
.display_width(config.tab_width);
|
||
|
assert!(self.view.column_offset <= column);
|
||
|
|
||
|
(
|
||
|
column - self.view.column_offset,
|
||
|
self.view.cursor_row - self.view.row_offset,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
pub fn height(&self) -> usize {
|
||
|
self.view.height
|
||
|
}
|
||
|
|
||
|
pub fn is_modified(&self) -> bool {
|
||
|
self.modified
|
||
|
}
|
||
|
|
||
|
pub fn is_dirty(&self) -> bool {
|
||
|
self.dirty
|
||
|
}
|
||
|
|
||
|
fn display_line(&self, config: &Config, term: &mut Term, row: usize, line: &Line) {
|
||
|
let mut pos = 0;
|
||
|
term.set_cursor_position(row, self.view.offset_x);
|
||
|
|
||
|
let span = line.skip_to_width(self.view.column_offset, config.tab_width);
|
||
|
let long_line = span.display_width(config.tab_width) > self.view.width;
|
||
|
|
||
|
for &ch in span.iter() {
|
||
|
if pos >= self.view.width {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
if ch == '\t' {
|
||
|
let old_pos = pos;
|
||
|
let new_pos = (pos + config.tab_width) & !(config.tab_width - 1);
|
||
|
pos = new_pos;
|
||
|
|
||
|
for i in old_pos..new_pos {
|
||
|
if i >= self.view.width {
|
||
|
break;
|
||
|
}
|
||
|
if i == old_pos {
|
||
|
term.set_foreground(Color::Blue);
|
||
|
term.put_byte(b'>');
|
||
|
term.set_foreground(Color::Default);
|
||
|
} else {
|
||
|
term.put_byte(b' ');
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
// TODO optimize later
|
||
|
let s = std::iter::once(ch).collect::<String>();
|
||
|
term.put_bytes(s.as_str());
|
||
|
pos += ch.width().unwrap_or(1);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if long_line {
|
||
|
term.set_cursor_position(row, self.view.width + self.view.offset_x);
|
||
|
term.set_foreground(Color::Black);
|
||
|
term.set_background(Color::White);
|
||
|
term.put_byte(b'>');
|
||
|
term.set_foreground(Color::Default);
|
||
|
term.set_background(Color::Default);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pub fn display(&mut self, config: &Config, term: &mut Term) {
|
||
|
for (row, line) in self
|
||
|
.lines
|
||
|
.iter()
|
||
|
.skip(self.view.row_offset)
|
||
|
.take(self.view.height)
|
||
|
.enumerate()
|
||
|
{
|
||
|
self.display_line(config, term, row, line);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pub fn newline_before(&mut self) {
|
||
|
self.lines.insert(self.view.cursor_row, Line::new());
|
||
|
}
|
||
|
|
||
|
pub fn newline_after(&mut self, break_line: bool) {
|
||
|
if self.lines.is_empty() {
|
||
|
self.lines.push(Line::new());
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let newline = if break_line {
|
||
|
self.lines[self.view.cursor_row].split_off(self.view.cursor_column)
|
||
|
} else {
|
||
|
Line::new()
|
||
|
};
|
||
|
self.lines.insert(self.view.cursor_row + 1, newline);
|
||
|
}
|
||
|
|
||
|
pub fn insert(&mut self, config: &Config, ch: char) {
|
||
|
if self.lines.is_empty() {
|
||
|
assert_eq!(self.view.cursor_row, 0);
|
||
|
self.lines.push(Line::new());
|
||
|
}
|
||
|
|
||
|
let line = &mut self.lines[self.view.cursor_row];
|
||
|
line.insert(self.view.cursor_column, ch);
|
||
|
self.move_cursor(config, 1, 0);
|
||
|
self.modified = true;
|
||
|
}
|
||
|
|
||
|
pub fn erase_backward(&mut self, config: &Config) {
|
||
|
if self.lines.is_empty() {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if self.view.cursor_column == 0 {
|
||
|
if self.view.cursor_row != 0 {
|
||
|
let line = self.lines.remove(self.view.cursor_row);
|
||
|
let prev_line = &mut self.lines[self.view.cursor_row - 1];
|
||
|
|
||
|
let len = prev_line.len();
|
||
|
prev_line.extend(line);
|
||
|
self.set_position(config, len, self.view.cursor_row - 1);
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let line = &mut self.lines[self.view.cursor_row];
|
||
|
line.remove(self.view.cursor_column - 1);
|
||
|
self.move_cursor(config, -1, 0);
|
||
|
self.modified = true;
|
||
|
}
|
||
|
|
||
|
pub fn erase_forward(&mut self) {
|
||
|
if self.lines.is_empty() {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let line = &mut self.lines[self.view.cursor_row];
|
||
|
if self.view.cursor_column == line.len() {
|
||
|
return;
|
||
|
}
|
||
|
line.remove(self.view.cursor_column);
|
||
|
self.dirty = true;
|
||
|
self.modified = true;
|
||
|
}
|
||
|
|
||
|
pub fn set_terminal_cursor(&mut self, config: &Config, term: &mut Term) {
|
||
|
let (x, y) = self.display_cursor(config);
|
||
|
if self.mode_dirty {
|
||
|
match self.mode {
|
||
|
Mode::Normal => term.set_cursor_style(CursorStyle::Default),
|
||
|
Mode::Insert => term.set_cursor_style(CursorStyle::Line),
|
||
|
}
|
||
|
}
|
||
|
term.set_cursor_position(y, x + self.view.offset_x);
|
||
|
self.mode_dirty = false;
|
||
|
}
|
||
|
|
||
|
pub fn number_width(&mut self) -> usize {
|
||
|
if self.lines.len() == 0 {
|
||
|
1
|
||
|
} else {
|
||
|
self.lines.len().ilog10() as usize + 1
|
||
|
}
|
||
|
}
|
||
|
}
|