commit 7c5794e110baf428c9dd242d0d5ec0a5626ad23a Author: able Date: Fri Sep 15 21:29:27 2023 -0500 Simple scripting diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..bf33089 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,379 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adit" +version = "0.1.1" +dependencies = [ + "dirs", + "rhai", + "serde", + "termion", + "toml", + "unicode-segmentation", +] + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "const-random", + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "const-random" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368a7a772ead6ce7e1de82bfb04c485f3db8ec744f72925af5735e29a22cc18e" +dependencies = [ + "const-random-macro", + "proc-macro-hack", +] + +[[package]] +name = "const-random-macro" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d7d6ab3c3a2282db210df5f02c4dab6e0a7057af0fb7ebd4070f30fe05c0ddb" +dependencies = [ + "getrandom", + "once_cell", + "proc-macro-hack", + "tiny-keccak", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "libc" +version = "0.2.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" + +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", +] + +[[package]] +name = "numtoa" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" + +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_termios" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" +dependencies = [ + "redox_syscall 0.1.56", +] + +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom", + "redox_syscall 0.2.10", +] + +[[package]] +name = "rhai" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637a4f79f65571b1fd1a0ebbae05bbbf58a01faf612abbc3eea15cda34f0b87a" +dependencies = [ + "ahash", + "bitflags 2.4.0", + "instant", + "num-traits", + "once_cell", + "rhai_codegen", + "smallvec", + "smartstring", +] + +[[package]] +name = "rhai_codegen" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "853977598f084a492323fe2f7896b4100a86284ee8473612de60021ea341310f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.35", +] + +[[package]] +name = "serde" +version = "1.0.134" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b3c34c1690edf8174f5b289a336ab03f568a4460d8c6df75f2f3a692b3bc6a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.134" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784ed1fbfa13fe191077537b0d70ec8ad1e903cfe04831da608aa36457cb653d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.86", +] + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "syn" +version = "2.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59bf04c28bee9043ed9ea1e41afc0552288d3aba9c6efdd78903b802926f4879" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termion" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8fb22f7cde82c8220e5aeacb3258ed7ce996142c77cba193f203515e26c330" +dependencies = [ + "libc", + "numtoa", + "redox_syscall 0.1.56", + "redox_termios", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-segmentation" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1967f4cdfc355b37fd76d2a954fb2ed3871034eb4f26d60537d88795cfc332a9" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..457a115 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "adit" +version = "0.1.1" +authors = ["able"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +termion = "1" +unicode-segmentation = "1" +toml = "0.5.8" +dirs = "*" +rhai = "1.16.1" + +[dependencies.serde] +version = "*" +features = ["derive"] diff --git a/assets/config.rhai b/assets/config.rhai new file mode 100644 index 0000000..f08b1b6 --- /dev/null +++ b/assets/config.rhai @@ -0,0 +1,6 @@ +fn status_bar() { + let sb = `${file_name}:${y}:${x} | ${file_type} | ${line_count}`; + + return sb; +} + diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..ecc21cc --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly" +components = ["rust-src", "llvm-tools"] diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..4d6009c --- /dev/null +++ b/shell.nix @@ -0,0 +1,37 @@ +{ pkgs ? import { } }: +pkgs.mkShell rec { + buildInputs = with pkgs; [ + clang + llvmPackages.bintools + rustup + + ]; + extraCmds = ''''; + RUSTC_VERSION = pkgs.lib.readFile ./rust-toolchain.toml; + # https://github.com/rust-lang/rust-bindgen#environment-variables + LIBCLANG_PATH = + pkgs.lib.makeLibraryPath [ pkgs.llvmPackages_latest.libclang.lib ]; + shellHook = '' + export PATH=$PATH:''${CARGO_HOME:-~/.cargo}/bin + export PATH=$PATH:''${RUSTUP_HOME:-~/.rustup}/toolchains/$RUSTC_VERSION-x86_64-unknown-linux-gnu/bin/ + ''; + # Add precompiled library to rustc search path + RUSTFLAGS = (builtins.map (a: "-L ${a}/lib") [ + # add libraries here (e.g. pkgs.libvmi) + ]); + # Add glibc, clang, glib and other headers to bindgen search path + BINDGEN_EXTRA_CLANG_ARGS = + # Includes with normal include path + (builtins.map (a: ''-I"${a}/include"'') [ + # add dev libraries here (e.g. pkgs.libvmi.dev) + pkgs.glibc.dev + ]) + # Includes with special directory paths + ++ [ + '' + -I"${pkgs.llvmPackages_latest.libclang.lib}/lib/clang/${pkgs.llvmPackages_latest.libclang.version}/include"'' + ''-I"${pkgs.glib.dev}/include/glib-2.0"'' + "-I${pkgs.glib.out}/lib/glib-2.0/include/" + ]; + +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..7caa2ff --- /dev/null +++ b/src/config.rs @@ -0,0 +1,96 @@ +use std::path::PathBuf; +use std::str::FromStr; +use std::{fs::File, io::Read}; + +use crate::Editor; +use serde::Deserialize; +use termion::color::Rgb; + +use rhai::Dynamic; +use rhai::Engine; +use rhai::Scope; +use rhai::AST; + +pub struct Config<'a> { + cfg: String, + eng: Engine, + pub scope: Scope<'a>, + ast: AST, +} +impl Config<'static> { + pub fn new() -> Self { + let mut engine = Engine::new(); + let mut scope = Scope::new(); + let ast = engine + .compile_file(PathBuf::from_str("assets/config.rhai").unwrap()) + .unwrap(); + let result = engine.call_fn::(&mut scope, &ast, "status_bar", ()); + + Self { + cfg: String::new(), + eng: Engine::new(), + scope: Scope::new(), + ast, + } + } + + pub fn call(&mut self, fn_name: &str) -> String { + self.eng + .call_fn::(&mut self.scope, &self.ast, fn_name, ()) + .unwrap() + } +} +#[derive(Deserialize)] +pub struct RGB(u8, u8, u8); + +impl RGB { + pub fn to_term_color(self) -> Rgb { + Rgb(self.0, self.1, self.2) + } +} + +#[derive(Deserialize)] +pub struct Theme { + pub status_fg_color: RGB, + pub status_bg_color: RGB, +} + +impl Theme { + pub fn default() -> Self { + Self { + status_fg_color: RGB(63, 63, 63), + status_bg_color: RGB(239, 239, 239), + } + } + + pub fn read_config() -> ThemeReturn { + match dirs::home_dir() { + Some(pathbuf) => { + let mut file = File::open(pathbuf); + match file { + Ok(mut file_handle) => { + let mut contents = String::new(); + match file_handle.read_to_string(&mut contents) { + Ok(ma) => { + let config: Theme = toml::from_str(&contents).unwrap(); + + return ThemeReturn::Theme(config); + } + + Err(_) => todo!(), + } + } + Err(_) => todo!(), + } + } + None => ThemeReturn::NoHomeDir, + } + } +} + +pub enum ThemeReturn { + Theme(Theme), + NoHomeDir, + + GenericError, +} diff --git a/src/document.rs b/src/document.rs new file mode 100644 index 0000000..ba7c027 --- /dev/null +++ b/src/document.rs @@ -0,0 +1,170 @@ +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 { + pub file_name: Option, + rows: Vec, + dirty: bool, + file_type: FileType, +} + +impl Document { + pub fn open(filename: &str) -> Result { + 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.rows.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 is_dirty(&self) -> bool { + self.dirty + } + #[allow(clippy::indexing_slicing)] + pub fn find(&self, query: &str, at: &Position, direction: SearchDirection) -> Option { + 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 + } + pub fn highlight(&mut self, word: &Option, until: Option) { + 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, + ); + } + } +} diff --git a/src/editor.rs b/src/editor.rs new file mode 100644 index 0000000..bcbb44e --- /dev/null +++ b/src/editor.rs @@ -0,0 +1,459 @@ +use crate::config::Config; +use crate::config::Theme; +use crate::Document; +use crate::Row; +use crate::Terminal; +use std::env; +use std::time::Duration; +use std::time::Instant; +use termion::event::Key; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); +const QUIT_TIMES: u8 = 2; + +#[derive(PartialEq, Copy, Clone)] +pub enum SearchDirection { + Forward, + Backward, +} + +#[derive(Default, Clone)] +pub struct Position { + pub x: usize, + pub y: usize, +} +#[derive(Clone)] +struct StatusMessage { + text: String, + time: Instant, +} +impl StatusMessage { + fn from(message: String) -> Self { + Self { + time: Instant::now(), + text: message, + } + } +} + +pub struct Editor<'a> { + should_quit: bool, + terminal: Terminal, + pub cursor_position: Position, + offset: Position, + pub document: Document, + status_message: StatusMessage, + quit_times: u8, + highlighted_word: Option, + pub config: Config<'a>, +} + +impl Editor<'static> { + 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 = 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() + }; + + let mut cfg = Config::new(); + + Self { + should_quit: false, + terminal: Terminal::default().expect("Failed to initialize terminal"), + document, + cursor_position: Position::default(), + offset: Position::default(), + status_message: StatusMessage::from(initial_status), + quit_times: QUIT_TIMES, + highlighted_word: None, + config: Config::new(), + } + } + + pub fn update_scope(&mut self) { + self.config + .scope + .set_or_push("line_count", self.document.len()); + + self.config + .scope + .set_or_push("file_type", self.document.file_type()); + + self.config + .scope + .set_or_push("x", self.cursor_position.x as u64); + self.config + .scope + .set_or_push("y", self.cursor_position.y as u64); + } + + fn refresh_screen(&mut self) -> Result<(), std::io::Error> { + self.update_scope(); + 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(), + + // NOTE: I am more of the opinion that adit should use hard tab. + Key::Char('\t') => { + for c in " ".chars() { + self.document.insert(&self.cursor_position, c); + self.move_cursor(Key::Right); + } + } + + 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!("Able 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(&mut 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); + } + // Make this line scriptable + + status = format!( + "{}:{}:{} | {} | {} lines{}", + file_name, + self.cursor_position.y.saturating_add(1), + self.cursor_position.x.saturating_add(1), + self.document.file_type(), + self.document.len(), + modified_indicator, + ); + + self.config.scope.set_or_push( + "file_name", + self.document + .file_name + .clone() + .unwrap_or("buffer".to_string()), + ); + {} + // status = self.config.call("status_bar"); + + #[allow(clippy::integer_arithmetic)] + let len = status.len(); + status.push_str(&" ".repeat(width.saturating_sub(len))); + status = format!(" {}", status); + status.truncate(width); + + let theme = Theme::default(); + + Terminal::set_bg_color(theme.status_bg_color.to_term_color()); + Terminal::set_fg_color(theme.status_fg_color.to_term_color()); + println!("{}\r", status); + Terminal::reset_fg_color(); + Terminal::reset_bg_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(&mut self, prompt: &str, mut callback: C) -> Result, 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); +} diff --git a/src/filetype.rs b/src/filetype.rs new file mode 100644 index 0000000..4b6707b --- /dev/null +++ b/src/filetype.rs @@ -0,0 +1,142 @@ +pub struct FileType { + name: String, + hl_opts: HighlightingOptions, +} + +#[derive(Default)] +pub struct HighlightingOptions { + numbers: bool, + strings: bool, + characters: bool, + // TODO make this configurable + comments: bool, + multiline_comments: bool, + primary_keywords: Vec, + secondary_keywords: Vec, +} + +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() + } +} + +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 primary_keywords(&self) -> &Vec { + &self.primary_keywords + } + pub fn secondary_keywords(&self) -> &Vec { + &self.secondary_keywords + } + pub fn multiline_comments(&self) -> bool { + self.multiline_comments + } +} diff --git a/src/highlighting.rs b/src/highlighting.rs new file mode 100644 index 0000000..8c95e7c --- /dev/null +++ b/src/highlighting.rs @@ -0,0 +1,28 @@ +use termion::color; +#[derive(PartialEq, Clone, Copy, Debug)] +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), + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..9382af8 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,29 @@ +#![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 config; +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(); +} diff --git a/src/row.rs b/src/row.rs new file mode 100644 index 0000000..5a093a5 --- /dev/null +++ b/src/row.rs @@ -0,0 +1,513 @@ +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, + 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' { + // TODO: Make this configurable + 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, + len: splitted_length, + is_highlighted: false, + highlighting: Vec::new(), + } + } + pub fn as_bytes(&self) -> &[u8] { + self.string.as_bytes() + } + pub fn find(&self, query: &str, at: usize, direction: SearchDirection) -> Option { + 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) { + 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 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, + start_with_comment: bool, + ) -> bool { + let chars: Vec = 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_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() +} + +#[cfg(test)] +mod test_super { + use super::*; + + #[test] + fn test_highlight_find() { + let mut row = Row::from("1testtest"); + row.highlighting = vec![ + highlighting::Type::Number, + highlighting::Type::None, + highlighting::Type::None, + highlighting::Type::None, + highlighting::Type::None, + highlighting::Type::None, + highlighting::Type::None, + highlighting::Type::None, + highlighting::Type::None, + ]; + row.highlight_match(&Some("t".to_string())); + assert_eq!( + vec![ + highlighting::Type::Number, + highlighting::Type::Match, + highlighting::Type::None, + highlighting::Type::None, + highlighting::Type::Match, + highlighting::Type::Match, + highlighting::Type::None, + highlighting::Type::None, + highlighting::Type::Match + ], + row.highlighting + ) + } + + #[test] + fn test_find() { + let row = Row::from("1testtest"); + assert_eq!(row.find("t", 0, SearchDirection::Forward), Some(1)); + assert_eq!(row.find("t", 2, SearchDirection::Forward), Some(4)); + assert_eq!(row.find("t", 5, SearchDirection::Forward), Some(5)); + } +} diff --git a/src/terminal.rs b/src/terminal.rs new file mode 100644 index 0000000..4fc852e --- /dev/null +++ b/src/terminal.rs @@ -0,0 +1,77 @@ +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}; + +#[derive(Clone)] +pub struct Size { + pub width: u16, + pub height: u16, +} + +pub struct Terminal { + size: Size, + _stdout: RawTerminal, +} + +impl Terminal { + pub fn default() -> Result { + 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 { + 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)); + } +}