rs-kilo/src/editor.rs

1610 lines
49 KiB
Rust
Raw Normal View History

2019-08-22 14:25:18 -04:00
//! Editor functionality
2019-09-06 13:24:29 -04:00
use crate::terminal_helpers::*;
2019-08-22 14:25:18 -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-09-05 15:26:04 -04:00
use std::ops::Range;
2019-08-29 14:13:09 -04:00
use std::time::{Duration, Instant};
2019-08-22 16:44:47 -04:00
2019-09-05 15:26:04 -04:00
use self::KeyCode::*;
2019-08-27 12:22:19 -04:00
// ------------------------------------------------------------------------
// Defines
// ------------------------------------------------------------------------
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
// ------------------------------------------------------------------------
// Data
// ------------------------------------------------------------------------
// Use an external package's macro to create a memory-safe
// bit flag alternative
bitflags! {
#[derive(Default)]
2019-09-05 15:26:04 -04:00
pub struct SyntaxFlags: u32 {
const HIGHLIGHT_NUMBERS = 0b00000001;
const HIGHLIGHT_STRINGS = 0b00000010;
}
}
2019-09-05 15:26:04 -04:00
/// Configuration for language syntax highlighting
#[derive(Clone, Debug, Default, PartialEq)]
2019-09-05 15:26:04 -04:00
pub struct Syntax {
/// Language name, to be shown in status bar
file_type: String,
2019-09-05 15:26:04 -04:00
/// File extensions
file_match: Vec<&'static str>,
2019-09-05 15:26:04 -04:00
/// Keywords
2019-09-04 16:39:56 -04:00
keywords1: Vec<&'static str>,
2019-09-05 15:26:04 -04:00
/// Type and/or secondary keywords
2019-09-04 16:39:56 -04:00
keywords2: Vec<&'static str>,
2019-09-05 15:26:04 -04:00
/// How does a single line comment start?
2019-09-04 16:39:56 -04:00
singleline_comment_start: String,
2019-09-05 15:26:04 -04:00
/// How does a multline comment start?
multiline_comment_start: String,
2019-09-05 15:26:04 -04:00
/// How does a multiline comment end?
multiline_comment_end: String,
2019-09-05 15:26:04 -04:00
/// Options for what to highlight
flags: SyntaxFlags,
}
2019-09-05 15:26:04 -04:00
/// Syntax highlighting token types
2019-09-04 11:20:57 -04:00
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Highlight {
2019-09-05 15:26:04 -04:00
/// No highlighting
2019-09-04 11:20:57 -04:00
Normal,
2019-09-05 15:26:04 -04:00
/// Single line comments
2019-09-04 16:39:56 -04:00
LineComment,
2019-09-05 15:26:04 -04:00
/// Multiple line comments
MultiLineComment,
2019-09-05 15:26:04 -04:00
/// Language keywords
2019-09-04 16:39:56 -04:00
Keyword1,
2019-09-05 15:26:04 -04:00
/// Language types/ secondary keywords
2019-09-04 16:39:56 -04:00
Keyword2,
2019-09-05 15:26:04 -04:00
/// Single-line strings
2019-09-04 15:53:36 -04:00
String,
2019-09-05 15:26:04 -04:00
/// Numbers
2019-09-04 11:20:57 -04:00
Number,
2019-09-05 15:26:04 -04:00
/// Search results
2019-09-04 11:20:57 -04:00
SearchMatch,
}
/// A representation of a line in the editor
2019-09-05 15:26:04 -04:00
#[derive(Clone, Debug, Default)]
pub struct Row {
/// The 'raw' representation of the original characters
2019-08-27 12:22:19 -04:00
chars: String,
2019-09-05 15:26:04 -04:00
/// The display characters for the editor
2019-08-28 16:35:48 -04:00
render: String,
2019-08-27 12:22:19 -04:00
2019-09-05 15:26:04 -04:00
/// The highlighting type for each character
highlight: Vec<Highlight>,
2019-08-28 16:35:48 -04:00
2019-09-05 15:26:04 -04:00
/// Are we currently highlighting a multi-line comment?
highlight_comment_start: bool,
2019-08-27 08:30:51 -04:00
}
2019-08-22 16:44:47 -04:00
/// Main structure for the editor
/// `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 {
cursor_x: usize,
cursor_y: usize,
2019-08-28 16:35:48 -04:00
render_x: usize,
col_offset: usize,
row_offset: usize,
2019-08-26 10:04:12 -04:00
screen_cols: usize,
screen_rows: usize,
2019-09-05 15:26:04 -04:00
rows: Vec<Row>,
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-09-05 15:26:04 -04:00
syntax: Option<Syntax>,
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-09-04 10:09:08 -04:00
search_last_match: i32,
search_direction: i8,
2019-09-04 11:32:05 -04:00
search_last_line: usize,
search_last_hightlight: Vec<Highlight>,
2019-08-23 16:46:04 -04:00
}
2019-08-22 14:25:18 -04:00
/// Keycode mapping enum
#[derive(Copy, Clone, Debug, PartialEq)]
2019-09-05 15:26:04 -04:00
pub enum KeyCode<T> {
2019-08-29 16:32:17 -04:00
Enter,
Escape,
2019-08-29 16:32:17 -04:00
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),
}
2019-09-05 15:26:04 -04:00
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,
2019-09-05 15:26:04 -04:00
_ => panic!("called `KeyCode::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(),
2019-09-06 13:24:29 -04:00
// This is the only reason I had to implement this method
// manually, instead of it being derived. Apparently an
// `Instant` struct has no default
2019-08-29 14:13:09 -04:00
status_message_time: Instant::now(),
2019-09-06 13:24:29 -04:00
syntax: None,
2019-09-04 10:09:08 -04:00
2019-08-29 14:13:09 -04:00
output_buffer: String::new(),
2019-08-30 11:20:52 -04:00
quit_times: KILO_QUIT_TIMES,
2019-09-04 10:09:08 -04:00
search_last_match: -1,
search_direction: 1,
2019-09-04 11:32:05 -04:00
search_last_line: 0,
search_last_hightlight: vec![],
2019-08-29 14:13:09 -04:00
}
}
}
2019-08-22 14:25:18 -04:00
impl Editor {
// ------------------------------------------------------------------------
// 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
}
// ------------------------------------------------------------------------
// Terminal
// ------------------------------------------------------------------------
/// Convert stdin to specific keypresses
2019-09-05 15:26:04 -04:00
fn read_key(&mut self) -> Option<KeyCode<char>> {
// --------------------------------------------------------------------
// Match single character
// --------------------------------------------------------------------
2019-08-22 16:44:47 -04:00
let stdin = io::stdin();
let stdin = stdin.lock();
let mut br = BufReader::with_capacity(5, stdin);
let mut first_read = [0; 1];
2019-09-04 11:20:57 -04:00
match br.read_exact(&mut first_read) {
Ok(_) => (),
Err(e) => {
if e.kind() != io::ErrorKind::UnexpectedEof {
2019-09-06 13:24:29 -04:00
let error = format!("{:?}", e);
self.set_status_message(&error);
2019-09-04 11:20:57 -04:00
}
}
}
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
2019-09-05 15:26:04 -04:00
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,
}
// --------------------------------------------------------------------
// Match escape sequence
// --------------------------------------------------------------------
2019-09-03 16:20:00 -04:00
let mut seq = [0; 4];
let mut seq_handle = br.take(4);
match seq_handle.read(&mut seq) {
2019-09-04 11:20:57 -04:00
Ok(_) => (),
Err(e) => {
if e.kind() != io::ErrorKind::UnexpectedEof {
2019-09-06 13:24:29 -04:00
let error = format!("{:?}", e);
self.set_status_message(&error);
2019-09-04 11:20:57 -04:00
}
}
}
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();
2019-08-22 16:44:47 -04:00
2019-09-05 15:26:04 -04:00
let mut input: Vec<KeyCode<char>> = vec![];
2019-09-05 15:26:04 -04:00
for ch in seq_str.chars() {
// Since the fixed array is always filled, there
// will be null characters. Ignore these.
2019-09-05 15:26:04 -04:00
if ch == '\0' {
continue;
}
2019-09-05 15:26:04 -04:00
input.push(match ch {
'\x1b' => Escape,
2019-09-05 15:26:04 -04:00
_ => 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);
2019-08-22 16:44:47 -04:00
}
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,
2019-09-03 14:57:52 -04:00
_ => 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),
2019-08-28 16:35:48 -04:00
}
}
_ => return Some(input[0]),
}
2019-08-23 16:46:04 -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-09-06 13:24:29 -04:00
// I could have implemented this, but I felt that parsing
// an escape code from stdin was of minimal value,
// when the ioctrl method works on any computer I've tried
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-09-04 11:20:57 -04:00
// ------------------------------------------------------------------------
// Syntax Highlighting
// ------------------------------------------------------------------------
fn update_syntax(&mut self, index: usize) {
2019-09-05 15:26:04 -04:00
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
2019-09-06 13:24:29 -04:00
// second mutable borrow...so a clone it is
2019-09-05 15:26:04 -04:00
Some((&mut rows[index - 1]).clone())
} else {
None
};
let row = &mut rows[index];
2019-09-06 13:24:29 -04:00
// Reset the highlighting of the row
2019-09-04 11:20:57 -04:00
row.highlight = vec![Highlight::Normal; row.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<T> into Option<&T>,
// which can then be unwrapped.
let current_syntax = self.syntax.as_ref().unwrap();
2019-09-04 16:39:56 -04:00
let keywords1 = &current_syntax.keywords1;
let keywords2 = &current_syntax.keywords2;
let scs = &current_syntax.singleline_comment_start;
let mcs = &current_syntax.multiline_comment_start;
let mce = &current_syntax.multiline_comment_end;
2019-09-04 16:39:56 -04:00
2019-09-06 13:24:29 -04:00
let mut prev_separator = true;
2019-09-04 15:53:36 -04:00
let mut in_string = false;
let mut str_start = '\0';
2019-09-05 15:26:04 -04:00
let mut in_comment = prev_row.map_or(false, |row| row.highlight_comment_start);
2019-09-04 13:15:22 -04:00
let mut i = 0;
let bytes = row.render.clone().into_bytes();
'outer: while i < row.render.len() {
2019-09-04 13:15:22 -04:00
let c = bytes[i] as char;
let prev_highlight = if i > 0 {
row.highlight[i - 1]
} else {
Highlight::Normal
};
2019-09-04 16:39:56 -04:00
// Single line comments
2019-09-05 15:26:04 -04:00
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 {
2019-09-04 16:39:56 -04:00
// Pretty simple, highlight from the match to the end of the line
2019-09-05 15:26:04 -04:00
highlight_range(
&mut row.highlight,
i as usize..row.render.len(),
Highlight::LineComment,
);
break;
2019-09-04 16:39:56 -04:00
}
}
// Multi-line comments
if mcs.len() > 0 && mce.len() > 0 && !in_string {
2019-09-05 15:26:04 -04:00
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 {
2019-09-05 15:26:04 -04:00
highlight_range(
&mut row.highlight,
mce_slice_range,
Highlight::MultiLineComment,
);
i += mce.len();
in_comment = false;
prev_separator = true;
continue;
} else {
i += 1;
continue;
}
} else if &row.render[mcs_slice_range.clone()] == mcs {
// Start of a multi-line comment
2019-09-05 15:26:04 -04:00
highlight_range(
&mut row.highlight,
mcs_slice_range,
Highlight::MultiLineComment,
);
i += mcs.len();
in_comment = true;
continue;
}
}
2019-09-04 15:53:36 -04:00
// Strings
if current_syntax
.flags
2019-09-05 15:26:04 -04:00
.contains(SyntaxFlags::HIGHLIGHT_STRINGS)
2019-09-04 15:53:36 -04:00
{
if in_string {
row.highlight[i as usize] = Highlight::String;
2019-09-04 16:39:56 -04:00
// Don't end highlighting for a string on an escaped quote
if c == '\\' && i + 1 < row.render.len() {
row.highlight[i as usize + 1] = Highlight::String;
i += 2;
continue;
}
// End delimiter for the string
2019-09-04 15:53:36 -04:00
if c == str_start {
in_string = false;
str_start = '\0';
}
2019-09-04 16:39:56 -04:00
i += 1;
prev_separator = true;
continue;
2019-09-04 15:53:36 -04:00
} else {
if (c == '"' || c == '\'') && prev_separator {
2019-09-04 15:53:36 -04:00
in_string = true;
str_start = c;
row.highlight[i as usize] = Highlight::String;
i += 1;
continue;
}
}
}
// Numbers
if current_syntax
.flags
2019-09-05 15:26:04 -04:00
.contains(SyntaxFlags::HIGHLIGHT_NUMBERS)
2019-09-04 13:15:22 -04:00
{
if (c.is_ascii_digit() && (prev_separator || prev_highlight == Highlight::Number))
|| (c == '.' && prev_highlight == Highlight::Number)
{
row.highlight[i as usize] = Highlight::Number;
i += 1;
prev_separator = false;
continue;
}
2019-09-04 11:20:57 -04:00
}
2019-09-04 13:15:22 -04:00
2019-09-04 16:39:56 -04:00
// Keywords
if prev_separator {
for &keyword in keywords1 {
2019-09-05 15:26:04 -04:00
let search_range = get_slice_range(i as usize, keyword.len(), row.render.len());
2019-09-05 15:26:04 -04:00
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();
2019-09-04 16:39:56 -04:00
}
}
for &keyword in keywords2 {
2019-09-05 15:26:04 -04:00
let search_range = get_slice_range(i as usize, keyword.len(), row.render.len());
2019-09-05 15:26:04 -04:00
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();
2019-09-04 16:39:56 -04:00
}
}
}
prev_separator = is_separator(c);
2019-09-04 13:15:22 -04:00
i += 1;
2019-09-04 11:20:57 -04:00
}
2019-09-05 15:26:04 -04:00
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);
}
2019-09-04 11:20:57 -04:00
}
fn syntax_to_color(syntax_type: Highlight) -> i32 {
2019-09-04 11:20:57 -04:00
use Highlight::*;
match syntax_type {
2019-09-04 16:39:56 -04:00
Keyword1 => 33, // Yellow
Keyword2 => 32, // Green
LineComment => 36, // Cyan
MultiLineComment => 36,
2019-09-04 11:20:57 -04:00
Normal => 37,
2019-09-04 16:39:56 -04:00
Number => 31, // Red
SearchMatch => 34, // Blue
String => 35, // Magenta
2019-09-04 11:20:57 -04:00
}
}
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());
2019-09-04 15:53:36 -04:00
// Re-highlight file when the type is determined
for x in 0..self.rows.len() {
self.update_syntax(x);
}
return;
}
}
}
}
// ------------------------------------------------------------------------
// Input
// ------------------------------------------------------------------------
2019-09-03 16:20:00 -04:00
fn prompt(
&mut self,
prompt: &str,
2019-09-05 15:26:04 -04:00
cb: Option<&mut dyn Fn(&mut Self, &str, KeyCode<char>)>,
2019-09-03 16:20:00 -04:00
) -> String {
2019-08-30 16:17:06 -04:00
let mut buffer = String::new();
2019-09-03 16:19:19 -04:00
let default_cb = &mut Self::_noop_prompt_cb;
let cb = if cb.is_some() {
cb.unwrap()
} else {
default_cb
};
2019-08-30 16:17:06 -04:00
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 {
2019-09-03 14:57:52 -04:00
Backspace => {
buffer.pop();
2019-09-03 16:20:00 -04:00
}
2019-09-03 14:57:52 -04:00
DeleteKey => {
buffer.pop();
2019-09-03 16:20:00 -04:00
}
2019-09-03 14:57:52 -04:00
Escape => {
self.set_status_message("");
2019-09-03 16:19:19 -04:00
cb(self, &String::from(""), char);
2019-09-03 14:57:52 -04:00
return String::from("");
}
2019-08-30 16:17:06 -04:00
Enter => {
if buffer.len() != 0 {
self.set_status_message("");
2019-09-03 16:19:19 -04:00
cb(self, &buffer, char);
2019-08-30 16:17:06 -04:00
return buffer;
}
}
OtherKey(ch) => {
if (!ch.is_ascii_control()) && (ch as u8) < 128 {
buffer.push(ch);
2019-09-03 16:19:19 -04:00
// continue;
2019-08-30 16:17:06 -04:00
}
}
_ => (),
};
2019-09-03 16:19:19 -04:00
cb(self, &buffer, char);
2019-08-30 16:17:06 -04:00
}
}
}
2019-09-05 15:26:04 -04:00
fn _noop_prompt_cb(&mut self, _: &str, _: KeyCode<char>) {}
2019-09-03 16:19:19 -04:00
2019-09-05 15:26:04 -04:00
fn move_cursor(&mut self, key: &KeyCode<char>) {
2019-08-28 16:35:48 -04:00
let row = self.rows.get(self.cursor_y);
match key {
ArrowLeft => {
2019-08-28 16:35:48 -04:00
if self.cursor_x != 0 {
// Move cursor left
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-27 08:30:51 -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
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-27 08:30:51 -04:00
}
ArrowUp => {
if self.cursor_y > 0 {
self.cursor_y -= 1;
}
2019-08-27 08:30:51 -04:00
}
ArrowDown => {
if self.cursor_y < self.rows.len() {
self.cursor_y += 1;
}
2019-08-27 08:30:51 -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-23 16:46:04 -04:00
/// Route user input to the appropriate handler method
2019-09-05 15:26:04 -04:00
pub fn process_keypress(&mut self) -> Option<KeyCode<char>> {
let key = self.read_key();
if key.is_some() {
let char = key.unwrap();
2019-08-29 16:32:17 -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(),
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),
HomeKey => {
self.cursor_x = 0;
2019-08-27 08:30:51 -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();
}
}
Function(_) => (),
2019-08-29 16:32:17 -04:00
OtherKey(c) => {
if c.is_ascii_control() {
2019-09-03 16:19:19 -04:00
if c == ctrl_key('f') {
self.find();
}
2019-08-29 16:32:17 -04:00
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-30 11:20:52 -04:00
self.quit_times = KILO_QUIT_TIMES;
2019-08-28 16:35:48 -04:00
return key;
}
// Continue the main input loop
Some(OtherKey('\0'))
}
2019-09-05 15:26:04 -04:00
fn _del_or_backspace(&mut self, key: KeyCode<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
}
2019-09-05 15:26:04 -04:00
fn _page_up_or_down(&mut self, key: KeyCode<char>) {
2019-08-27 08:30:51 -04:00
let mut times = self.screen_rows;
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
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-22 14:25:18 -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-09-04 11:20:57 -04:00
fn append_out_char(&mut self, ch: char) {
self.output_buffer.push(ch);
}
fn scroll(&mut self) {
2019-08-28 16:35:48 -04:00
self.render_x = 0;
if self.cursor_y < self.rows.len() {
2019-09-03 16:19:19 -04:00
self.render_x = self.row_cx_to_rx(self.cursor_y, self.cursor_x);
2019-08-28 16:35:48 -04:00
}
// 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;
}
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-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 {
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")
);
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 {
2019-09-06 13:24:29 -04:00
self.append_out_char('~');
2019-08-27 12:22:19 -04:00
padding -= 1;
}
for _ in 0..padding {
2019-09-06 13:24:29 -04:00
self.append_out_char(' ');
2019-08-27 12:22:19 -04:00
}
2019-08-26 10:04:12 -04:00
2019-08-27 12:22:19 -04:00
self.append_out(&welcome);
} else {
2019-09-06 13:24:29 -04:00
self.append_out_char('~');
2019-08-26 10:04:12 -04:00
}
} else {
2019-08-28 16:35:48 -04:00
let output = self.rows[file_row].render.clone();
2019-09-04 11:20:57 -04:00
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 {
2019-09-04 11:20:57 -04:00
if current_color != -1 {
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]);
2019-09-04 11:20:57 -04:00
if color != current_color {
current_color = color;
let code = format!("\x1b[{}m", color);
self.append_out(&code);
}
self.append_out_char(ch);
}
}
self.append_out("\x1b[39m");
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);
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();
2019-08-28 16:35:48 -04:00
if len > self.screen_cols {
len = self.screen_cols;
left_message.truncate(len);
2019-08-28 16:35:48 -04:00
}
self.append_out(&left_message);
2019-08-28 16:35:48 -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() {
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) {
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
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();
// Move cursor to state position
2019-09-06 13:24:29 -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);
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-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();
}
// ------------------------------------------------------------------------
// Row Operations
// ------------------------------------------------------------------------
2019-09-03 16:19:19 -04:00
fn row_cx_to_rx(&mut self, index: usize, cx: usize) -> usize {
2019-08-28 16:35:48 -04:00
let mut rx: usize = 0;
2019-09-03 16:19:19 -04:00
let mut i: usize = 0;
2019-08-28 16:35:48 -04:00
2019-09-04 11:20:57 -04:00
for char in self.rows[index].chars.chars() {
2019-08-28 16:35:48 -04:00
if char == '\t' {
rx += (KILO_TAB_STOP - 1) - (rx % KILO_TAB_STOP);
2019-09-03 16:19:19 -04:00
} else {
rx += 1;
2019-08-28 16:35:48 -04:00
}
2019-09-03 16:19:19 -04:00
if i > cx {
return rx;
}
i += 1;
2019-08-28 16:35:48 -04:00
}
rx
}
2019-09-03 16:19:19 -04:00
fn row_rx_to_cx(&mut self, index: usize, rx: usize) -> usize {
2019-09-03 16:20:00 -04:00
let mut current_rx: usize = 0;
2019-09-03 16:19:19 -04:00
let mut cx: usize = 0;
2019-09-04 11:20:57 -04:00
for char in self.rows[index].chars.chars() {
2019-09-03 16:19:19 -04:00
if char == '\t' {
current_rx += (KILO_TAB_STOP - 1) - (current_rx % KILO_TAB_STOP);
} else {
current_rx += 1;
}
if current_rx > rx {
return cx;
}
cx += 1;
}
cx
}
2019-09-05 15:26:04 -04:00
/// Convert file characters to their display equivalents
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-09-04 11:20:57 -04:00
2019-09-05 15:26:04 -04:00
// Syntax highlighting
2019-09-04 11:20:57 -04:00
self.update_syntax(index);
2019-08-28 16:35:48 -04:00
}
2019-09-05 15:26:04 -04:00
fn insert_row(&mut self, at: usize, content: &str) {
2019-08-30 15:10:19 -04:00
if at > self.rows.len() {
return;
}
2019-09-05 15:26:04 -04:00
let row = Row::new(content);
2019-08-30 15:10:19 -04:00
2019-09-05 15:26:04 -04:00
self.rows.insert(at, row);
2019-08-30 15:10:19 -04:00
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-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
}
}
// ------------------------------------------------------------------------
// 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!
2019-09-06 13:24:29 -04:00
output += &row.chars;
output.push('\n');
2019-08-29 16:32:17 -04:00
}
output
}
/// 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();
self.select_syntax_highlight();
2019-08-28 16:35:48 -04:00
let file = File::open(&self.filename)?;
2019-08-27 12:22:19 -04:00
let buf_reader = BufReader::new(file);
2019-09-06 13:24:29 -04:00
let lines = buf_reader.lines().map(|l| clean_unwrap(l));
2019-08-27 12:22:19 -04:00
for line in lines {
2019-08-30 15:10:19 -04:00
self.insert_row(self.rows.len(), &line);
}
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-09-03 16:19:19 -04:00
self.filename = self.prompt("Save as (ESC to cancel):", None);
2019-09-03 14:57:52 -04:00
if self.filename.len() == 0 {
self.set_status_message("Save aborted");
2019-09-03 16:20:00 -04:00
return Ok(());
2019-09-03 14:57:52 -04:00
}
2019-09-06 13:24:29 -04:00
self.select_syntax_highlight();
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-09-03 16:19:19 -04:00
// ------------------------------------------------------------------------
// Find
// ------------------------------------------------------------------------
2019-09-05 15:26:04 -04:00
fn find_callback(&mut self, query: &str, key: KeyCode<char>) {
2019-09-04 11:32:05 -04:00
if !self.search_last_hightlight.is_empty() {
self.rows[self.search_last_line].highlight = self.search_last_hightlight.clone();
self.search_last_hightlight.clear();
}
2019-09-03 16:19:19 -04:00
if key == Enter || key == Escape {
2019-09-04 10:09:08 -04:00
self.search_last_match = -1;
self.search_direction = 1;
2019-09-03 16:19:19 -04:00
return;
2019-09-04 10:09:08 -04:00
} 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;
2019-09-03 16:19:19 -04:00
}
if query.is_empty() {
return;
}
2019-09-04 10:09:08 -04:00
let mut current = self.search_last_match;
2019-09-03 16:19:19 -04:00
for x in 0..self.rows.len() {
2019-09-04 10:09:08 -04:00
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;
}
2019-09-04 11:20:57 -04:00
match self.rows[current as usize].render.find(query) {
2019-09-03 16:19:19 -04:00
None => (),
Some(start) => {
2019-09-04 10:09:08 -04:00
self.search_last_match = current;
self.cursor_y = current as usize;
2019-09-03 16:19:19 -04:00
self.cursor_x = self.row_rx_to_cx(x, start);
self.row_offset = self.rows.len();
2019-09-04 11:32:05 -04:00
self.search_last_line = current as usize;
self.search_last_hightlight = self.rows[current as usize].highlight.clone();
2019-09-04 11:20:57 -04:00
// Highlight matching search result
let len = start + query.len();
2019-09-05 15:26:04 -04:00
highlight_range(
&mut self.rows[current as usize].highlight,
start..len,
Highlight::SearchMatch,
);
2019-09-04 11:20:57 -04:00
2019-09-03 16:19:19 -04:00
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;
2019-09-04 13:15:22 -04:00
let query = self.prompt(
"Search (Use ESC/Arrows/Enter):",
Some(&mut Self::find_callback),
);
2019-09-03 16:19:19 -04:00
if query.is_empty() {
self.cursor_x = saved_cx;
self.cursor_y = saved_cy;
self.col_offset = saved_coloff;
self.row_offset = saved_rowoff;
}
}
2019-08-27 08:30:51 -04:00
}
// ------------------------------------------------------------------------
// Functions
// ------------------------------------------------------------------------
2019-09-06 13:24:29 -04:00
/// Get the language highlighting config
2019-09-05 15:26:04 -04:00
fn get_syntax_db() -> Vec<Syntax> {
2019-09-04 15:53:36 -04:00
vec![
2019-09-05 15:26:04 -04:00
Syntax::new(
2019-09-04 15:53:36 -04:00
"c",
vec![".c", ".h", ".cpp"],
2019-09-04 16:39:56 -04:00
vec![
2019-09-05 15:26:04 -04:00
"continue", "typedef", "switch", "return", "static", "while", "break", "struct",
"union", "class", "else", "enum", "for", "case", "if",
],
vec![
2019-09-06 13:24:29 -04:00
"#include", "unsigned", "#define", "#ifndef", "double", "signed", "#endif",
"#ifdef", "float", "#error", "#undef", "long", "char", "int", "void", "#if",
2019-09-04 16:39:56 -04:00
],
"//",
"/*",
"*/",
2019-09-05 15:26:04 -04:00
SyntaxFlags::HIGHLIGHT_NUMBERS | SyntaxFlags::HIGHLIGHT_STRINGS,
2019-09-04 15:53:36 -04:00
),
2019-09-05 15:26:04 -04:00
Syntax::new(
2019-09-04 15:53:36 -04:00
"rust",
vec![".rs"],
2019-09-04 16:39:56 -04:00
vec![
2019-09-05 15:26:04 -04:00
"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",
2019-09-04 16:39:56 -04:00
],
vec![
2019-09-05 15:26:04 -04:00
"DoubleEndedIterator",
"ExactSizeIterator",
"IntoIterator",
2019-09-04 16:39:56 -04:00
"PartialOrd",
2019-09-05 15:26:04 -04:00
"PartialEq",
2019-09-04 16:39:56 -04:00
"Iterator",
2019-09-05 15:26:04 -04:00
"ToString",
"Default",
"ToOwned",
2019-09-04 16:39:56 -04:00
"Extend",
2019-09-05 15:26:04 -04:00
"FnOnce",
2019-09-04 16:39:56 -04:00
"Option",
2019-09-05 15:26:04 -04:00
"String",
"AsMut",
"AsRef",
"Clone",
"Debug",
"FnMut",
"Sized",
"Unpin",
"array",
"isize",
"usize",
"&str",
"Copy",
"Drop",
"From",
"Into",
2019-09-04 16:39:56 -04:00
"None",
2019-09-05 15:26:04 -04:00
"Self",
"Send",
"Some",
"Sync",
"Sync",
"bool",
"char",
"i128",
"u128",
"Box",
2019-09-04 16:39:56 -04:00
"Err",
2019-09-05 15:26:04 -04:00
"Ord",
2019-09-04 16:39:56 -04:00
"Vec",
2019-09-05 15:26:04 -04:00
"dyn",
"f32",
"f64",
"i16",
"i32",
"i64",
2019-09-04 16:55:05 -04:00
"str",
"u16",
"u32",
"u64",
2019-09-05 15:26:04 -04:00
"Eq",
"Fn",
"Ok",
"i8",
"u8",
2019-09-04 16:39:56 -04:00
],
"//",
"/*",
"*/",
2019-09-05 15:26:04 -04:00
SyntaxFlags::HIGHLIGHT_NUMBERS | SyntaxFlags::HIGHLIGHT_STRINGS,
2019-09-04 15:53:36 -04:00
),
]
}
2019-09-06 13:24:29 -04:00
/// Convert Ctrl+letter chords to their
/// ASCII table equivalents
pub fn ctrl_key(c: char) -> char {
let key = c as u8;
if !c.is_ascii() {
panic!("CTRL_KEY only accepts ASCII characters");
}
// Intentionally "overflow" u8 to wrap around to the
// beginning of the ASCII table. Ctrl+a is 1, Ctrl+b is 2, etc.
(key & 0x1f) as char
}
2019-09-05 15:26:04 -04:00
/// Determine whether a character is one which separates tokens
/// in the language to highlight
fn is_separator(input_char: char) -> bool {
2019-09-05 15:26:04 -04:00
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
}
2019-09-06 13:24:29 -04:00
/// 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.
2019-09-05 15:26:04 -04:00
///
/// 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;
}
}
2019-09-06 13:24:29 -04:00
/// Do the equivalent of a Result::unwrap, but cleanup terminal output
/// first, so it doesn't destroy console output afterwards.
fn clean_unwrap<O, E>(res: Result<O, E>) -> O
where
E: std::fmt::Debug,
{
match res {
Ok(value) => value,
Err(e) => {
disable_raw_mode();
println!("\r\n");
panic!("{:?}", e);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
2019-09-06 13:24:29 -04:00
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() {
assert_eq!(is_separator(' '), true);
assert_eq!(is_separator('_'), false);
}
}