initial hecto commit

This commit is contained in:
Sebastian Hugentobler 2021-01-24 14:36:14 +01:00
commit 3fb59e1670
Signed by: shu
GPG Key ID: BB32CF3CA052C2F0
10 changed files with 1542 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

69
Cargo.lock generated Normal file
View File

@ -0,0 +1,69 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "bitflags"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "hecto"
version = "0.1.0"
dependencies = [
"termion",
"unicode-segmentation",
]
[[package]]
name = "libc"
version = "0.2.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929"
[[package]]
name = "numtoa"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
[[package]]
name = "redox_syscall"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]]
name = "redox_syscall"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05ec8ca9416c5ea37062b502703cd7fcb207736bc294f6e0cf367ac6fc234570"
dependencies = [
"bitflags",
]
[[package]]
name = "redox_termios"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f"
dependencies = [
"redox_syscall 0.2.4",
]
[[package]]
name = "termion"
version = "1.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c22cec9d8978d906be5ac94bceb5a010d885c626c4c8855721a4dbd20e3ac905"
dependencies = [
"libc",
"numtoa",
"redox_syscall 0.1.57",
"redox_termios",
]
[[package]]
name = "unicode-segmentation"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"

9
Cargo.toml Normal file
View File

@ -0,0 +1,9 @@
[package]
name = "hecto"
version = "0.1.0"
authors = ["Sebastian Hugentobler <sebastian@vanwa.ch>"]
edition = "2018"
[dependencies]
termion = "1.5.5"
unicode-segmentation = "1.7.1"

196
src/document.rs Normal file
View File

@ -0,0 +1,196 @@
use crate::FileType;
use crate::Position;
use crate::Row;
use crate::SearchDirection;
use std::fs;
use std::io::{Error, Write};
#[derive(Default)]
pub struct Document {
rows: Vec<Row>,
pub file_name: Option<String>,
dirty: bool,
file_type: FileType,
}
impl Document {
pub fn open(filename: &str) -> Result<Self, Error> {
let contents = fs::read_to_string(filename)?;
let file_type = FileType::from(filename);
let mut rows = Vec::new();
for value in contents.lines() {
rows.push(Row::from(value));
}
Ok(Self {
rows,
file_name: Some(filename.to_string()),
dirty: false,
file_type,
})
}
pub fn file_type(&self) -> String {
self.file_type.name()
}
pub fn row(&self, index: usize) -> Option<&Row> {
self.rows.get(index)
}
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
pub fn len(&self) -> usize {
self.rows.len()
}
fn insert_newline(&mut self, at: &Position) {
if at.y > self.rows.len() {
return;
}
if at.y == self.rows.len() {
self.rows.push(Row::default());
return;
}
#[allow(clippy::indexing_slicing)]
let current_row = &mut self.rows[at.y];
let new_row = current_row.split(at.x);
#[allow(clippy::integer_arithmetic)]
self.rows.insert(at.y + 1, new_row);
}
pub fn insert(&mut self, at: &Position, c: char) {
if at.y > self.rows.len() {
return;
}
self.dirty = true;
if c == '\n' {
self.insert_newline(at);
} else if at.y == self.rows.len() {
let mut row = Row::default();
row.insert(0, c);
self.rows.push(row);
} else {
#[allow(clippy::indexing_slicing)]
let row = &mut self.rows[at.y];
row.insert(at.x, c);
}
self.unhighlight_rows(at.y);
}
fn unhighlight_rows(&mut self, start: usize) {
let start = start.saturating_sub(1);
for row in self.rows.iter_mut().skip(start) {
row.is_highlighted = false;
}
}
#[allow(clippy::integer_arithmetic, clippy::indexing_slicing)]
pub fn delete(&mut self, at: &Position) {
let len = self.len();
if at.y >= len {
return;
}
self.dirty = true;
if at.x == self.rows[at.y].len() && at.y + 1 < len {
let next_row = self.rows.remove(at.y + 1);
let row = &mut self.rows[at.y];
row.append(&next_row);
} else {
let row = &mut self.rows[at.y];
row.delete(at.x);
}
self.unhighlight_rows(at.y);
}
pub fn save(&mut self) -> Result<(), Error> {
if let Some(file_name) = &self.file_name {
let mut file = fs::File::create(file_name)?;
self.file_type = FileType::from(file_name);
for row in &mut self.rows {
file.write_all(row.as_bytes())?;
file.write_all(b"\n")?;
}
self.dirty = false;
}
Ok(())
}
pub fn highlight(&mut self, word: &Option<String>, until: Option<usize>) {
let mut start_with_comment = false;
let until = if let Some(until) = until {
if until.saturating_add(1) < self.rows.len() {
until.saturating_add(1)
} else {
self.rows.len()
}
} else {
self.rows.len()
};
#[allow(clippy::indexing_slicing)]
for row in &mut self.rows[..until] {
start_with_comment = row.highlight(
&self.file_type.highlighting_options(),
word,
start_with_comment,
);
}
}
pub fn is_dirty(&self) -> bool {
self.dirty
}
#[allow(clippy::indexing_slicing)]
pub fn find(&self, query: &str, at: &Position, direction: SearchDirection) -> Option<Position> {
if at.y >= self.rows.len() {
return None;
}
let mut position = Position { x: at.x, y: at.y };
let start = if direction == SearchDirection::Forward {
at.y
} else {
0
};
let end = if direction == SearchDirection::Forward {
self.rows.len()
} else {
at.y.saturating_add(1)
};
for _ in start..end {
if let Some(row) = self.rows.get(position.y) {
if let Some(x) = row.find(&query, position.x, direction) {
position.x = x;
return Some(position);
}
if direction == SearchDirection::Forward {
position.y = position.y.saturating_add(1);
position.x = 0;
} else {
position.y = position.y.saturating_sub(1);
position.x = self.rows[position.y].len();
}
} else {
return None;
}
}
None
}
}

