via/src/modules/editor.rs

703 lines
24 KiB
Rust

use std::fs::File;
use std::io::{BufRead, BufReader};
use std::cmp::{min, max};
use std::path::Path;
use super::piece_table::PieceTable;
/// An editor window
pub(crate) struct Editor {
/// The piece table
piece_table: PieceTable,
/// Index we are currently at in `self.piece_table` (0-indexed)
pt_index: usize,
/// Path of file being editing (may not yet exist).
/// Empty string if no file specified
file_path: String,
/// Reader of the file (Error if is an nonexistent file)
reader: Result<BufReader<File>, String>,
/// Whether we have read all of `self.reader`
eof_reached: bool,
/// Represents each line of the editor, and how many characters are in that line
lines: Vec<usize>,
/// Row cursor is at (1-indexed)
row: usize,
/// Column cursor is at (1-indexed)
col: usize,
/// Column cursor we want (1-indexed). When we move vertically, from a long
/// line to short one, we want to try to get to a specific column
col_want: usize,
}
impl Editor {
/// Initialize a new editor from a file path (read a single line)
pub(crate) fn new(file_path: String) -> Editor {
// let (reader, eof_reached) = create_reader(file_path);
let reader;
let eof_reached;
if file_path == "" {
reader = Err("No file specified".to_string());
eof_reached = true;
} else if Path::new(&file_path).is_file() {
reader = Ok(BufReader::new(File::open(file_path.clone()).unwrap()));
eof_reached = false;
} else if Path::new(&file_path).is_dir() || file_path.ends_with("/") {
panic!("No support (yet) for writing to directories");
} else {
reader = Err("File doesn't exist".to_string());
eof_reached = true;
}
let mut editor = Editor {piece_table: PieceTable::new(),
pt_index: 0,
file_path: file_path,
reader: reader,
eof_reached: eof_reached,
lines: Vec::new(),
row: 1,
col: 1,
col_want: 1,
};
if editor.read_lines(1) == 0 {
editor.lines.push(0);
} else {
editor.lines.push(editor.piece_table.text_len());
}
editor
}
/// Returns visible text
pub(crate) fn text(&mut self) -> &str {
self.piece_table.text()
}
/// Returns file path
pub(crate) fn file_path(&self) -> &str {
self.file_path.as_str()
}
/// Update the file path
pub(crate) fn update_file_path(&mut self, file_path: String) {
self.file_path = file_path;
}
/// Returns the current row
pub(crate) fn row(&self) -> usize {
self.row
}
/// Returns the current column
pub(crate) fn col(&self) -> usize {
self.col
}
/// Returns the number of columns in the specified `row` (1-indexed)
pub(crate) fn num_cols(&self, row: usize) -> usize {
*self.lines.get(row - 1).unwrap()
}
/// Returns the number of lines
pub(crate) fn num_lines(&self) -> usize {
self.lines.len()
}
/// Returns the length of the line (1-indexed)
pub(crate) fn line_len(&self, line: usize) -> usize {
*self.lines.get(line - 1).unwrap()
}
/// Returns whether the text of the file matches the text of `self.piece_table`
pub(crate) fn text_matches(&self) -> bool {
!self.piece_table.actions_taken()
}
/// Returns visible text from line `first` (inclusive) to `last` (exclusive)
pub(crate) fn text_lines(&mut self, first: usize, last: usize) -> &str {
if first >= last {
panic!("First row ({}) must be less last ({})", first, last);
}
let mut start_index = 0;
let mut end_index = 0;
for (i, line) in self.lines.iter().enumerate() {
if i < first - 1 {
start_index += line + 1;
end_index = start_index;
} else if i >= last - 1 {
break
} else {
end_index += line + 1;
}
}
self.piece_table.text().get(start_index..end_index - 1).unwrap()
}
/// Returns the visible text for a single row
pub(crate) fn text_line(&mut self, line: usize) -> &str {
self.text_lines(line, line + 1)
}
/// Adds `text` at the current cursor position
pub(crate) fn add_text(&mut self, text: String) {
let mut from_end = 0;
let mut num_lines = 0;
let text_len = text.len();
let mut last_line_len = 0;
for (i, line) in text.split('\n').enumerate() {
if i == 0 {
let curr_line_len = self.lines.get_mut(self.row - 1).unwrap();
from_end = *curr_line_len + 1 - self.col;
if text.contains('\n') {
*curr_line_len -= from_end;
}
*curr_line_len += line.len();
} else if self.row + i >= self.lines.len() {
self.lines.push(line.len() + from_end);
from_end = 0;
} else {
self.lines.insert(self.row + i, line.len() + from_end);
from_end = 0;
}
num_lines += 1;
last_line_len = line.len();
}
self.piece_table.add_text(text, self.pt_index);
if num_lines == 1 {
self.right(text_len);
} else {
self.down(num_lines - 1);
self.goto_col(last_line_len + 1);
}
}
/// Deletes from current cursor position to (row, col) which are 1-indexed
pub(crate) fn delete_text(&mut self, row: usize, col: usize) -> Result<(), String> {
if row == self.row {
if row == self.row && col == self.col {
return Ok(())
// return Err("No text to delete".to_string());
}
let line_len = self.lines.get_mut(row - 1).unwrap();
let first_col = min(self.col, col);
let last_col = if first_col == self.col {col} else {self.col};
if last_col > *line_len + 1 {
// panic!("Can't delete from {} to {} of line length {}", first_col, last_col, *line_len);
return Err(format!("Can't delete from {} to {} of line length {}", first_col, last_col, *line_len))
}
let len = last_col - first_col;
*(line_len) -= len;
if first_col == self.col {
self.piece_table.delete_text(self.pt_index, self.pt_index + len);
} else {
self.piece_table.delete_text(self.pt_index - len, self.pt_index);
self.col -= len;
self.col_want = self.col;
}
return Ok(())
}
let mut size = 0;
let first_row = min(self.row, row);
let first_col = if first_row == self.row {self.col} else {col};
let last_row = if first_row == self.row {row} else {self.row};
let last_col = if first_row == self.row {col} else {self.col};
if last_row == usize::MAX {
// TODO: Don't actually read to end of file. Just pretend you did
// If you do this, you have to update undo and redo to update self.eof_reached
self.read_to_eof()
} else {
self.read_lines(max(self.lines.len(), row) - min(self.lines.len(), row));
}
let first_line_len = self.lines.get_mut(first_row - 1).unwrap();
if first_col > *first_line_len + 1 {
panic!("Invalid beginning column {} for row {}", first_col, first_row);
}
size += *first_line_len + 1 - (first_col - 1);
*first_line_len -= *first_line_len - (first_col - 1);
let first_line_len_copy = *first_line_len;
let to_last_row = last_row == self.lines.len();
for _ in first_row + 1..last_row {
let line_len = self.lines.remove(first_row);
size += line_len + 1;
}
let last_line_len = self.lines.get_mut(last_row - (last_row - first_row) - 1).unwrap();
if last_col - 1 > *last_line_len {
panic!("Invalid ending column {} for row {}", last_col, last_row);
}
*(last_line_len) -= last_col - 1;
size += last_col - 1;
if to_last_row {
self.lines.pop();
}
if first_row == self.row {
self.piece_table.delete_text(self.pt_index, self.pt_index + size);
} else {
self.piece_table.delete_text(self.pt_index - size, self.pt_index);
self.row = first_row;
self.col = first_line_len_copy;
self.col_want = self.col;
}
Ok(())
}
/// Delete all text
pub(crate) fn delete_all(&mut self) {
if self.piece_table.text_len() == 0 {
return
}
self.piece_table.delete_text(0, self.piece_table.text_len());
self.row = 1;
self.col = 1;
self.col_want = 1;
self.pt_index = 0;
}
pub(crate) fn delete_to_end(&mut self) {
self.delete_text(usize::MAX, usize::MAX).unwrap();
}
/// Read `num_lines` from `reader`, updating `self.piece_table` & `self.lines`
/// Returns number of lines actually read
fn read_lines(&mut self, num_lines: usize) -> usize {
if num_lines == 0 || self.eof_reached {
return 0;
}
let mut lines_read = 0;
let reader = self.reader.as_mut().unwrap();
for _ in 0..num_lines {
let mut temp_str = String::new();
match reader.read_line(&mut temp_str) {
Ok(0) => {
self.eof_reached = true;
break
},
Ok(len) => {
lines_read += 1;
self.lines.push(len);
self.piece_table.update_original_buffer(temp_str);
},
Err(e) => panic!("Error reading file: {:?}", e),
}
}
lines_read
}
/// Read to EOF, updating `self.piece_table` & `self.lines`
fn read_to_eof(&mut self) {
if self.eof_reached {
return;
}
let reader = self.reader.as_mut().unwrap();
loop {
let mut temp_str = String::new();
match reader.read_line(&mut temp_str) {
Ok(0) => {
self.eof_reached = true;
break
},
Ok(len) => {
self.lines.push(len);
self.piece_table.update_original_buffer(temp_str);
},
Err(e) => panic!("Error reading file: {:?}", e),
}
}
}
/// Move the cursor up `num` places
/// If unable to go up all the way, go to first row
pub(crate) fn up(&mut self, num: usize) {
if num == 0 || self.row == 1 {
return
} else if num >= self.row {
self.up(self.row - 1);
return
}
self.pt_index -= self.col;
for i in 1..num {
self.pt_index -= self.lines.get(self.row - i - 1).unwrap() + 1;
}
self.row -= num;
let line_cols = self.lines.get(self.row - 1).unwrap();
self.col = min(self.col_want, line_cols + 1);
self.pt_index -= line_cols + 1 - self.col;
}
/// Move the cursor down `num` places.
/// If unable to go all the way down, go to last row
pub(crate) fn down(&mut self, num: usize) {
if num == 0 {
return
} else if self.row + num > self.lines.len() {
let lines_read = self.read_lines(self.row + num - self.lines.len());
self.down(lines_read);
return
}
self.pt_index += self.lines.get(self.row - 1).unwrap() + 1 - self.col + 1;
for i in 1..num {
self.pt_index += self.lines.get(self.row + i - 1).unwrap() + 1;
}
self.row += num;
self.col = min(self.col_want, self.lines.get(self.row - 1).unwrap() + 1);
self.pt_index += self.col - 1;
}
/// Move the cursor right `num` places.
/// If unable to go all the way right, go to last column
pub(crate) fn right(&mut self, num: usize) {
let line_len = self.lines.get(self.row - 1).unwrap();
if num == 0 || self.col == line_len + 1 {
return
} else if self.col + num > line_len + 1 {
self.goto_last_col();
return
}
self.col += num;
self.pt_index += num;
self.col_want = self.col;
}
/// Move the cursor left `num` places.
/// If unable to go all the way left, go to first column
pub(crate) fn left(&mut self, num: usize) {
if num == 0 {
return
} else if num >= self.col {
self.left(self.col - 1);
return
}
self.col -= num;
self.pt_index -= num;
self.col_want = self.col;
}
/// Move to a certain column in the current row
pub(crate) fn goto_col(&mut self, col: usize) {
if col > *self.lines.get(self.row - 1).unwrap() + 1 {
self.goto_last_col();
} else if self.col == col {
self.col_want = col;
} else if col < self.col {
self.left(self.col - col)
} else {
self.right(col - self.col)
}
}
/// Move to a certain row
pub(crate) fn goto_row(&mut self, row: usize) {
if self.row == row {
return
} else if row < self.row {
self.up(self.row - row)
} else {
self.down(row - self.row)
}
}
/// Move to (closest) `row` and `col`
pub(crate) fn goto(&mut self, row: usize, col: usize) {
self.goto_row(row);
self.goto_col(col);
}
/// Move to the last column in the current row
pub(crate) fn goto_last_col(&mut self) {
self.goto_col(*self.lines.get(self.row - 1).unwrap() + 1)
}
/// Move to the last row
pub(crate) fn goto_last_row(&mut self) {
self.read_to_eof();
self.goto(self.lines.len(), 1)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_text() {
let mut editor = Editor::new(String::new());
let mut want_str = "hello";
editor.add_text(want_str.to_string());
assert_eq!(editor.text(), want_str);
editor = Editor::new(String::new());
want_str = "hello\nbye";
editor.add_text(want_str.to_string());
assert_eq!(editor.text(), want_str);
editor = Editor::new(String::new());
editor.add_text("hello\n".to_string());
editor.add_text("bye".to_string());
want_str = "hello\nbye";
assert_eq!(editor.text(), want_str);
editor = Editor::new(String::new());
editor.add_text("hello\n\n".to_string());
editor.add_text("bye".to_string());
want_str = "hello\n\nbye";
assert_eq!(editor.text(), want_str);
editor = Editor::new(String::new());
editor.add_text("hello".to_string());
editor.add_text("\nbye".to_string());
want_str = "hello\nbye";
assert_eq!(editor.text(), want_str);
editor = Editor::new(String::new());
editor.add_text("\nhello".to_string());
editor.add_text("\nbye".to_string());
want_str = "\nhello\nbye";
assert_eq!(editor.text(), want_str);
}
#[test]
fn delete_text() {
let mut editor = Editor::new(String::new());
let mut want_str = "";
editor.add_text("abcd".to_string());
editor.delete_text(1, 1).unwrap();
assert_eq!(editor.text(), want_str);
editor = Editor::new(String::new());
want_str = "ab\nef";
editor.add_text("ab\ncd\nef".to_string());
editor.goto(2, 1);
editor.delete_text(3, 1).unwrap();
assert_eq!(editor.text(), want_str);
assert_eq!(editor.num_lines(), want_str.lines().count());
editor = Editor::new(String::new());
want_str = "ab\n\ncd";
editor.add_text("ab\n\n\ncd".to_string());
editor.goto(3, 1);
editor.delete_text(4, 1).unwrap();
assert_eq!(editor.text(), want_str);
editor = Editor::new(String::new());
want_str = "ab\n\ncd";
editor.add_text("ab\n\n\ncd".to_string());
editor.goto(4, 1);
editor.delete_text(3, 1).unwrap();
assert_eq!(editor.text(), want_str);
editor = Editor::new(String::new());
editor.add_text("h".to_string());
editor.add_text("\n".to_string());
editor.add_text("b".to_string());
want_str = "h\nb";
assert_eq!(editor.text(), want_str);
}
#[test]
fn movement() {
let mut editor = Editor::new(String::new());
let mut want_str = "hello\nbye";
editor.add_text("hello\n".to_string());
editor.down(1);
editor.add_text("bye".to_string());
assert_eq!(editor.text(), want_str);
editor = Editor::new(String::new());
want_str = "hello\nbye";
editor.add_text("h\n".to_string());
editor.down(1);
editor.add_text("bye".to_string());
editor.up(1);
editor.add_text("ello".to_string());
assert_eq!(editor.text(), want_str);
editor = Editor::new(String::new());
want_str = "ab\nabcd";
editor.add_text("ab\n".to_string());
editor.down(1);
editor.add_text("abc".to_string());
editor.up(1);
editor.down(1);
editor.add_text("d".to_string());
assert_eq!(editor.text(), want_str);
editor = Editor::new(String::new());
want_str = "abcde\na\n\na";
editor.add_text("acd\n".to_string());
editor.add_text("a\n\n".to_string());
assert_eq!(editor.text(), "acd\na\n\n");
editor.up(3);
editor.right(1);
editor.add_text("b".to_string());
assert_eq!(editor.text(), "abcd\na\n\n");
editor.goto_last_col();
editor.add_text("e".to_string());
assert_eq!(editor.text(), "abcde\na\n\n");
editor.down(3);
editor.add_text("a".to_string());
assert_eq!(editor.text(), want_str);
editor = Editor::new(String::new());
editor.add_text("he".to_string());
editor.left(1);
editor.add_text("\n".to_string());
editor.add_text("b".to_string());
want_str = "h\nbe";
assert_eq!(editor.text(), want_str);
editor = Editor::new(String::new());
editor.add_text("hellobye".to_string());
editor.left(3);
editor.add_text("\n".to_string());
want_str = "hello\nbye";
assert_eq!(editor.text(), want_str);
editor = Editor::new(String::new());
editor.add_text("helloye".to_string());
editor.left(2);
editor.add_text("\nb".to_string());
want_str = "hello\nbye";
assert_eq!(editor.text(), want_str);
editor = Editor::new(String::new());
editor.add_text("abc".to_string());
editor.left(2);
editor.add_text("d\n".to_string());
editor.add_text("e".to_string());
want_str = "ad\nebc";
assert_eq!(editor.text(), want_str);
editor = Editor::new(String::new());
editor.add_text("helloe".to_string());
editor.left(1);
editor.add_text("\nb".to_string());
editor.add_text("y".to_string());
want_str = "hello\nbye";
assert_eq!(editor.text(), want_str);
editor = Editor::new(String::new());
editor.add_text("hellye".to_string());
editor.left(2);
editor.add_text("\nb".to_string());
editor.up(1);
editor.goto_last_col();
editor.add_text("o".to_string());
want_str = "hello\nbye";
assert_eq!(editor.text(), want_str);
editor = Editor::new(String::new());
editor.add_text("hello".to_string());
editor.add_text("\n".to_string());
editor.add_text("by".to_string());
editor.up(1);
editor.goto_last_col();
editor.add_text("\n".to_string());
editor.add_text("and".to_string());
editor.down(1);
editor.goto_last_col();
editor.add_text("e".to_string());
want_str = "hello\nand\nbye";
assert_eq!(editor.text(), want_str);
}
#[test]
fn edge_cases() {
let mut editor = Editor::new(String::new());
let mut want_str = "hell";
editor.add_text("hello\n\n".to_string());
editor.delete_text(1, 5).unwrap();
assert_eq!(editor.text(), want_str);
editor = Editor::new(String::new());
want_str = "hello";
editor.add_text("hello\n\n".to_string());
editor.delete_text(1, 6).unwrap();
assert_eq!(editor.text(), want_str);
editor = Editor::new(String::new());
want_str = "";
editor.add_text("\n\n".to_string());
editor.delete_text(1, 1).unwrap();
assert_eq!(editor.text(), want_str);
}
#[test]
fn more_cases() {
let mut editor = Editor::new(String::new());
let mut want_str = "hello\nbye";
editor.add_text("hello\n\n".to_string());
editor.add_text("bye".to_string());
editor.up(2);
editor.goto_last_col();
editor.delete_text(editor.row() + 1, 1).unwrap();
assert_eq!(editor.text(), want_str);
println!("{:?}", editor.lines);
assert_eq!(editor.num_lines(), want_str.lines().count());
editor = Editor::new(String::new());
want_str = "hellobye";
editor.add_text("hello\n\n".to_string());
editor.add_text("bye".to_string());
editor.up(2);
editor.goto_last_col();
editor.delete_text(editor.row() + 2, 1).unwrap();
assert_eq!(editor.text(), want_str);
assert_eq!(editor.num_lines(), want_str.lines().count());
}
#[test]
fn text_lines() {
let mut editor = Editor::new(String::new());
let mut want_str = "abc";
editor.add_text("abc".to_string());
assert_eq!(editor.text_lines(1, 2), want_str);
editor = Editor::new(String::new());
want_str = "abc";
editor.add_text("abc\n\ncd".to_string());
assert_eq!(editor.text_line(1), want_str);
editor = Editor::new(String::new());
want_str = "abc";
editor.add_text("abc\n\ncd".to_string());
assert_eq!(editor.text_lines(1, 2), want_str);
editor = Editor::new(String::new());
want_str = "";
editor.add_text("abc\n\ncd".to_string());
assert_eq!(editor.text_line(2), want_str);
editor = Editor::new(String::new());
want_str = "\ncd\ne";
editor.add_text("abc\n\ncd\ne".to_string());
assert_eq!(editor.text_lines(2, 5), want_str);
}
}
/*
/// Returns a reader for a file and if a temp file had to be created
// fn create_reader(file_path: String) -> Result<(BufReader<File>, bool), String> {
fn create_reader(file_path: String) -> (Result<BufReader<File>, String>, bool) {
// Only will work in Linux/MacOS systems
let tmp_path;
if file_path.starts_with("/") {
tmp_path = "/tmp/via/".to_string() + &file_path.replace("/", "%");
} else {
let curr_dir = env::current_dir().unwrap().to_str().unwrap();
tmp_path = "/tmp/via/".to_string() + &curr_dir.replace("/", "%") + &file_path.replace("/", "%");
}
if Path::new(&file_path).is_file() {
// return Ok((BufReader::new(File::open(file_path).unwrap()), false))
return (Ok(BufReader::new(File::open(file_path).unwrap())), false);
} else if !Path::new(&tmp_path).is_file() {
// return Ok((BufReader::new(File::open(tmp_path).unwrap()), true))
return (Ok(BufReader::new(File::open(tmp_path).unwrap())), true);
}
/*
let error_message = "File is already being edited and/or last session of Via didn't exit cleanly\n
To remove this message, delete ".to_string() + tmp_path.as_str();
Err(error_message.to_string())
*/
panic!("File is already being edited and/or last session of Via didn't exit cleanly\n
To remove this message, delete {}", tmp_path);
}
*/