From 5fe766fd59f6009276dfd6cba6b6df3a4606da34 Mon Sep 17 00:00:00 2001 From: Andrew Dinh Date: Tue, 19 Jan 2021 02:58:12 -0800 Subject: [PATCH] Worked on TUI --- Cargo.lock | 56 ---- Cargo.toml | 3 +- README.md | 18 +- src/main.rs | 113 +------- src/modules/editor.rs | 524 +++++++++++++++++++++++++++++++------ src/modules/piece_table.rs | 16 +- src/modules/via.rs | 325 +++++++++++++++++++---- 7 files changed, 758 insertions(+), 297 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5f820ca..85538ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,32 +1,11 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -[[package]] -name = "aho-corasick" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" -dependencies = [ - "memchr", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - [[package]] name = "libc" version = "0.2.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9f8082297d534141b30c8d39e9b1773713ab50fdbe4ff30f750d063b3bfd701" -[[package]] -name = "memchr" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" - [[package]] name = "numtoa" version = "0.1.0" @@ -48,24 +27,6 @@ dependencies = [ "redox_syscall", ] -[[package]] -name = "regex" -version = "1.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", - "thread_local", -] - -[[package]] -name = "regex-syntax" -version = "0.6.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" - [[package]] name = "termion" version = "1.5.5" @@ -78,26 +39,9 @@ dependencies = [ "redox_termios", ] -[[package]] -name = "thread_local" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "unicode-segmentation" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" - [[package]] name = "via" version = "0.1.0" dependencies = [ - "regex", "termion", - "unicode-segmentation", ] diff --git a/Cargo.toml b/Cargo.toml index 65a019c..9380a73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,5 +15,4 @@ categories = ["command-line-utilities", "text-editors"] [dependencies] termion = "^1.5" -regex = "^1.3" -unicode-segmentation = "^1.7.1" +# unicode-segmentation = "^1.7.1" diff --git a/README.md b/README.md index e5a024a..03bfaed 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,21 @@ # via -An efficient text editor +An efficient text editor written in Rust. If you've ever used Vim, you'll immediately be able to use Via. However, Via was built from the ground up and is a very separate project from any other text editor out there. It runs anywhere Rust can compile, and only has one external dependency to get you started as soon as possible. It even supports using a mouse! It has many structures like a piece table that would be useful in other projects as well. This project was done as a proof of concept for future, more GUI-oriented text editors. **This software is in alpha. Don't use it for anything important** -Mirrors: [GitHub](https://github.com/andrewkdinh/via) (main), [GitLab](https://gitlab.com/andrewkdinh/via), [Gitea](https://gitea.andrewkdinh.com/andrewkdinh/via) \ No newline at end of file +## Compiling + +1. [Install Rust](https://www.rust-lang.org/tools/install) +2. `git clone https://github.com/andrewkdinh/via.git && cd via && cargo run` +3. Enjoy! + +## Credits + +- Build with [Rust](https://rust-lang.org/) and [Termion](https://gitlab.redox-os.org/redox-os/termion/) + +If you're interested in publishing this software on any software repository (AUR, PPA, etc.), please reach out to me (contact info available on my [website](https://andrewkdinh.com)). + +Mirrors: [GitHub](https://github.com/andrewkdinh/via) (main), [GitLab](https://gitlab.com/andrewkdinh/via), [Gitea](https://gitea.andrewkdinh.com/andrewkdinh/via) + +Licensed under [AGPL-3.0](./LICENSE) | Copyright (c) 2021 Andrew Dinh diff --git a/src/main.rs b/src/main.rs index 68343de..528d5ed 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,113 +9,12 @@ mod modules; use modules::via::Via; fn main() { - // let mut via = Via::new(env::args().collect()); - /* - let mut editor = initialize(process_args(&args)); - // Print the current file text onto screen - let stdin = stdin(); - let mut stdout = stdout().into_raw_mode().unwrap(); - - let mut editor_x: usize = 1; - let mut editor_y: usize = 0; - write!(stdout, - "{}{}", - termion::clear::All, - termion::cursor::Goto(1, 1)).unwrap(); - - for line in editor.piece_table.text().lines() { - editor_y += 1; - write!(stdout, - "{}{}", - termion::cursor::Goto(editor_x.try_into().unwrap(), editor_y.try_into().unwrap()), - line).unwrap(); + let (mut file_paths, options) = Via::process_args(env::args().collect()); + if file_paths.is_empty() { + file_paths.push(String::new()); } - stdout.flush().unwrap(); - - let mut buffer_index = editor.piece_table.text_len(); - let mut cmd_index = 0; - let mut cmd_piece_table = piece_table::PieceTable::new(); - let mut mode = 1; // [0: insert, 1: visual, 2: command] - for c in stdin.keys() { - write!(stdout, - "{}{}", - termion::cursor::Goto(1, 1), - termion::clear::CurrentLine) - .unwrap(); - - editor.process(c.unwrap()); - */ - /* - if i == Key::Esc { - mode = 1 - } else if mode == 0 { - // insert mode - if i == Key::Backspace { - if buffer_index != 0 { - piece_table.delete_text(buffer_index - 1, buffer_index); - buffer_index -= 1; - - if editor_x == 1 { - write!(stdout, - "{}", - termion::cursor::Up(1)).unwrap(); - // TODO: how to go to right x value? - } else { - write!(stdout, "{}", termion::cursor::Up(1)).unwrap(); - } - } - } else if i == Key::Up { - - } else if i == Key::Right { - - } else if i == Key::Down { - - } else if i == Key::Left { - - } - } else if mode == 1 { - // visual mode - if i == Key::Char('i') { - mode = 0; - } else if i == Key::Char(':') { - mode = 2; - // TODO: Go to bottom line thing - } else if i == Key::Up || i == Key::Char('k') { - if let Ok(prev_line) = piece_table.line(editor_y - 2) { - let prev_line_len = prev_line.len(); - buffer_index -= editor_x; // Including the new line - if prev_line_len < editor_x - 1 { - editor_x = prev_line_len + 1 - } - buffer_index -= prev_line_len - (editor_x - 1); - editor_y -= 1; - write!(stdout, "{}", termion::cursor::Goto(editor_x.try_into().unwrap(), editor_y.try_into().unwrap())).unwrap() - } - // TODO: FIXME - } else if i == Key::Right || i == Key::Char('l') { - if let Ok(curr_line) = piece_table.line(editor_y - 1) { - if curr_line.len() < editor_x - 1 { - editor_x -= 1; - write!(stdout, "{}", termion::cursor::Right(1)).unwrap(); - buffer_index += 1; - } - } - } else if i == Key::Down || i == Key::Char('j') { - write!(stdout, "{}", termion::cursor::Down(1)).unwrap(); - // TODO: FIXME - } else if i == Key::Left || i == Key::Char('h') { - if editor_y != 1 { - write!(stdout, "{}", termion::cursor::Left(1)).unwrap(); - buffer_index -= 1; - } - } - } else if mode == 2 { - // command mode - cmd_piece_table.add_text("q".to_string(), cmd_index); - cmd_index += 1; - } - stdout.flush().unwrap(); + for file_path in file_paths { + let mut via = Via::new(file_path, options.clone()); + via.init(); } - write!(stdout, "{}", termion::cursor::Show).unwrap(); - */ } diff --git a/src/modules/editor.rs b/src/modules/editor.rs index 7ec9bf1..ac57df4 100644 --- a/src/modules/editor.rs +++ b/src/modules/editor.rs @@ -1,12 +1,10 @@ use std::fs::File; use std::io::{BufRead, BufReader}; -// use std::env; -use std::cmp::min; +use std::cmp::{min, max}; use std::path::Path; use super::piece_table::PieceTable; -#[derive(Debug)] /// An editor window pub(crate) struct Editor { /// The piece table @@ -68,40 +66,200 @@ impl Editor { } /// Returns visible text - fn text(&mut self) -> &str { + pub(crate) fn text(&mut self) -> &str { self.piece_table.text() } - /// Adds `text` at the current cursor position - fn add_text(&mut self, text: String) { - let mut num_lines = 0; - let mut last_line_len = self.col_want - 1; - for (i, line) in text.split("\n").enumerate() { - // TODO: Insert text to visual editor - if self.row + i - 1 >= self.lines.len() { - self.lines.push(line.len()); - } else if i == 0 { - *(self.lines.get_mut(self.row + i - 1).unwrap()) += line.len(); + /// 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 { - self.lines.insert(self.row + i - 1, line.len()); + 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(last_line_len).unwrap(); + self.right(text_len); } else { - self.down(num_lines - 1).unwrap(); - self.goto_col(last_line_len + 1).unwrap(); - println!("{:?}", self.lines); + 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 self.eof_reached { + if num_lines == 0 || self.eof_reached { return 0; } let mut lines_read = 0; @@ -126,7 +284,6 @@ impl Editor { /// Read to EOF, updating `self.piece_table` & `self.lines` fn read_to_eof(&mut self) { - // Maybe use self.read_lines(usize::MAX) instead? if self.eof_reached { return; } @@ -148,29 +305,33 @@ impl Editor { } /// Move the cursor up `num` places - pub(crate) fn up(&mut self, num: usize) -> Result<(), String> { - if self.row == 1 || num >= self.row { - return Err("Can't go up".to_string()); + /// 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 + 1; + self.pt_index -= self.col; for i in 1..num { - self.pt_index -= self.lines.get(self.row - i).unwrap() + 1; + 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; - Ok(()) } - /// Move the cursor down `num` places - pub(crate) fn down(&mut self, num: usize) -> Result<(), String> { - if self.row + num > self.lines.len() { - let from_bottom = self.row + num - self.lines.len(); - let lines_read = self.read_lines(from_bottom); - for _ in lines_read..from_bottom { - self.lines.push(0); - } + /// 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 { @@ -179,39 +340,43 @@ impl Editor { self.row += num; self.col = min(self.col_want, self.lines.get(self.row - 1).unwrap() + 1); self.pt_index += self.col - 1; - Ok(()) } - /// Move the cursor right `num` places - pub(crate) fn right(&mut self, num: usize) -> Result<(), String> { - let line_cols = self.lines.get(self.row - 1).unwrap() + 1; - if self.col + num > line_cols { - return Err("Can't go right".to_string()); + /// 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; - Ok(()) } - /// Move the cursor left `num` places - pub(crate) fn left(&mut self, num: usize) -> Result<(), String> { - if num >= self.col { - return Err("Can't go left".to_string()); + /// 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; - Ok(()) } /// Move to a certain column in the current row - pub(crate) fn goto_col(&mut self, col: usize) -> Result<(), String> { + pub(crate) fn goto_col(&mut self, col: usize) { if col > *self.lines.get(self.row - 1).unwrap() + 1 { - return Err("col greater than columns in row".to_string()); - } - if self.col == col { - Ok(()) + self.goto_last_col(); + } else if self.col == col { + self.col_want = col; } else if col < self.col { self.left(self.col - col) } else { @@ -220,9 +385,9 @@ impl Editor { } /// Move to a certain row - pub(crate) fn goto_row(&mut self, row: usize) -> Result<(), String> { + pub(crate) fn goto_row(&mut self, row: usize) { if self.row == row { - Ok(()) + return } else if row < self.row { self.up(self.row - row) } else { @@ -230,9 +395,21 @@ impl Editor { } } + /// 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) -> Result<(), String> { - self.goto_col(*self.lines.get(self.row - 1).unwrap()) + 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) } } @@ -242,68 +419,257 @@ mod tests { #[test] fn add_text() { - let mut editor = Editor::new("".to_string()); + 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("".to_string()); + 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("".to_string()); + 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("".to_string()); + 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("".to_string()); + let mut editor = Editor::new(String::new()); let mut want_str = "hello\nbye"; - editor.add_text("hello".to_string()); - editor.down(1).unwrap(); + 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("".to_string()); + editor = Editor::new(String::new()); want_str = "hello\nbye"; - editor.add_text("h".to_string()); - editor.down(1).unwrap(); + editor.add_text("h\n".to_string()); + editor.down(1); editor.add_text("bye".to_string()); - editor.up(1).unwrap(); + editor.up(1); editor.add_text("ello".to_string()); assert_eq!(editor.text(), want_str); - editor = Editor::new("".to_string()); + editor = Editor::new(String::new()); want_str = "ab\nabcd"; - editor.add_text("ab".to_string()); - editor.down(1).unwrap(); + editor.add_text("ab\n".to_string()); + editor.down(1); editor.add_text("abc".to_string()); - editor.up(1).unwrap(); - editor.down(1).unwrap(); + editor.up(1); + editor.down(1); editor.add_text("d".to_string()); assert_eq!(editor.text(), want_str); - editor = Editor::new("".to_string()); + editor = Editor::new(String::new()); want_str = "abcde\na\n\na"; - editor.add_text("acd".to_string()); - editor.down(1).unwrap(); - editor.add_text("a".to_string()); - editor.up(1).unwrap(); + 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()); - editor.goto_last_col().unwrap(); + assert_eq!(editor.text(), "abcd\na\n\n"); + editor.goto_last_col(); editor.add_text("e".to_string()); - editor.down(3).unwrap(); + 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); } } diff --git a/src/modules/piece_table.rs b/src/modules/piece_table.rs index 5fcb36e..98b49fc 100644 --- a/src/modules/piece_table.rs +++ b/src/modules/piece_table.rs @@ -1,6 +1,5 @@ -use unicode_segmentation::UnicodeSegmentation; +// use unicode_segmentation::UnicodeSegmentation; -#[derive(Debug)] /// The main structure for storing text pub(crate) struct PieceTable { /// The main table, contains `TableEntry`'s @@ -38,6 +37,11 @@ impl PieceTable { } } + /// Returns if any actions have been taken + pub(crate) fn actions_taken(&self) -> bool { + !self.actions.is_empty() + } + /// Append text to the original buffer and add a table entry pub(crate) fn update_original_buffer(&mut self, text: String) { let org_buffer_len = self.original_buffer.len(); @@ -230,7 +234,7 @@ impl PieceTable { /// Returns the text represented by a table entry fn table_entry_text(&self, table_entry: &TableEntry) -> &str { let buffer = if table_entry.is_add_buffer {&self.add_buffer} else {&self.original_buffer}; - buffer.get(table_entry.start_index..table_entry.end_index).unwrap() // TODO: Get this working with Unicode + buffer.get(table_entry.start_index..table_entry.end_index).unwrap() } /// Returns length of text @@ -287,8 +291,6 @@ impl PieceTable { } } -#[derive(Copy, Clone)] // Needed for PieceTable.actions -#[derive(Debug)] /// An entry in PieceTable's table struct TableEntry { /// Whether this table entry points to the add buffer @@ -471,7 +473,7 @@ mod tests { piece_table.add_text("šŸ˜€".to_string(), 0); want_str = "šŸ˜€"; assert_eq!(want_str.len(), 4); - assert_eq!(want_str.graphemes(true).count(), 1); + // assert_eq!(want_str.graphemes(true).count(), 1); assert_eq!(piece_table.text_len, want_str.len()); assert_eq!(piece_table.text(), want_str); @@ -480,7 +482,7 @@ mod tests { want_str = "eĢ"; assert_eq!(want_str.len(), 3); assert_eq!(want_str.chars().count(), 2); - assert_eq!(want_str.graphemes(true).count(), 1); + // assert_eq!(want_str.graphemes(true).count(), 1); assert_eq!(piece_table.text_len, want_str.len()); assert_eq!(piece_table.text(), want_str); } diff --git a/src/modules/via.rs b/src/modules/via.rs index c0021db..6e9dfb2 100644 --- a/src/modules/via.rs +++ b/src/modules/via.rs @@ -1,76 +1,312 @@ extern crate termion; -extern crate regex; -// use termion::event::Key; +use termion::event::{Key, Event, MouseEvent}; +use termion::input::{TermRead, MouseTerminal}; +use termion::raw::IntoRawMode; -use regex::Regex; +use std::cmp::min; +use std::io::{Write, stdout, stdin}; +use std::convert::TryInto; +use std::fs::File; use super::editor::Editor; use super::piece_table::PieceTable; -#[derive(Debug)] /// Via main class, comprised of `Editor`'s pub struct Via { /// List of current editors editors: Vec, - /// List of files to edit - file_paths: Vec, + /// Editor representing the command line + cmd_editor: Editor, /// Configured options options: ViaOptions, + /** Mode Via is currently in + * 0: normal + * 1: visual (not implemented) + * 2: select (not implemented) + * 3: insert + * 4: command line + * 5: ex (not implemented) + */ + mode: usize, /// Piece table of the command line cmd_piece_table: PieceTable, } impl Via { /// Initialize a new instance of Via from arguments - pub fn new(args: Vec) -> Via { - let (mut file_paths, options) = process_args(args); - if file_paths.is_empty() { - file_paths.push("".to_string()); - } + pub(crate) fn new(file_path: String, options: ViaOptions) -> Via { Via { - editors: vec![Editor::new((*file_paths.first().unwrap()).as_str().to_string())], - file_paths: file_paths, + editors: vec![Editor::new(file_path)], + cmd_editor: Editor::new("".to_string()), options: options, + mode: 0, cmd_piece_table: PieceTable::new(), } } - /* - fn initialize(editor_options: editor::ViaOptions) -> editor::Editor { - // TODO: Might not want to create file, but instead write to memory then at the end then write to file - let file_name = &editor_options.file_name; - let file_path = Path::new(&file_name); - let file: File; - if !file_path.is_file() { - panic!("{} is not a file", file_name); - } else if file_path.exists() { - file = File::open(file_name).expect("Failed to open file"); - } else { - File::create(file_path).expect("Unable to create file"); - file = File::open(file_name).expect("Failed to open file"); + /// Initialize Via and start editing + pub fn init(&mut self) { + // let stdin = termion::async_stdin(); + let stdin = stdin(); + let mut stdout = MouseTerminal::from(stdout().into_raw_mode().unwrap()); + + write!(stdout, "{}{}{}", termion::clear::All, termion::cursor::Goto(1, 1), termion::cursor::BlinkingBlock).unwrap(); + stdout.flush().unwrap(); + + // TODO: Implement support for multiple editors concurrently + let editor = self.editors.get_mut(0).unwrap(); + let mut visual_first_row: usize = 1; + + // TODO: Remove me + editor.goto_last_row(); + editor.goto_last_col(); + + let mut full_render = false; + for c in stdin.events() { + let (term_rows_u16, term_cols_u16) = termion::terminal_size().unwrap(); + let term_rows: usize = term_rows_u16.try_into().unwrap(); + let _term_cols: usize = term_cols_u16.try_into().unwrap(); + let evt = c.unwrap(); + match evt { + Event::Key(key) => { + if key == Key::Esc { + if self.mode == 4 { + write!(stdout, "{}", termion::cursor::Restore).unwrap(); + } else if self.mode == 3 { + editor.left(1); + } + self.mode = 0; + } else if self.mode == 0 { + // Normal mode + match key { + Key::Char('h') | Key::Left | Key::Backspace => editor.left(1), + Key::Char('j') | Key::Down => { + if visual_first_row + term_rows == editor.row() { + visual_first_row += 1; + } + editor.down(1); + if editor.col() - 1 == editor.line_len(editor.row()) { + editor.left(1); + } + }, + Key::Char('\n') => { + if visual_first_row + term_rows == editor.row() { + visual_first_row += 1; + } + if editor.row() != editor.num_lines() { + editor.goto(editor.row() + 1, 1); + } + }, + Key::Char('k') | Key::Up => { + editor.up(1); + if editor.col() - 1 == editor.line_len(editor.row()) { + editor.left(1); + } + }, + Key::Char('l') | Key:: Right => { + if editor.col() < editor.line_len(editor.row()) { + editor.right(1) + } + }, + Key::Char('i') => self.mode = 3, + Key::Char('a') => { + editor.right(1); + self.mode = 3 + }, + Key::Char('o') => { + editor.goto_last_col(); + editor.add_text("\n".to_string()); + self.mode = 3; + full_render = true; + }, + Key::Char('O') => { + editor.goto_col(0); + editor.add_text("\n".to_string()); + editor.up(1); + self.mode = 3; + full_render = true; + }, + Key::Char(':') => { + write!(stdout, "{}", termion::cursor::Save).unwrap(); + self.cmd_editor.delete_all(); + self.cmd_editor.add_text(":".to_string()); + self.mode = 4 + }, + Key::Char('$') => { + editor.goto_last_col(); + editor.left(1); + }, + Key::Char('0') => { + editor.goto_col(0); + }, + Key::Char('A') => { + editor.goto_last_col(); + self.mode = 3; + } + Key::Delete => { + let at_line_end = editor.col() == editor.num_cols(editor.row()); + if at_line_end && editor.row() == editor.num_lines() { + break + } else if at_line_end { + editor.delete_text(editor.row() + 1, 1).unwrap(); + full_render = true; + } else { + editor.delete_text(editor.row(), editor.col() + 1).unwrap(); + write!(stdout, "{}{}", termion::clear::CurrentLine, editor.text_line(editor.row())).unwrap(); + } + }, + Key::Home => editor.goto_col(0), + Key::End => editor.goto_last_col(), + _ => {}, + } + } else if self.mode == 3 { + // Insert mode + match key { + Key::Char(c) => { + if c == '\n' { + editor.add_text(c.to_string()); + full_render = true; + } else { + editor.add_text(c.to_string()); + write!(stdout, "{}{}", "\r", editor.text_line(editor.row())).unwrap(); + } + }, + Key::Left => editor.left(1), + Key::Down => editor.down(1), + Key::Up => editor.up(1), + Key::Right => editor.right(1), + Key::Backspace => { + if editor.row() == 1 && editor.col() == 1 { + + } else if editor.col() == 1 { + editor.up(1); + editor.goto_last_col(); + editor.delete_text(editor.row() + 1, 1).unwrap(); + full_render = true; + } else { + editor.left(1); + editor.delete_text(editor.row(), editor.col() + 1).unwrap(); + write!(stdout, "{}{}{}", termion::clear::CurrentLine, termion::cursor::Goto(1, (editor.row() - visual_first_row + 1).try_into().unwrap()), editor.text_line(editor.row())).unwrap(); + } + }, + Key::Delete => { + let at_line_end = editor.col() == editor.num_cols(editor.row()); + if at_line_end && editor.row() == editor.num_lines() { + break + } else if at_line_end { + editor.delete_text(editor.row() + 1, 1).unwrap(); + full_render = true; + } else { + editor.delete_text(editor.row(), editor.col() + 1).unwrap(); + write!(stdout, "{}{}", termion::clear::CurrentLine, editor.text_line(editor.row())).unwrap(); + } + }, + Key::Home => editor.goto_col(0), + Key::End => editor.goto_last_col(), + _ => {} + } + } else if self.mode == 4 { + // Command line mode + match key { + Key::Char('\n') => { + let mut write = false; + let mut skip_write = false; + let mut quit = false; + match self.cmd_editor.text() { + ":wq" | "x" => {write = true; quit = true;}, + ":q" => quit = true, + ":q!" => {skip_write = true; quit = true} + text => { + // Don't panic, but output an error message + panic!(text.to_string()) + }, + } + if write { + if editor.file_path() == "" { + // Don't panic, but output an error message + panic!("FIXME") + } else { + File::create(editor.file_path()).unwrap().write_all(editor.text().as_bytes()).unwrap(); + } + } + if quit { + if editor.text_matches() { + write!(stdout, "{}", termion::cursor::BlinkingBlock).unwrap(); + break + } else if skip_write { + break + } + // Don't panic, but output an error message + panic!("Quit without saving") + } + panic!("Invalid command") + } + Key::Char(c) => { + self.cmd_editor.add_text(c.to_string()); + }, + Key::Left => self.cmd_editor.left(1), + Key::Right => self.cmd_editor.right(1), + Key::Backspace => { + if self.cmd_editor.col() != 1 { + self.cmd_editor.left(1); + self.cmd_editor.delete_text(self.cmd_editor.row(), self.cmd_editor.col() + 1).unwrap(); + } + }, + Key::Delete => { + let at_line_end = self.cmd_editor.col() == self.cmd_editor.num_cols(self.cmd_editor.row()); + if !at_line_end { + self.cmd_editor.delete_text(self.cmd_editor.row(), self.cmd_editor.col() + 1).unwrap(); + write!(stdout, "{}{}", termion::clear::CurrentLine, self.cmd_editor.text_line(self.cmd_editor.row())).unwrap(); + } + }, + Key::Home => self.cmd_editor.goto_col(0), + Key::End => self.cmd_editor.goto_last_col(), + _ => {} + } + } else { + panic!("Mode {} not implemented yet", self.mode); + } + }, + Event::Mouse(me) => { + match me { + MouseEvent::Press(_, x, y) => { + editor.goto(y.try_into().unwrap(), min(x.try_into().unwrap(), editor.line_len(editor.row()))); + self.mode = 0; + }, + _ => (), + } + } + _ => {} + } + if full_render { + // write!(stdout, "{}{}", termion::clear::All, termion::cursor::Goto(1, 1)).unwrap(); + for (i, line) in editor.text_lines(visual_first_row, visual_first_row + min(editor.num_lines(), term_rows)).lines().enumerate() { + write!(stdout, "{}{}", termion::cursor::Goto(1, (i + 1).try_into().unwrap()), line).unwrap(); + } + full_render = false; + } + write!(stdout, "{}", termion::cursor::Goto(editor.col().try_into().unwrap(), (editor.row() - visual_first_row + 1).try_into().unwrap())).unwrap(); + if self.mode == 0 { + write!(stdout, "{}", termion::cursor::BlinkingBlock).unwrap(); + } else { + write!(stdout, "{}", termion::cursor::BlinkingBar).unwrap(); + } + stdout.flush().unwrap(); } - let mut reader = BufReader::new(file); - // Read until viewport is filled - // For now, only read 2 lines - let mut initial_text =String::new(); - let eof_reached = read_lines(&mut reader, 1000000000000000, &mut initial_text); // TODO: FIX ME - // TODO: Add this to initialization of piece table - - editor::Editor::new(piece_table::PieceTable::new(initial_text), reader, eof_reached, editor_options) + write!(stdout, "{}{}{}{}", termion::clear::All, termion::cursor::Goto(1, 1), termion::cursor::Show, termion::cursor::BlinkingBlock).unwrap(); } - */ -} - -/// Process command line options and returns the files to edit and ViaOptions -// fn process_args(&mut self, args: &Vec) -> Result<(Vec, ViaOptions), &str> { - fn process_args(args: Vec) -> (Vec, ViaOptions) { + + /// Process command line options and returns the files to edit and ViaOptions + // fn process_args(&mut self, args: &Vec) -> Result<(Vec, ViaOptions), &str> { + pub(crate) fn process_args(args: Vec) -> (Vec, ViaOptions) { let mut flags = Vec::new(); let mut file_paths = Vec::new(); - let flags_regex = Regex::new(r"--?\w+").unwrap(); // let default_files = vec!["target/debug/via", "via"]; for arg in &args[1..] { - if flags_regex.is_match(&arg) { + if arg == "--" { + break + } else if arg.starts_with("-") { flags.push(arg); } else { file_paths.push((*arg).as_str().to_string()); @@ -86,9 +322,10 @@ impl Via { } (file_paths, via_options) } +} -#[derive(Debug)] -struct ViaOptions { +#[derive(Clone)] +pub(crate) struct ViaOptions { /// Level of verboseness verboseness: usize, }