diff --git a/clippy.toml b/clippy.toml
new file mode 100644
index 00000000..c64d65f8
--- /dev/null
+++ b/clippy.toml
@@ -0,0 +1,3 @@
+allow-expect-in-tests = true
+allow-panic-in-tests = true
+allow-unwrap-in-tests = true
diff --git a/count_loc.sh b/count_loc.sh
deleted file mode 100755
index eafa2e0c..00000000
--- a/count_loc.sh
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/usr/bin/env bash
-
-# Count the number of lines of code in kibi source code, display a result
-# table. Exit code 0 will be returned if the total LOC count is below 1024.
-
-# The lines of codes are counted using `tokei`, after removing the following
-# from the code:
-# * Clippy directives
-# * Anything after #[cfg(test)]
-
-set -euo pipefail
-
-declare -i file_loc total_loc left_col_width
-declare -A file_locs per_platform_total_locs
-
-paths=("$(dirname "${BASH_SOURCE[0]:-$0}")/"{.,src}/*.rs)
-
-left_col_width=6
-per_platform_total_locs['unix']=0
-per_platform_total_locs['wasi']=0
-per_platform_total_locs['windows']=0
-
-for path in "${paths[@]}"; do
- if (( ${#path} > left_col_width )); then left_col_width=${#path}; fi;
-
- tempfile=$(mktemp --suffix .rs)
- # Ignore Clippy directives
- code=$(grep -v -P '^\s*#!?\[(?:allow|warn|deny)\(clippy::' "${path}")
- # Ignore everything after #[cfg(test)]
- echo "${code%'#[cfg(test)]'*}" > "${tempfile}"
- file_loc=$(tokei "${tempfile}" -t=Rust -o json | jq .Rust.code)
- rm "${tempfile}"
-
- file_locs[${path}]=${file_loc}
-
- if [[ "${path}" == "./src/unix.rs" ]]; then
- per_platform_total_locs['unix']=$((per_platform_total_locs['unix'] + file_loc))
- elif [[ "${path}" == "./src/wasi.rs" ]]; then
- per_platform_total_locs['wasi']=$((per_platform_total_locs['wasi'] + file_loc))
- elif [[ "${path}" == "./src/windows.rs" ]]; then
- per_platform_total_locs['windows']=$((per_platform_total_locs['windows'] + file_loc))
- else
- for platform in "${!per_platform_total_locs[@]}"; do
- per_platform_total_locs[${platform}]=$((per_platform_total_locs[${platform}] + file_loc))
- done
- fi
-done
-
-for path in "${paths[@]}"; do
- printf "%-${left_col_width}s %4i\n" "${path}" "${file_locs[${path}]}"
-done
-
-loc_too_high=false
-for platform in "${!per_platform_total_locs[@]}"; do
- total_loc=${per_platform_total_locs[${platform}]}
- printf "%b%-${left_col_width}s %4i %b" '\x1b[1m' "Total (${platform})" "${total_loc}" '\x1b[0m'
- if [[ ${total_loc} -gt 1024 ]]; then
- echo -e ' \x1b[31m(> 1024)\x1b[0m'
- loc_too_high=true
- else
- echo -e ' \x1b[32m(≤ 1024)\x1b[0m'
- fi
-done
-
-if [[ ${loc_too_high} = true ]]; then
- exit 1
-fi
diff --git a/deny.toml b/deny.toml
new file mode 100644
index 00000000..60668593
--- /dev/null
+++ b/deny.toml
@@ -0,0 +1,14 @@
+[graph]
+all-features = true
+
+# This section is considered when running `cargo deny check licenses`
+# More documentation for the licenses section can be found here:
+# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
+[licenses]
+# List of explicitly allowed licenses
+# See https://spdx.org/licenses/ for list of possible licenses
+# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
+allow = [
+ "MIT",
+ "Apache-2.0",
+]
diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock
index 3510362a..eb10077f 100644
--- a/fuzz/Cargo.lock
+++ b/fuzz/Cargo.lock
@@ -8,12 +8,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c38b6b6b79f671c25e1a3e785b7b82d7562ffc9cd3efdc98627e5668a2472490"
-[[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.1"
@@ -48,14 +42,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
dependencies = [
"libc",
- "windows-sys",
+ "windows-sys 0.52.0",
]
[[package]]
name = "fastrand"
-version = "2.0.0"
+version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764"
+checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
[[package]]
name = "jobserver"
@@ -88,73 +82,63 @@ dependencies = [
[[package]]
name = "libc"
-version = "0.2.151"
+version = "0.2.162"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4"
+checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
[[package]]
name = "libfuzzer-sys"
-version = "0.4.7"
+version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7"
+checksum = "9b9569d2f74e257076d8c6bfa73fb505b46b851e51ddaecc825944aa3bed17fa"
dependencies = [
"arbitrary",
"cc",
- "once_cell",
]
[[package]]
name = "linux-raw-sys"
-version = "0.4.12"
+version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456"
+checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "once_cell"
-version = "1.9.0"
+version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
-
-[[package]]
-name = "redox_syscall"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
-dependencies = [
- "bitflags 1.3.2",
-]
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "rustix"
-version = "0.38.28"
+version = "0.38.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316"
+checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0"
dependencies = [
- "bitflags 2.4.1",
+ "bitflags",
"errno",
"libc",
"linux-raw-sys",
- "windows-sys",
+ "windows-sys 0.52.0",
]
[[package]]
name = "tempfile"
-version = "3.9.0"
+version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa"
+checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
dependencies = [
"cfg-if",
"fastrand",
- "redox_syscall",
+ "once_cell",
"rustix",
- "windows-sys",
+ "windows-sys 0.59.0",
]
[[package]]
name = "unicode-width"
-version = "0.1.11"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
+checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "winapi"
@@ -174,11 +158,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
-version = "0.1.6"
+version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
+checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
- "winapi",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -196,15 +180,25 @@ dependencies = [
"windows-targets",
]
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets",
+]
+
[[package]]
name = "windows-targets"
-version = "0.52.0"
+version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
+ "windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
@@ -213,42 +207,48 @@ dependencies = [
[[package]]
name = "windows_aarch64_gnullvm"
-version = "0.52.0"
+version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
-version = "0.52.0"
+version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
-version = "0.52.0"
+version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
-version = "0.52.0"
+version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
-version = "0.52.0"
+version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
-version = "0.52.0"
+version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
-version = "0.52.0"
+version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml
index 9b118a33..7b16e1d3 100644
--- a/fuzz/Cargo.toml
+++ b/fuzz/Cargo.toml
@@ -10,7 +10,7 @@ cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.4"
-tempfile = "3.9.0"
+tempfile = "3.14.0"
env-test-util = "1.0.1"
[dependencies.kibi]
diff --git a/rustfmt.toml b/rustfmt.toml
index f44e2af7..343e3e11 100644
--- a/rustfmt.toml
+++ b/rustfmt.toml
@@ -2,10 +2,20 @@ brace_style = "PreferSameLine"
condense_wildcard_suffixes = true
fn_params_layout = "Compressed"
fn_single_line = true
+format_code_in_doc_comments = true
+format_macro_matchers = true
+format_strings = true
+group_imports = "StdExternalCrate"
inline_attribute_width = 50
+match_arm_blocks = false
+normalize_comments = true
+normalize_doc_attributes = true
+overflow_delimited_expr = true
+reorder_impl_items = true
+style_edition = "2024"
use_field_init_shorthand = true
use_small_heuristics = "Max"
use_try_shorthand = true
+type_punctuation_density = "Compressed"
where_single_line = true
-match_arm_blocks = false
-group_imports = "StdExternalCrate"
+wrap_comments = true
diff --git a/src/ansi_escape.rs b/src/ansi_escape.rs
index 147b8ec0..533ffbde 100644
--- a/src/ansi_escape.rs
+++ b/src/ansi_escape.rs
@@ -1,7 +1,10 @@
//! # ANSI Escape sequences
-/// Clear from cursor to beginning of the screen
-pub const CLEAR_SCREEN: &str = "\x1b[2J";
+/// Switches to the main buffer.
+pub(crate) const USE_MAIN_SCREEN: &str = "\x1b[?1049l";
+
+/// Switches to a new alternate screen buffer.
+pub(crate) const USE_ALTERNATE_SCREEN: &str = "\x1b[?1049h";
/// Reset the formatting
pub(crate) const RESET_FMT: &str = "\x1b[m";
@@ -10,7 +13,7 @@ pub(crate) const RESET_FMT: &str = "\x1b[m";
pub(crate) const REVERSE_VIDEO: &str = "\x1b[7m";
/// Move the cursor to 1:1
-pub const MOVE_CURSOR_TO_START: &str = "\x1b[H";
+pub(crate) const MOVE_CURSOR_TO_START: &str = "\x1b[H";
/// DECTCTEM: Make the cursor invisible
pub(crate) const HIDE_CURSOR: &str = "\x1b[?25l";
diff --git a/src/config.rs b/src/config.rs
index e5cf8189..948d1fd6 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -6,15 +6,15 @@ use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::{fmt::Display, fs::File, str::FromStr, time::Duration};
-use crate::{sys::conf_dirs as cdirs, Error, Error::Config as ConfErr};
+use crate::{Error, Error::Config as ConfErr, sys::conf_dirs as cdirs};
/// The global Kibi configuration.
#[derive(Debug, PartialEq, Eq)]
pub struct Config {
/// The size of a tab. Must be > 0.
pub tab_stop: usize,
- /// The number of confirmations needed before quitting, when changes have been made since the
- /// file was last changed.
+ /// The number of confirmations needed before quitting, when changes have
+ /// been made since the file was last changed.
pub quit_times: usize,
/// The duration for which messages are shown in the status bar.
pub message_dur: Duration,
@@ -30,18 +30,20 @@ impl Default for Config {
}
impl Config {
- /// Load the configuration, potentially overridden using `config.ini` files that can be located
- /// in the following directories:
+ /// Load the configuration, potentially overridden using `config.ini` files
+ /// that can be located in the following directories:
/// - On Linux, macOS, and other *nix systems:
/// - `/etc/kibi` (system-wide configuration).
- /// - `$XDG_CONFIG_HOME/kibi` if environment variable `$XDG_CONFIG_HOME` is defined,
- /// `$HOME/.config/kibi` otherwise (user-level configuration).
+ /// - `$XDG_CONFIG_HOME/kibi` if environment variable `$XDG_CONFIG_HOME`
+ /// is defined, `$HOME/.config/kibi` otherwise (user-level
+ /// configuration).
/// - On Windows:
/// - `%APPDATA%\Kibi`
///
/// # Errors
///
- /// Will return `Err` if one of the configuration file cannot be parsed properly.
+ /// Will return `Err` if one of the configuration file cannot be parsed
+ /// properly.
pub fn load() -> Result {
let mut conf = Self::default();
@@ -50,13 +52,12 @@ impl Config {
for path in paths.iter().filter(|p| p.is_file()).rev() {
process_ini_file(path, &mut |key, value| {
match key {
- "tab_stop" => match parse_value(value)? {
- 0 => return Err("tab_stop must be > 0".into()),
- tab_stop => conf.tab_stop = tab_stop,
- },
+ "tab_stop" =>
+ conf.tab_stop = parse_value(value).map_err(|_| "tab_stop must be > 0")?,
"quit_times" => conf.quit_times = parse_value(value)?,
"message_duration" =>
- conf.message_dur = Duration::from_secs_f32(parse_value(value)?),
+ conf.message_dur = Duration::try_from_secs_f32(parse_value(value)?)
+ .map_err(|x| x.to_string())?,
"show_line_numbers" => conf.show_line_num = parse_value(value)?,
_ => return Err(format!("Invalid key: {key}")),
};
@@ -70,8 +71,8 @@ impl Config {
/// Process an INI file.
///
-/// The `kv_fn` function will be called for each key-value pair in the file. Typically, this
-/// function will update a configuration instance.
+/// The `kv_fn` function will be called for each key-value pair in the file.
+/// Typically, this function will update a configuration instance.
pub fn process_ini_file(path: &Path, kv_fn: &mut F) -> Result<(), Error>
where F: FnMut(&str, &str) -> Result<(), String> {
let file = File::open(path).map_err(|e| ConfErr(path.into(), 0, e.to_string()))?;
@@ -89,13 +90,13 @@ where F: FnMut(&str, &str) -> Result<(), String> {
}
/// Trim a value (right-hand side of a key=value INI line) and parses it.
-pub fn parse_value, E: Display>(value: &str) -> Result {
+pub fn parse_value, E: Display>(value: &str) -> Result {
value.trim().parse().map_err(|e| format!("Parser error: {e}"))
}
-/// Split a comma-separated list of values (right-hand side of a key=value1,value2,... INI line) and
-/// parse it as a Vec.
-pub fn parse_values, E: Display>(value: &str) -> Result, String> {
+/// Split a comma-separated list of values (right-hand side of a
+/// key=value1,value2,... INI line) and parse it as a Vec.
+pub fn parse_values, E: Display>(value: &str) -> Result, String> {
value.split(',').map(parse_value).collect()
}
@@ -103,9 +104,9 @@ pub fn parse_values, E: Display>(value: &str) -> Result) -> TempEnvVar {
+ fn new(key: &OsStr, value: Option<&OsStr>) -> Self {
let orig_value = env::var_os(key);
match value {
Some(value) => env::set_var(key, value),
None => env::remove_var(key),
}
- TempEnvVar { key: key.into(), orig_value }
+ Self { key: key.into(), orig_value }
}
}
impl Drop for TempEnvVar {
fn drop(&mut self) {
- match self.orig_value {
- Some(ref orig_value) => env::set_var(&self.key, orig_value),
+ match &self.orig_value {
+ Some(orig_value) => env::set_var(&self.key, orig_value),
None => env::remove_var(&self.key),
}
}
@@ -245,10 +246,13 @@ mod tests {
assert_eq!(config, custom_config);
}
+ #[cfg(unix)]
+ static XDG_CONFIG_FLAG_LOCK: LazyLock> = LazyLock::new(Mutex::default);
+
#[cfg(unix)]
#[test]
- #[serial]
fn xdg_config_home() {
+ let _lock = XDG_CONFIG_FLAG_LOCK.lock();
let tmp_config_home = TempDir::new().expect("Could not create temporary directory");
test_config_dir(
"XDG_CONFIG_HOME".as_ref(),
@@ -259,8 +263,8 @@ mod tests {
#[cfg(unix)]
#[test]
- #[serial]
fn config_home() {
+ let _lock = XDG_CONFIG_FLAG_LOCK.lock();
let _temp_env_var = TempEnvVar::new(OsStr::new("XDG_CONFIG_HOME"), None);
let tmp_home = TempDir::new().expect("Could not create temporary directory");
test_config_dir(
@@ -272,7 +276,6 @@ mod tests {
#[cfg(windows)]
#[test]
- #[serial]
fn app_data() {
let tmp_home = TempDir::new().expect("Could not create temporary directory");
test_config_dir(
diff --git a/src/editor.rs b/src/editor.rs
index fe3c1e13..58c2f181 100644
--- a/src/editor.rs
+++ b/src/editor.rs
@@ -3,10 +3,12 @@
use std::fmt::{Display, Write as _};
use std::io::{self, BufRead, BufReader, ErrorKind, Read, Seek, Write};
use std::iter::{self, repeat, successors};
-use std::{fs::File, path::Path, process::Command, thread, time::Instant};
+use std::{cell::RefCell, fs::File, path::Path, process::Command, thread, time::Instant};
+
+use unicode_width::UnicodeWidthStr;
use crate::row::{HlState, Row};
-use crate::{ansi_escape::*, syntax::Conf as SyntaxConf, sys, terminal, Config, Error};
+use crate::{Config, Error, ansi_escape::*, syntax::Conf as SyntaxConf, sys, terminal};
const fn ctrl_key(key: u8) -> u8 { key & 0x1f }
const EXIT: u8 = ctrl_key(b'Q');
@@ -23,20 +25,23 @@ const EXECUTE: u8 = ctrl_key(b'E');
const REMOVE_LINE: u8 = ctrl_key(b'R');
const BACKSPACE: u8 = 127;
-const HELP_MESSAGE: &str =
- "^S save | ^Q quit | ^F find | ^G go to | ^D duplicate | ^E execute | ^C copy | ^X cut | ^V paste";
+const HELP_MESSAGE: &str = "^S save | ^Q quit | ^F find | ^G go to | ^D duplicate | ^E execute | \
+ ^C copy | ^X cut | ^V paste";
/// `set_status!` sets a formatted status message for the editor.
-/// Example usage: `set_status!(editor, "{} written to {}", file_size, file_name)`
-macro_rules! set_status {
- ($editor:expr, $($arg:expr),*) => ($editor.status_msg = Some(StatusMessage::new(format!($($arg),*))))
-}
+/// Example usage: `set_status!(editor, "{} written to {}", file_size,
+/// file_name)`
+macro_rules! set_status { ($editor:expr, $($arg:expr),*) => ($editor.status_msg = Some(StatusMessage::new(format!($($arg),*)))) }
+
+// `width!` returns the display width of a string, plus one for the cursor
+fn dsp_width(msg: &String) -> usize { UnicodeWidthStr::width(msg.as_str()) + 1 }
/// Enum of input keys
enum Key {
Arrow(AKey),
CtrlArrow(AKey),
- Page(PageKey),
+ PageUp,
+ PageDown,
Home,
End,
Delete,
@@ -52,12 +57,6 @@ enum AKey {
Down,
}
-/// Enum of page keys
-enum PageKey {
- Up,
- Down,
-}
-
/// Describes the cursor position and the screen offset
#[derive(Default, Clone)]
struct CursorState {
@@ -74,42 +73,47 @@ struct CursorState {
impl CursorState {
fn move_to_next_line(&mut self) { (self.x, self.y) = (0, self.y + 1); }
- /// Scroll the terminal window vertically and horizontally (i.e. adjusting the row offset and
- /// the column offset) so that the cursor can be shown.
+ /// Scroll the terminal window vertically and horizontally (i.e. adjusting
+ /// the row offset and the column offset) so that the cursor can be
+ /// shown.
fn scroll(&mut self, rx: usize, screen_rows: usize, screen_cols: usize) {
self.roff = self.roff.clamp(self.y.saturating_sub(screen_rows.saturating_sub(1)), self.y);
self.coff = self.coff.clamp(rx.saturating_sub(screen_cols.saturating_sub(1)), rx);
}
}
-/// The `Editor` struct, contains the state and configuration of the text editor.
+/// The `Editor` struct, contains the state and configuration of the text
+/// editor.
#[derive(Default)]
pub struct Editor {
- /// If not `None`, the current prompt mode (Save, Find, GoTo). If `None`, we are in regular
- /// edition mode.
+ /// If not `None`, the current prompt mode (`Save`, `Find`, `GoTo`, or
+ /// `Execute`). If `None`, we are in regular edition mode.
prompt_mode: Option,
/// The current state of the cursor.
cursor: CursorState,
/// The padding size used on the left for line numbering.
ln_pad: usize,
- /// The width of the current window. Will be updated when the window is resized.
+ /// The width of the current window. Will be updated when the window is
+ /// resized.
window_width: usize,
- /// The number of rows that can be used for the editor, excluding the status bar and the message
- /// bar
+ /// The number of rows that can be used for the editor, excluding the status
+ /// bar and the message bar
screen_rows: usize,
- /// The number of columns that can be used for the editor, excluding the part used for line numbers
+ /// The number of columns that can be used for the editor, excluding the
+ /// part used for line numbers
screen_cols: usize,
- /// The collection of rows, including the content and the syntax highlighting information.
+ /// The collection of rows, including the content and the syntax
+ /// highlighting information.
rows: Vec,
/// Whether the document has been modified since it was open.
dirty: bool,
/// The configuration for the editor.
config: Config,
- /// The number of warnings remaining before we can quit without saving. Defaults to
- /// `config.quit_times`, then decreases to 0.
+ /// The number of warnings remaining before we can quit without saving.
+ /// Defaults to `config.quit_times`, then decreases to 0.
quit_times: usize,
- /// The file name. If None, the user will be prompted for a file name the first time they try to
- /// save.
+ /// The file name. If None, the user will be prompted for a file name the
+ /// first time they try to save.
// TODO: It may be better to store a PathBuf instead
file_name: Option,
/// The current status message being shown.
@@ -118,7 +122,8 @@ pub struct Editor {
syntax: SyntaxConf,
/// The number of bytes contained in `rows`. This excludes new lines.
n_bytes: u64,
- /// The original terminal mode. It will be restored when the `Editor` instance is dropped.
+ /// The original terminal mode. It will be restored when the `Editor`
+ /// instance is dropped.
orig_term_mode: Option,
/// The copied buffer of a row
copied_row: Vec,
@@ -143,16 +148,17 @@ fn format_size(n: u64) -> String {
return format!("{n}B");
}
// i is the largest value such that 1024 ^ i < n
- // To find i we compute the smallest b such that n <= 1024 ^ b and subtract 1 from it
+ // To find i we compute the smallest b such that n <= 1024 ^ b and subtract 1
+ // from it
let i = (64 - n.leading_zeros() + 9) / 10 - 1;
- // Compute the size with two decimal places (rounded down) as the last two digits of q
- // This avoid float formatting reducing the binary size
+ // Compute the size with two decimal places (rounded down) as the last two
+ // digits of q This avoid float formatting reducing the binary size
let q = 100 * n / (1024 << ((i - 1) * 10));
format!("{}.{:02}{}B", q / 100, q % 100, b" kMGTPEZ"[i as usize] as char)
}
-/// `slice_find` returns the index of `needle` in slice `s` if `needle` is a subslice of `s`,
-/// otherwise returns `None`.
+/// `slice_find` returns the index of `needle` in slice `s` if `needle` is a
+/// subslice of `s`, otherwise returns `None`.
fn slice_find(s: &[T], needle: &[T]) -> Option {
(0..(s.len() + 1).saturating_sub(needle.len())).find(|&i| s[i..].starts_with(needle))
}
@@ -162,8 +168,8 @@ impl Editor {
///
/// # Errors
///
- /// Will return `Err` if an error occurs when enabling termios raw mode, creating the signal hook
- /// or when obtaining the terminal window size.
+ /// Will return `Err` if an error occurs when enabling termios raw mode,
+ /// creating the signal hook or when obtaining the terminal window size.
#[allow(clippy::field_reassign_with_default)] // False positive : https://github.com/rust-lang/rust-clippy/issues/6312
pub fn new(config: Config) -> Result {
sys::register_winsize_change_signal_handler()?;
@@ -172,18 +178,21 @@ impl Editor {
// Enable raw mode and store the original (non-raw) terminal mode.
editor.orig_term_mode = Some(sys::enable_raw_mode()?);
- editor.update_window_size()?;
+ print!("{USE_ALTERNATE_SCREEN}");
+ editor.update_window_size()?;
set_status!(editor, "{}", HELP_MESSAGE);
Ok(editor)
}
- /// Return the current row if the cursor points to an existing row, `None` otherwise.
+ /// Return the current row if the cursor points to an existing row, `None`
+ /// otherwise.
fn current_row(&self) -> Option<&Row> { self.rows.get(self.cursor.y) }
- /// Return the position of the cursor, in terms of rendered characters (as opposed to
- /// `self.cursor.x`, which is the position of the cursor in terms of bytes).
+ /// Return the position of the cursor, in terms of rendered characters (as
+ /// opposed to `self.cursor.x`, which is the position of the cursor in
+ /// terms of bytes).
fn rx(&self) -> usize { self.current_row().map_or(0, |r| r.cx2rx[self.cursor.x]) }
/// Move the cursor following an arrow key (← → ↑ ↓).
@@ -220,16 +229,17 @@ impl Editor {
self.update_cursor_x_position();
}
- /// Update the cursor x position. If the cursor y position has changed, the current position
- /// might be illegal (x is further right than the last character of the row). If that is the
- /// case, clamp `self.cursor.x`.
+ /// Update the cursor x position. If the cursor y position has changed, the
+ /// current position might be illegal (x is further right than the last
+ /// character of the row). If that is the case, clamp `self.cursor.x`.
fn update_cursor_x_position(&mut self) {
self.cursor.x = self.cursor.x.min(self.current_row().map_or(0, |row| row.chars.len()));
}
- /// Run a loop to obtain the key that was pressed. At each iteration of the loop (until a key is
- /// pressed), we listen to the `ws_changed` channel to check if a window size change signal has
- /// been received. When bytes are received, we match to a corresponding `Key`. In particular,
+ /// Run a loop to obtain the key that was pressed. At each iteration of the
+ /// loop (until a key is pressed), we listen to the `ws_changed` channel
+ /// to check if a window size change signal has been received. When
+ /// bytes are received, we match to a corresponding `Key`. In particular,
/// we handle ANSI escape codes to return `Key::Delete`, `Key::Home` etc.
fn loop_until_keypress(&mut self) -> Result {
loop {
@@ -239,8 +249,8 @@ impl Editor {
self.refresh_screen()?;
}
let mut bytes = sys::stdin()?.bytes();
- // Match on the next byte received or, if the first byte is ('\x1b'), on the next
- // few bytes.
+ // Match on the next byte received or, if the first byte is ('\x1b'), on
+ // the next few bytes.
match bytes.next().transpose()? {
Some(b'\x1b') => {
return Ok(match bytes.next().transpose()? {
@@ -253,7 +263,7 @@ impl Editor {
(b'[' | b'O', Some(b'F')) => Key::End,
(b'[', mut c @ Some(b'0'..=b'8')) => {
let mut d = bytes.next().transpose()?;
- if let (Some(b'1'), Some(b';')) = (c, d) {
+ if (c, d) == (Some(b'1'), Some(b';')) {
// 1 is the default modifier value. Therefore, [1;5C is
// equivalent to [5C, etc.
c = bytes.next().transpose()?;
@@ -263,8 +273,8 @@ impl Editor {
(Some(c), Some(b'~')) if c == b'1' || c == b'7' => Key::Home,
(Some(c), Some(b'~')) if c == b'4' || c == b'8' => Key::End,
(Some(b'3'), Some(b'~')) => Key::Delete,
- (Some(b'5'), Some(b'~')) => Key::Page(PageKey::Up),
- (Some(b'6'), Some(b'~')) => Key::Page(PageKey::Down),
+ (Some(b'5'), Some(b'~')) => Key::PageUp,
+ (Some(b'6'), Some(b'~')) => Key::PageDown,
(Some(b'5'), Some(b'A')) => Key::CtrlArrow(AKey::Up),
(Some(b'5'), Some(b'B')) => Key::CtrlArrow(AKey::Down),
(Some(b'5'), Some(b'C')) => Key::CtrlArrow(AKey::Right),
@@ -287,7 +297,8 @@ impl Editor {
}
}
- /// Update the `screen_rows`, `window_width`, `screen_cols` and `ln_padding` attributes.
+ /// Update the `screen_rows`, `window_width`, `screen_cols` and `ln_padding`
+ /// attributes.
fn update_window_size(&mut self) -> Result<(), Error> {
let wsize = sys::get_window_size().or_else(|_| terminal::get_window_size_using_cursor())?;
// Make room for the status bar and status message
@@ -296,12 +307,13 @@ impl Editor {
Ok(())
}
- /// Update the `screen_cols` and `ln_padding` attributes based on the maximum number of digits
- /// for line numbers (since the left padding depends on this number of digits).
+ /// Update the `screen_cols` and `ln_padding` attributes based on the
+ /// maximum number of digits for line numbers (since the left padding
+ /// depends on this number of digits).
fn update_screen_cols(&mut self) {
- // The maximum number of digits to use for the line number is the number of digits of the
- // last line number. This is equal to the number of times we can divide this number by ten,
- // computed below using `successors`.
+ // The maximum number of digits to use for the line number is the number of
+ // digits of the last line number. This is equal to the number of times
+ // we can divide this number by ten, computed below using `successors`.
let n_digits =
successors(Some(self.rows.len()), |u| Some(u / 10).filter(|u| *u > 0)).count();
let show_line_num = self.config.show_line_num && n_digits + 2 < self.window_width / 4;
@@ -309,9 +321,10 @@ impl Editor {
self.screen_cols = self.window_width.saturating_sub(self.ln_pad);
}
- /// Given a file path, try to find a syntax highlighting configuration that matches the path
- /// extension in one of the config directories (`/etc/kibi/syntax.d`, etc.). If such a
- /// configuration is found, set the `syntax` attribute of the editor.
+ /// Given a file path, try to find a syntax highlighting configuration that
+ /// matches the path extension in one of the config directories
+ /// (`/etc/kibi/syntax.d`, etc.). If such a configuration is found, set
+ /// the `syntax` attribute of the editor.
fn select_syntax_highlight(&mut self, path: &Path) -> Result<(), Error> {
let extension = path.extension().and_then(std::ffi::OsStr::to_str);
if let Some(s) = extension.and_then(|e| SyntaxConf::get(e).transpose()) {
@@ -320,9 +333,9 @@ impl Editor {
Ok(())
}
- /// Update a row, given its index. If `ignore_following_rows` is `false` and the highlight state
- /// has changed during the update (for instance, it is now in "multi-line comment" state, keep
- /// updating the next rows
+ /// Update a row, given its index. If `ignore_following_rows` is `false` and
+ /// the highlight state has changed during the update (for instance, it
+ /// is now in "multi-line comment" state, keep updating the next rows
fn update_row(&mut self, y: usize, ignore_following_rows: bool) {
let mut hl_state = if y > 0 { self.rows[y - 1].hl_state } else { HlState::Normal };
for row in self.rows.iter_mut().skip(y) {
@@ -331,8 +344,9 @@ impl Editor {
if ignore_following_rows || hl_state == previous_hl_state {
return;
}
- // If the state has changed (for instance, a multi-line comment started in this row),
- // continue updating the following rows
+ // If the state has changed (for instance, a multi-line comment
+ // started in this row), continue updating the following
+ // rows
}
}
@@ -344,8 +358,8 @@ impl Editor {
}
}
- /// Insert a byte at the current cursor position. If there is no row at the current cursor
- /// position, add a new row and insert the byte.
+ /// Insert a byte at the current cursor position. If there is no row at the
+ /// current cursor position, add a new row and insert the byte.
fn insert_byte(&mut self, c: u8) {
if let Some(row) = self.rows.get_mut(self.cursor.y) {
row.chars.insert(self.cursor.x, c);
@@ -358,13 +372,15 @@ impl Editor {
(self.cursor.x, self.n_bytes, self.dirty) = (self.cursor.x + 1, self.n_bytes + 1, true);
}
- /// Insert a new line at the current cursor position and move the cursor to the start of the new
- /// line. If the cursor is in the middle of a row, split off that row.
+ /// Insert a new line at the current cursor position and move the cursor to
+ /// the start of the new line. If the cursor is in the middle of a row,
+ /// split off that row.
fn insert_new_line(&mut self) {
let (position, new_row_chars) = if self.cursor.x == 0 {
(self.cursor.y, Vec::new())
} else {
- // self.rows[self.cursor.y] must exist, since cursor.x = 0 for any cursor.y ≥ row.len()
+ // self.rows[self.cursor.y] must exist, since cursor.x = 0 for any cursor.y ≥
+ // row.len()
let new_chars = self.rows[self.cursor.y].chars.split_off(self.cursor.x);
self.update_row(self.cursor.y, false);
(self.cursor.y + 1, new_chars)
@@ -376,13 +392,15 @@ impl Editor {
self.dirty = true;
}
- /// Delete a character at the current cursor position. If the cursor is located at the beginning
- /// of a row that is not the first or last row, merge the current row and the previous row. If
- /// the cursor is located after the last row, move up to the last character of the previous row.
+ /// Delete a character at the current cursor position. If the cursor is
+ /// located at the beginning of a row that is not the first or last row,
+ /// merge the current row and the previous row. If the cursor is located
+ /// after the last row, move up to the last character of the previous row.
fn delete_char(&mut self) {
if self.cursor.x > 0 {
let row = &mut self.rows[self.cursor.y];
- // Obtain the number of bytes to be removed: could be 1-4 (UTF-8 character size).
+ // Obtain the number of bytes to be removed: could be 1-4 (UTF-8 character
+ // size).
let n_bytes_to_remove = row.get_char_size(row.cx2rx[self.cursor.x] - 1);
row.chars.splice(self.cursor.x - n_bytes_to_remove..self.cursor.x, iter::empty());
self.update_row(self.cursor.y, false);
@@ -400,8 +418,8 @@ impl Editor {
self.update_screen_cols();
(self.dirty, self.cursor.y) = (self.dirty, self.cursor.y - 1);
} else if self.cursor.y == self.rows.len() {
- // If the cursor is located after the last row, pressing backspace is equivalent to
- // pressing the left arrow key.
+ // If the cursor is located after the last row, pressing backspace is equivalent
+ // to pressing the left arrow key.
self.move_cursor(&AKey::Left, false);
}
}
@@ -442,35 +460,34 @@ impl Editor {
self.update_screen_cols();
}
- /// Try to load a file. If found, load the rows and update the render and syntax highlighting.
- /// If not found, do not return an error.
+ /// Try to load a file. If found, load the rows and update the render and
+ /// syntax highlighting. If not found, do not return an error.
fn load(&mut self, path: &Path) -> Result<(), Error> {
- let ft = std::fs::metadata(path)?.file_type();
+ let mut file = match File::open(path) {
+ Err(e) if e.kind() == ErrorKind::NotFound => {
+ self.rows.push(Row::new(Vec::new()));
+ return Ok(());
+ }
+ r => r,
+ }?;
+ let ft = file.metadata()?.file_type();
if !(ft.is_file() || ft.is_symlink()) {
return Err(io::Error::new(ErrorKind::InvalidInput, "Invalid input file type").into());
}
-
- match File::open(path) {
- Ok(file) => {
- for line in BufReader::new(file).split(b'\n') {
- self.rows.push(Row::new(line?));
- }
- // If the file ends with an empty line or is empty, we need to append an empty row
- // to `self.rows`. Unfortunately, BufReader::split doesn't yield an empty Vec in
- // this case, so we need to check the last byte directly.
- let mut file = File::open(path)?;
- file.seek(io::SeekFrom::End(0))?;
- if file.bytes().next().transpose()?.map_or(true, |b| b == b'\n') {
- self.rows.push(Row::new(Vec::new()));
- }
- self.update_all_rows();
- // The number of rows has changed. The left padding may need to be updated.
- self.update_screen_cols();
- self.n_bytes = self.rows.iter().map(|row| row.chars.len() as u64).sum();
- }
- Err(e) if e.kind() == ErrorKind::NotFound => self.rows.push(Row::new(Vec::new())),
- Err(e) => return Err(e.into()),
+ for line in BufReader::new(&file).split(b'\n') {
+ self.rows.push(Row::new(line?));
}
+ // If the file ends with an empty line or is empty, we need to append an empty
+ // row to `self.rows`. Unfortunately, BufReader::split doesn't yield an
+ // empty Vec in this case, so we need to check the last byte directly.
+ file.seek(io::SeekFrom::End(0))?;
+ if file.bytes().next().transpose()?.map_or(true, |b| b == b'\n') {
+ self.rows.push(Row::new(Vec::new()));
+ }
+ self.update_all_rows();
+ // The number of rows has changed. The left padding may need to be updated.
+ self.update_screen_cols();
+ self.n_bytes = self.rows.iter().map(|row| row.chars.len() as u64).sum();
Ok(())
}
@@ -482,7 +499,7 @@ impl Editor {
file.write_all(&row.chars)?;
written += row.chars.len();
if i != (self.rows.len() - 1) {
- file.write_all(&[b'\n'])?;
+ file.write_all(b"\n")?;
written += 1;
}
}
@@ -490,8 +507,9 @@ impl Editor {
Ok(written)
}
- /// Save the text to a file and handle all errors. Errors and success messages will be printed
- /// to the status bar. Return whether the file was successfully saved.
+ /// Save the text to a file and handle all errors. Errors and success
+ /// messages will be printed to the status bar. Return whether the file
+ /// was successfully saved.
fn save_and_handle_io_errors(&mut self, file_name: &str) -> bool {
let saved = self.save(file_name);
// Print error or success message to the status bar
@@ -504,8 +522,9 @@ impl Editor {
saved.is_ok()
}
- /// Save to a file after obtaining the file path from the prompt. If successful, the `file_name`
- /// attribute of the editor will be set and syntax highlighting will be updated.
+ /// Save to a file after obtaining the file path from the prompt. If
+ /// successful, the `file_name` attribute of the editor will be set and
+ /// syntax highlighting will be updated.
fn save_as(&mut self, file_name: String) -> Result<(), Error> {
// TODO: What if file_name already exists?
if self.save_and_handle_io_errors(&file_name) {
@@ -526,11 +545,13 @@ impl Editor {
Ok(())
}
- /// Return whether the file being edited is empty or not. If there is more than one row, even if
- /// all the rows are empty, `is_empty` returns `false`, since the text contains new lines.
+ /// Return whether the file being edited is empty or not. If there is more
+ /// than one row, even if all the rows are empty, `is_empty` returns
+ /// `false`, since the text contains new lines.
fn is_empty(&self) -> bool { self.rows.len() <= 1 && self.n_bytes == 0 }
- /// Draw rows of text and empty rows on the terminal, by adding characters to the buffer.
+ /// Draw rows of text and empty rows on the terminal, by adding characters
+ /// to the buffer.
fn draw_rows(&self, buffer: &mut String) -> Result<(), Error> {
let row_it = self.rows.iter().map(Some).chain(repeat(None)).enumerate();
for (i, row) in row_it.skip(self.cursor.roff).take(self.screen_rows) {
@@ -571,7 +592,8 @@ impl Editor {
Ok(())
}
- /// Draw the message bar on the terminal, by adding characters to the buffer.
+ /// Draw the message bar on the terminal, by adding characters to the
+ /// buffer.
fn draw_message_bar(&self, buffer: &mut String) {
buffer.push_str(CLEAR_LINE_RIGHT_OF_CURSOR);
let msg_duration = self.config.message_dur;
@@ -580,8 +602,8 @@ impl Editor {
}
}
- /// Refresh the screen: update the offsets, draw the rows, the status bar, the message bar, and
- /// move the cursor to the correct position.
+ /// Refresh the screen: update the offsets, draw the rows, the status bar,
+ /// the message bar, and move the cursor to the correct position.
fn refresh_screen(&mut self) -> Result<(), Error> {
self.cursor.scroll(self.rx(), self.screen_rows, self.screen_cols);
let mut buffer = format!("{HIDE_CURSOR}{MOVE_CURSOR_TO_START}");
@@ -589,33 +611,35 @@ impl Editor {
self.draw_status_bar(&mut buffer)?;
self.draw_message_bar(&mut buffer);
let (cursor_x, cursor_y) = if self.prompt_mode.is_none() {
- // If not in prompt mode, position the cursor according to the `cursor` attributes.
+ // If not in prompt mode, position the cursor according to the `cursor`
+ // attributes.
(self.rx() - self.cursor.coff + 1 + self.ln_pad, self.cursor.y - self.cursor.roff + 1)
} else {
- // If in prompt mode, position the cursor on the prompt line at the end of the line.
- (self.status_msg.as_ref().map_or(0, |sm| sm.msg.len() + 1), self.screen_rows + 2)
+ // If in prompt mode, position the cursor on the prompt line at the end of the
+ // line.
+ (self.status_msg.as_ref().map_or(0, |s| dsp_width(&s.msg)), self.screen_rows + 2)
};
// Finally, print `buffer` and move the cursor
print!("{buffer}\x1b[{cursor_y};{cursor_x}H{SHOW_CURSOR}");
io::stdout().flush().map_err(Error::from)
}
- /// Process a key that has been pressed, when not in prompt mode. Returns whether the program
- /// should exit, and optionally the prompt mode to switch to.
+ /// Process a key that has been pressed, when not in prompt mode. Returns
+ /// whether the program should exit, and optionally the prompt mode to
+ /// switch to.
fn process_keypress(&mut self, key: &Key) -> (bool, Option) {
// This won't be mutated, unless key is Key::Character(EXIT)
let mut quit_times = self.config.quit_times;
let mut prompt_mode = None;
match key {
- // TODO: CtrlArrow should move to next word
Key::Arrow(arrow) => self.move_cursor(arrow, false),
Key::CtrlArrow(arrow) => self.move_cursor(arrow, true),
- Key::Page(PageKey::Up) => {
+ Key::PageUp => {
self.cursor.y = self.cursor.roff.saturating_sub(self.screen_rows);
self.update_cursor_x_position();
}
- Key::Page(PageKey::Down) => {
+ Key::PageDown => {
self.cursor.y = (self.cursor.roff + 2 * self.screen_rows - 1).min(self.rows.len());
self.update_cursor_x_position();
}
@@ -662,11 +686,12 @@ impl Editor {
(false, prompt_mode)
}
- /// Try to find a query, this is called after pressing Ctrl-F and for each key that is pressed.
- /// `last_match` is the last row that was matched, `forward` indicates whether to search forward
- /// or backward. Returns the row of a new match, or `None` if the search was unsuccessful.
+ /// Try to find a query, this is called after pressing Ctrl-F and for each
+ /// key that is pressed. `last_match` is the last row that was matched,
+ /// `forward` indicates whether to search forward or backward. Returns
+ /// the row of a new match, or `None` if the search was unsuccessful.
#[allow(clippy::trivially_copy_pass_by_ref)] // This Clippy recommendation is only relevant on 32 bit platforms.
- fn find(&mut self, query: &str, last_match: &Option, forward: bool) -> Option {
+ fn find(&mut self, query: &str, last_match: Option, forward: bool) -> Option {
let num_rows = self.rows.len();
let mut current = last_match.unwrap_or_else(|| num_rows.saturating_sub(1));
// TODO: Handle multiple matches per line
@@ -674,11 +699,12 @@ impl Editor {
current = (current + if forward { 1 } else { num_rows - 1 }) % num_rows;
let row = &mut self.rows[current];
if let Some(cx) = slice_find(&row.chars, query.as_bytes()) {
- // self.cursor.coff: Try to reset the column offset; if the match is after the offset, this
- // will be updated in self.cursor.scroll() so that the result is visible
+ // self.cursor.coff: Try to reset the column offset; if the match is after the
+ // offset, this will be updated in self.cursor.scroll() so that
+ // the result is visible
(self.cursor.x, self.cursor.y, self.cursor.coff) = (cx, current, 0);
let rx = row.cx2rx[cx];
- row.match_segment = Some(rx..rx + query.len());
+ row.match_segment = Some(rx..rx + UnicodeWidthStr::width(query));
return Some(current);
}
}
@@ -700,7 +726,7 @@ impl Editor {
self.file_name = None;
}
loop {
- if let Some(mode) = self.prompt_mode.as_ref() {
+ if let Some(mode) = &self.prompt_mode {
set_status!(self, "{}", mode.status_msg());
}
self.refresh_screen()?;
@@ -719,13 +745,14 @@ impl Editor {
}
impl Drop for Editor {
+ #[allow(clippy::expect_used)]
/// When the editor is dropped, restore the original terminal mode.
fn drop(&mut self) {
if let Some(orig_term_mode) = self.orig_term_mode.take() {
sys::set_term_mode(&orig_term_mode).expect("Could not restore original terminal mode.");
}
if !thread::panicking() {
- print!("{CLEAR_SCREEN}{MOVE_CURSOR_TO_START}");
+ print!("{USE_MAIN_SCREEN}");
io::stdout().flush().expect("Could not flush stdout");
}
}
@@ -743,8 +770,8 @@ enum PromptMode {
Execute(String),
}
-// TODO: Use trait with mode_status_msg and process_keypress, implement the trait for separate
-// structs for Save and Find?
+// TODO: Use trait with mode_status_msg and process_keypress, implement the
+// trait for separate structs for Save and Find?
impl PromptMode {
/// Return the status message to print for the selected `PromptMode`.
fn status_msg(&self) -> String {
@@ -771,13 +798,14 @@ impl PromptMode {
}
match process_prompt_keypress(b, key) {
PromptState::Active(query) => {
+ #[allow(clippy::wildcard_enum_match_arm)]
let (last_match, forward) = match key {
Key::Arrow(AKey::Right | AKey::Down) | Key::Char(FIND) =>
(last_match, true),
Key::Arrow(AKey::Left | AKey::Up) => (last_match, false),
_ => (None, true),
};
- let curr_match = ed.find(&query, &last_match, forward);
+ let curr_match = ed.find(&query, last_match, forward);
return Ok(Some(Self::Find(query, saved_cursor, curr_match)));
}
// The prompt was cancelled. Restore the previous position.
@@ -837,18 +865,23 @@ enum PromptState {
Cancelled,
}
+thread_local! (static CHARACTER: RefCell> = {let cache = Vec::new(); RefCell::new(cache)});
/// Process a prompt keypress event and return the new state for the prompt.
fn process_prompt_keypress(mut buffer: String, key: &Key) -> PromptState {
+ #[allow(clippy::wildcard_enum_match_arm)]
match key {
Key::Char(b'\r') => return PromptState::Completed(buffer),
Key::Escape | Key::Char(EXIT) => return PromptState::Cancelled,
- Key::Char(BACKSPACE | DELETE_BIS) => {
- buffer.pop();
- }
+ Key::Char(BACKSPACE | DELETE_BIS) => _ = buffer.pop(),
Key::Char(c @ 0..=126) if !c.is_ascii_control() => buffer.push(*c as char),
+ Key::Char(c @ 128..=255) => CHARACTER.with(|cache| cache.borrow_mut().push(*c)),
// No-op
_ => (),
}
+ let character = CHARACTER.with(|cache| String::from_utf8(cache.borrow_mut().clone()));
+ let _ = character.clone().map_or((), |c| buffer.push_str(c.as_str()));
+ let _ = character.map_or((), |_| CHARACTER.with(|cache| cache.borrow_mut().clear()));
+
PromptState::Active(buffer)
}
@@ -917,12 +950,12 @@ mod tests {
editor.insert_byte(*b);
}
editor.delete_char();
- assert_eq!(editor.rows[0].chars, "Hello world".as_bytes());
+ assert_eq!(editor.rows[0].chars, b"Hello world");
editor.move_cursor(&AKey::Left, true);
editor.move_cursor(&AKey::Left, false);
editor.move_cursor(&AKey::Left, false);
editor.delete_char();
- assert_eq!(editor.rows[0].chars, "Helo world".as_bytes());
+ assert_eq!(editor.rows[0].chars, b"Helo world");
}
#[test]
@@ -1084,4 +1117,70 @@ mod tests {
assert_eq!(editor.cursor.x, 0);
assert_eq!(editor.cursor.y, 3);
}
+
+ #[test]
+ fn editor_press_home_key() {
+ let mut editor = Editor::default();
+ for b in b"Hello\nWorld\nand\nFerris!" {
+ if *b == b'\n' {
+ editor.insert_new_line();
+ } else {
+ editor.insert_byte(*b);
+ }
+ }
+
+ // check current position
+ assert_eq!(editor.cursor.x, 7);
+ assert_eq!(editor.cursor.y, 3);
+
+ editor.process_keypress(&Key::Home);
+ assert_eq!(editor.cursor.x, 0);
+ assert_eq!(editor.cursor.y, 3);
+
+ editor.move_cursor(&AKey::Up, false);
+ editor.move_cursor(&AKey::Up, false);
+ editor.move_cursor(&AKey::Up, false);
+
+ assert_eq!(editor.cursor.x, 0);
+ assert_eq!(editor.cursor.y, 0);
+
+ editor.move_cursor(&AKey::Right, true);
+ assert_eq!(editor.cursor.x, 5);
+ assert_eq!(editor.cursor.y, 0);
+
+ editor.process_keypress(&Key::Home);
+ assert_eq!(editor.cursor.x, 0);
+ assert_eq!(editor.cursor.y, 0);
+ }
+
+ #[test]
+ fn editor_press_end_key() {
+ let mut editor = Editor::default();
+ for b in b"Hello\nWorld\nand\nFerris!" {
+ if *b == b'\n' {
+ editor.insert_new_line();
+ } else {
+ editor.insert_byte(*b);
+ }
+ }
+
+ // check current position
+ assert_eq!(editor.cursor.x, 7);
+ assert_eq!(editor.cursor.y, 3);
+
+ editor.process_keypress(&Key::End);
+ assert_eq!(editor.cursor.x, 7);
+ assert_eq!(editor.cursor.y, 3);
+
+ editor.move_cursor(&AKey::Up, false);
+ editor.move_cursor(&AKey::Up, false);
+ editor.move_cursor(&AKey::Up, false);
+
+ assert_eq!(editor.cursor.x, 3);
+ assert_eq!(editor.cursor.y, 0);
+
+ editor.process_keypress(&Key::End);
+ assert_eq!(editor.cursor.x, 5);
+ assert_eq!(editor.cursor.y, 0);
+ }
}
diff --git a/src/error.rs b/src/error.rs
index 3745239a..333c3c2f 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -7,15 +7,16 @@ pub enum Error {
Io(std::io::Error),
/// Wrapper around `std::fmt::Error`
Fmt(std::fmt::Error),
- /// Error returned when the window size obtained through a system call is invalid.
+ /// Error returned when the window size obtained through a system call is
+ /// invalid.
InvalidWindowSize,
/// Error setting or retrieving the cursor position.
CursorPosition,
- /// Configuration error. The three attributes correspond the file path, the line number and the
- /// error message.
+ /// Configuration error. The three attributes correspond the file path, the
+ /// line number and the error message.
Config(std::path::PathBuf, usize, String),
- /// Too many arguments given to kibi. The attribute corresponds to the total number of command
- /// line arguments.
+ /// Too many arguments given to kibi. The attribute corresponds to the total
+ /// number of command line arguments.
TooManyArguments(usize),
/// Unrecognized option given as a command line argument.
UnrecognizedOption(String),
diff --git a/src/lib.rs b/src/lib.rs
index 50740b0c..96a7c817 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -12,13 +12,9 @@ mod row;
mod syntax;
mod terminal;
-#[cfg(windows)] mod windows;
-#[cfg(windows)] use windows as sys;
+#[cfg_attr(windows, path = "windows.rs")]
+#[cfg_attr(unix, path = "unix.rs")]
+#[cfg_attr(target_os = "wasi", path = "wasi.rs")]
+mod sys;
-#[cfg(unix)] mod unix;
-#[cfg(unix)] mod xdg;
-#[cfg(unix)] use unix as sys;
-
-#[cfg(target_os = "wasi")] mod wasi;
-#[cfg(target_os = "wasi")] mod xdg;
-#[cfg(target_os = "wasi")] use wasi as sys;
+#[cfg(any(unix, target_os = "wasi"))] mod xdg;
diff --git a/src/main.rs b/src/main.rs
index 9dd0a0c5..99b15e2e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,15 +2,16 @@
use kibi::{Config, Editor, Error};
-/// Load the configuration, initialize the editor and run the program, optionally opening a file if
-/// an argument is given.
+/// Load the configuration, initialize the editor and run the program,
+/// optionally opening a file if an argument is given.
///
/// # Errors
///
-/// Any error that occur during the execution of the program will be returned by this function.
+/// Any error that occur during the execution of the program will be returned by
+/// this function.
fn main() -> Result<(), Error> {
let mut args = std::env::args();
- match (args.nth(1), /*remaining_args=*/ args.len()) {
+ match (args.nth(1), /* remaining_args= */ args.len()) {
(Some(arg), 0) if arg == "--version" => println!("kibi {}", env!("KIBI_VERSION")),
(Some(arg), 0) if arg.starts_with('-') => return Err(Error::UnrecognizedOption(arg)),
(file_name, 0) => Editor::new(Config::load()?)?.run(&file_name)?,
diff --git a/src/row.rs b/src/row.rs
index fb853a6e..f7cbe13d 100644
--- a/src/row.rs
+++ b/src/row.rs
@@ -1,7 +1,7 @@
//! # Row
//!
-//! Utilities for rows. A `Row` owns the underlying characters, the rendered string and the syntax
-//! highlighting information.
+//! Utilities for rows. A `Row` owns the underlying characters, the rendered
+//! string and the syntax highlighting information.
use std::{fmt::Write, iter::repeat};
@@ -19,7 +19,8 @@ pub enum HlState {
Normal,
/// A multi-line comment has been open, but not yet closed.
MultiLineComment,
- /// A string has been open with the given quote character (for instance b'\'' or b'"'), but not yet closed.
+ /// A string has been open with the given quote character (for instance
+ /// b'\'' or b'"'), but not yet closed.
String(u8),
/// A multi-line string has been open, but not yet closed.
MultiLineString,
@@ -30,18 +31,22 @@ pub enum HlState {
pub struct Row {
/// The characters of the row.
pub chars: Vec,
- /// How the characters are rendered. In particular, tabs are converted into several spaces, and
- /// bytes may be combined into single UTF-8 characters.
+ /// How the characters are rendered. In particular, tabs are converted into
+ /// several spaces, and bytes may be combined into single UTF-8
+ /// characters.
render: String,
- /// Mapping from indices in `self.chars` to the corresponding indices in `self.render`.
+ /// Mapping from indices in `self.chars` to the corresponding indices in
+ /// `self.render`.
pub cx2rx: Vec,
- /// Mapping from indices in `self.render` to the corresponding indices in `self.chars`.
+ /// Mapping from indices in `self.render` to the corresponding indices in
+ /// `self.chars`.
pub rx2cx: Vec,
/// The vector of `HLType` for each rendered character.
hl: Vec,
/// The final state of the row.
pub hl_state: HlState,
- /// If not `None`, the range that is currently matched during a FIND operation.
+ /// If not `None`, the range that is currently matched during a FIND
+ /// operation.
pub match_segment: Option>,
}
@@ -59,17 +64,17 @@ impl Row {
// The number of rendered characters
let n_rend_chars = if c == '\t' { tab - (rx % tab) } else { c.width().unwrap_or(1) };
self.render.push_str(&(if c == '\t' { " ".repeat(n_rend_chars) } else { c.into() }));
- self.cx2rx.extend(std::iter::repeat(rx).take(c.len_utf8()));
- self.rx2cx.extend(std::iter::repeat(cx).take(n_rend_chars));
+ self.cx2rx.extend(repeat(rx).take(c.len_utf8()));
+ self.rx2cx.extend(repeat(cx).take(n_rend_chars));
(rx, cx) = (rx + n_rend_chars, cx + c.len_utf8());
}
let (..) = (self.cx2rx.push(rx), self.rx2cx.push(cx));
self.update_syntax(syntax, hl_state)
}
- /// Obtain the character size, in bytes, given its position in `self.render`. This is done in
- /// constant time by using the difference between `self.rx2cx[rx]` and the cx for the next
- /// character.
+ /// Obtain the character size, in bytes, given its position in
+ /// `self.render`. This is done in constant time by using the difference
+ /// between `self.rx2cx[rx]` and the cx for the next character.
pub fn get_char_size(&self, rx: usize) -> usize {
let cx0 = self.rx2cx[rx];
self.rx2cx.iter().skip(rx + 1).map(|cx| cx - cx0).find(|d| *d > 0).unwrap_or(1)
@@ -80,14 +85,14 @@ impl Row {
self.hl.clear();
let line = self.render.as_bytes();
- // Delimiters for multi-line comments and multi-line strings, as Option<&String, &String>
+ // Delimiters for multi-line comments and multi-line strings, as Option<&String,
+ // &String>
let ml_comment_delims = syntax.ml_comment_delims.as_ref().map(|(start, end)| (start, end));
let ml_string_delims = syntax.ml_string_delim.as_ref().map(|x| (x, x));
'syntax_loop: while self.hl.len() < line.len() {
let i = self.hl.len();
- let find_str =
- |s: &str| line.get(i..(i + s.len())).map_or(false, |r| r.eq(s.as_bytes()));
+ let find_str = |s: &str| line.get(i..(i + s.len())).is_some_and(|r| r.eq(s.as_bytes()));
if hl_state == HlState::Normal && syntax.sl_comment_start.iter().any(|s| find_str(s)) {
self.hl.extend(repeat(HlType::Comment).take(line.len() - i));
@@ -167,9 +172,10 @@ impl Row {
self.hl_state
}
- /// Draw the row and write the result to a buffer. An `offset` can be given, as well as a limit
- /// on the length of the row (`max_len`). After writing the characters, clear the rest of the
- /// line and move the cursor to the start of the next line.
+ /// Draw the row and write the result to a buffer. An `offset` can be given,
+ /// as well as a limit on the length of the row (`max_len`). After
+ /// writing the characters, clear the rest of the line and move the
+ /// cursor to the start of the next line.
pub fn draw(&self, offset: usize, max_len: usize, buffer: &mut String) -> Result<(), Error> {
let mut current_hl_type = HlType::Normal;
let chars = self.render.chars().skip(offset).take(max_len);
@@ -206,6 +212,6 @@ impl Row {
}
/// Return whether `c` is an ASCII separator.
-fn is_sep(c: u8) -> bool {
+const fn is_sep(c: u8) -> bool {
c.is_ascii_whitespace() || c == b'\0' || (c.is_ascii_punctuation() && c != b'_')
}
diff --git a/src/syntax.rs b/src/syntax.rs
index 65dc106c..f33949b1 100644
--- a/src/syntax.rs
+++ b/src/syntax.rs
@@ -2,13 +2,13 @@ use std::fmt::{self, Display, Formatter};
use std::path::{Path, PathBuf};
use crate::config::{self, parse_value as pv, parse_values as pvs};
-use crate::{sys, Error};
+use crate::{Error, sys};
/// Type of syntax highlighting for a single rendered character.
///
-/// Each `HLType` is associated with a color, via its discriminant. The ANSI color is equal
-/// to the discriminant, modulo 100. The colors are described here:
-///
+/// Each `HLType` is associated with a color, via its discriminant. The ANSI
+/// color is equal to the discriminant, modulo 100. The colors are described
+/// here:
#[derive(PartialEq, Eq, Copy, Clone)]
pub enum HlType {
Normal = 39, // Default foreground color
@@ -23,7 +23,8 @@ pub enum HlType {
}
impl Display for HlType {
- /// Write the ANSI color escape sequence for the `HLType` using the given formatter.
+ /// Write the ANSI color escape sequence for the `HLType` using the given
+ /// formatter.
fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!(f, "\x1b[{}m", (*self as u32) % 100) }
}
@@ -40,16 +41,17 @@ pub struct Conf {
pub sl_comment_start: Vec,
/// The tokens that start and end a multi-line comment, e.g. ("/*", "*/").
pub ml_comment_delims: Option<(String, String)>,
- /// The token that start and end a multi-line strings, e.g. "\"\"\"" for Python.
+ /// The token that start and end a multi-line strings, e.g. "\"\"\"" for
+ /// Python.
pub ml_string_delim: Option,
- /// Keywords to highlight and there corresponding HLType (typically
- /// HLType::Keyword1 or HLType::Keyword2)
+ /// Keywords to highlight and there corresponding `HLType` (typically
+ /// `HLType::Keyword1` or `HLType::Keyword2`)
pub keywords: Vec<(HlType, Vec)>,
}
impl Conf {
- /// Return the syntax configuration corresponding to the given file extension, if a matching
- /// INI file is found in a config directory.
+ /// Return the syntax configuration corresponding to the given file
+ /// extension, if a matching INI file is found in a config directory.
pub fn get(ext: &str) -> Result