//! Editor functionality use crate::helpers::*; use std::cmp::PartialEq; use std::fs::File; use std::io; use std::io::prelude::*; use std::io::BufReader; use std::time::{Duration, Instant}; use self::EditorKey::*; const KILO_TAB_STOP: usize = 4; const KILO_QUIT_TIMES: u8 = 3; /// A representation of a line in the editor #[derive(Debug, Default)] pub struct EditorRow { chars: String, render: String, } impl EditorRow { pub fn new(chars: &str) -> Self { let mut instance = EditorRow::default(); instance.chars = chars.to_owned(); instance } } /// Main structure for the editor /// `EditorConfig` struct in C version #[derive(Debug)] pub struct Editor { cursor_x: usize, cursor_y: usize, render_x: usize, col_offset: usize, row_offset: usize, screen_cols: usize, screen_rows: usize, rows: Vec, dirty: u64, filename: String, status_message: String, status_message_time: Instant, // Properties not present in C version output_buffer: String, quit_times: u8, } /// Keycode mapping enum #[derive(Copy, Clone, Debug, PartialEq)] pub enum EditorKey { Enter, Escape, Backspace, ArrowLeft, ArrowRight, ArrowUp, ArrowDown, DeleteKey, HomeKey, EndKey, PageUp, PageDown, /// Function keys (F1, etc.) T holds the index Function(T), /// Any other type of character OtherKey(T), } impl EditorKey { pub fn unwrap(self) -> char { match self { self::OtherKey(val) => val, _ => panic!("called `EditorKey::unwrap()` on a `None` value"), } } } 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![], dirty: 0, filename: String::new(), status_message: String::new(), status_message_time: Instant::now(), output_buffer: String::new(), quit_times: KILO_QUIT_TIMES, } } } impl Editor { // ------------------------------------------------------------------------ // Init // ------------------------------------------------------------------------ pub fn new() -> Self { let mut instance = Self::default(); let size = instance.get_window_size(); instance.screen_cols = size.cols as usize; instance.screen_rows = (size.rows - 2) as usize; instance } // ------------------------------------------------------------------------ // Terminal // ------------------------------------------------------------------------ fn read_key(&mut self) -> Option> { /* TODO: Read 1 byte by default, and read additional bytes if the first byte is an escape character, to resolve the unintentional input throttling */ let stdin = io::stdin(); let stdin = stdin.lock(); let mut in_str = String::new(); let mut buffer = BufReader::with_capacity(4, stdin); buffer.read_to_string(&mut in_str).unwrap(); let mut input: Vec> = vec![]; for char in in_str.chars() { input.push(match char { '\x08' => Backspace, '\x7f' => Backspace, '\x1b' => Escape, '\r' => Enter, _ => OtherKey(char), }); } if input.is_empty() { return None; } 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); } } 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, _ => input[2], // Escape, }; return Some(action); } } 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 _ => input[2], //Escape, }; return Some(action); } // 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'), _ => input[2], //Escape, }; return Some(action); } _ => return Some(input[1]), } } _ => return Some(input[0]), } } // If the character doesn't match any escape sequences, just // pass that character on return Some(input[0]); } fn get_window_size(&mut self) -> TermSize { match get_term_size() { Some(size) => size, None => unimplemented!("The easy way usually works"), } } // ------------------------------------------------------------------------ // Input // ------------------------------------------------------------------------ fn move_cursor(&mut self, key: &EditorKey) { let row = self.rows.get(self.cursor_y); match key { ArrowLeft => { if self.cursor_x != 0 { // Move cursor left self.cursor_x -= 1; } 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(); } } ArrowRight => { if row.is_some() && self.cursor_x < row.unwrap().chars.len() { // Move cursor right self.cursor_x += 1; } 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; } } ArrowUp => { if self.cursor_y > 0 { self.cursor_y -= 1; } } ArrowDown => { if self.cursor_y < self.rows.len() { self.cursor_y += 1; } } _ => (), }; 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; } } /// Route user input to the appropriate handler method pub fn process_keypress(&mut self) -> Option> { let key = self.read_key(); if key.is_some() { let char = key.unwrap(); match char { Backspace => self._del_or_backspace(Backspace), DeleteKey => self._del_or_backspace(DeleteKey), Enter => self.insert_new_line(), Escape => (), ArrowUp => self.move_cursor(&ArrowUp), ArrowDown => self.move_cursor(&ArrowDown), ArrowLeft => self.move_cursor(&ArrowLeft), ArrowRight => self.move_cursor(&ArrowRight), PageUp => self._page_up_or_down(PageUp), PageDown => self._page_up_or_down(PageDown), HomeKey => { self.cursor_x = 0; } EndKey => { if self.cursor_y < self.rows.len() { self.cursor_x = self.rows[self.cursor_y].chars.len(); } } OtherKey(c) => { if c.is_ascii_control() { if c == ctrl_key('q') { 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')); } print!("\x1b[2J"); print!("\x1b[H"); // Break out of the input loop return None; } if c == ctrl_key('s') { // Save success/error message handled by save method match self.save() { Ok(_) => (), Err(_) => (), } } if c == ctrl_key('h') { self._del_or_backspace(Backspace); } } else { self.insert_char(c); } } _ => (), }; self.quit_times = KILO_QUIT_TIMES; return key; } // Continue the main input loop Some(OtherKey('\0')) } fn _del_or_backspace(&mut self, key: EditorKey) { if key == DeleteKey { self.move_cursor(&ArrowRight); } self.delete_char(); } fn _page_up_or_down(&mut self, key: EditorKey) { let mut times = self.screen_rows; // 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 while times > 1 { times -= 1; self.move_cursor(match key { PageUp => &ArrowUp, PageDown => &ArrowDown, _ => &OtherKey('\0'), }) } } // ------------------------------------------------------------------------ // Output // ------------------------------------------------------------------------ /// 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); } fn scroll(&mut self) { self.render_x = 0; if self.cursor_y < self.rows.len() { self.render_x = self.row_cx_to_rx(self.cursor_y); } // Vertical scrolling 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; } // 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; } } fn draw_rows(&mut self) { for y in 0..self.screen_rows { let file_row = y + self.row_offset; if file_row >= self.rows.len() { if self.rows.is_empty() && y == (self.screen_rows / 3) { let mut welcome = format!( "Oxidized Kilo editor -- version {}", env!("CARGO_PKG_VERSION") ); 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; } for _ in 0..padding { self.append_out(" "); } self.append_out(&welcome); } else { self.append_out("~"); } } else { let mut len = self.rows[file_row].render.len() - self.col_offset; if len > self.screen_cols { len = self.screen_cols; } 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]); } self.append_out("\x1b[K"); self.append_out("\r\n"); } } fn draw_status_bar(&mut self) { self.append_out("\x1b[7m"); let filename = if self.filename.is_empty() { "[No Name]" } else { &self.filename }; let modified = if self.dirty > 0 { "(modified}" } else { "" }; let mut left_message = format!("{:.80} - {} lines {}", filename, self.rows.len(), modified); let right_message = format!("{}/{}", self.cursor_y + 1, self.rows.len()); let mut len = left_message.len(); if len > self.screen_cols { len = self.screen_cols; left_message.truncate(len); } self.append_out(&left_message); for x in len..self.screen_cols { if self.screen_cols - x == right_message.len() { self.append_out(&right_message); break; } self.append_out(" "); } self.append_out("\x1b[m"); 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); } } pub fn refresh_screen(&mut self) -> io::Result<()> { self.scroll(); self.output_buffer.clear(); // Hide cursor, reposition cursor self.append_out("\x1b[?25l"); self.append_out("\x1b[H"); self.draw_rows(); self.draw_status_bar(); self.draw_message_bar(); // Move cursor to state position let y = (self.cursor_y - self.row_offset) + 1; let x = (self.render_x - self.col_offset) + 1; let cursor_code = format!("\x1b[{y};{x}H", y = y, x = x); self.append_out(&cursor_code); // Show cursor self.append_out("\x1b[?25h"); let stdout = io::stdout(); let mut handle = stdout.lock(); handle.write_all(&self.output_buffer.as_bytes()) } /// 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(); } // ------------------------------------------------------------------------ // Row Operations // ------------------------------------------------------------------------ fn row_cx_to_rx(&mut self, index: usize) -> usize { 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 } 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; } 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); self.dirty += 1; } 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); self.dirty += 1; } 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; } 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; } // ------------------------------------------------------------------------ // Editor Operations // ------------------------------------------------------------------------ fn insert_char(&mut self, ch: char) { if self.cursor_y == self.rows.len() { self.insert_row(self.rows.len(), ""); } self.row_insert_char(self.cursor_y, self.cursor_x, ch); self.cursor_x += 1; } fn insert_new_line(&mut self) { if self.cursor_x == 0 { self.insert_row(self.cursor_y, ""); } else { let row = &mut self.rows[self.cursor_y]; let row_chars = row.chars.clone(); let len = row.chars.len() - self.cursor_x; // Truncate the old row if you aren't at the end of a line if self.cursor_x < row.chars.len() { row.chars.truncate(self.cursor_x); } 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; } fn delete_char(&mut self) { if self.cursor_y == self.rows.len() { return; } if self.cursor_x == 0 && self.cursor_y == 0 { return; } if self.cursor_x > 0 { self.row_delete_char(self.cursor_y, self.cursor_x - 1); self.cursor_x -= 1; } 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; } } // ------------------------------------------------------------------------ // File I/O // ------------------------------------------------------------------------ fn rows_to_string(&mut self) -> String { let mut output = String::new(); 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) } output } /// Open a file for display pub fn open(&mut self, filename: &str) -> io::Result<()> { self.filename = filename.to_owned(); let file = File::open(&self.filename)?; let buf_reader = BufReader::new(file); let lines = buf_reader.lines().map(|l| l.unwrap()); for line in lines { self.insert_row(self.rows.len(), &line); } self.dirty = 0; Ok(()) } fn save(&mut self) -> io::Result<()> { if self.filename.len() == 0 { return Ok(()); } let mut file = File::create(&self.filename)?; let data = &mut self.rows_to_string(); 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)), }; file.sync_all()?; Ok(()) } }