466
src/editor.rs Normal file
View File

@ -0,0 +1,466 @@
use crate::Document;
use crate::Row;
use crate::Terminal;
use std::env;
use std::time::{Duration, Instant};
use termion::color;
use termion::event::Key;
const VERSION: &str = env!("CARGO_PKG_VERSION");
const STATUS_BG_COLOR: color::Rgb = color::Rgb(239, 239, 239);
const STATUS_FG_COLOR: color::Rgb = color::Rgb(63, 63, 63);
const QUIT_TIMES: u8 = 3;
#[derive(PartialEq, Copy, Clone)]
pub enum SearchDirection {
Forward,
Backward,
}
#[derive(Default, Clone)]
pub struct Position {
pub x: usize,
pub y: usize,
}
struct StatusMessage {
text: String,
time: Instant,
}
impl StatusMessage {
fn from(message: String) -> Self {
Self {
time: Instant::now(),
text: message,
}
}
}
pub struct Editor {
should_quit: bool,
terminal: Terminal,
cursor_position: Position,
offset: Position,
document: Document,
status_message: StatusMessage,
quit_times: u8,
highlighted_word: Option<String>,
}
impl Editor {
pub fn run(&mut self) {
loop {
if let Err(error) = self.refresh_screen() {
die(error);
}
if self.should_quit {
break;
}
if let Err(error) = self.process_keypress() {
die(error);
}
}
}
pub fn default() -> Self {
let args: Vec<String> = env::args().collect();
let mut initial_status =
String::from("HELP: Ctrl-F = find | Ctrl-S = save | Ctrl-Q = quit");
let document = if let Some(file_name) = args.get(1) {
let doc = Document::open(file_name);
if let Ok(doc) = doc {
doc
} else {
initial_status = format!("ERR: Could not open file: {}", file_name);
Document::default()
}
} else {
Document::default()
};
Self {
should_quit: false,
terminal: Terminal::default().expect("Failed to initialize terminal"),
cursor_position: Position::default(),
offset: Position::default(),
document,
status_message: StatusMessage::from(initial_status),
quit_times: QUIT_TIMES,
highlighted_word: None,
}
}
fn refresh_screen(&mut self) -> Result<(), std::io::Error> {
Terminal::cursor_hide();
Terminal::cursor_position(&Position::default());
if self.should_quit {
Terminal::clear_screen();
println!("Goodbye.\r");
} else {
self.document.highlight(
&self.highlighted_word,
Some(
self.offset
.y
.saturating_add(self.terminal.size().height as usize),
),
);
self.draw_rows();
self.draw_status_bar();
self.draw_message_bar();
Terminal::cursor_position(&Position {
x: self.cursor_position.x.saturating_sub(self.offset.x),
y: self.cursor_position.y.saturating_sub(self.offset.y),
});
}
Terminal::cursor_show();
Terminal::flush()
}
fn save(&mut self) {
if self.document.file_name.is_none() {
let new_name = self.prompt("Save as: ", |_, _, _| {}).unwrap_or(None);
if new_name.is_none() {
self.status_message = StatusMessage::from("Save aborted.".to_string());
return;
}
self.document.file_name = new_name;
}
if self.document.save().is_ok() {
self.status_message = StatusMessage::from("File saved successfully.".to_string());
} else {
self.status_message = StatusMessage::from("Error writing file!".to_string());
}
}
fn search(&mut self) {
let old_position = self.cursor_position.clone();
let mut direction = SearchDirection::Forward;
let query = self
.prompt(
"Search (ESC to cancel, Arrows to navigate): ",
|editor, key, query| {
let mut moved = false;
match key {
Key::Right | Key::Down => {
direction = SearchDirection::Forward;
editor.move_cursor(Key::Right);
moved = true;
}
Key::Left | Key::Up => direction = SearchDirection::Backward,
_ => direction = SearchDirection::Forward,
}
if let Some(position) =
editor
.document
.find(&query, &editor.cursor_position, direction)
{
editor.cursor_position = position;
editor.scroll();
} else if moved {
editor.move_cursor(Key::Left);
}
editor.highlighted_word = Some(query.to_string());
},
)
.unwrap_or(None);
if query.is_none() {
self.cursor_position = old_position;
self.scroll();
}
self.highlighted_word = None;
}
fn process_keypress(&mut self) -> Result<(), std::io::Error> {
let pressed_key = Terminal::read_key()?;
match pressed_key {
Key::Ctrl('q') => {
if self.quit_times > 0 && self.document.is_dirty() {
self.status_message = StatusMessage::from(format!(
"WARNING! File has unsaved changes. Press Ctrl-Q {} more times to quit.",
self.quit_times
));
self.quit_times -= 1;
return Ok(());
}
self.should_quit = true
}
Key::Ctrl('s') => self.save(),
Key::Ctrl('f') => self.search(),
Key::Char(c) => {
self.document.insert(&self.cursor_position, c);
self.move_cursor(Key::Right);
}
Key::Delete => self.document.delete(&self.cursor_position),
Key::Backspace => {
if self.cursor_position.x > 0 || self.cursor_position.y > 0 {
self.move_cursor(Key::Left);
self.document.delete(&self.cursor_position);
}
}
Key::Up
| Key::Down
| Key::Left
| Key::Right
| Key::PageUp
| Key::PageDown
| Key::End
| Key::Home => self.move_cursor(pressed_key),
_ => (),
}
self.scroll();
if self.quit_times < QUIT_TIMES {
self.quit_times = QUIT_TIMES;
self.status_message = StatusMessage::from(String::new());
}
Ok(())
}
fn scroll(&mut self) {
let Position { x, y } = self.cursor_position;
let width = self.terminal.size().width as usize;
let height = self.terminal.size().height as usize;
let mut offset = &mut self.offset;
if y < offset.y {
offset.y = y;
} else if y >= offset.y.saturating_add(height) {
offset.y = y.saturating_sub(height).saturating_add(1);
}
if x < offset.x {
offset.x = x;
} else if x >= offset.x.saturating_add(width) {
offset.x = x.saturating_sub(width).saturating_add(1);
}
}
fn move_cursor(&mut self, key: Key) {
let terminal_height = self.terminal.size().height as usize;
let Position { mut y, mut x } = self.cursor_position;
let height = self.document.len();
let mut width = if let Some(row) = self.document.row(y) {
row.len()
} else {
0
};
match key {
Key::Up => y = y.saturating_sub(1),
Key::Down => {
if y < height {
y = y.saturating_add(1);
}
}
Key::Left => {
if x > 0 {
x -= 1;
} else if y > 0 {
y -= 1;
if let Some(row) = self.document.row(y) {
x = row.len();
} else {
x = 0;
}
}
}
Key::Right => {
if x < width {
x += 1;
} else if y < height {
y += 1;
x = 0;
}
}
Key::PageUp => {
y = if y > terminal_height {
y.saturating_sub(terminal_height)
} else {
0
}
}
Key::PageDown => {
y = if y.saturating_add(terminal_height) < height {
y.saturating_add(terminal_height)
} else {
height
}
}
Key::Home => x = 0,
Key::End => x = width,
_ => (),
}
width = if let Some(row) = self.document.row(y) {
row.len()
} else {
0
};
if x > width {
x = width;
}
self.cursor_position = Position { x, y }
}
fn draw_welcome_message(&self) {
let mut welcome_message = format!("Hecto editor -- version {}", VERSION);
let width = self.terminal.size().width as usize;
let len = welcome_message.len();
#[allow(clippy::integer_arithmetic, clippy::integer_division)]
let padding = width.saturating_sub(len) / 2;
let spaces = " ".repeat(padding.saturating_sub(1));
welcome_message = format!("~{}{}", spaces, welcome_message);
welcome_message.truncate(width);
println!("{}\r", welcome_message);
}
pub fn draw_row(&self, row: &Row) {
let width = self.terminal.size().width as usize;
let start = self.offset.x;
let end = self.offset.x.saturating_add(width);
let row = row.render(start, end);
println!("{}\r", row)
}
#[allow(clippy::integer_division, clippy::integer_arithmetic)]
fn draw_rows(&self) {
let height = self.terminal.size().height;
for terminal_row in 0..height {
Terminal::clear_current_line();
if let Some(row) = self
.document
.row(self.offset.y.saturating_add(terminal_row as usize))
{
self.draw_row(row);
} else if self.document.is_empty() && terminal_row == height / 3 {
self.draw_welcome_message();
} else {
println!("~\r");
}
}
}
fn draw_status_bar(&self) {
let mut status;
let width = self.terminal.size().width as usize;
let modified_indicator = if self.document.is_dirty() {
" (modified)"
} else {
""
};
let mut file_name = "[No Name]".to_string();
if let Some(name) = &self.document.file_name {
file_name = name.clone();
file_name.truncate(20);
}
status = format!(
"{} - {} lines{}",
file_name,
self.document.len(),
modified_indicator
);
let line_indicator = format!(
"{} | {}/{}",
self.document.file_type(),
self.cursor_position.y.saturating_add(1),
self.document.len()
);
#[allow(clippy::integer_arithmetic)]
let len = status.len() + line_indicator.len();
status.push_str(&" ".repeat(width.saturating_sub(len)));
status = format!("{}{}", status, line_indicator);
status.truncate(width);
Terminal::set_bg_color(STATUS_BG_COLOR);
Terminal::set_fg_color(STATUS_FG_COLOR);
println!("{}\r", status);
Terminal::reset_bg_color();
Terminal::reset_fg_color();
}
fn draw_message_bar(&self) {
Terminal::clear_current_line();
let message = &self.status_message;
if Instant::now() - message.time < Duration::new(5, 0) {
let mut text = message.text.clone();
text.truncate(self.terminal.size().width as usize);
print!("{}", text);
}
}
fn prompt<C>(&mut self, prompt: &str, mut callback: C) -> Result<Option<String>, std::io::Error>
where
C: FnMut(&mut Self, Key, &String),
{
let mut result = String::new();
loop {
self.status_message = StatusMessage::from(format!("{}{}", prompt, result));
self.refresh_screen()?;
let key = Terminal::read_key()?;
match key {
Key::Backspace => result.truncate(result.len().saturating_sub(1)),
Key::Char('\n') => break,
Key::Char(c) => {
if !c.is_control() {
result.push(c);
}
}
Key::Esc => {
result.truncate(0);
break;
}
_ => (),
}
callback(self, key, &result);
}
self.status_message = StatusMessage::from(String::new());
if result.is_empty() {
return Ok(None);
}
Ok(Some(result))
}
}
fn die(e: std::io::Error) {
Terminal::clear_screen();
panic!(e);
}

