2019-08-22 14:25:18 -04:00
|
|
|
//! Editor functionality
|
2019-08-22 16:44:47 -04:00
|
|
|
use crate::helpers::*;
|
2019-08-22 14:25:18 -04:00
|
|
|
|
2019-08-26 16:39:52 -04:00
|
|
|
use std::cmp::PartialEq;
|
2019-08-27 12:22:19 -04:00
|
|
|
use std::fs::File;
|
2019-08-22 16:44:47 -04:00
|
|
|
use std::io;
|
|
|
|
use std::io::prelude::*;
|
2019-08-23 16:46:04 -04:00
|
|
|
use std::io::BufReader;
|
2019-08-29 14:13:09 -04:00
|
|
|
use std::time::{Duration, Instant};
|
2019-08-22 16:44:47 -04:00
|
|
|
|
2019-08-27 12:22:19 -04:00
|
|
|
use self::EditorKey::*;
|
|
|
|
|
2019-08-28 16:35:48 -04:00
|
|
|
const KILO_TAB_STOP: usize = 4;
|
2019-08-30 11:20:52 -04:00
|
|
|
const KILO_QUIT_TIMES: u8 = 3;
|
2019-08-28 16:35:48 -04:00
|
|
|
|
2019-08-27 15:13:32 -04:00
|
|
|
/// A representation of a line in the editor
|
2019-08-27 12:22:19 -04:00
|
|
|
#[derive(Debug, Default)]
|
2019-08-27 08:30:51 -04:00
|
|
|
pub struct EditorRow {
|
2019-08-27 12:22:19 -04:00
|
|
|
chars: String,
|
2019-08-28 16:35:48 -04:00
|
|
|
render: String,
|
2019-08-27 12:22:19 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
impl EditorRow {
|
2019-08-28 16:35:48 -04:00
|
|
|
pub fn new(chars: &str) -> Self {
|
|
|
|
let mut instance = EditorRow::default();
|
|
|
|
instance.chars = chars.to_owned();
|
|
|
|
|
|
|
|
instance
|
2019-08-27 12:22:19 -04:00
|
|
|
}
|
2019-08-27 08:30:51 -04:00
|
|
|
}
|
|
|
|
|
2019-08-22 16:44:47 -04:00
|
|
|
/// Main structure for the editor
|
2019-08-27 15:13:32 -04:00
|
|
|
/// `EditorConfig` struct in C version
|
2019-08-29 14:13:09 -04:00
|
|
|
#[derive(Debug)]
|
2019-08-23 16:46:04 -04:00
|
|
|
pub struct Editor {
|
2019-08-26 16:39:52 -04:00
|
|
|
cursor_x: usize,
|
|
|
|
cursor_y: usize,
|
2019-08-28 16:35:48 -04:00
|
|
|
render_x: usize,
|
|
|
|
col_offset: usize,
|
2019-08-27 15:13:32 -04:00
|
|
|
row_offset: usize,
|
2019-08-26 10:04:12 -04:00
|
|
|
screen_cols: usize,
|
|
|
|
screen_rows: usize,
|
2019-08-28 16:35:48 -04:00
|
|
|
rows: Vec<EditorRow>,
|
2019-08-30 11:20:52 -04:00
|
|
|
dirty: u64,
|
2019-08-28 16:35:48 -04:00
|
|
|
filename: String,
|
2019-08-29 14:13:09 -04:00
|
|
|
status_message: String,
|
|
|
|
status_message_time: Instant,
|
2019-08-30 11:20:52 -04:00
|
|
|
|
|
|
|
// Properties not present in C version
|
2019-08-26 10:04:12 -04:00
|
|
|
output_buffer: String,
|
2019-08-30 11:20:52 -04:00
|
|
|
quit_times: u8,
|
2019-08-23 16:46:04 -04:00
|
|
|
}
|
2019-08-22 14:25:18 -04:00
|
|
|
|
2019-08-27 15:13:32 -04:00
|
|
|
/// Keycode mapping enum
|
2019-08-26 16:39:52 -04:00
|
|
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
2019-08-27 17:38:05 -04:00
|
|
|
pub enum EditorKey<T> {
|
2019-08-29 16:32:17 -04:00
|
|
|
Enter,
|
2019-08-26 16:39:52 -04:00
|
|
|
Escape,
|
2019-08-29 16:32:17 -04:00
|
|
|
Backspace,
|
2019-08-26 16:39:52 -04:00
|
|
|
ArrowLeft,
|
|
|
|
ArrowRight,
|
|
|
|
ArrowUp,
|
|
|
|
ArrowDown,
|
|
|
|
DeleteKey,
|
|
|
|
HomeKey,
|
|
|
|
EndKey,
|
|
|
|
PageUp,
|
|
|
|
PageDown,
|
2019-08-27 17:38:05 -04:00
|
|
|
/// Function keys (F1, etc.) T holds the index
|
|
|
|
Function(T),
|
2019-08-27 15:13:32 -04:00
|
|
|
/// Any other type of character
|
2019-08-26 16:39:52 -04:00
|
|
|
OtherKey(T),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl EditorKey<char> {
|
|
|
|
pub fn unwrap(self) -> char {
|
|
|
|
match self {
|
|
|
|
self::OtherKey(val) => val,
|
|
|
|
_ => panic!("called `EditorKey::unwrap()` on a `None` value"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-29 14:13:09 -04:00
|
|
|
impl Default for Editor {
|
|
|
|
fn default() -> Self {
|
|
|
|
Editor {
|
|
|
|
cursor_x: 0,
|
|
|
|
cursor_y: 0,
|
|
|
|
render_x: 0,
|
|
|
|
col_offset: 0,
|
|
|
|
row_offset: 0,
|
|
|
|
screen_cols: 0,
|
|
|
|
screen_rows: 0,
|
|
|
|
rows: vec![],
|
2019-08-30 11:20:52 -04:00
|
|
|
dirty: 0,
|
2019-08-29 14:13:09 -04:00
|
|
|
filename: String::new(),
|
|
|
|
status_message: String::new(),
|
|
|
|
status_message_time: Instant::now(),
|
|
|
|
output_buffer: String::new(),
|
2019-08-30 11:20:52 -04:00
|
|
|
quit_times: KILO_QUIT_TIMES,
|
2019-08-29 14:13:09 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-22 14:25:18 -04:00
|
|
|
impl Editor {
|
2019-08-27 15:13:32 -04:00
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
// Init
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
|
2019-08-22 14:25:18 -04:00
|
|
|
pub fn new() -> Self {
|
2019-08-23 16:46:04 -04:00
|
|
|
let mut instance = Self::default();
|
2019-08-29 14:13:09 -04:00
|
|
|
|
2019-08-23 16:46:04 -04:00
|
|
|
let size = instance.get_window_size();
|
2019-08-29 14:13:09 -04:00
|
|
|
|
2019-08-26 10:04:12 -04:00
|
|
|
instance.screen_cols = size.cols as usize;
|
2019-08-29 14:13:09 -04:00
|
|
|
instance.screen_rows = (size.rows - 2) as usize;
|
2019-08-23 16:46:04 -04:00
|
|
|
|
|
|
|
instance
|
2019-08-22 16:44:47 -04:00
|
|
|
}
|
|
|
|
|
2019-08-27 15:13:32 -04:00
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
// Terminal
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
|
2019-08-27 17:38:05 -04:00
|
|
|
fn read_key(&mut self) -> Option<EditorKey<char>> {
|
2019-08-30 15:10:19 -04:00
|
|
|
/*
|
|
|
|
TODO: Read 1 byte by default, and read additional bytes
|
|
|
|
if the first byte is an escape character, to resolve the
|
|
|
|
unintentional input throttling
|
|
|
|
*/
|
2019-08-22 16:44:47 -04:00
|
|
|
let stdin = io::stdin();
|
2019-08-23 14:57:26 -04:00
|
|
|
let stdin = stdin.lock();
|
2019-08-22 16:44:47 -04:00
|
|
|
let mut in_str = String::new();
|
2019-08-27 17:38:05 -04:00
|
|
|
let mut buffer = BufReader::with_capacity(4, stdin);
|
|
|
|
buffer.read_to_string(&mut in_str).unwrap();
|
2019-08-22 16:44:47 -04:00
|
|
|
|
2019-08-27 17:38:05 -04:00
|
|
|
let mut input: Vec<EditorKey<char>> = vec![];
|
2019-08-23 13:34:00 -04:00
|
|
|
|
|
|
|
for char in in_str.chars() {
|
2019-08-27 17:38:05 -04:00
|
|
|
input.push(match char {
|
2019-08-30 11:20:52 -04:00
|
|
|
'\x08' => Backspace,
|
2019-08-29 16:32:17 -04:00
|
|
|
'\x7f' => Backspace,
|
2019-08-26 16:39:52 -04:00
|
|
|
'\x1b' => Escape,
|
2019-08-29 16:32:17 -04:00
|
|
|
'\r' => Enter,
|
2019-08-26 16:39:52 -04:00
|
|
|
_ => OtherKey(char),
|
|
|
|
});
|
2019-08-23 13:34:00 -04:00
|
|
|
}
|
|
|
|
|
2019-08-27 17:38:05 -04:00
|
|
|
if input.is_empty() {
|
2019-08-23 14:57:26 -04:00
|
|
|
return None;
|
2019-08-22 16:44:47 -04:00
|
|
|
}
|
|
|
|
|
2019-08-27 17:38:05 -04:00
|
|
|
if input[0].eq(&Escape) {
|
|
|
|
match input.len() {
|
|
|
|
5 => {
|
|
|
|
// Escape code of the form `^[[NM~`
|
|
|
|
if input[4].eq(&OtherKey('~')) {
|
|
|
|
let action = match (input[2].unwrap(), input[3].unwrap()) {
|
|
|
|
('1', '5') => Function('5'),
|
|
|
|
('1', '7') => Function('6'),
|
|
|
|
('1', '8') => Function('7'),
|
|
|
|
('1', '9') => Function('8'),
|
|
|
|
('2', '0') => Function('9'),
|
|
|
|
('2', '1') => Function('X'), // F10
|
|
|
|
('2', '4') => Function('T'), // F12
|
|
|
|
_ => Escape,
|
|
|
|
};
|
|
|
|
|
|
|
|
return Some(action);
|
|
|
|
}
|
2019-08-26 16:39:52 -04:00
|
|
|
}
|
2019-08-27 17:38:05 -04:00
|
|
|
4 => {
|
|
|
|
// Escape code of the form `^[[N~`
|
|
|
|
if input[3].eq(&OtherKey('~')) {
|
|
|
|
let action = match input[2].unwrap() {
|
|
|
|
'1' => HomeKey,
|
|
|
|
'3' => DeleteKey,
|
|
|
|
'4' => EndKey,
|
|
|
|
'5' => PageUp,
|
|
|
|
'6' => PageDown,
|
|
|
|
'7' => HomeKey,
|
|
|
|
'8' => EndKey,
|
2019-08-28 16:35:48 -04:00
|
|
|
_ => input[2], // Escape,
|
2019-08-27 17:38:05 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
return Some(action);
|
|
|
|
}
|
2019-08-28 16:35:48 -04:00
|
|
|
}
|
2019-08-27 17:38:05 -04:00
|
|
|
3 => {
|
|
|
|
match input[1] {
|
|
|
|
// Escape code of the form `^[[X`
|
|
|
|
OtherKey('[') => {
|
|
|
|
let action = match input[2].unwrap() {
|
|
|
|
'A' => ArrowUp,
|
|
|
|
'B' => ArrowDown,
|
|
|
|
'C' => ArrowRight,
|
|
|
|
'D' => ArrowLeft,
|
|
|
|
'H' => HomeKey,
|
|
|
|
'F' => EndKey,
|
|
|
|
|
|
|
|
// Eh, just return escape otherwise
|
2019-08-28 16:35:48 -04:00
|
|
|
_ => input[2], //Escape,
|
2019-08-27 17:38:05 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
return Some(action);
|
2019-08-28 16:35:48 -04:00
|
|
|
}
|
2019-08-27 17:38:05 -04:00
|
|
|
// Escape code of the form `^[OX`
|
|
|
|
OtherKey('O') => {
|
|
|
|
let action = match input[2].unwrap() {
|
|
|
|
'H' => HomeKey,
|
|
|
|
'F' => EndKey,
|
|
|
|
'P' => Function('1'),
|
|
|
|
'Q' => Function('2'),
|
|
|
|
'R' => Function('3'),
|
|
|
|
'S' => Function('4'),
|
2019-08-28 16:35:48 -04:00
|
|
|
_ => input[2], //Escape,
|
2019-08-27 17:38:05 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
return Some(action);
|
2019-08-28 16:35:48 -04:00
|
|
|
}
|
|
|
|
_ => return Some(input[1]),
|
2019-08-27 17:38:05 -04:00
|
|
|
}
|
2019-08-28 16:35:48 -04:00
|
|
|
}
|
2019-08-27 17:38:05 -04:00
|
|
|
_ => return Some(input[0]),
|
2019-08-26 16:39:52 -04:00
|
|
|
}
|
|
|
|
}
|
2019-08-23 16:46:04 -04:00
|
|
|
|
2019-08-27 17:38:05 -04:00
|
|
|
// If the character doesn't match any escape sequences, just
|
|
|
|
// pass that character on
|
|
|
|
return Some(input[0]);
|
2019-08-23 16:46:04 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
fn get_window_size(&mut self) -> TermSize {
|
|
|
|
match get_term_size() {
|
|
|
|
Some(size) => size,
|
2019-08-27 08:30:51 -04:00
|
|
|
None => unimplemented!("The easy way usually works"),
|
2019-08-23 16:46:04 -04:00
|
|
|
}
|
|
|
|
}
|
2019-08-22 16:44:47 -04:00
|
|
|
|
2019-08-27 15:13:32 -04:00
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
// Input
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
|
2019-08-30 16:17:06 -04:00
|
|
|
fn prompt(&mut self, prompt: &str) -> String {
|
|
|
|
let mut buffer = String::new();
|
|
|
|
|
|
|
|
loop {
|
|
|
|
self.set_status_message(&format!("{} {}", prompt, buffer));
|
|
|
|
self.refresh_screen();
|
|
|
|
|
|
|
|
let char = self.read_key();
|
|
|
|
if char.is_some() {
|
|
|
|
let char = char.unwrap();
|
|
|
|
match char {
|
|
|
|
Enter => {
|
|
|
|
if buffer.len() != 0 {
|
|
|
|
self.set_status_message("");
|
|
|
|
return buffer;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
OtherKey(ch) => {
|
|
|
|
if (!ch.is_ascii_control()) && (ch as u8) < 128 {
|
|
|
|
buffer.push(ch);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_ => (),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-26 16:39:52 -04:00
|
|
|
fn move_cursor(&mut self, key: &EditorKey<char>) {
|
2019-08-28 16:35:48 -04:00
|
|
|
let row = self.rows.get(self.cursor_y);
|
2019-08-26 16:39:52 -04:00
|
|
|
match key {
|
|
|
|
ArrowLeft => {
|
2019-08-28 16:35:48 -04:00
|
|
|
if self.cursor_x != 0 {
|
|
|
|
// Move cursor left
|
2019-08-26 16:39:52 -04:00
|
|
|
self.cursor_x -= 1;
|
2019-08-28 16:35:48 -04:00
|
|
|
} else if self.cursor_y > 0 {
|
|
|
|
// Move to the end of the previous line
|
|
|
|
self.cursor_y -= 1;
|
|
|
|
self.cursor_x = self.rows[self.cursor_y].chars.len();
|
2019-08-26 16:39:52 -04:00
|
|
|
}
|
2019-08-27 08:30:51 -04:00
|
|
|
}
|
2019-08-26 16:39:52 -04:00
|
|
|
ArrowRight => {
|
2019-08-28 16:35:48 -04:00
|
|
|
if row.is_some() && self.cursor_x < row.unwrap().chars.len() {
|
|
|
|
// Move cursor right
|
2019-08-26 16:39:52 -04:00
|
|
|
self.cursor_x += 1;
|
2019-08-28 16:35:48 -04:00
|
|
|
} else if row.is_some() && self.cursor_x == row.unwrap().chars.len() {
|
|
|
|
// Move to start of next line
|
|
|
|
self.cursor_y += 1;
|
|
|
|
self.cursor_x = 0;
|
2019-08-26 16:39:52 -04:00
|
|
|
}
|
2019-08-27 08:30:51 -04:00
|
|
|
}
|
2019-08-26 16:39:52 -04:00
|
|
|
ArrowUp => {
|
2019-08-27 17:38:05 -04:00
|
|
|
if self.cursor_y > 0 {
|
2019-08-26 16:39:52 -04:00
|
|
|
self.cursor_y -= 1;
|
|
|
|
}
|
2019-08-27 08:30:51 -04:00
|
|
|
}
|
2019-08-26 16:39:52 -04:00
|
|
|
ArrowDown => {
|
2019-08-27 17:38:05 -04:00
|
|
|
if self.cursor_y < self.rows.len() {
|
2019-08-26 16:39:52 -04:00
|
|
|
self.cursor_y += 1;
|
|
|
|
}
|
2019-08-27 08:30:51 -04:00
|
|
|
}
|
2019-08-26 16:39:52 -04:00
|
|
|
_ => (),
|
|
|
|
};
|
2019-08-28 16:35:48 -04:00
|
|
|
|
|
|
|
let row = self.rows.get(self.cursor_y);
|
|
|
|
let row_len = if row.is_some() {
|
|
|
|
row.unwrap().chars.len()
|
|
|
|
} else {
|
|
|
|
0
|
|
|
|
};
|
|
|
|
|
|
|
|
// Snap to end of line when scrolling down
|
|
|
|
if self.cursor_x > row_len {
|
|
|
|
self.cursor_x = row_len;
|
|
|
|
}
|
2019-08-26 16:39:52 -04:00
|
|
|
}
|
|
|
|
|
2019-08-23 16:46:04 -04:00
|
|
|
/// Route user input to the appropriate handler method
|
2019-08-27 17:38:05 -04:00
|
|
|
pub fn process_keypress(&mut self) -> Option<EditorKey<char>> {
|
|
|
|
let key = self.read_key();
|
|
|
|
if key.is_some() {
|
|
|
|
let char = key.unwrap();
|
2019-08-29 16:32:17 -04:00
|
|
|
|
2019-08-27 17:38:05 -04:00
|
|
|
match char {
|
2019-08-29 16:45:54 -04:00
|
|
|
Backspace => self._del_or_backspace(Backspace),
|
2019-08-30 11:20:52 -04:00
|
|
|
DeleteKey => self._del_or_backspace(DeleteKey),
|
2019-08-30 15:10:19 -04:00
|
|
|
Enter => self.insert_new_line(),
|
2019-08-27 17:38:05 -04:00
|
|
|
Escape => (),
|
|
|
|
ArrowUp => self.move_cursor(&ArrowUp),
|
|
|
|
ArrowDown => self.move_cursor(&ArrowDown),
|
|
|
|
ArrowLeft => self.move_cursor(&ArrowLeft),
|
|
|
|
ArrowRight => self.move_cursor(&ArrowRight),
|
2019-08-29 16:32:17 -04:00
|
|
|
PageUp => self._page_up_or_down(PageUp),
|
|
|
|
PageDown => self._page_up_or_down(PageDown),
|
2019-08-26 16:39:52 -04:00
|
|
|
HomeKey => {
|
|
|
|
self.cursor_x = 0;
|
2019-08-27 08:30:51 -04:00
|
|
|
}
|
2019-08-26 16:39:52 -04:00
|
|
|
EndKey => {
|
2019-08-28 16:35:48 -04:00
|
|
|
if self.cursor_y < self.rows.len() {
|
|
|
|
self.cursor_x = self.rows[self.cursor_y].chars.len();
|
|
|
|
}
|
|
|
|
}
|
2019-08-29 16:32:17 -04:00
|
|
|
OtherKey(c) => {
|
|
|
|
if c.is_ascii_control() {
|
|
|
|
if c == ctrl_key('q') {
|
2019-08-30 11:20:52 -04:00
|
|
|
if self.dirty > 0 && self.quit_times > 0 {
|
|
|
|
self.set_status_message(&format!("WARNING!!! File has unsaved changes. Press Ctrl-Q {} more times to quit.", self.quit_times));
|
|
|
|
self.quit_times -= 1;
|
|
|
|
return Some(OtherKey('\0'));
|
|
|
|
}
|
2019-08-29 16:32:17 -04:00
|
|
|
print!("\x1b[2J");
|
|
|
|
print!("\x1b[H");
|
|
|
|
// Break out of the input loop
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
if c == ctrl_key('s') {
|
2019-08-30 11:20:52 -04:00
|
|
|
// Save success/error message handled by save method
|
2019-08-29 16:45:54 -04:00
|
|
|
match self.save() {
|
|
|
|
Ok(_) => (),
|
2019-08-30 11:20:52 -04:00
|
|
|
Err(_) => (),
|
2019-08-29 16:45:54 -04:00
|
|
|
}
|
2019-08-29 16:32:17 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if c == ctrl_key('h') {
|
|
|
|
self._del_or_backspace(Backspace);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
self.insert_char(c);
|
|
|
|
}
|
|
|
|
}
|
2019-08-26 16:39:52 -04:00
|
|
|
_ => (),
|
|
|
|
};
|
2019-08-27 17:38:05 -04:00
|
|
|
|
2019-08-30 11:20:52 -04:00
|
|
|
self.quit_times = KILO_QUIT_TIMES;
|
|
|
|
|
2019-08-28 16:35:48 -04:00
|
|
|
return key;
|
2019-08-26 16:39:52 -04:00
|
|
|
}
|
2019-08-27 15:13:32 -04:00
|
|
|
|
|
|
|
// Continue the main input loop
|
2019-08-27 17:38:05 -04:00
|
|
|
Some(OtherKey('\0'))
|
2019-08-26 16:39:52 -04:00
|
|
|
}
|
2019-08-23 13:34:00 -04:00
|
|
|
|
2019-08-29 16:32:17 -04:00
|
|
|
fn _del_or_backspace(&mut self, key: EditorKey<char>) {
|
2019-08-30 11:20:52 -04:00
|
|
|
if key == DeleteKey {
|
|
|
|
self.move_cursor(&ArrowRight);
|
|
|
|
}
|
|
|
|
self.delete_char();
|
2019-08-29 16:32:17 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
fn _page_up_or_down(&mut self, key: EditorKey<char>) {
|
2019-08-27 08:30:51 -04:00
|
|
|
let mut times = self.screen_rows;
|
2019-08-23 13:34:00 -04:00
|
|
|
|
2019-08-28 16:35:48 -04:00
|
|
|
// Update the cursor position
|
|
|
|
match key {
|
|
|
|
PageUp => {
|
|
|
|
self.cursor_y = self.row_offset;
|
|
|
|
}
|
|
|
|
PageDown => {
|
|
|
|
self.cursor_y = self.row_offset + self.screen_rows - 1;
|
|
|
|
if self.cursor_y > self.rows.len() {
|
|
|
|
self.cursor_y = self.rows.len();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_ => (),
|
|
|
|
}
|
|
|
|
|
|
|
|
// Scroll the file up or down
|
2019-08-26 16:39:52 -04:00
|
|
|
while times > 1 {
|
|
|
|
times -= 1;
|
2019-08-28 16:35:48 -04:00
|
|
|
self.move_cursor(match key {
|
|
|
|
PageUp => &ArrowUp,
|
|
|
|
PageDown => &ArrowDown,
|
|
|
|
_ => &OtherKey('\0'),
|
|
|
|
})
|
2019-08-23 13:34:00 -04:00
|
|
|
}
|
2019-08-22 14:25:18 -04:00
|
|
|
}
|
2019-08-23 13:34:00 -04:00
|
|
|
|
2019-08-27 15:13:32 -04:00
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
// Output
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
|
2019-08-26 10:04:12 -04:00
|
|
|
/// Equivalent of the abAppend function
|
|
|
|
/// in the original tutorial, just appends
|
|
|
|
/// to the `output_buffer` String in the
|
|
|
|
/// editor struct.
|
|
|
|
fn append_out(&mut self, str: &str) {
|
|
|
|
self.output_buffer.push_str(str);
|
|
|
|
}
|
|
|
|
|
2019-08-27 17:38:05 -04:00
|
|
|
fn scroll(&mut self) {
|
2019-08-28 16:35:48 -04:00
|
|
|
self.render_x = 0;
|
|
|
|
if self.cursor_y < self.rows.len() {
|
2019-08-29 16:45:54 -04:00
|
|
|
self.render_x = self.row_cx_to_rx(self.cursor_y);
|
2019-08-28 16:35:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Vertical scrolling
|
2019-08-27 17:38:05 -04:00
|
|
|
if self.cursor_y < self.row_offset {
|
|
|
|
self.row_offset = self.cursor_y;
|
|
|
|
}
|
|
|
|
if self.cursor_y >= self.row_offset + self.screen_rows {
|
|
|
|
self.row_offset = self.cursor_y - self.screen_rows + 1;
|
|
|
|
}
|
2019-08-28 16:35:48 -04:00
|
|
|
|
|
|
|
// Horizontal scrolling
|
|
|
|
if self.render_x < self.col_offset {
|
|
|
|
self.col_offset = self.render_x;
|
|
|
|
}
|
|
|
|
if self.render_x >= self.col_offset + self.screen_cols {
|
|
|
|
self.col_offset = self.render_x - self.screen_cols + 1;
|
|
|
|
}
|
2019-08-27 17:38:05 -04:00
|
|
|
}
|
|
|
|
|
2019-08-23 16:46:04 -04:00
|
|
|
fn draw_rows(&mut self) {
|
2019-08-26 10:04:12 -04:00
|
|
|
for y in 0..self.screen_rows {
|
2019-08-27 17:38:05 -04:00
|
|
|
let file_row = y + self.row_offset;
|
|
|
|
if file_row >= self.rows.len() {
|
2019-08-27 15:13:32 -04:00
|
|
|
if self.rows.is_empty() && y == (self.screen_rows / 3) {
|
|
|
|
let mut welcome = format!(
|
|
|
|
"Oxidized Kilo editor -- version {}",
|
|
|
|
env!("CARGO_PKG_VERSION")
|
|
|
|
);
|
2019-08-27 12:22:19 -04:00
|
|
|
if welcome.len() > self.screen_cols {
|
|
|
|
welcome.truncate(self.screen_cols)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Center welcome message
|
|
|
|
let mut padding = (self.screen_cols - welcome.len()) / 2;
|
|
|
|
if padding > 0 {
|
|
|
|
self.append_out("~");
|
|
|
|
padding -= 1;
|
|
|
|
}
|
2019-08-27 15:13:32 -04:00
|
|
|
for _ in 0..padding {
|
2019-08-27 12:22:19 -04:00
|
|
|
self.append_out(" ");
|
|
|
|
}
|
2019-08-26 10:04:12 -04:00
|
|
|
|
2019-08-27 12:22:19 -04:00
|
|
|
self.append_out(&welcome);
|
|
|
|
} else {
|
2019-08-26 10:04:12 -04:00
|
|
|
self.append_out("~");
|
|
|
|
}
|
|
|
|
} else {
|
2019-08-28 16:35:48 -04:00
|
|
|
let mut len = self.rows[file_row].render.len() - self.col_offset;
|
|
|
|
if len > self.screen_cols {
|
|
|
|
len = self.screen_cols;
|
2019-08-27 12:22:19 -04:00
|
|
|
}
|
2019-08-28 16:35:48 -04:00
|
|
|
|
|
|
|
let output = self.rows[file_row].render.clone();
|
|
|
|
// let mut output = self.rows[file_row].render.clone();
|
|
|
|
// output.truncate(len);
|
|
|
|
self.append_out(&output[self.col_offset..len]);
|
2019-08-26 10:04:12 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
self.append_out("\x1b[K");
|
2019-08-28 16:35:48 -04:00
|
|
|
self.append_out("\r\n");
|
2019-08-23 16:46:04 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-28 16:35:48 -04:00
|
|
|
fn draw_status_bar(&mut self) {
|
|
|
|
self.append_out("\x1b[7m");
|
|
|
|
|
|
|
|
let filename = if self.filename.is_empty() {
|
|
|
|
"[No Name]"
|
|
|
|
} else {
|
|
|
|
&self.filename
|
|
|
|
};
|
|
|
|
|
2019-08-30 11:20:52 -04:00
|
|
|
let modified = if self.dirty > 0 { "(modified}" } else { "" };
|
|
|
|
|
|
|
|
let mut left_message = format!("{:.80} - {} lines {}", filename, self.rows.len(), modified);
|
2019-08-29 16:45:54 -04:00
|
|
|
let right_message = format!("{}/{}", self.cursor_y + 1, self.rows.len());
|
2019-08-29 11:47:22 -04:00
|
|
|
let mut len = left_message.len();
|
2019-08-28 16:35:48 -04:00
|
|
|
if len > self.screen_cols {
|
|
|
|
len = self.screen_cols;
|
2019-08-29 11:47:22 -04:00
|
|
|
left_message.truncate(len);
|
2019-08-28 16:35:48 -04:00
|
|
|
}
|
2019-08-29 11:47:22 -04:00
|
|
|
self.append_out(&left_message);
|
2019-08-28 16:35:48 -04:00
|
|
|
|
2019-08-29 11:47:22 -04:00
|
|
|
for x in len..self.screen_cols {
|
2019-08-29 16:45:54 -04:00
|
|
|
if self.screen_cols - x == right_message.len() {
|
2019-08-29 11:47:22 -04:00
|
|
|
self.append_out(&right_message);
|
|
|
|
break;
|
|
|
|
}
|
2019-08-28 16:35:48 -04:00
|
|
|
self.append_out(" ");
|
|
|
|
}
|
|
|
|
self.append_out("\x1b[m");
|
2019-08-29 14:13:09 -04:00
|
|
|
self.append_out("\r\n");
|
|
|
|
}
|
|
|
|
|
|
|
|
fn draw_message_bar(&mut self) {
|
|
|
|
self.append_out("\x1b[K");
|
|
|
|
|
|
|
|
let mut message = self.status_message.clone();
|
|
|
|
let message_len = message.len();
|
|
|
|
if message_len > self.screen_cols {
|
|
|
|
message.truncate(self.screen_cols);
|
|
|
|
}
|
|
|
|
|
|
|
|
let five_seconds = Duration::from_secs(5);
|
|
|
|
if message_len > 0 && self.status_message_time.elapsed() < five_seconds {
|
|
|
|
self.append_out(&message);
|
|
|
|
}
|
2019-08-28 16:35:48 -04:00
|
|
|
}
|
|
|
|
|
2019-08-30 16:17:06 -04:00
|
|
|
pub fn refresh_screen(&mut self) {
|
2019-08-27 17:38:05 -04:00
|
|
|
self.scroll();
|
2019-08-26 10:04:12 -04:00
|
|
|
self.output_buffer.clear();
|
2019-08-23 16:46:04 -04:00
|
|
|
|
2019-08-26 10:04:12 -04:00
|
|
|
// Hide cursor, reposition cursor
|
2019-08-27 15:13:32 -04:00
|
|
|
self.append_out("\x1b[?25l");
|
|
|
|
self.append_out("\x1b[H");
|
2019-08-23 16:46:04 -04:00
|
|
|
|
|
|
|
self.draw_rows();
|
2019-08-28 16:35:48 -04:00
|
|
|
self.draw_status_bar();
|
2019-08-29 14:13:09 -04:00
|
|
|
self.draw_message_bar();
|
2019-08-23 13:34:00 -04:00
|
|
|
|
2019-08-26 16:39:52 -04:00
|
|
|
// Move cursor to state position
|
2019-08-28 16:35:48 -04:00
|
|
|
let y = (self.cursor_y - self.row_offset) + 1;
|
|
|
|
let x = (self.render_x - self.col_offset) + 1;
|
2019-08-29 14:13:09 -04:00
|
|
|
let cursor_code = format!("\x1b[{y};{x}H", y = y, x = x);
|
2019-08-26 16:39:52 -04:00
|
|
|
self.append_out(&cursor_code);
|
|
|
|
|
|
|
|
// Show cursor
|
2019-08-26 10:04:12 -04:00
|
|
|
self.append_out("\x1b[?25h");
|
|
|
|
|
|
|
|
let stdout = io::stdout();
|
|
|
|
let mut handle = stdout.lock();
|
2019-08-30 16:17:06 -04:00
|
|
|
|
|
|
|
// If you can't write to stdout, you might as well just panic
|
|
|
|
handle.write_all(&self.output_buffer.as_bytes()).unwrap();
|
2019-08-23 14:57:26 -04:00
|
|
|
}
|
2019-08-27 08:30:51 -04:00
|
|
|
|
2019-08-29 14:13:09 -04:00
|
|
|
/// Set the status bar message
|
|
|
|
///
|
|
|
|
/// To avoid creating a macro that would just forward to
|
|
|
|
/// the `format!` macro, this method only accepts a pre-formatted
|
|
|
|
/// string.
|
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
|
|
|
/// ```no-run
|
|
|
|
/// # use rs-kilo::editor::Editor;
|
|
|
|
/// # let editor = Editor::new();
|
|
|
|
/// let message = format!("{} is {}", key, status);
|
|
|
|
/// editor.set_status_message(&message);
|
|
|
|
/// ```
|
|
|
|
pub fn set_status_message(&mut self, message: &str) {
|
|
|
|
self.status_message = message.to_owned();
|
|
|
|
self.status_message_time = Instant::now();
|
|
|
|
}
|
|
|
|
|
2019-08-27 15:13:32 -04:00
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
// Row Operations
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
|
2019-08-29 16:45:54 -04:00
|
|
|
fn row_cx_to_rx(&mut self, index: usize) -> usize {
|
2019-08-28 16:35:48 -04:00
|
|
|
let mut rx: usize = 0;
|
|
|
|
let row = &mut self.rows[index];
|
|
|
|
|
|
|
|
for char in row.chars.chars() {
|
|
|
|
if char == '\t' {
|
|
|
|
rx += (KILO_TAB_STOP - 1) - (rx % KILO_TAB_STOP);
|
|
|
|
}
|
|
|
|
rx += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
rx
|
|
|
|
}
|
|
|
|
|
2019-08-30 16:17:06 -04:00
|
|
|
/// Convert tab characters to spaces for display
|
2019-08-28 16:35:48 -04:00
|
|
|
fn update_row(&mut self, index: usize) {
|
|
|
|
let row = &mut self.rows[index];
|
|
|
|
let str = row.chars.clone();
|
|
|
|
|
|
|
|
// Cheat at rendering tabs as spaces
|
|
|
|
let str = str.replace('\t', " ");
|
|
|
|
row.render = str;
|
|
|
|
}
|
|
|
|
|
2019-08-30 15:10:19 -04:00
|
|
|
fn insert_row(&mut self, at: usize, row: &str) {
|
|
|
|
if at > self.rows.len() {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let row = EditorRow::new(row);
|
|
|
|
self.rows.insert(at, row);
|
|
|
|
|
|
|
|
self.update_row(at);
|
|
|
|
|
|
|
|
self.dirty += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
fn delete_row(&mut self, row_index: usize) {
|
|
|
|
if row_index > self.rows.len() {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
self.rows.remove(row_index);
|
2019-08-30 11:20:52 -04:00
|
|
|
|
|
|
|
self.dirty += 1;
|
2019-08-27 15:13:32 -04:00
|
|
|
}
|
|
|
|
|
2019-08-29 16:32:17 -04:00
|
|
|
fn row_insert_char(&mut self, row_index: usize, char_index: usize, ch: char) {
|
|
|
|
let mut at = char_index;
|
|
|
|
let row = &mut self.rows[row_index];
|
|
|
|
|
|
|
|
if at > row.chars.len() {
|
|
|
|
at = row.chars.len();
|
|
|
|
}
|
|
|
|
|
|
|
|
row.chars.insert(at, ch);
|
|
|
|
|
|
|
|
self.update_row(row_index);
|
2019-08-30 11:20:52 -04:00
|
|
|
|
|
|
|
self.dirty += 1;
|
|
|
|
}
|
|
|
|
|
2019-08-30 15:10:19 -04:00
|
|
|
fn row_append_string(&mut self, row_index: usize, strng: &str) {
|
|
|
|
let row = &mut self.rows[row_index];
|
|
|
|
row.chars += strng;
|
|
|
|
|
|
|
|
self.update_row(row_index);
|
|
|
|
|
|
|
|
self.dirty += 1;
|
|
|
|
}
|
|
|
|
|
2019-08-30 11:20:52 -04:00
|
|
|
fn row_delete_char(&mut self, row_index: usize, char_index: usize) {
|
|
|
|
let row = &mut self.rows[row_index];
|
|
|
|
if char_index >= row.chars.len() {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
row.chars.remove(char_index);
|
|
|
|
self.update_row(row_index);
|
|
|
|
|
|
|
|
self.dirty += 1;
|
2019-08-29 16:32:17 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
// Editor Operations
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
|
|
|
|
fn insert_char(&mut self, ch: char) {
|
|
|
|
if self.cursor_y == self.rows.len() {
|
2019-08-30 15:10:19 -04:00
|
|
|
self.insert_row(self.rows.len(), "");
|
2019-08-29 16:32:17 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
self.row_insert_char(self.cursor_y, self.cursor_x, ch);
|
|
|
|
self.cursor_x += 1;
|
|
|
|
}
|
|
|
|
|
2019-08-30 15:10:19 -04:00
|
|
|
fn insert_new_line(&mut self) {
|
|
|
|
if self.cursor_x == 0 {
|
|
|
|
self.insert_row(self.cursor_y, "");
|
|
|
|
} else {
|
2019-08-30 16:17:06 -04:00
|
|
|
// Clone the contents of the current row
|
2019-08-30 15:10:19 -04:00
|
|
|
let row = &mut self.rows[self.cursor_y];
|
|
|
|
let row_chars = row.chars.clone();
|
|
|
|
|
2019-08-30 16:17:06 -04:00
|
|
|
// Truncate the original row up to the cursor
|
|
|
|
row.chars.truncate(self.cursor_x);
|
2019-08-30 15:10:19 -04:00
|
|
|
|
2019-08-30 16:17:06 -04:00
|
|
|
// Create the new row as a slice of the contents of the old
|
|
|
|
// row, from the cursor to the end of the line
|
2019-08-30 15:10:19 -04:00
|
|
|
let slice = &row_chars[self.cursor_x..];
|
|
|
|
self.insert_row(self.cursor_y + 1, slice);
|
|
|
|
|
|
|
|
self.update_row(self.cursor_y);
|
|
|
|
}
|
|
|
|
|
|
|
|
self.cursor_y += 1;
|
|
|
|
self.cursor_x = 0;
|
|
|
|
}
|
|
|
|
|
2019-08-30 11:20:52 -04:00
|
|
|
fn delete_char(&mut self) {
|
|
|
|
if self.cursor_y == self.rows.len() {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-08-30 15:10:19 -04:00
|
|
|
if self.cursor_x == 0 && self.cursor_y == 0 {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-08-30 11:20:52 -04:00
|
|
|
if self.cursor_x > 0 {
|
|
|
|
self.row_delete_char(self.cursor_y, self.cursor_x - 1);
|
|
|
|
self.cursor_x -= 1;
|
2019-08-30 15:10:19 -04:00
|
|
|
} else {
|
|
|
|
// When deleting the first character in the row, collapse that row into the previous one
|
|
|
|
self.cursor_x = self.rows[self.cursor_y - 1].chars.len();
|
|
|
|
self.row_append_string(self.cursor_y - 1, &self.rows[self.cursor_y].chars.clone());
|
|
|
|
self.delete_row(self.cursor_y);
|
|
|
|
|
|
|
|
self.cursor_y -= 1;
|
2019-08-30 11:20:52 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-27 15:13:32 -04:00
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
// File I/O
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
|
2019-08-29 16:32:17 -04:00
|
|
|
fn rows_to_string(&mut self) -> String {
|
|
|
|
let mut output = String::new();
|
|
|
|
|
2019-08-29 16:45:54 -04:00
|
|
|
for row in &self.rows {
|
|
|
|
// When the file is opened, newlines are stripped
|
|
|
|
// make sure to add them back when saving!
|
|
|
|
let row_chars = row.chars.clone() + "\n";
|
|
|
|
output.push_str(&row_chars)
|
2019-08-29 16:32:17 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
output
|
|
|
|
}
|
|
|
|
|
2019-08-27 15:13:32 -04:00
|
|
|
/// Open a file for display
|
2019-08-27 12:22:19 -04:00
|
|
|
pub fn open(&mut self, filename: &str) -> io::Result<()> {
|
2019-08-28 16:35:48 -04:00
|
|
|
self.filename = filename.to_owned();
|
|
|
|
let file = File::open(&self.filename)?;
|
2019-08-27 12:22:19 -04:00
|
|
|
let buf_reader = BufReader::new(file);
|
|
|
|
|
2019-08-27 15:13:32 -04:00
|
|
|
let lines = buf_reader.lines().map(|l| l.unwrap());
|
2019-08-27 12:22:19 -04:00
|
|
|
|
2019-08-27 15:13:32 -04:00
|
|
|
for line in lines {
|
2019-08-30 15:10:19 -04:00
|
|
|
self.insert_row(self.rows.len(), &line);
|
2019-08-27 15:13:32 -04:00
|
|
|
}
|
2019-08-27 12:22:19 -04:00
|
|
|
|
2019-08-30 11:20:52 -04:00
|
|
|
self.dirty = 0;
|
|
|
|
|
2019-08-27 12:22:19 -04:00
|
|
|
Ok(())
|
|
|
|
}
|
2019-08-29 16:32:17 -04:00
|
|
|
|
|
|
|
fn save(&mut self) -> io::Result<()> {
|
|
|
|
if self.filename.len() == 0 {
|
2019-08-30 16:17:06 -04:00
|
|
|
self.filename = self.prompt("Save as:");
|
2019-08-29 16:32:17 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
let mut file = File::create(&self.filename)?;
|
|
|
|
let data = &mut self.rows_to_string();
|
|
|
|
|
2019-08-30 11:20:52 -04:00
|
|
|
let res = file.write_all(data.as_bytes());
|
|
|
|
|
|
|
|
match res {
|
|
|
|
Ok(()) => {
|
|
|
|
self.dirty = 0;
|
|
|
|
|
|
|
|
self.set_status_message(&format!("{} bytes written to disk", data.len()));
|
|
|
|
}
|
|
|
|
Err(e) => self.set_status_message(&format!("Failed to save: {:?}", e)),
|
|
|
|
};
|
|
|
|
|
2019-08-29 16:32:17 -04:00
|
|
|
file.sync_all()?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2019-08-27 08:30:51 -04:00
|
|
|
}
|