diff --git a/src/editor.rs b/src/editor.rs index ddb040f..6ee5bc1 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -6,9 +6,10 @@ 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::EditorKey::*; +use self::KeyCode::*; // ------------------------------------------------------------------------ // Defines @@ -25,75 +26,82 @@ const KILO_QUIT_TIMES: u8 = 3; // bit flag alternative bitflags! { #[derive(Default)] - pub struct EditorSyntaxFlags: u32 { + pub struct SyntaxFlags: u32 { const HIGHLIGHT_NUMBERS = 0b00000001; const HIGHLIGHT_STRINGS = 0b00000010; } } +/// Configuration for language syntax highlighting #[derive(Clone, Debug, Default, PartialEq)] -pub struct EditorSyntax { +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, - flags: EditorSyntaxFlags, -} - -impl EditorSyntax { - 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: EditorSyntaxFlags, - ) -> Self { - EditorSyntax { - 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, - } - } + + /// 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(Debug, Default)] -pub struct EditorRow { +#[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, -} -impl EditorRow { - pub fn new(chars: &str) -> Self { - let mut instance = EditorRow::default(); - instance.chars = chars.to_owned(); - - instance - } + /// Are we currently highlighting a multi-line comment? + highlight_comment_start: bool, } /// Main structure for the editor @@ -107,12 +115,12 @@ pub struct Editor { row_offset: usize, screen_cols: usize, screen_rows: usize, - rows: Vec, + rows: Vec, dirty: u64, filename: String, status_message: String, status_message_time: Instant, - syntax: Option, + syntax: Option, // Properties not present in C version output_buffer: String, @@ -125,7 +133,7 @@ pub struct Editor { /// Keycode mapping enum #[derive(Copy, Clone, Debug, PartialEq)] -pub enum EditorKey { +pub enum KeyCode { Enter, Escape, Backspace, @@ -144,11 +152,44 @@ pub enum EditorKey { OtherKey(T), } -impl EditorKey { +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::OtherKey(val) => val, - _ => panic!("called `EditorKey::unwrap()` on a `None` value"), + _ => panic!("called `KeyCode::unwrap()` on a `None` value"), } } } @@ -201,7 +242,7 @@ impl Editor { // ------------------------------------------------------------------------ /// Convert stdin to specific keypresses - fn read_key(&mut self) -> Option> { + fn read_key(&mut self) -> Option> { // -------------------------------------------------------------------- // Match single character // -------------------------------------------------------------------- @@ -225,19 +266,18 @@ impl Editor { let first_str = first_str.unwrap(); // Read the first character, if it isn't escape, just return it - let mut chars = first_str.chars(); - let char = chars.next(); - if char.is_none() { - return None; - } - let char = char.unwrap(); - match char { - '\0' => return None, - '\x1b' => (), - '\x08' => return Some(Backspace), - '\x7f' => return Some(Backspace), - '\r' => return Some(Enter), - c => return Some(OtherKey(c)), + 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), + _ => return Some(OtherKey(ch)), + }, + None => return None, } // -------------------------------------------------------------------- @@ -261,18 +301,18 @@ impl Editor { } let seq_str = seq_str.unwrap(); - let mut input: Vec> = vec![]; + let mut input: Vec> = vec![]; - for char in seq_str.chars() { + for ch in seq_str.chars() { // Since the fixed array is always filled, there // will be null characters. Ignore these. - if char == '\0' { + if ch == '\0' { continue; } - input.push(match char { + input.push(match ch { '\x1b' => Escape, - _ => OtherKey(char), + _ => OtherKey(ch), }); } @@ -372,7 +412,20 @@ impl Editor { // ------------------------------------------------------------------------ fn update_syntax(&mut self, index: usize) { - let row = &mut self.rows[index]; + 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 + Some((&mut rows[index - 1]).clone()) + } else { + None + }; + + let row = &mut rows[index]; row.highlight = vec![Highlight::Normal; row.render.len()]; if self.syntax.is_none() { @@ -394,7 +447,7 @@ impl Editor { let mut prev_separator = false; let mut in_string = false; let mut str_start = '\0'; - let mut in_comment = false; + 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(); @@ -407,37 +460,33 @@ impl Editor { }; // Single line comments - if scs.len() > 0 && !in_string { - let comment = row.render.find(scs); - if comment.is_some() { + if scs.len() > 0 && !in_string && !in_comment { + let range = get_slice_range(i as usize, scs.len(), row.render.len()); + if &row.render[range] == scs { // Pretty simple, highlight from the match to the end of the line - let comment_start = comment.unwrap(); - for x in comment_start..row.render.len() { - row.highlight[x] = Highlight::LineComment; - } + highlight_range( + &mut row.highlight, + i as usize..row.render.len(), + Highlight::LineComment, + ); + break; } } // Multi-line comments if mcs.len() > 0 && mce.len() > 0 && !in_string { - let mce_slice_range = if i as usize + mce.len() >= row.render.len() { - i as usize..row.render.len() - } else { - i as usize..(i as usize + mce.len()) - }; - let mcs_slice_range = if i as usize + mcs.len() >= row.render.len() { - i as usize..row.render.len() - } else { - i as usize..(i as usize + mcs.len()) - }; + let mce_slice_range = get_slice_range(i as usize, mce.len(), row.render.len()); + let mcs_slice_range = get_slice_range(i as usize, mcs.len(), row.render.len()); if in_comment { row.highlight[i as usize] = Highlight::MultiLineComment; // End of a comment if &row.render[mce_slice_range.clone()] == mce { - for x in mce_slice_range { - row.highlight[x] = Highlight::MultiLineComment; - } + highlight_range( + &mut row.highlight, + mce_slice_range, + Highlight::MultiLineComment, + ); i += mce.len(); in_comment = false; @@ -449,9 +498,11 @@ impl Editor { } } else if &row.render[mcs_slice_range.clone()] == mcs { // Start of a multi-line comment - for x in mcs_slice_range { - row.highlight[x] = Highlight::MultiLineComment; - } + highlight_range( + &mut row.highlight, + mcs_slice_range, + Highlight::MultiLineComment, + ); i += mcs.len(); in_comment = true; @@ -462,7 +513,7 @@ impl Editor { // Strings if current_syntax .flags - .contains(EditorSyntaxFlags::HIGHLIGHT_STRINGS) + .contains(SyntaxFlags::HIGHLIGHT_STRINGS) { if in_string { row.highlight[i as usize] = Highlight::String; @@ -495,7 +546,7 @@ impl Editor { // Numbers if current_syntax .flags - .contains(EditorSyntaxFlags::HIGHLIGHT_NUMBERS) + .contains(SyntaxFlags::HIGHLIGHT_NUMBERS) { if (c.is_ascii_digit() && (prev_separator || prev_highlight == Highlight::Number)) || (c == '.' && prev_highlight == Highlight::Number) @@ -510,48 +561,38 @@ impl Editor { // Keywords if prev_separator { for &keyword in keywords1 { - let matches = row.render.match_indices(keyword); - for (start, _) in matches { - let next_char_offset = start + keyword.len() + 1; - let is_end_of_line = next_char_offset >= row.render.len(); - let next_char = if is_end_of_line { - '\0' - } else { - bytes[next_char_offset] as char - }; + let search_range = get_slice_range(i as usize, keyword.len(), row.render.len()); - if is_separator(next_char) { - let end = start + keyword.len(); - for x in start..end { - row.highlight[x] = Highlight::Keyword1; - } - i += keyword.len(); - prev_separator = false; - continue 'outer; - } + let next_char_offset = i as usize + keyword.len() + 1; + let is_end_of_line = next_char_offset >= row.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(); + break; } } for &keyword in keywords2 { - let matches = row.render.match_indices(keyword); - for (start, _) in matches { - let next_char_offset = start + keyword.len() + 1; - let is_end_of_line = next_char_offset >= row.render.len(); - let next_char = if is_end_of_line { - '\0' - } else { - bytes[next_char_offset] as char - }; + let search_range = get_slice_range(i as usize, keyword.len(), row.render.len()); - if is_separator(next_char) { - let end = start + keyword.len(); - for x in start..end { - row.highlight[x] = Highlight::Keyword2; - } - i += keyword.len(); - prev_separator = false; - continue 'outer; - } + let next_char_offset = i as usize + keyword.len() + 1; + let is_end_of_line = next_char_offset >= row.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(); + break; } } } @@ -559,6 +600,15 @@ impl Editor { 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 { @@ -615,7 +665,7 @@ impl Editor { fn prompt( &mut self, prompt: &str, - cb: Option<&mut dyn Fn(&mut Self, &str, EditorKey)>, + cb: Option<&mut dyn Fn(&mut Self, &str, KeyCode)>, ) -> String { let mut buffer = String::new(); let default_cb = &mut Self::_noop_prompt_cb; @@ -666,9 +716,9 @@ impl Editor { } } - fn _noop_prompt_cb(&mut self, _: &str, _: EditorKey) {} + fn _noop_prompt_cb(&mut self, _: &str, _: KeyCode) {} - fn move_cursor(&mut self, key: &EditorKey) { + fn move_cursor(&mut self, key: &KeyCode) { let row = self.rows.get(self.cursor_y); match key { ArrowLeft => { @@ -718,7 +768,7 @@ impl Editor { } /// Route user input to the appropriate handler method - pub fn process_keypress(&mut self) -> Option> { + pub fn process_keypress(&mut self) -> Option> { let key = self.read_key(); if key.is_some() { let char = key.unwrap(); @@ -787,14 +837,14 @@ impl Editor { Some(OtherKey('\0')) } - fn _del_or_backspace(&mut self, key: EditorKey) { + 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: EditorKey) { + fn _page_up_or_down(&mut self, key: KeyCode) { let mut times = self.screen_rows; // Update the cursor position @@ -908,7 +958,6 @@ impl Editor { 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[39m"); @@ -1076,7 +1125,7 @@ impl Editor { cx } - /// Convert tab characters to spaces for display + /// 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(); @@ -1085,17 +1134,18 @@ impl Editor { let str = str.replace('\t', " "); row.render = str; + // Syntax highlighting self.update_syntax(index); } - fn insert_row(&mut self, at: usize, row: &str) { + fn insert_row(&mut self, at: usize, content: &str) { if at > self.rows.len() { return; } - let row = EditorRow::new(row); - self.rows.insert(at, row); + let row = Row::new(content); + self.rows.insert(at, row); self.update_row(at); self.dirty += 1; @@ -1275,7 +1325,7 @@ impl Editor { // Find // ------------------------------------------------------------------------ - fn find_callback(&mut self, query: &str, key: EditorKey) { + 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(); @@ -1325,9 +1375,11 @@ impl Editor { // Highlight matching search result let len = start + query.len(); - for x in start..len { - self.rows[current as usize].highlight[x] = Highlight::SearchMatch; - } + highlight_range( + &mut self.rows[current as usize].highlight, + start..len, + Highlight::SearchMatch, + ); break; } @@ -1359,97 +1411,103 @@ impl Editor { // Functions // ------------------------------------------------------------------------ -fn get_syntax_db() -> Vec { +fn get_syntax_db() -> Vec { vec![ - EditorSyntax::new( + Syntax::new( "c", vec![".c", ".h", ".cpp"], vec![ - "switch", "if", "while", "for", "break", "continue", "return", "else", "struct", - "union", "typedef", "static", "enum", "class", "case", + "continue", "typedef", "switch", "return", "static", "while", "break", "struct", + "union", "class", "else", "enum", "for", "case", "if", + ], + vec![ + "unsigned", "double", "signed", "float", "long", "char", "int", "void", ], - vec!["int", "long", "double", "float", "char", "unsigned", "signed", "void"], "//", "/*", "*/", - EditorSyntaxFlags::HIGHLIGHT_NUMBERS | EditorSyntaxFlags::HIGHLIGHT_STRINGS, + SyntaxFlags::HIGHLIGHT_NUMBERS | SyntaxFlags::HIGHLIGHT_STRINGS, ), - EditorSyntax::new( + Syntax::new( "rust", vec![".rs"], vec![ - "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", - "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", - "pub", "ref", "return", "self", "static", "struct", "super", "trait", "true", - "type", "unsafe", "use", "where", "while", + "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![ - "dyn", - "Self", - "Copy", - "Send", - "Sync", - "Sized", - "Sync", - "Unpin", - "Drop", - "Fn", - "FnMut", - "FnOnce", - "Box", - "ToOwned", - "Clone", - "PartialEq", - "PartialOrd", - "Eq", - "Ord", - "AsRef", - "AsMut", - "Into", - "From", - "Default", - "Iterator", - "Extend", - "IntoIterator", "DoubleEndedIterator", "ExactSizeIterator", - "Option", - "Some", - "None", - "Ok", - "Err", - "String", + "IntoIterator", + "PartialOrd", + "PartialEq", + "Iterator", "ToString", - "Vec", - "str", - "char", - "u8", - "i8", - "u16", - "i16", - "u32", - "i32", - "usize", - "isize", - "u64", - "i64", - "u128", - "i128", - "bool", + "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" + "f64", + "i16", + "i32", + "i64", + "str", + "u16", + "u32", + "u64", + "Eq", + "Fn", + "Ok", + "i8", + "u8", ], "//", "/*", "*/", - EditorSyntaxFlags::HIGHLIGHT_NUMBERS | EditorSyntaxFlags::HIGHLIGHT_STRINGS, + SyntaxFlags::HIGHLIGHT_NUMBERS | SyntaxFlags::HIGHLIGHT_STRINGS, ), ] } +/// 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' { + if input_char.is_ascii_whitespace() || input_char == '\0' { return true; } @@ -1464,6 +1522,27 @@ fn is_separator(input_char: char) -> bool { false } +/// Get a range for a slice of a string or vector. +/// +/// 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::*;