149
src/filetype.rs Normal file
View File

@ -0,0 +1,149 @@
pub struct FileType {
name: String,
hl_opts: HighlightingOptions,
}
#[derive(Default)]
pub struct HighlightingOptions {
numbers: bool,
strings: bool,
characters: bool,
comments: bool,
multiline_comments: bool,
primary_keywords: Vec<String>,
secondary_keywords: Vec<String>,
}
impl HighlightingOptions {
pub fn numbers(&self) -> bool {
self.numbers
}
pub fn strings(&self) -> bool {
self.strings
}
pub fn characters(&self) -> bool {
self.characters
}
pub fn comments(&self) -> bool {
self.comments
}
pub fn multiline_comments(&self) -> bool {
self.multiline_comments
}
pub fn primary_keywords(&self) -> &Vec<String> {
&self.primary_keywords
}
pub fn secondary_keywords(&self) -> &Vec<String> {
&self.secondary_keywords
}
}
impl Default for FileType {
fn default() -> Self {
Self {
name: String::from("No filetype"),
hl_opts: HighlightingOptions::default(),
}
}
}
impl FileType {
pub fn name(&self) -> String {
self.name.clone()
}
pub fn highlighting_options(&self) -> &HighlightingOptions {
&self.hl_opts
}
pub fn from(file_name: &str) -> Self {
if file_name.ends_with(".rs") {
return Self {
name: String::from("Rust"),
hl_opts: HighlightingOptions {
numbers: true,
strings: true,
characters: true,
comments: true,
multiline_comments: true,
primary_keywords: vec![
"as".to_string(),
"break".to_string(),
"const".to_string(),
"continue".to_string(),
"crate".to_string(),
"else".to_string(),
"enum".to_string(),
"extern".to_string(),
"false".to_string(),
"fn".to_string(),
"for".to_string(),
"if".to_string(),
"impl".to_string(),
"in".to_string(),
"let".to_string(),
"loop".to_string(),
"match".to_string(),
"mod".to_string(),
"move".to_string(),
"mut".to_string(),
"pub".to_string(),
"ref".to_string(),
"return".to_string(),
"self".to_string(),
"Self".to_string(),
"static".to_string(),
"struct".to_string(),
"super".to_string(),
"trait".to_string(),
"true".to_string(),
"type".to_string(),
"unsafe".to_string(),
"use".to_string(),
"where".to_string(),
"while".to_string(),
"dyn".to_string(),
"abstract".to_string(),
"become".to_string(),
"box".to_string(),
"do".to_string(),
"final".to_string(),
"macro".to_string(),
"override".to_string(),
"priv".to_string(),
"typeof".to_string(),
"unsized".to_string(),
"virtual".to_string(),
"yield".to_string(),
"async".to_string(),
"await".to_string(),
"try".to_string(),
],
secondary_keywords: vec![
"bool".to_string(),
"char".to_string(),
"i8".to_string(),
"i16".to_string(),
"i32".to_string(),
"i64".to_string(),
"isize".to_string(),
"u8".to_string(),
"u16".to_string(),
"u32".to_string(),
"u64".to_string(),
"usize".to_string(),
"f32".to_string(),
"f64".to_string(),
],
},
};
}
Self::default()
}
}

