//! Editor functionality use crate::terminal_helpers::*; use std::cmp::PartialEq; use std::fs::File; use std::io; use std::io::prelude::*; use std::io::BufReader; use std::ops::Range; use std::time::{Duration, Instant}; use self::KeyCode::*; // ------------------------------------------------------------------------ // Defines // ------------------------------------------------------------------------ const KILO_TAB_STOP: usize = 4; const KILO_QUIT_TIMES: u8 = 3; // ------------------------------------------------------------------------ // Data // ------------------------------------------------------------------------ // Use an external package's macro to create a memory-safe // bit flag alternative bitflags! { #[derive(Default)] pub struct SyntaxFlags: u32 { const HIGHLIGHT_NUMBERS = 0b00000001; const HIGHLIGHT_STRINGS = 0b00000010; } } /// Configuration for language syntax highlighting #[derive(Clone, Debug, Default, PartialEq)] pub struct Syntax { /// Language name, to be shown in status bar file_type: String, /// File extensions file_match: Vec<&'static str>, /// Keywords keywords1: Vec<&'static str>, /// Type and/or secondary keywords keywords2: Vec<&'static str>, /// How does a single line comment start? singleline_comment_start: String, /// How does a multline comment start? multiline_comment_start: String, /// How does a multiline comment end? multiline_comment_end: String, /// Options for what to highlight flags: SyntaxFlags, } /// Syntax highlighting token types #[derive(Copy, Clone, Debug, PartialEq)] pub enum Highlight { /// No highlighting Normal, /// Single line comments LineComment, /// Multiple line comments MultiLineComment, /// Language keywords Keyword1, /// Language types/ secondary keywords Keyword2, /// Single-line strings String, /// Numbers Number, /// Search results SearchMatch, } /// A representation of a line in the editor #[derive(Clone, Debug, Default)] pub struct Row { /// The 'raw' representation of the original characters chars: String, /// The display characters for the editor render: String, /// The highlighting type for each character highlight: Vec, /// Are we currently highlighting a multi-line comment? highlight_comment_start: bool, } /// 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, syntax: Option, // Properties not present in C version output_buffer: String, quit_times: u8, search_last_match: i32, search_direction: i8, search_last_line: usize, search_last_hightlight: Vec, } /// Keycode mapping enum #[derive(Copy, Clone, Debug, PartialEq)] pub enum KeyCode { Enter, Escape, Backspace, ArrowLeft, ArrowRight, ArrowUp, ArrowDown, DeleteKey, HomeKey, EndKey, PageUp, PageDown, /// Control key chords Ctrl(T), /// Function keys (F1, etc.) T holds the index Function(T), /// Any other type of character OtherKey(T), } impl Syntax { pub fn new( file_type: &str, file_match: Vec<&'static str>, keywords1: Vec<&'static str>, keywords2: Vec<&'static str>, single_line_comment_start: &str, multi_line_comment_start: &str, multi_line_comment_end: &str, flags: SyntaxFlags, ) -> Self { Syntax { file_type: file_type.to_owned(), file_match, keywords1, keywords2, singleline_comment_start: single_line_comment_start.to_owned(), multiline_comment_start: multi_line_comment_start.to_owned(), multiline_comment_end: multi_line_comment_end.to_owned(), flags, } } } impl Row { pub fn new(chars: &str) -> Self { let mut instance = Row::default(); instance.chars = chars.to_owned(); instance } } impl KeyCode { pub fn unwrap(self) -> char { match self { self::Ctrl(val) => val, self::Function(val) => val, self::OtherKey(val) => val, _ => panic!("called `KeyCode::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(), // This is the only reason I had to implement this method // manually, instead of it being derived. Apparently an // `Instant` struct has no default status_message_time: Instant::now(), syntax: None, output_buffer: String::new(), quit_times: KILO_QUIT_TIMES, search_last_match: -1, search_direction: 1, search_last_line: 0, search_last_hightlight: vec![], } } } 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 // ------------------------------------------------------------------------ /// Convert stdin to specific keypresses fn read_key(&mut self) -> Option> { // -------------------------------------------------------------------- // Match single character // -------------------------------------------------------------------- let stdin = io::stdin(); let stdin = stdin.lock(); let mut br = BufReader::with_capacity(5, stdin); let mut first_read = [0; 1]; match br.read_exact(&mut first_read) { Ok(_) => (), Err(e) => { if e.kind() != io::ErrorKind::UnexpectedEof { let error = format!("{:?}", e); self.set_status_message(&error); } } } let first_str = String::from_utf8(first_read.to_vec()); if first_str.is_err() { return None; } let first_str = first_str.unwrap(); // Read the first character, if it isn't escape, just return it let mut chs = first_str.chars(); let ch = chs.next(); match ch { Some(ch) => match ch { '\0' => return None, '\x1b' => (), '\x08' => return Some(Backspace), '\x7f' => return Some(Backspace), '\r' => return Some(Enter), ch => { if ch.is_ascii_control() { return Some(Ctrl(ctrl_to_letter(ch))); } return Some(OtherKey(ch)); } }, None => return None, } // -------------------------------------------------------------------- // Match escape sequence // -------------------------------------------------------------------- let mut seq = [0; 4]; let mut seq_handle = br.take(4); match seq_handle.read(&mut seq) { Ok(_) => (), Err(e) => { if e.kind() != io::ErrorKind::UnexpectedEof { let error = format!("{:?}", e); self.set_status_message(&error); } } } let seq_str = String::from_utf8(seq.to_vec()); // On error, just continue the input loop if seq_str.is_err() { return None; } let seq_str = seq_str.unwrap(); let mut input: Vec> = vec![]; for ch in seq_str.chars() { // Since the fixed array is always filled, there // will be null characters. Ignore these. if ch == '\0' { continue; } input.push(match ch { '\x1b' => Escape, _ => OtherKey(ch), }); } // Since we matched Escape earlier, if the input is empty, // this must be the escape key if input.is_empty() { return Some(Escape); } match input.len() { 4 => { // Escape code of the form `^[[NM~` if input[3].eq(&OtherKey('~')) { let action = match (input[1].unwrap(), input[2].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); } } 3 => { // Escape code of the form `^[[N~` if input[2].eq(&OtherKey('~')) { let action = match input[1].unwrap() { '1' => HomeKey, '3' => DeleteKey, '4' => EndKey, '5' => PageUp, '6' => PageDown, '7' => HomeKey, '8' => EndKey, _ => Escape, }; return Some(action); } } 2 => { match input[0] { // Escape code of the form `^[[X` OtherKey('[') => { let action = match input[1].unwrap() { 'A' => ArrowUp, 'B' => ArrowDown, 'C' => ArrowRight, 'D' => ArrowLeft, 'H' => HomeKey, 'F' => EndKey, // Eh, just return escape otherwise _ => Escape, }; return Some(action); } // Escape code of the form `^[OX` OtherKey('O') => { let action = match input[1].unwrap() { 'H' => HomeKey, 'F' => EndKey, 'P' => Function('1'), 'Q' => Function('2'), 'R' => Function('3'), 'S' => Function('4'), _ => Escape, }; return Some(action); } _ => return Some(Escape), } } _ => return Some(input[0]), } // If the character doesn't match any escape sequences, just // pass that character on return Some(input[0]); } /// Get terminal size in rows and columns fn get_window_size(&mut self) -> TermSize { match get_term_size() { Some(size) => size, None => get_cursor_position(), } } // ------------------------------------------------------------------------ // Syntax Highlighting // ------------------------------------------------------------------------ fn update_syntax(&mut self, index: usize) { let rows = &mut self.rows; let prev_row = if index > 0 { // I shouldn't have to clone this, but the lifetime is // different than the `row` variable above, so it // can't be a immutable borrow. It also can't be a // mutable borrow, because it would be considered a // second mutable borrow...so a clone it is Some((&mut rows[index - 1]).clone()) } else { None }; let row = &mut rows[index]; let render_len = row.render.len(); // Reset the highlighting of the row row.highlight = vec![Highlight::Normal; render_len]; if self.syntax.is_none() { return; } // This is dumb. This lets you get a reference to the item in // the option, by turning Option into Option<&T>, // which can then be unwrapped. let current_syntax = self.syntax.as_ref().unwrap(); let keywords1 = ¤t_syntax.keywords1; let keywords2 = ¤t_syntax.keywords2; let scs = ¤t_syntax.singleline_comment_start; let mcs = ¤t_syntax.multiline_comment_start; let mce = ¤t_syntax.multiline_comment_end; let mut prev_separator = true; let mut in_string = false; let mut str_start = '\0'; let mut in_comment = prev_row.map_or(false, |row| row.highlight_comment_start); let mut i = 0; let bytes = row.render.clone().into_bytes(); while i < render_len { let c = bytes[i] as char; let prev_highlight = if i > 0 { row.highlight[i - 1] } else { Highlight::Normal }; // Single line comments if scs.len() > 0 && !in_string && !in_comment { let range = get_slice_range(i, scs.len(), render_len); if &row.render[range] == scs { // Pretty simple, highlight from the match to the end of the line highlight_range(&mut row.highlight, i..render_len, Highlight::LineComment); break; } } // Multi-line comments if mcs.len() > 0 && mce.len() > 0 && !in_string { let mce_range = get_slice_range(i, mce.len(), render_len); let mcs_range = get_slice_range(i, mcs.len(), render_len); if in_comment { row.highlight[i] = Highlight::MultiLineComment; // End of a comment if &row.render[mce_range.clone()] == mce { highlight_range(&mut row.highlight, mce_range, Highlight::MultiLineComment); i += mce.len(); in_comment = false; prev_separator = true; continue; } else { i += 1; continue; } } else if &row.render[mcs_range.clone()] == mcs { // Start of a multi-line comment highlight_range(&mut row.highlight, mcs_range, Highlight::MultiLineComment); i += mcs.len(); in_comment = true; continue; } } // Strings if current_syntax .flags .contains(SyntaxFlags::HIGHLIGHT_STRINGS) { if in_string { row.highlight[i] = Highlight::String; // Don't end highlighting for a string on an escaped quote if c == '\\' && i + 1 < render_len { row.highlight[i + 1] = Highlight::String; i += 2; continue; } // End delimiter for the string if c == str_start { in_string = false; str_start = '\0'; } i += 1; prev_separator = true; continue; } else { if (c == '"' || c == '\'') && prev_separator { in_string = true; str_start = c; row.highlight[i] = Highlight::String; i += 1; continue; } } } // Numbers if current_syntax .flags .contains(SyntaxFlags::HIGHLIGHT_NUMBERS) { if (c.is_ascii_digit() && (prev_separator || prev_highlight == Highlight::Number)) || (c == '.' && prev_highlight == Highlight::Number) { row.highlight[i] = Highlight::Number; i += 1; prev_separator = false; continue; } } // Keywords if prev_separator { for &keyword in keywords1 { if i + keyword.len() >= render_len { continue; } let search_range = get_slice_range(i, keyword.len(), render_len); let next_char_offset = i + keyword.len(); let is_end_of_line = next_char_offset >= render_len; let next_char = if is_end_of_line { '\0' } else { bytes[next_char_offset] as char }; if &row.render[search_range.clone()] == keyword && is_separator(next_char) { highlight_range(&mut row.highlight, search_range, Highlight::Keyword1); i += keyword.len() - 1; } } for &keyword in keywords2 { if i + keyword.len() >= render_len { continue; } let search_range = get_slice_range(i, keyword.len(), render_len); let next_char_offset = i + keyword.len(); let is_end_of_line = next_char_offset >= render_len; let next_char = if is_end_of_line { '\0' } else { bytes[next_char_offset] as char }; if &row.render[search_range.clone()] == keyword && is_separator(next_char) { highlight_range(&mut row.highlight, search_range, Highlight::Keyword2); i += keyword.len() - 1; } } } prev_separator = is_separator(c); i += 1; } let changed = row.highlight_comment_start != in_comment; row.highlight_comment_start = in_comment; // If a multi-line comment is opened or closed, // update the next row as well. if changed && index + 1 < self.rows.len() { self.update_syntax(index + 1); } } fn syntax_to_color(syntax_type: Highlight) -> i32 { use Highlight::*; match syntax_type { Keyword1 => 33, // Yellow Keyword2 => 32, // Green LineComment => 36, // Cyan MultiLineComment => 90, // Bright Black Normal => 37, // White Number => 31, // Red SearchMatch => 7, // Reverse! String => 35, // Magenta } } fn select_syntax_highlight(&mut self) { if self.filename.is_empty() { return; } let mut parts: Vec<&str> = self.filename.split('.').collect(); let file_ext = String::from(".") + match parts.pop() { Some(ext) => ext, None => return, }; let languages = &get_syntax_db(); for language in languages { let file_match = language.file_match.clone(); for ext in file_match { if ext == file_ext { self.syntax = Some(language.clone()); // Re-highlight file when the type is determined for x in 0..self.rows.len() { self.update_syntax(x); } return; } } } } // ------------------------------------------------------------------------ // Input // ------------------------------------------------------------------------ fn prompt( &mut self, prompt: &str, cb: Option<&mut dyn Fn(&mut Self, &str, KeyCode)>, ) -> String { let mut buffer = String::new(); let default_cb = &mut Self::_noop_prompt_cb; let cb = if cb.is_some() { cb.unwrap() } else { default_cb }; loop { self.set_status_message(&format!("{} {}", prompt, buffer)); self.refresh_screen(); let ch = self.read_key(); if ch.is_some() { let ch = ch.unwrap(); match ch { Backspace => { buffer.pop(); } DeleteKey => { buffer.pop(); } Escape => { self.set_status_message(""); cb(self, &String::from(""), ch); return String::from(""); } Enter => { if buffer.len() != 0 { self.set_status_message(""); cb(self, &buffer, ch); return buffer; } } OtherKey(ch) => { if (!ch.is_ascii_control()) && (ch as u8) < 128 { buffer.push(ch); // continue; } } _ => (), }; cb(self, &buffer, ch); } } } fn _noop_prompt_cb(&mut self, _: &str, _: KeyCode) {} fn move_cursor(&mut self, key: &KeyCode) { 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 ch = key.unwrap(); match ch { 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(); } } Ctrl(c) => match c { 'f' => self.find(), 's' => { // Save success/error message handled by save method match self.save() { Ok(_) => (), Err(_) => (), } } '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; } _ => (), } Function(_) => (), OtherKey(c) => { 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: KeyCode) { if key == DeleteKey { self.move_cursor(&ArrowRight); } self.delete_char(); } fn _page_up_or_down(&mut self, key: KeyCode) { 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 append_out_char(&mut self, ch: char) { self.output_buffer.push(ch); } 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, self.cursor_x); } // 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_char('~'); padding -= 1; } for _ in 0..padding { self.append_out_char(' '); } self.append_out(&welcome); } else { self.append_out_char('~'); } } else { let output = self.rows[file_row].render.clone(); let mut current_color: i32 = -1; for (x, ch) in output.char_indices() { if ch.is_ascii_control() { // Display unprintable characters in inverted colors let sym = if ch as u8 <= 26 { ('@' as u8 + ch as u8) as char } else { '?' }; self.append_out("\x1b[7m"); self.append_out_char(sym); self.append_out("\x1b[m"); if current_color != -1 { let code = format!("\x1b[{}m", current_color); self.append_out(&code); } } else if self.rows[file_row].highlight[x] == Highlight::Normal { if current_color != -1 { self.append_out("\x1b[0m"); self.append_out("\x1b[39m"); current_color = -1; } self.append_out_char(ch); } else { let color = Self::syntax_to_color(self.rows[file_row].highlight[x]); if color != current_color { current_color = color; let code = format!("\x1b[{}m", color); self.append_out("\x1b[0m"); self.append_out(&code); } self.append_out_char(ch); } } self.append_out("\x1b[0m"); self.append_out("\x1b[39m"); } 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 file_type = match &self.syntax { Some(s) => &s.file_type, None => "no ft", }; let right_message = format!("{} | {}/{}", file_type, 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) { 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[{};{}H", y, x); self.append_out(&cursor_code.as_str()); // Show cursor self.append_out("\x1b[?25h"); let stdout = io::stdout(); let mut handle = stdout.lock(); // If you can't write to stdout, you might as well just panic handle.write_all(&self.output_buffer.as_bytes()).unwrap(); } /// 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 // ------------------------------------------------------------------------ /// Convert cursor x position to the rendered x position fn row_cx_to_rx(&mut self, index: usize, cx: usize) -> usize { let mut rx: usize = 0; for (i, ch) in self.rows[index].chars.char_indices() { if i == cx { return rx; } if ch == '\t' { rx += (KILO_TAB_STOP - 1) - (rx % KILO_TAB_STOP); } rx += 1; } rx } /// Convert rendered x position to cursor x position fn row_rx_to_cx(&mut self, index: usize, rx: usize) -> usize { let mut current_rx: usize = 0; let mut cx: usize = 0; for ch in self.rows[index].chars.chars() { if ch == '\t' { current_rx += (KILO_TAB_STOP - 1) - (current_rx % KILO_TAB_STOP); } current_rx += 1; if current_rx > rx { return cx; } cx += 1; } cx } /// Convert file characters to their display equivalents 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; // Syntax highlighting self.update_syntax(index); } fn insert_row(&mut self, at: usize, content: &str) { if at > self.rows.len() { return; } let row = Row::new(content); 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 { // Clone the contents of the current row let row = &mut self.rows[self.cursor_y]; let row_chars = row.chars.clone(); // Truncate the original row up to the cursor row.chars.truncate(self.cursor_x); // Create the new row as a slice of the contents of the old // row, from the cursor to the end of the line 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! output += &row.chars; output.push('\n'); } output } /// Open a file for display pub fn open(&mut self, filename: &str) -> io::Result<()> { self.filename = filename.to_owned(); self.select_syntax_highlight(); let file = File::open(&self.filename)?; let buf_reader = BufReader::new(file); let lines = buf_reader.lines().map(|l| clean_unwrap(l)); 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 { self.filename = self.prompt("Save as (ESC to cancel):", None); if self.filename.len() == 0 { self.set_status_message("Save aborted"); return Ok(()); } self.select_syntax_highlight(); } 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(()) } // ------------------------------------------------------------------------ // Find // ------------------------------------------------------------------------ fn find_callback(&mut self, query: &str, key: KeyCode) { if !self.search_last_hightlight.is_empty() { self.rows[self.search_last_line].highlight = self.search_last_hightlight.clone(); self.search_last_hightlight.clear(); } if key == Enter || key == Escape { self.search_last_match = -1; self.search_direction = 1; return; } else if key == ArrowRight || key == ArrowDown { self.search_direction = 1; } else if key == ArrowLeft || key == ArrowUp { self.search_direction = -1; } else { self.search_last_match = -1; self.search_direction = 1; } if self.search_last_match == -1 { self.search_direction = 1; } if query.is_empty() { return; } let mut current = self.search_last_match; for x in 0..self.rows.len() { current += self.search_direction as i32; if current == -1 { current = self.rows.len() as i32 - 1; } else if current == self.rows.len() as i32 { current = 0; } match self.rows[current as usize].render.find(query) { None => (), Some(start) => { self.search_last_match = current; self.cursor_y = current as usize; self.cursor_x = self.row_rx_to_cx(x, start); self.row_offset = self.rows.len(); self.search_last_line = current as usize; self.search_last_hightlight = self.rows[current as usize].highlight.clone(); // Highlight matching search result let len = start + query.len(); highlight_range( &mut self.rows[current as usize].highlight, start..len, Highlight::SearchMatch, ); break; } } } } fn find(&mut self) { let saved_cx = self.cursor_x; let saved_cy = self.cursor_y; let saved_coloff = self.col_offset; let saved_rowoff = self.row_offset; let query = self.prompt( "Search (Use ESC/Arrows/Enter):", Some(&mut Self::find_callback), ); if query.is_empty() { self.cursor_x = saved_cx; self.cursor_y = saved_cy; self.col_offset = saved_coloff; self.row_offset = saved_rowoff; } } } // ------------------------------------------------------------------------ // Functions // ------------------------------------------------------------------------ /// Get the language highlighting config fn get_syntax_db() -> Vec { vec![ Syntax::new( "C/C++", vec![".c", ".h", ".cpp"], vec![ "continue", "typedef", "switch", "return", "static", "while", "break", "struct", "union", "class", "else", "enum", "for", "case", "if", ], vec![ "#include", "unsigned", "#define", "#ifndef", "double", "signed", "#endif", "#ifdef", "float", "#error", "#undef", "long", "char", "int", "void", "#if", ], "//", "/*", "*/", SyntaxFlags::HIGHLIGHT_NUMBERS | SyntaxFlags::HIGHLIGHT_STRINGS, ), Syntax::new( "Rust", vec![".rs"], vec![ "continue", "return", "static", "struct", "unsafe", "break", "const", "crate", "extern", "match", "super", "trait", "where", "else", "enum", "false", "impl", "loop", "move", "self", "type", "while", "for", "let", "mod", "pub", "ref", "true", "use", "mut", "as", "fn", "if", "in", ], vec![ "DoubleEndedIterator", "ExactSizeIterator", "IntoIterator", "PartialOrd", "PartialEq", "Iterator", "ToString", "Default", "ToOwned", "Extend", "FnOnce", "Option", "String", "AsMut", "AsRef", "Clone", "Debug", "FnMut", "Sized", "Unpin", "array", "isize", "usize", "&str", "Copy", "Drop", "From", "Into", "None", "Self", "Send", "Some", "Sync", "Sync", "bool", "char", "i128", "u128", "Box", "Err", "Ord", "Vec", "dyn", "f32", "f64", "i16", "i32", "i64", "str", "u16", "u32", "u64", "Eq", "Fn", "Ok", "i8", "u8", ], "//", "/*", "*/", SyntaxFlags::HIGHLIGHT_NUMBERS | SyntaxFlags::HIGHLIGHT_STRINGS, ), Syntax::new( "JavaScript/TypeScript", vec![".js", ".mjs", ".jsx", ".ts", ".tsx"], vec![ "instanceof", "continue", "debugger", "function", "default", "extends", "finally", "delete", "export", "import", "return", "switch", "typeof", "break", "catch", "class", "const", "super", "throw", "while", "yield", "case", "else", "this", "void", "with", "from", "for", "new", "try", "var", "do", "if", "in", "as", ], vec![ "=>", "Number", "String", "Object", "Math", "JSON", "Boolean", ], "//", "/*", "*/", SyntaxFlags::HIGHLIGHT_NUMBERS | SyntaxFlags::HIGHLIGHT_STRINGS, ), ] } /// Convert Ctrl+letter chord to their /// letter equivalent pub fn ctrl_to_letter(c: char) -> char { let key = c as u8; if (!c.is_ascii_control()) || c == '\x7f' { panic!("Only ascii control characters have associated letters") } // Shift forward to the letter equivalent (key + 0x60) as char } /// Determine whether a character is one which separates tokens /// in the language to highlight fn is_separator(input_char: char) -> bool { if input_char.is_ascii_whitespace() || input_char == '\0' { return true; } let separator_chars = ",.()+-/*=~%<>[];"; for ch in separator_chars.chars() { if input_char == ch { return true; } } false } /// Get a range for a slice of a string or vector, checking the length of the /// string or vector to prevent panics on invalid access. /// /// If `start` to `start + search_len`, is within the size of the search target (`haystack_len`) /// that range is returned. Otherwise, the range is from `start` to `haystack_len`. fn get_slice_range(start: usize, needle_len: usize, haystack_len: usize) -> Range { let search_len = start + needle_len; if search_len >= haystack_len { start..haystack_len } else { start..search_len } } /// Set the highlighting type for the specified range /// Kind of similar to the C memset calls fn highlight_range(vec: &mut Vec, range: Range, value: Highlight) { for x in range { vec[x] = value; } } #[cfg(test)] mod tests { use super::*; #[test] fn select_syntax_highlight_selects_language() { let langs = get_syntax_db(); let mut editor = Editor::new(); editor.filename = String::from("foo.c"); editor.select_syntax_highlight(); assert_eq!(editor.syntax.as_ref(), Some(&langs[0])); } #[test] fn is_separator_works() { // Check each explicit character for ch in ",.()+-/*=~%<>[];".chars() { assert_eq!(is_separator(ch), true); } // Check each whitespace character for ch in " \t\n\r\x0c".chars() { assert_eq!( is_separator(ch), true, "Character {:#} should be a separator", ch as u8 ); } // Letters are not separators! for ch in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_".chars() { assert_eq!(is_separator(ch), false); } } #[test] fn ctrl_to_letter_() { let a = ctrl_to_letter('\x01'); assert_eq!(a, 'a', "ctrl_to_letter gives letter from ctrl chord"); } #[test] #[should_panic] fn ctrl_to_letter_panic() { // Del code doesn't map to Ctrl+letter combo ctrl_to_letter('\x7f'); } }