Worked on TUI

This commit is contained in:
Andrew Dinh 2021-01-19 02:58:12 -08:00
parent 277fe8092f
commit 5fe766fd59
Signed by: andrewkdinh
GPG Key ID: 2B557D93C6C08B86
7 changed files with 758 additions and 297 deletions

56
Cargo.lock generated
View File

@ -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",
]

View File

@ -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"

View File

@ -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)
## 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

View File

@ -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();
*/
}

View File

@ -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);
}
}

View File

@ -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 = "";
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);
}

View File

@ -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<Editor>,
/// List of files to edit
file_paths: Vec<String>,
/// 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<String>) -> 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<String>) -> Result<(Vec<String>, ViaOptions), &str> {
fn process_args(args: Vec<String>) -> (Vec<String>, ViaOptions) {
/// Process command line options and returns the files to edit and ViaOptions
// fn process_args(&mut self, args: &Vec<String>) -> Result<(Vec<String>, ViaOptions), &str> {
pub(crate) fn process_args(args: Vec<String>) -> (Vec<String>, 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,
}