29
src/highlighting.rs Normal file
View File

@ -0,0 +1,29 @@
use termion::color;
#[derive(PartialEq, Clone, Copy)]
pub enum Type {
None,
Number,
Match,
String,
Character,
Comment,
MultilineComment,
PrimaryKeywords,
SecondaryKeywords,
}
impl Type {
pub fn to_color(self) -> impl color::Color {
match self {
Type::Number => color::Rgb(220, 163, 163),
Type::Match => color::Rgb(38, 139, 210),
Type::String => color::Rgb(211, 54, 130),
Type::Character => color::Rgb(108, 113, 196),
Type::Comment | Type::MultilineComment => color::Rgb(133, 153, 0),
Type::PrimaryKeywords => color::Rgb(181, 137, 0),
Type::SecondaryKeywords => color::Rgb(42, 161, 152),
_ => color::Rgb(255, 255, 255),
}
}
}

28
src/main.rs Normal file
View File

@ -0,0 +1,28 @@
#![warn(clippy::all, clippy::pedantic, clippy::restriction)]
#![allow(
clippy::missing_docs_in_private_items,
clippy::implicit_return,
clippy::shadow_reuse,
clippy::print_stdout,
clippy::wildcard_enum_match_arm,
clippy::else_if_without_else
)]
mod document;
mod editor;
mod filetype;
mod highlighting;
mod row;
mod terminal;
pub use document::Document;
use editor::Editor;
pub use editor::Position;
pub use editor::SearchDirection;
pub use filetype::FileType;
pub use filetype::HighlightingOptions;
pub use row::Row;
pub use terminal::Terminal;
fn main() {
Editor::default().run();
}

