1687 lines
39 KiB
Rust
1687 lines
39 KiB
Rust
|
#![forbid(unsafe_code)]
|
||
|
//! 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<Highlight>,
|
||
|
|
||
|
/// 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<Row>,
|
||
|
dirty: u64,
|
||
|
filename: String,
|
||
|
status_message: String,
|
||
|
status_message_time: Instant,
|
||
|
syntax: Option<Syntax>,
|
||
|
|
||
|
// 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<Highlight>,
|
||
|
}
|
||
|
|
||
|
/// Keycode mapping enum
|
||
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||
|
pub enum KeyCode<T = char> {
|
||
|
Enter,
|
||
|
Escape,
|
||
|
Backspace,
|
||
|
ArrowLeft,
|
||
|
ArrowRight,
|
||
|
ArrowUp,
|
||
|
ArrowDown,
|
||
|
DeleteKey,
|
||
|
HomeKey,
|
||
|
EndKey,
|
||
|
PageUp,
|
||
|
PageDown,
|
||
|
Tab,
|
||
|
/// 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<char> {
|
||
|
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 size = Self::get_window_size();
|
||
|
|
||
|
Editor {
|
||
|
screen_cols: size.cols as usize,
|
||
|
screen_rows: (size.rows - 2) as usize,
|
||
|
..Editor::default()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// ------------------------------------------------------------------------
|
||
|
// Terminal
|
||
|
// ------------------------------------------------------------------------
|
||
|
|
||
|
/// Convert stdin to specific keypresses
|
||
|
fn read_key(&mut self) -> Option<KeyCode> {
|
||
|
// --------------------------------------------------------------------
|
||
|
// 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),
|
||
|
'\t' => return Some(Tab),
|
||
|
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<KeyCode<char>> = 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() -> 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<T> 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 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<char>)>,
|
||
|
) -> String {
|
||
|
let mut buffer = String::new();
|
||
|
let default_cb = &mut Self::_noop_prompt_cb;
|
||
|
|
||
|
let cb = match cb {
|
||
|
Some(cb) => cb,
|
||
|
None => 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<char>) {}
|
||
|
|
||
|
fn move_cursor(&mut self, key: &KeyCode<char>) {
|
||
|
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<KeyCode<char>> {
|
||
|
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();
|
||
|
}
|
||
|
}
|
||
|
Tab => self.insert_char('\t'),
|
||
|
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<char>) {
|
||
|
if key == DeleteKey {
|
||
|
self.move_cursor(&ArrowRight);
|
||
|
}
|
||
|
self.delete_char();
|
||
|
}
|
||
|
|
||
|
fn _page_up_or_down(&mut self, key: KeyCode<char>) {
|
||
|
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 = 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();
|
||
|
|
||
|
// Explicitly flush output so that the cursor actually appears correctly
|
||
|
io::stdout().flush().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<()> {
|
||
|
let file = File::open(filename);
|
||
|
|
||
|
match file {
|
||
|
Ok(_) => {
|
||
|
let buf_reader = BufReader::new(file.unwrap());
|
||
|
|
||
|
let lines = buf_reader.lines().map(|l| clean_unwrap(l));
|
||
|
|
||
|
for line in lines {
|
||
|
self.insert_row(self.rows.len(), &line);
|
||
|
}
|
||
|
|
||
|
// Make sure the whole file is loaded before
|
||
|
// showing the filename and selecting the syntax
|
||
|
// for highlighting
|
||
|
self.filename = filename.to_owned();
|
||
|
self.select_syntax_highlight();
|
||
|
|
||
|
self.dirty = 0;
|
||
|
},
|
||
|
// Gracefully handle errors opening a file
|
||
|
Err(e) => {
|
||
|
self.set_status_message(&e.to_string());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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.to_string())),
|
||
|
};
|
||
|
|
||
|
file.sync_all()?;
|
||
|
|
||
|
Ok(())
|
||
|
}
|
||
|
|
||
|
// ------------------------------------------------------------------------
|
||
|
// Find
|
||
|
// ------------------------------------------------------------------------
|
||
|
|
||
|
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();
|
||
|
}
|
||
|
|
||
|
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<Syntax> {
|
||
|
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
|
||
|
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 the highlight color escape sequence for
|
||
|
/// the type of syntax
|
||
|
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
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// 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<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::*;
|
||
|
|
||
|
#[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');
|
||
|
}
|
||
|
}
|