Finish tutorial, much refactoring

This commit is contained in:
Timothy Warren 2019-09-05 15:26:04 -04:00
parent ca51d8f1f5
commit 389a526f41
1 changed files with 282 additions and 203 deletions

View File

@ -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<Highlight>,
}
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<EditorRow>,
rows: Vec<Row>,
dirty: u64,
filename: String,
status_message: String,
status_message_time: Instant,
syntax: Option<EditorSyntax>,
syntax: Option<Syntax>,
// 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<T> {
pub enum KeyCode<T> {
Enter,
Escape,
Backspace,
@ -144,11 +152,44 @@ pub enum EditorKey<T> {
OtherKey(T),
}
impl EditorKey<char> {
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<char> {
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<EditorKey<char>> {
fn read_key(&mut self) -> Option<KeyCode<char>> {
// --------------------------------------------------------------------
// 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<EditorKey<char>> = vec![];
let mut input: Vec<KeyCode<char>> = 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<char>)>,
cb: Option<&mut dyn Fn(&mut Self, &str, KeyCode<char>)>,
) -> 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<char>) {}
fn _noop_prompt_cb(&mut self, _: &str, _: KeyCode<char>) {}
fn move_cursor(&mut self, key: &EditorKey<char>) {
fn move_cursor(&mut self, key: &KeyCode<char>) {
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<EditorKey<char>> {
pub fn process_keypress(&mut self) -> Option<KeyCode<char>> {
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<char>) {
fn _del_or_backspace(&mut self, key: KeyCode<char>) {
if key == DeleteKey {
self.move_cursor(&ArrowRight);
}
self.delete_char();
}
fn _page_up_or_down(&mut self, key: EditorKey<char>) {
fn _page_up_or_down(&mut self, key: KeyCode<char>) {
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<char>) {
fn find_callback(&mut self, query: &str, key: KeyCode<char>) {
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<EditorSyntax> {
fn get_syntax_db() -> Vec<Syntax> {
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<usize> {
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<Highlight>, range: Range<usize>, value: Highlight) {
for x in range {
vec[x] = value;
}
}
#[cfg(test)]
mod tests {
use super::*;