505
src/row.rs Normal file
View File

@ -0,0 +1,505 @@
use crate::highlighting;
use crate::HighlightingOptions;
use crate::SearchDirection;
use std::cmp;
use termion::color;
use unicode_segmentation::UnicodeSegmentation;
#[derive(Default)]
pub struct Row {
string: String,
highlighting: Vec<highlighting::Type>,
pub is_highlighted: bool,
len: usize,
}
impl From<&str> for Row {
fn from(slice: &str) -> Self {
Self {
string: String::from(slice),
highlighting: Vec::new(),
is_highlighted: false,
len: slice.graphemes(true).count(),
}
}
}
impl Row {
pub fn render(&self, start: usize, end: usize) -> String {
let end = cmp::min(end, self.string.len());
let start = cmp::min(start, end);
let mut result = String::new();
let mut current_highlighting = &highlighting::Type::None;
#[allow(clippy::integer_arithmetic)]
for (index, grapheme) in self.string[..]
.graphemes(true)
.enumerate()
.skip(start)
.take(end - start)
{
if let Some(c) = grapheme.chars().next() {
let highlighting_type = self
.highlighting
.get(index)
.unwrap_or(&highlighting::Type::None);
if highlighting_type != current_highlighting {
current_highlighting = highlighting_type;
let start_highlight =
format!("{}", termion::color::Fg(highlighting_type.to_color()));
result.push_str(&start_highlight[..]);
}
if c == '\t' {
result.push_str(" ");
} else {
result.push(c);
}
}
}
let end_highlight = format!("{}", termion::color::Fg(color::Reset));
result.push_str(&end_highlight[..]);
result
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn insert(&mut self, at: usize, c: char) {
if at >= self.len() {
self.string.push(c);
self.len += 1;
return;
}
let mut result: String = String::new();
let mut length = 0;
for (index, grapheme) in self.string[..].graphemes(true).enumerate() {
length += 1;
if index == at {
length += 1;
result.push(c);
}
result.push_str(grapheme);
}
self.len = length;
self.string = result;
}
pub fn delete(&mut self, at: usize) {
if at >= self.len() {
return;
}
let mut result: String = String::new();
let mut length = 0;
for (index, grapheme) in self.string[..].graphemes(true).enumerate() {
if index != at {
length += 1;
result.push_str(grapheme);
}
}
self.len = length;
self.string = result;
}
pub fn append(&mut self, new: &Self) {
self.string = format!("{}{}", self.string, new.string);
self.len += new.len;
}
pub fn split(&mut self, at: usize) -> Self {
let mut row: String = String::new();
let mut length = 0;
let mut splitted_row: String = String::new();
let mut splitted_length = 0;
for (index, grapheme) in self.string[..].graphemes(true).enumerate() {
if index < at {
length += 1;
row.push_str(grapheme);
} else {
splitted_length += 1;
splitted_row.push_str(grapheme);
}
}
self.string = row;
self.len = length;
self.is_highlighted = false;
Self {
string: splitted_row,
highlighting: Vec::new(),
is_highlighted: false,
len: splitted_length,
}
}
pub fn as_bytes(&self) -> &[u8] {
self.string.as_bytes()
}
pub fn find(&self, query: &str, at: usize, direction: SearchDirection) -> Option<usize> {
if at > self.len || query.is_empty() {
return None;
}
let start = if direction == SearchDirection::Forward {
at
} else {
0
};
let end = if direction == SearchDirection::Forward {
self.len
} else {
at
};
#[allow(clippy::integer_arithmetic)]
let substring: String = self.string[..]
.graphemes(true)
.skip(start)
.take(end - start)
.collect();
let matching_byte_index = if direction == SearchDirection::Forward {
substring.find(query)
} else {
substring.rfind(query)
};
if let Some(matching_byte_index) = matching_byte_index {
for (grapheme_index, (byte_index, _)) in
substring[..].grapheme_indices(true).enumerate()
{
if matching_byte_index == byte_index {
#[allow(clippy::integer_arithmetic)]
return Some(start + grapheme_index);
}
}
}
None
}
fn highlight_match(&mut self, word: &Option<String>) {
if let Some(word) = word {
if word.is_empty() {
return;
}
let mut index = 0;
while let Some(search_match) = self.find(word, index, SearchDirection::Forward) {
if let Some(next_index) = search_match.checked_add(word[..].graphemes(true).count())
{
#[allow(clippy::indexing_slicing)]
for i in index.saturating_add(search_match)..next_index {
self.highlighting[i] = highlighting::Type::Match;
}
index = next_index;
} else {
break;
}
}
}
}
fn highlight_str(
&mut self,
index: &mut usize,
substring: &str,
chars: &[char],
hl_type: highlighting::Type,
) -> bool {
if substring.is_empty() {
return false;
}
for (substring_index, c) in substring.chars().enumerate() {
if let Some(next_char) = chars.get(index.saturating_add(substring_index)) {
if *next_char != c {
return false;
}
} else {
return false;
}
}
for _ in 0..substring.len() {
self.highlighting.push(hl_type);
*index += 1;
}
true
}
fn highlight_keywords(
&mut self,
index: &mut usize,
chars: &[char],
keywords: &[String],
hl_type: highlighting::Type,
) -> bool {
if *index > 0 {
#[allow(clippy::indexing_slicing, clippy::integer_arithmetic)]
let prev_char = chars[*index - 1];
if !is_separator(prev_char) {
return false;
}
}
for word in keywords {
if *index < chars.len().saturating_sub(word.len()) {
#[allow(clippy::indexing_slicing, clippy::integer_arithmetic)]
let next_char = chars[*index + word.len()];
if !is_separator(next_char) {
continue;
}
}
if self.highlight_str(index, &word, chars, hl_type) {
return true;
}
}
false
}
fn highlight_primary_keywords(
&mut self,
index: &mut usize,
opts: &HighlightingOptions,
chars: &[char],
) -> bool {
self.highlight_keywords(
index,
chars,
opts.primary_keywords(),
highlighting::Type::PrimaryKeywords,
)
}
fn highlight_secondary_keywords(
&mut self,
index: &mut usize,
opts: &HighlightingOptions,
chars: &[char],
) -> bool {
self.highlight_keywords(
index,
chars,
opts.secondary_keywords(),
highlighting::Type::SecondaryKeywords,
)
}
fn highlight_char(
&mut self,
index: &mut usize,
opts: &HighlightingOptions,
c: char,
chars: &[char],
) -> bool {
if opts.characters() && c == '\'' {
if let Some(next_char) = chars.get(index.saturating_add(1)) {
let closing_index = if *next_char == '\\' {
index.saturating_add(3)
} else {
index.saturating_add(2)
};
if let Some(closing_char) = chars.get(closing_index) {
if *closing_char == '\'' {
for _ in 0..=closing_index.saturating_sub(*index) {
self.highlighting.push(highlighting::Type::Character);
*index += 1;
}
return true;
}
}
}
}
false
}
fn highlight_comment(
&mut self,
index: &mut usize,
opts: &HighlightingOptions,
c: char,
chars: &[char],
) -> bool {
if opts.comments() && c == '/' && *index < chars.len() {
if let Some(next_char) = chars.get(index.saturating_add(1)) {
if *next_char == '/' {
for _ in *index..chars.len() {
self.highlighting.push(highlighting::Type::Comment);
*index += 1;
}
return true;
}
};
}
false
}
#[allow(clippy::indexing_slicing, clippy::integer_arithmetic)]
fn highlight_multiline_comment(
&mut self,
index: &mut usize,
opts: &HighlightingOptions,
c: char,
chars: &[char],
) -> bool {
if opts.comments() && c == '/' && *index < chars.len() {
if let Some(next_char) = chars.get(index.saturating_add(1)) {
if *next_char == '*' {
let closing_index =
if let Some(closing_index) = self.string[*index + 2..].find("*/") {
*index + closing_index + 4
} else {
chars.len()
};
for _ in *index..closing_index {
self.highlighting.push(highlighting::Type::MultilineComment);
*index += 1;
}
return true;
}
};
}
false
}
fn highlight_string(
&mut self,
index: &mut usize,
opts: &HighlightingOptions,
c: char,
chars: &[char],
) -> bool {
if opts.strings() && c == '"' {
loop {
self.highlighting.push(highlighting::Type::String);
*index += 1;
if let Some(next_char) = chars.get(*index) {
if *next_char == '"' {
break;
}
} else {
break;
}
}
self.highlighting.push(highlighting::Type::String);
*index += 1;
return true;
}
false
}
fn highlight_number(
&mut self,
index: &mut usize,
opts: &HighlightingOptions,
c: char,
chars: &[char],
) -> bool {
if opts.numbers() && c.is_ascii_digit() {
if *index > 0 {
#[allow(clippy::indexing_slicing, clippy::integer_arithmetic)]
let prev_char = chars[*index - 1];
if !is_separator(prev_char) {
return false;
}
}
loop {
self.highlighting.push(highlighting::Type::Number);
*index += 1;
if let Some(next_char) = chars.get(*index) {
if *next_char != '.' && !next_char.is_ascii_digit() {
break;
}
} else {
break;
}
}
return true;
}
false
}
#[allow(clippy::indexing_slicing, clippy::integer_arithmetic)]
pub fn highlight(
&mut self,
opts: &HighlightingOptions,
word: &Option<String>,
start_with_comment: bool,
) -> bool {
let chars: Vec<char> = self.string.chars().collect();
if self.is_highlighted && word.is_none() {
if let Some(hl_type) = self.highlighting.last() {
if *hl_type == highlighting::Type::MultilineComment
&& self.string.len() > 1
&& self.string[self.string.len() - 2..] == *"*/"
{
return true;
}
}
return false;
}
self.highlighting = Vec::new();
let mut index = 0;
let mut in_ml_comment = start_with_comment;
if in_ml_comment {
let closing_index = if let Some(closing_index) = self.string.find("*/") {
closing_index + 2
} else {
chars.len()
};
for _ in 0..closing_index {
self.highlighting.push(highlighting::Type::MultilineComment);
}
index = closing_index;
}
while let Some(c) = chars.get(index) {
if self.highlight_multiline_comment(&mut index, &opts, *c, &chars) {
in_ml_comment = true;
continue;
}
in_ml_comment = false;
if self.highlight_char(&mut index, opts, *c, &chars)
|| self.highlight_comment(&mut index, opts, *c, &chars)
|| self.highlight_multiline_comment(&mut index, &opts, *c, &chars)
|| self.highlight_primary_keywords(&mut index, &opts, &chars)
|| self.highlight_secondary_keywords(&mut index, &opts, &chars)
|| self.highlight_string(&mut index, opts, *c, &chars)
|| self.highlight_number(&mut index, opts, *c, &chars)
{
continue;
}
self.highlighting.push(highlighting::Type::None);
index += 1;
}
self.highlight_match(word);
if in_ml_comment && &self.string[self.string.len().saturating_sub(2)..] != "*/" {
return true;
}
self.is_highlighted = true;
false
}
}
fn is_separator(c: char) -> bool {
c.is_ascii_punctuation() || c.is_ascii_whitespace()
}

90
src/terminal.rs Normal file
View File

@ -0,0 +1,90 @@
use crate::Position;
use std::io::{self, stdout, Write};
use termion::color;
use termion::event::Key;
use termion::input::TermRead;
use termion::raw::{IntoRawMode, RawTerminal};
pub struct Size {
pub width: u16,
pub height: u16,
}
pub struct Terminal {
size: Size,
_stdout: RawTerminal<std::io::Stdout>,
}
impl Terminal {
pub fn default() -> Result<Self, std::io::Error> {
let size = termion::terminal_size()?;
Ok(Self {
size: Size {
width: size.0,
height: size.1.saturating_sub(2),
},
_stdout: stdout().into_raw_mode()?,
})
}
pub fn size(&self) -> &Size {
&self.size
}
pub fn clear_screen() {
print!("{}", termion::clear::All);
}
#[allow(clippy::cast_possible_truncation)]
pub fn cursor_position(position: &Position) {
let Position { mut x, mut y } = position;
x = x.saturating_add(1);
y = y.saturating_add(1);
let x = x as u16;
let y = y as u16;
print!("{}", termion::cursor::Goto(x, y));
}
pub fn flush() -> Result<(), std::io::Error> {
io::stdout().flush()
}
pub fn read_key() -> Result<Key, std::io::Error> {
loop {
if let Some(key) = io::stdin().lock().keys().next() {
return key;
}
}
}
pub fn cursor_hide() {
print!("{}", termion::cursor::Hide);
}
pub fn cursor_show() {
print!("{}", termion::cursor::Show);
}
pub fn clear_current_line() {
print!("{}", termion::clear::CurrentLine);
}
pub fn set_bg_color(color: color::Rgb) {
print!("{}", color::Bg(color));
}
pub fn reset_bg_color() {
print!("{}", color::Bg(color::Reset));
}
pub fn set_fg_color(color: color::Rgb) {
print!("{}", color::Fg(color));
}
pub fn reset_fg_color() {
print!("{}", color::Fg(color::Reset));
}
}