red: add syntax highlighting and lysp support
This commit is contained in:
Generated
+8
-191
@@ -644,31 +644,6 @@ version = "0.8.21"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossterm"
|
|
||||||
version = "0.27.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.11.0",
|
|
||||||
"crossterm_winapi",
|
|
||||||
"libc",
|
|
||||||
"mio",
|
|
||||||
"parking_lot",
|
|
||||||
"signal-hook",
|
|
||||||
"signal-hook-mio",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossterm_winapi"
|
|
||||||
version = "0.9.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
|
||||||
dependencies = [
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypt"
|
name = "crypt"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -1057,15 +1032,6 @@ dependencies = [
|
|||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "error-chain"
|
|
||||||
version = "0.12.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc"
|
|
||||||
dependencies = [
|
|
||||||
"version_check",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
@@ -1349,17 +1315,6 @@ dependencies = [
|
|||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hostname"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"match_cfg",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
@@ -1812,12 +1767,6 @@ dependencies = [
|
|||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "match_cfg"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "md2txt"
|
name = "md2txt"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -1859,18 +1808,6 @@ dependencies = [
|
|||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mio"
|
|
||||||
version = "0.8.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"log",
|
|
||||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
|
||||||
"windows-sys 0.48.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ndk"
|
name = "ndk"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -2029,15 +1966,6 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num_threads"
|
|
||||||
version = "0.1.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc-sys"
|
name = "objc-sys"
|
||||||
version = "0.3.5"
|
version = "0.3.5"
|
||||||
@@ -2310,29 +2238,6 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "parking_lot"
|
|
||||||
version = "0.12.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
|
|
||||||
dependencies = [
|
|
||||||
"lock_api",
|
|
||||||
"parking_lot_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "parking_lot_core"
|
|
||||||
version = "0.9.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
"redox_syscall 0.5.13",
|
|
||||||
"smallvec",
|
|
||||||
"windows-targets 0.52.6",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pci-ids"
|
name = "pci-ids"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@@ -2764,10 +2669,11 @@ name = "red"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cross",
|
"cross",
|
||||||
"crossterm",
|
|
||||||
"libc",
|
|
||||||
"libterm",
|
"libterm",
|
||||||
"syslog",
|
"log",
|
||||||
|
"logsink",
|
||||||
|
"lysp",
|
||||||
|
"regex",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"unicode-width 0.1.14",
|
"unicode-width 0.1.14",
|
||||||
]
|
]
|
||||||
@@ -2792,9 +2698,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.11.1"
|
version = "1.12.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -2804,9 +2710,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.4.9"
|
version = "0.4.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -3119,36 +3025,6 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "signal-hook"
|
|
||||||
version = "0.3.18"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"signal-hook-registry",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "signal-hook-mio"
|
|
||||||
version = "0.2.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"mio",
|
|
||||||
"signal-hook",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "signal-hook-registry"
|
|
||||||
version = "1.4.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signature"
|
name = "signature"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
@@ -3365,19 +3241,6 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "syslog"
|
|
||||||
version = "6.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dfc7e95b5b795122fafe6519e27629b5ab4232c73ebb2428f568e82b1a457ad3"
|
|
||||||
dependencies = [
|
|
||||||
"error-chain",
|
|
||||||
"hostname",
|
|
||||||
"libc",
|
|
||||||
"log",
|
|
||||||
"time",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sysutils"
|
name = "sysutils"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -3481,14 +3344,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
|
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
"itoa",
|
|
||||||
"libc",
|
|
||||||
"num-conv",
|
"num-conv",
|
||||||
"num_threads",
|
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
"serde",
|
|
||||||
"time-core",
|
"time-core",
|
||||||
"time-macros",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3497,16 +3355,6 @@ version = "0.1.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
|
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "time-macros"
|
|
||||||
version = "0.2.22"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
|
|
||||||
dependencies = [
|
|
||||||
"num-conv",
|
|
||||||
"time-core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tiny-skia"
|
name = "tiny-skia"
|
||||||
version = "0.11.4"
|
version = "0.11.4"
|
||||||
@@ -3961,22 +3809,6 @@ dependencies = [
|
|||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[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]]
|
[[package]]
|
||||||
name = "winapi-util"
|
name = "winapi-util"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
@@ -3986,12 +3818,6 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi-x86_64-pc-windows-gnu"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.45.0"
|
version = "0.45.0"
|
||||||
@@ -4001,15 +3827,6 @@ dependencies = [
|
|||||||
"windows-targets 0.42.2",
|
"windows-targets 0.42.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-sys"
|
|
||||||
version = "0.48.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
|
||||||
dependencies = [
|
|
||||||
"windows-targets 0.48.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
(import "examples/io.lysp")
|
(import "io.lysp")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
|
path::Path,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ pub struct FunctionBlock {
|
|||||||
identifier: Option<IdentifierValue>,
|
identifier: Option<IdentifierValue>,
|
||||||
docstring: Option<StringValue>,
|
docstring: Option<StringValue>,
|
||||||
signature: FunctionSignature,
|
signature: FunctionSignature,
|
||||||
|
script_path: Option<Rc<Path>>,
|
||||||
|
|
||||||
// Data
|
// Data
|
||||||
pub(crate) constants: Vec<Value>,
|
pub(crate) constants: Vec<Value>,
|
||||||
@@ -81,6 +83,7 @@ pub struct CompileContext {
|
|||||||
pub function_blocks: Vec<FunctionBlock>,
|
pub function_blocks: Vec<FunctionBlock>,
|
||||||
pub(crate) current: usize,
|
pub(crate) current: usize,
|
||||||
pub(crate) options: CompileOptions,
|
pub(crate) options: CompileOptions,
|
||||||
|
script_path: Option<Rc<Path>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Compile {
|
pub trait Compile {
|
||||||
@@ -88,11 +91,16 @@ pub trait Compile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CompileContext {
|
impl CompileContext {
|
||||||
pub fn new(options: CompileOptions, root_name: Option<IdentifierValue>) -> Self {
|
pub fn new(
|
||||||
|
options: CompileOptions,
|
||||||
|
root_name: Option<IdentifierValue>,
|
||||||
|
script_path: Option<Rc<Path>>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
function_blocks: vec![FunctionBlock::root(root_name)],
|
function_blocks: vec![FunctionBlock::root(root_name, script_path.clone())],
|
||||||
current: 0,
|
current: 0,
|
||||||
options,
|
options,
|
||||||
|
script_path,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,8 +108,9 @@ impl CompileContext {
|
|||||||
options: CompileOptions,
|
options: CompileOptions,
|
||||||
chunk_name: Option<IdentifierValue>,
|
chunk_name: Option<IdentifierValue>,
|
||||||
value: &Value,
|
value: &Value,
|
||||||
|
script_path: Option<Rc<Path>>,
|
||||||
) -> Result<Rc<BytecodeFunction>, CompileError> {
|
) -> Result<Rc<BytecodeFunction>, CompileError> {
|
||||||
let mut cx = Self::new(options, chunk_name);
|
let mut cx = Self::new(options, chunk_name, script_path);
|
||||||
let expression = Expression::parse(value).map_err(CompileError::Parse)?;
|
let expression = Expression::parse(value).map_err(CompileError::Parse)?;
|
||||||
let value = expression.compile(&mut cx)?;
|
let value = expression.compile(&mut cx)?;
|
||||||
cx.compile_return_value(value)?;
|
cx.compile_return_value(value)?;
|
||||||
@@ -141,7 +150,13 @@ impl CompileContext {
|
|||||||
if self.options.trace_compile {
|
if self.options.trace_compile {
|
||||||
eprintln!("COMPILE: push_lambda_context({identifier:?})");
|
eprintln!("COMPILE: push_lambda_context({identifier:?})");
|
||||||
}
|
}
|
||||||
let block = FunctionBlock::new(Some(self.current), identifier, docstring, signature);
|
let block = FunctionBlock::new(
|
||||||
|
Some(self.current),
|
||||||
|
identifier,
|
||||||
|
docstring,
|
||||||
|
signature,
|
||||||
|
self.script_path.clone(),
|
||||||
|
);
|
||||||
let index = self.function_blocks.len();
|
let index = self.function_blocks.len();
|
||||||
self.function_blocks.push(block);
|
self.function_blocks.push(block);
|
||||||
self.current = index;
|
self.current = index;
|
||||||
@@ -384,6 +399,7 @@ impl FunctionBlock {
|
|||||||
identifier: Option<IdentifierValue>,
|
identifier: Option<IdentifierValue>,
|
||||||
docstring: Option<StringValue>,
|
docstring: Option<StringValue>,
|
||||||
signature: &FunctionSignature,
|
signature: &FunctionSignature,
|
||||||
|
script_path: Option<Rc<Path>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut block = Self {
|
let mut block = Self {
|
||||||
parent,
|
parent,
|
||||||
@@ -397,6 +413,7 @@ impl FunctionBlock {
|
|||||||
instructions: vec![],
|
instructions: vec![],
|
||||||
labels: vec![],
|
labels: vec![],
|
||||||
loop_stack: vec![],
|
loop_stack: vec![],
|
||||||
|
script_path,
|
||||||
};
|
};
|
||||||
for required in signature.required_arguments.iter() {
|
for required in signature.required_arguments.iter() {
|
||||||
block
|
block
|
||||||
@@ -416,7 +433,7 @@ impl FunctionBlock {
|
|||||||
block
|
block
|
||||||
}
|
}
|
||||||
|
|
||||||
fn root(identifier: Option<IdentifierValue>) -> Self {
|
fn root(identifier: Option<IdentifierValue>, script_path: Option<Rc<Path>>) -> Self {
|
||||||
Self::new(
|
Self::new(
|
||||||
None,
|
None,
|
||||||
identifier,
|
identifier,
|
||||||
@@ -426,6 +443,7 @@ impl FunctionBlock {
|
|||||||
optional_arguments: vec![],
|
optional_arguments: vec![],
|
||||||
rest_argument: None,
|
rest_argument: None,
|
||||||
},
|
},
|
||||||
|
script_path,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,6 +549,7 @@ impl FunctionBlock {
|
|||||||
required_count: self.signature.required_arguments.len(),
|
required_count: self.signature.required_arguments.len(),
|
||||||
optional_count: self.signature.optional_arguments.len(),
|
optional_count: self.signature.optional_arguments.len(),
|
||||||
has_rest: self.signature.rest_argument.is_some(),
|
has_rest: self.signature.rest_argument.is_some(),
|
||||||
|
script: self.script_path.clone(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -631,7 +650,7 @@ impl FunctionBlock {
|
|||||||
pub fn test_compile(
|
pub fn test_compile(
|
||||||
expression: &Expression,
|
expression: &Expression,
|
||||||
) -> Result<(CompileContext, CompileValue), CompileError> {
|
) -> Result<(CompileContext, CompileValue), CompileError> {
|
||||||
let mut cx = CompileContext::new(Default::default(), None);
|
let mut cx = CompileContext::new(Default::default(), None, None);
|
||||||
let value = expression.compile(&mut cx)?;
|
let value = expression.compile(&mut cx)?;
|
||||||
Ok((cx, value))
|
Ok((cx, value))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ fn run_module<P: AsRef<Path>>(
|
|||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let name = format!("{}", path.display());
|
let name = format!("{}", path.display());
|
||||||
let reader = BufReader::new(File::open(path)?);
|
let reader = BufReader::new(File::open(path)?);
|
||||||
let module_reader = ModuleReader::new(reader, vm.trace_macros);
|
let module_reader = ModuleReader::new(reader, path, vm.trace_macros);
|
||||||
let function = match module_reader.compile(Some(name.into()), compile_options, env) {
|
let function = match module_reader.compile(Some(name.into()), compile_options, env) {
|
||||||
Ok(function) => function,
|
Ok(function) => function,
|
||||||
Err(error) => return Err(handle_module_error(error)),
|
Err(error) => return Err(handle_module_error(error)),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
io::{BufRead, Write, stdin, stdout},
|
io::{BufRead, Write, stdin, stdout},
|
||||||
|
path::PathBuf,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ pub struct FileReader<R: BufRead> {
|
|||||||
|
|
||||||
pub struct ModuleReader<R: BufRead> {
|
pub struct ModuleReader<R: BufRead> {
|
||||||
reader: FileReader<R>,
|
reader: FileReader<R>,
|
||||||
|
path: PathBuf,
|
||||||
macro_machine: Machine,
|
macro_machine: Machine,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,11 +95,12 @@ impl<R: BufRead> Reader for FileReader<R> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<R: BufRead> ModuleReader<R> {
|
impl<R: BufRead> ModuleReader<R> {
|
||||||
pub fn new(reader: R, trace_macros: bool) -> Self {
|
pub fn new<P: Into<PathBuf>>(reader: R, path: P, trace_macros: bool) -> Self {
|
||||||
let mut macro_machine = Machine::default();
|
let mut macro_machine = Machine::default();
|
||||||
macro_machine.trace_macros = trace_macros;
|
macro_machine.trace_macros = trace_macros;
|
||||||
Self {
|
Self {
|
||||||
reader: FileReader::new(reader),
|
reader: FileReader::new(reader),
|
||||||
|
path: path.into(),
|
||||||
macro_machine,
|
macro_machine,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,7 +133,8 @@ impl<R: BufRead> ModuleReader<R> {
|
|||||||
options: &CompileOptions,
|
options: &CompileOptions,
|
||||||
env: &Rc<Environment>,
|
env: &Rc<Environment>,
|
||||||
) -> Result<Rc<BytecodeFunction>, Either<MachineErrorAt, Vec<ParseError>>> {
|
) -> Result<Rc<BytecodeFunction>, Either<MachineErrorAt, Vec<ParseError>>> {
|
||||||
let mut cx = CompileContext::new(options.clone(), module_name);
|
let mut cx =
|
||||||
|
CompileContext::new(options.clone(), module_name, Some(self.path.clone().into()));
|
||||||
let mut body = FunctionBody {
|
let mut body = FunctionBody {
|
||||||
head: vec![],
|
head: vec![],
|
||||||
tail: Rc::new(Expression::Nil),
|
tail: Rc::new(Expression::Nil),
|
||||||
|
|||||||
@@ -26,10 +26,11 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
struct CallFrame {
|
#[derive(Debug)]
|
||||||
closure: ClosureValue,
|
pub struct CallFrame {
|
||||||
ip: usize,
|
pub closure: ClosureValue,
|
||||||
base_pointer: usize,
|
pub ip: usize,
|
||||||
|
pub base_pointer: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Machine {
|
pub struct Machine {
|
||||||
@@ -95,6 +96,10 @@ impl Machine {
|
|||||||
.ok_or(MachineError::DataStackUnderflow)
|
.ok_or(MachineError::DataStackUnderflow)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn read_call_stack(&self, depth: usize) -> Option<&CallFrame> {
|
||||||
|
self.call_stack.nth(depth)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn current_location(&self) -> Option<MachineErrorLocation> {
|
pub fn current_location(&self) -> Option<MachineErrorLocation> {
|
||||||
self.call_stack.head().map(|frame| MachineErrorLocation {
|
self.call_stack.head().map(|frame| MachineErrorLocation {
|
||||||
function: frame.closure.function.clone(),
|
function: frame.closure.function.clone(),
|
||||||
@@ -691,9 +696,10 @@ impl Machine {
|
|||||||
value: Value,
|
value: Value,
|
||||||
) -> Result<Value, MachineErrorAt> {
|
) -> Result<Value, MachineErrorAt> {
|
||||||
let value_expanded = self.macro_expand(env, &value)?;
|
let value_expanded = self.macro_expand(env, &value)?;
|
||||||
let function = CompileContext::compile_value(compile_options, chunk_name, &value_expanded)
|
let function =
|
||||||
.map_err(MachineError::Compile)
|
CompileContext::compile_value(compile_options, chunk_name, &value_expanded, None)
|
||||||
.map_err(MachineErrorAt::at_unknown)?;
|
.map_err(MachineError::Compile)
|
||||||
|
.map_err(MachineErrorAt::at_unknown)?;
|
||||||
|
|
||||||
let closure = ClosureValue {
|
let closure = ClosureValue {
|
||||||
function,
|
function,
|
||||||
@@ -718,7 +724,7 @@ impl Machine {
|
|||||||
.map_err(MachineError::Read)
|
.map_err(MachineError::Read)
|
||||||
.map_err(MachineErrorAt::at_unknown)?,
|
.map_err(MachineErrorAt::at_unknown)?,
|
||||||
);
|
);
|
||||||
let module_reader = ModuleReader::new(reader, self.trace_macros);
|
let module_reader = ModuleReader::new(reader, path, self.trace_macros);
|
||||||
let function = match module_reader.compile(Some(name.into()), &compile_options, env) {
|
let function = match module_reader.compile(Some(name.into()), &compile_options, env) {
|
||||||
Ok(function) => function,
|
Ok(function) => function,
|
||||||
Err(error) => todo!("Handle error: {error:?}"),
|
Err(error) => todo!("Handle error: {error:?}"),
|
||||||
@@ -799,6 +805,7 @@ mod tests {
|
|||||||
required_count: 0,
|
required_count: 0,
|
||||||
optional_count: 0,
|
optional_count: 0,
|
||||||
has_rest: false,
|
has_rest: false,
|
||||||
|
script: None,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
let mut machine = Machine::default();
|
let mut machine = Machine::default();
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
use std::rc::Rc;
|
use std::{
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
rc::Rc,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::MachineError,
|
error::MachineError,
|
||||||
@@ -18,11 +21,31 @@ pub fn load(env: &Rc<Environment>) {
|
|||||||
return Err(MachineError::InvalidArgumentCount);
|
return Err(MachineError::InvalidArgumentCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
for arg in args {
|
let caller = vm
|
||||||
let path = StringValue::try_from_value(arg)?;
|
.read_call_stack(0)
|
||||||
|
.ok_or(MachineError::CallStackUnderflow)?;
|
||||||
|
let caller_directory = caller
|
||||||
|
.closure
|
||||||
|
.function
|
||||||
|
.script
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|path| path.parent())
|
||||||
|
.map(Path::to_owned)
|
||||||
|
.unwrap_or_else(|| PathBuf::from("."));
|
||||||
|
|
||||||
vm.load_file(Default::default(), env, &*path)
|
for arg in args {
|
||||||
.map_err(|error| MachineError::LoadError(path, error.into()))?;
|
let path_str = StringValue::try_from_value(arg)?;
|
||||||
|
let mut path = PathBuf::from(&*path_str);
|
||||||
|
|
||||||
|
if path.is_relative() {
|
||||||
|
let in_caller_directory = caller_directory.join(&path);
|
||||||
|
if in_caller_directory.exists() {
|
||||||
|
path = in_caller_directory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.load_file(Default::default(), env, &path)
|
||||||
|
.map_err(|error| MachineError::LoadError(path_str, error.into()))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Value::Nil)
|
Ok(Value::Nil)
|
||||||
|
|||||||
@@ -55,6 +55,14 @@ impl<T> Stack<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn nth(&self, n: usize) -> Option<&T> {
|
||||||
|
if n >= self.pointer {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(unsafe { self.data[self.pointer - n - 1].assume_init_ref() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get(&self, index: usize) -> Option<&T> {
|
pub fn get(&self, index: usize) -> Option<&T> {
|
||||||
if index < self.pointer {
|
if index < self.pointer {
|
||||||
Some(unsafe { self.data[index].assume_init_ref() })
|
Some(unsafe { self.data[index].assume_init_ref() })
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::fmt;
|
use std::{fmt, path::Path, rc::Rc};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
compile::UpvalueDef,
|
compile::UpvalueDef,
|
||||||
@@ -16,6 +16,7 @@ pub struct BytecodeFunction {
|
|||||||
pub instructions: Box<[u8]>,
|
pub instructions: Box<[u8]>,
|
||||||
pub constants: Box<[Value]>,
|
pub constants: Box<[Value]>,
|
||||||
pub upvalues: Box<[UpvalueDef]>,
|
pub upvalues: Box<[UpvalueDef]>,
|
||||||
|
pub script: Option<Rc<Path>>,
|
||||||
|
|
||||||
pub required_count: usize,
|
pub required_count: usize,
|
||||||
pub optional_count: usize,
|
pub optional_count: usize,
|
||||||
@@ -51,6 +52,10 @@ impl BytecodeFunction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn script_path(&self) -> Option<&Path> {
|
||||||
|
self.script.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn min_arity(&self) -> usize {
|
pub fn min_arity(&self) -> usize {
|
||||||
self.required_count
|
self.required_count
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+614
-211
File diff suppressed because it is too large
Load Diff
@@ -4,16 +4,25 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["Mark Poliakov <mark@alnyan.me>"]
|
authors = ["Mark Poliakov <mark@alnyan.me>"]
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
libterm.workspace = true
|
libterm.workspace = true
|
||||||
cross.workspace = true
|
cross.workspace = true
|
||||||
|
lysp.workspace = true
|
||||||
|
logsink.workspace = true
|
||||||
|
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
|
||||||
unicode-width = "0.1.11"
|
unicode-width = "0.1.11"
|
||||||
|
regex = "1.12.3"
|
||||||
|
|
||||||
[target.'cfg(not(target_os = "yggdrasil"))'.dependencies]
|
[features]
|
||||||
libc = "0.2.150"
|
default = []
|
||||||
crossterm = "0.27.0"
|
runtime = []
|
||||||
syslog = "6.1.0"
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
# [target.'cfg(not(target_os = "yggdrasil"))'.dependencies]
|
||||||
|
# libc = "0.2.150"
|
||||||
|
# syslog = "6.1.0"
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
(declare-command "q" () (red/quit))
|
||||||
|
(declare-command "q!" () (red/quit #t))
|
||||||
|
(declare-command
|
||||||
|
"w" (&optional filename)
|
||||||
|
(if filename
|
||||||
|
(red/buffer/write filename)
|
||||||
|
(red/buffer/write)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(declare-command
|
||||||
|
"wq" ()
|
||||||
|
(red/buffer/write)
|
||||||
|
(red/quit)
|
||||||
|
)
|
||||||
|
(declare-command
|
||||||
|
"e" (&optional filename)
|
||||||
|
(if filename
|
||||||
|
(red/buffer/open filename)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(declare-command
|
||||||
|
"e!" (&optional filename)
|
||||||
|
(if filename
|
||||||
|
(red/buffer/open filename #t)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(declare-command
|
||||||
|
"source" (filename)
|
||||||
|
(import filename)
|
||||||
|
)
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
;; External API
|
||||||
|
|
||||||
|
(defmacro ignore (&rest expressions) nil)
|
||||||
|
|
||||||
|
(defmacro declare-key (mode seq action-head &rest action-tail)
|
||||||
|
(let (bind-function (symbol (+ "red/bind-" (->string mode) "-hook")))
|
||||||
|
`(,bind-function ,seq (lambda () (progn ,action-head ,@action-tail)))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(defmacro declare-command
|
||||||
|
(command args body-head &rest body-tail)
|
||||||
|
;; TODO auto-rest?
|
||||||
|
`(setq
|
||||||
|
_red/command-table
|
||||||
|
(cons
|
||||||
|
(list ,command (lambda (,@args &rest _) ,body-head ,@body-tail))
|
||||||
|
_red/command-table
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(defun try-import (path)
|
||||||
|
(when (fs/file? path) (import path) #t))
|
||||||
|
|
||||||
|
(setq _red/command-table nil)
|
||||||
|
|
||||||
|
;; Hooks for the editor/buffer events
|
||||||
|
(defun _red/lookup-command (name)
|
||||||
|
(find (lambda (cmd) (= name (car cmd))) _red/command-table)
|
||||||
|
)
|
||||||
|
|
||||||
|
(defun _red/root-command-hook (command &rest args)
|
||||||
|
(let (entry (_red/lookup-command command))
|
||||||
|
(if entry
|
||||||
|
(apply (cadr entry) args)
|
||||||
|
(red/message (+ "Unhandled command: :" command))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(defun _red/root-post-render-hook (width height)
|
||||||
|
nil
|
||||||
|
)
|
||||||
|
|
||||||
|
;; Bind the hooks
|
||||||
|
(red/bind-command-hook _red/root-command-hook)
|
||||||
|
(red/bind-post-render-hook _red/root-post-render-hook)
|
||||||
|
|
||||||
|
;; Child modules
|
||||||
|
(import "keyboard.lysp")
|
||||||
|
(import "command.lysp")
|
||||||
|
(import "highlight.lysp")
|
||||||
|
|
||||||
|
;; User configuration
|
||||||
|
(try-import "/etc/red/init.lysp")
|
||||||
|
(try-import (+ (fs/home-directory) "/.red.d/init.lysp"))
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
;; TODO
|
||||||
|
(defun _map (f xs)
|
||||||
|
(if (nil? xs)
|
||||||
|
nil
|
||||||
|
(cons (f (car xs)) (_map f (cdr xs))))
|
||||||
|
)
|
||||||
|
|
||||||
|
(defun _red/syntax-make-keyword-rule (syntax pattern category prev-state next-state)
|
||||||
|
`(red/syntax/define-keyword-rule ,syntax ,prev-state ,pattern ,category ,next-state))
|
||||||
|
(defun _red/syntax-rule-keyword (syntax clause)
|
||||||
|
;; (:keyword "PATTERNS"... :category "CATEGORY" :prev-state N :next-state M)
|
||||||
|
(let (
|
||||||
|
prev-state 0
|
||||||
|
next-state nil
|
||||||
|
patterns nil
|
||||||
|
category nil
|
||||||
|
)
|
||||||
|
(while (not (nil? clause))
|
||||||
|
(cond
|
||||||
|
; :prev-state N
|
||||||
|
((= (car clause) ':prev-state)
|
||||||
|
(progn
|
||||||
|
(setq prev-state (cadr clause))
|
||||||
|
(setq clause (cdr clause))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
; :next-state N
|
||||||
|
((= (car clause) ':next-state)
|
||||||
|
(progn
|
||||||
|
(setq next-state (cadr clause))
|
||||||
|
(setq clause (cdr clause))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
; :category "CATEGORY"
|
||||||
|
((= (car clause) ':category)
|
||||||
|
(progn
|
||||||
|
(setq category (cadr clause))
|
||||||
|
(setq clause (cdr clause))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
; "PATTERNS"...
|
||||||
|
(&otherwise (setq patterns (cons (car clause) patterns)))
|
||||||
|
)
|
||||||
|
(setq clause (cdr clause))
|
||||||
|
)
|
||||||
|
|
||||||
|
; (when (or (nil? category) (nil? patterns))
|
||||||
|
; (error "Invalid clause")
|
||||||
|
; )
|
||||||
|
|
||||||
|
(cons
|
||||||
|
'progn
|
||||||
|
(_map
|
||||||
|
(lambda (pattern) (_red/syntax-make-keyword-rule syntax pattern category prev-state next-state))
|
||||||
|
patterns
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(defun _red/syntax-rule-regex (syntax clause)
|
||||||
|
;; (:regex "REGEX" "CATEGORY" :prev-state N :next-state M)
|
||||||
|
(let (
|
||||||
|
prev-state 0
|
||||||
|
next-state nil
|
||||||
|
regex (car clause)
|
||||||
|
category (cadr clause)
|
||||||
|
)
|
||||||
|
(setq clause (cdr (cdr clause)))
|
||||||
|
(while (not (nil? clause))
|
||||||
|
(cond
|
||||||
|
; :prev-state N
|
||||||
|
((= (car clause) ':prev-state)
|
||||||
|
(progn
|
||||||
|
(setq prev-state (cadr clause))
|
||||||
|
(setq clause (cdr clause))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
; :next-state M
|
||||||
|
((= (car clause) ':next-state)
|
||||||
|
(progn
|
||||||
|
(setq next-state (cadr clause))
|
||||||
|
(setq clause (cdr clause))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(setq clause (cdr clause))
|
||||||
|
)
|
||||||
|
`(red/syntax/define-regex-rule ,syntax ,prev-state ,regex ,category ,next-state)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(defun _red/syntax-style-category (syntax clause)
|
||||||
|
;; ("CATEGORY" :foreground 'COLOR :background 'COLOR :bold)
|
||||||
|
(let (category (car clause) foreground nil background nil bold nil)
|
||||||
|
(setq clause (cdr clause))
|
||||||
|
(while (not (nil? clause))
|
||||||
|
(cond
|
||||||
|
; :foreground 'COLOR
|
||||||
|
((or (= (car clause) ':foreground) (= (car clause) ':fg))
|
||||||
|
(progn
|
||||||
|
(setq foreground (cadr clause))
|
||||||
|
(setq clause (cdr clause))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
; :background 'COLOR
|
||||||
|
((or (= (car clause) ':background) (= (car clause) ':bg))
|
||||||
|
(progn
|
||||||
|
(setq background (cadr clause))
|
||||||
|
(setq clause (cdr clause))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
; :bold
|
||||||
|
((= (car clause) ':bold) (setq bold #t))
|
||||||
|
)
|
||||||
|
(setq clause (cdr clause))
|
||||||
|
)
|
||||||
|
(when (not (or foreground background bold))
|
||||||
|
(error "Empty clause")
|
||||||
|
)
|
||||||
|
`(red/syntax/define-category-style ,syntax ,category ,foreground ,background ,bold)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(defun _red/define-syntax (syntax clauses)
|
||||||
|
(let (output nil current-state nil)
|
||||||
|
(while (cons? clauses)
|
||||||
|
(let (clause (car clauses))
|
||||||
|
(cond
|
||||||
|
((= clause ':styles) (setq current-state 'styles))
|
||||||
|
((= clause ':rules) (setq current-state 'rules))
|
||||||
|
;; ("style" ...) clauses
|
||||||
|
((= current-state 'styles) (setq output (cons (_red/syntax-style-category syntax clause) output)))
|
||||||
|
;; (:regex ...) clauses
|
||||||
|
((and
|
||||||
|
(= current-state 'rules)
|
||||||
|
(cons? clause)
|
||||||
|
(= (car clause) ':regex)
|
||||||
|
(cons? (cdr clause))
|
||||||
|
)
|
||||||
|
(setq output (cons (_red/syntax-rule-regex syntax (cdr clause)) output))
|
||||||
|
)
|
||||||
|
;; (:keyword ...) clauses
|
||||||
|
((and
|
||||||
|
(= current-state 'rules)
|
||||||
|
(cons? clause)
|
||||||
|
(= (car clause) ':keyword)
|
||||||
|
(cons? (cdr clause))
|
||||||
|
)
|
||||||
|
(setq output (cons (_red/syntax-rule-keyword syntax (cdr clause)) output))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(setq clauses (cdr clauses))
|
||||||
|
)
|
||||||
|
(if (nil? output)
|
||||||
|
nil
|
||||||
|
(cons 'progn output)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(defmacro define-syntax (syntax &rest clauses)
|
||||||
|
(_red/define-syntax syntax clauses)
|
||||||
|
)
|
||||||
|
|
||||||
|
;; Load syntax files
|
||||||
|
;; TODO glob
|
||||||
|
(import "syntax/lysp.lysp")
|
||||||
|
(import "syntax/rust.lysp")
|
||||||
|
|
||||||
|
(red/syntax/reset)
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
(declare-key normal '(z z)
|
||||||
|
(when (red/buffer/path)
|
||||||
|
(red/buffer/write)
|
||||||
|
)
|
||||||
|
(red/quit)
|
||||||
|
)
|
||||||
|
(declare-key normal '(Z Z) (red/quit #t))
|
||||||
|
|
||||||
|
(declare-key normal 'a (red/buffer/set-mode 'insert-after))
|
||||||
|
(declare-key normal 'i (red/buffer/set-mode 'insert-before))
|
||||||
|
(declare-key normal 'I
|
||||||
|
(red/buffer/move 'line-start)
|
||||||
|
(red/buffer/set-mode 'insert-before))
|
||||||
|
(declare-key normal 'A
|
||||||
|
(red/buffer/move 'line-end)
|
||||||
|
(red/buffer/set-mode 'insert-after))
|
||||||
|
|
||||||
|
(declare-key normal '(g g) (red/buffer/move 'first-line))
|
||||||
|
(declare-key normal 'G (red/buffer/move 'last-line))
|
||||||
|
|
||||||
|
(declare-key normal 'h (red/buffer/move 'prev-char))
|
||||||
|
(declare-key normal 'l (red/buffer/move 'next-char))
|
||||||
|
(declare-key normal 'k (red/buffer/move 'prev-line))
|
||||||
|
(declare-key normal 'j (red/buffer/move 'next-line))
|
||||||
|
(declare-key normal 'J (red/buffer/move 'next-page))
|
||||||
|
(declare-key normal 'K (red/buffer/move 'prev-page))
|
||||||
|
(declare-key normal 'o
|
||||||
|
(red/buffer/insert-line-after)
|
||||||
|
(red/buffer/move 'next-line)
|
||||||
|
(red/buffer/set-mode 'insert-before))
|
||||||
|
(declare-key normal 'O
|
||||||
|
(red/buffer/insert-line-before)
|
||||||
|
(red/buffer/set-mode 'insert-before))
|
||||||
|
(declare-key normal '(d d)
|
||||||
|
(red/buffer/kill-line)
|
||||||
|
(red/buffer/move 'prev-line))
|
||||||
|
(declare-key normal 'newline
|
||||||
|
(red/buffer/move 'next-line)
|
||||||
|
(red/buffer/move 'line-start))
|
||||||
|
|
||||||
|
(declare-key normal 'left (red/buffer/move 'prev-char))
|
||||||
|
(declare-key normal 'right (red/buffer/move 'next-char))
|
||||||
|
(declare-key normal 'up (red/buffer/move 'prev-line))
|
||||||
|
(declare-key normal 'down (red/buffer/move 'next-line))
|
||||||
|
(declare-key normal 'home (red/buffer/move 'line-start))
|
||||||
|
(declare-key normal 'end (red/buffer/move 'line-end))
|
||||||
|
|
||||||
|
(declare-key insert 'left (red/buffer/move 'prev-char))
|
||||||
|
(declare-key insert 'right (red/buffer/move 'next-char))
|
||||||
|
(declare-key insert 'up (red/buffer/move 'prev-line))
|
||||||
|
(declare-key insert 'down (red/buffer/move 'next-line))
|
||||||
|
(declare-key insert 'home (red/buffer/move 'line-start))
|
||||||
|
(declare-key insert 'end (red/buffer/move 'line-end))
|
||||||
|
(declare-key insert 'backspace (red/buffer/erase-backward))
|
||||||
|
(declare-key insert 'newline
|
||||||
|
(red/buffer/insert-line-after #t)
|
||||||
|
(red/buffer/move 'next-line)
|
||||||
|
(red/buffer/move 'line-start)
|
||||||
|
(red/buffer/set-mode 'insert-before)
|
||||||
|
)
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
;; Test expression for syntax highlight
|
||||||
|
(ignore (let print 1234 0x1234 "a string" empty-string "" after-empty-string #t #F))
|
||||||
|
|
||||||
|
(define-syntax
|
||||||
|
"lysp"
|
||||||
|
:styles
|
||||||
|
("keyword" :fg 'red)
|
||||||
|
("symbol" :fg 'cyan)
|
||||||
|
("comment" :fg 'white :bold)
|
||||||
|
("string" :fg 'yellow)
|
||||||
|
("custom-syntax" :fg 'red :bold)
|
||||||
|
("number" :fg 'cyan :bold)
|
||||||
|
("constant" :fg 'cyan :bold)
|
||||||
|
:rules
|
||||||
|
;; keywords
|
||||||
|
(:keyword
|
||||||
|
"let" "let*" "progn" "defun" "defmacro" "setq"
|
||||||
|
"when" "unless" "if" "while" "loop" "&optional"
|
||||||
|
"&rest" "&otherwise" "cond" "nil" "lambda" "ignore"
|
||||||
|
:category "keyword"
|
||||||
|
)
|
||||||
|
;; prelude functions
|
||||||
|
(:keyword
|
||||||
|
"car" "cdr" "caar" "cadr" "cdar" "cddr" "cadar"
|
||||||
|
"caddr" "list?" "nil?" "cons?" "string?" "symbol?"
|
||||||
|
"cons" "list" "print" "not" "or" "and" "=" ">="
|
||||||
|
"<=" "/=" "error" "import" "apply" "find"
|
||||||
|
:category "symbol"
|
||||||
|
)
|
||||||
|
(:keyword
|
||||||
|
"#t" "#T" "#f" "#F"
|
||||||
|
:category "constant"
|
||||||
|
)
|
||||||
|
;; strings
|
||||||
|
(:regex "." "string" :prev-state 1)
|
||||||
|
(:regex "[^\\\\]\"" "string" :prev-state 1 :next-state 0)
|
||||||
|
(:regex "\"" "string" :next-state 1)
|
||||||
|
(:regex "\"\"" "string")
|
||||||
|
;; :keywords
|
||||||
|
(:regex ":[\\-\\w]+" "custom-syntax")
|
||||||
|
;; numbers
|
||||||
|
(:regex "(0x|0o|0b)?\\d+" "number")
|
||||||
|
;; comments
|
||||||
|
(:regex ";.*$" "comment")
|
||||||
|
)
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
;; TODO very WIP
|
||||||
|
(define-syntax
|
||||||
|
"rust"
|
||||||
|
:styles
|
||||||
|
("keyword" :fg 'red)
|
||||||
|
("symbol" :fg 'cyan)
|
||||||
|
("comment" :fg 'white :bold)
|
||||||
|
("string" :fg 'yellow)
|
||||||
|
("number" :fg 'cyan :bold)
|
||||||
|
("constant" :fg 'cyan :bold)
|
||||||
|
:rules
|
||||||
|
;; Keywords
|
||||||
|
(:keyword
|
||||||
|
"use" "pub" "mod" "enum" "struct" "impl"
|
||||||
|
"for" "=>" "->" "match" "for" "while" "loop"
|
||||||
|
"if" "fn" "&mut" "&" "let" "where" "?"
|
||||||
|
:category "keyword"
|
||||||
|
)
|
||||||
|
;; Identifiers
|
||||||
|
(:keyword
|
||||||
|
"Rc" "RefCell" "Clone" "Copy" "PartialEq" "Eq"
|
||||||
|
"PartialOrd" "Ord" "Debug" "Result" "Option"
|
||||||
|
"bool" "i8" "i16" "i32" "i64" "i128" "u8" "u16"
|
||||||
|
"u32" "u64" "u128" "str" "&str"
|
||||||
|
"Self" "self" "&self" "&mut self" "Ok" "Err"
|
||||||
|
:category "symbol"
|
||||||
|
)
|
||||||
|
(:keyword "true" "false" :category "constant")
|
||||||
|
;; strings
|
||||||
|
(:regex "." "string" :prev-state 1)
|
||||||
|
(:regex "[^\\\\]*\"" "string" :prev-state 1 :next-state 0)
|
||||||
|
(:regex "\"" "string" :next-state 1)
|
||||||
|
;; numbers
|
||||||
|
(:regex "(0x|0o|0b)?\\d+" "number")
|
||||||
|
;; Comments
|
||||||
|
(:regex "//.*$" "comment")
|
||||||
|
)
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
use std::{
|
||||||
|
fmt,
|
||||||
|
ops::Index,
|
||||||
|
slice::{self, SliceIndex},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
highlight::LineHighlight,
|
||||||
|
text::{Span, Text, TextLike, TextLikeMut},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Line {
|
||||||
|
pub text: Text,
|
||||||
|
pub highlight: LineHighlight,
|
||||||
|
pub highlight_dirty: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Line {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::from(Text::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.text.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.text.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextLike for Line {
|
||||||
|
type Iter<'a>
|
||||||
|
= slice::Iter<'a, char>
|
||||||
|
where
|
||||||
|
Self: 'a;
|
||||||
|
type Span<'a>
|
||||||
|
= Span<'a>
|
||||||
|
where
|
||||||
|
Self: 'a;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn display_width(&self, tab_width: usize) -> usize {
|
||||||
|
self.text.display_width(tab_width)
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
fn iter(&self) -> Self::Iter<'_> {
|
||||||
|
self.text.iter()
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
fn span<R: SliceIndex<[char], Output = [char]>>(&self, range: R) -> Self::Span<'_> {
|
||||||
|
self.text.span(range)
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
fn skip_to_width(&self, offset: usize, tab_width: usize) -> (Self::Span<'_>, usize, usize) {
|
||||||
|
self.text.skip_to_width(offset, tab_width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextLikeMut for Line {
|
||||||
|
fn split_off(&mut self, at: usize) -> Self {
|
||||||
|
let tail = self.text.split_off(at);
|
||||||
|
self.highlight_dirty = true;
|
||||||
|
Self::from(tail)
|
||||||
|
}
|
||||||
|
fn extend(&mut self, other: Self) {
|
||||||
|
self.text.extend(other.text);
|
||||||
|
self.highlight_dirty = true;
|
||||||
|
}
|
||||||
|
fn remove(&mut self, at: usize) {
|
||||||
|
self.text.remove(at);
|
||||||
|
self.highlight_dirty = true;
|
||||||
|
}
|
||||||
|
fn insert(&mut self, at: usize, ch: char) {
|
||||||
|
self.text.insert(at, ch);
|
||||||
|
self.highlight_dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Text> for Line {
|
||||||
|
fn from(text: Text) -> Self {
|
||||||
|
Self {
|
||||||
|
text,
|
||||||
|
highlight: LineHighlight::default(),
|
||||||
|
highlight_dirty: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Index<usize> for Line {
|
||||||
|
type Output = char;
|
||||||
|
|
||||||
|
fn index(&self, index: usize) -> &Self::Output {
|
||||||
|
&self.text[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Line {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
fmt::Display::fmt(&self.text, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,17 +4,23 @@ use std::{
|
|||||||
fs::File,
|
fs::File,
|
||||||
io::{self, BufRead, BufReader, BufWriter, Write as IoWrite},
|
io::{self, BufRead, BufReader, BufWriter, Write as IoWrite},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
rc::Rc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use libterm::{Color, CursorStyle, Term};
|
use libterm::{Color, CursorStyle, Term};
|
||||||
use unicode_width::UnicodeWidthChar;
|
use unicode_width::UnicodeWidthChar;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
buffer::{line::Line, style::Style},
|
||||||
|
config::EditorConfig,
|
||||||
error::Error,
|
error::Error,
|
||||||
line::{Line, TextLike},
|
highlight::Highlighter,
|
||||||
|
text::{Text, TextLike, TextLikeMut},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub mod line;
|
||||||
|
pub mod style;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct View {
|
pub struct View {
|
||||||
cursor_column: usize,
|
cursor_column: usize,
|
||||||
@@ -47,6 +53,7 @@ pub struct Buffer {
|
|||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
path: Option<PathBuf>,
|
path: Option<PathBuf>,
|
||||||
modified: bool,
|
modified: bool,
|
||||||
|
filetype: Rc<str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mode {
|
impl Mode {
|
||||||
@@ -69,7 +76,7 @@ impl View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_column(&mut self, config: &Config, col: usize, line: Option<&Line>) {
|
pub fn set_column(&mut self, config: &EditorConfig, col: usize, line: Option<&Line>) {
|
||||||
let Some(line) = line else {
|
let Some(line) = line else {
|
||||||
self.column_offset = 0;
|
self.column_offset = 0;
|
||||||
self.cursor_column = 0;
|
self.cursor_column = 0;
|
||||||
@@ -121,6 +128,7 @@ impl Buffer {
|
|||||||
name: None,
|
name: None,
|
||||||
path: None,
|
path: None,
|
||||||
modified: false,
|
modified: false,
|
||||||
|
filetype: "text".into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +137,8 @@ impl Buffer {
|
|||||||
let lines = input.lines().collect::<Result<Vec<_>, _>>()?;
|
let lines = input.lines().collect::<Result<Vec<_>, _>>()?;
|
||||||
let lines = lines
|
let lines = lines
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|line| Line::from_str(line.trim_end_matches('\n')))
|
.map(|line| Text::from_str(line.trim_end_matches('\n')))
|
||||||
|
.map(Line::from)
|
||||||
.collect();
|
.collect();
|
||||||
Ok(lines)
|
Ok(lines)
|
||||||
}
|
}
|
||||||
@@ -143,7 +152,7 @@ impl Buffer {
|
|||||||
vec![]
|
vec![]
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self {
|
let mut this = Self {
|
||||||
lines,
|
lines,
|
||||||
name,
|
name,
|
||||||
path: Some(path.into()),
|
path: Some(path.into()),
|
||||||
@@ -152,7 +161,13 @@ impl Buffer {
|
|||||||
mode_dirty: true,
|
mode_dirty: true,
|
||||||
view: View::default(),
|
view: View::default(),
|
||||||
modified: false,
|
modified: false,
|
||||||
})
|
filetype: "text".into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.update_filetype();
|
||||||
|
this.reset_highlight();
|
||||||
|
|
||||||
|
Ok(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reopen<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
pub fn reopen<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
||||||
@@ -173,10 +188,34 @@ impl Buffer {
|
|||||||
|
|
||||||
self.path = Some(path.into());
|
self.path = Some(path.into());
|
||||||
self.name = name;
|
self.name = name;
|
||||||
|
self.update_filetype();
|
||||||
|
self.reset_highlight();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_filetype(&mut self) {
|
||||||
|
let extension = self
|
||||||
|
.path
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|path| path.extension())
|
||||||
|
.and_then(|ext| ext.to_str());
|
||||||
|
|
||||||
|
let filetype = match extension {
|
||||||
|
Some("lysp") => "lysp",
|
||||||
|
Some("rs") => "rust",
|
||||||
|
_ => "text",
|
||||||
|
};
|
||||||
|
|
||||||
|
self.filetype = filetype.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_highlight(&mut self) {
|
||||||
|
for line in self.lines.iter_mut() {
|
||||||
|
line.highlight_dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn save(&mut self) -> Result<(), Error> {
|
pub fn save(&mut self) -> Result<(), Error> {
|
||||||
let path = self.path.as_ref().ok_or(Error::NoPath)?;
|
let path = self.path.as_ref().ok_or(Error::NoPath)?;
|
||||||
let mut writer = BufWriter::new(File::create(path).map_err(Error::WriteError)?);
|
let mut writer = BufWriter::new(File::create(path).map_err(Error::WriteError)?);
|
||||||
@@ -201,7 +240,7 @@ impl Buffer {
|
|||||||
self.name = name;
|
self.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_mode(&mut self, config: &Config, mode: SetMode) {
|
pub fn set_mode(&mut self, config: &EditorConfig, mode: SetMode) {
|
||||||
let dst_mode = match mode {
|
let dst_mode = match mode {
|
||||||
SetMode::Normal => Mode::Normal,
|
SetMode::Normal => Mode::Normal,
|
||||||
SetMode::InsertAfter | SetMode::InsertBefore => Mode::Insert,
|
SetMode::InsertAfter | SetMode::InsertBefore => Mode::Insert,
|
||||||
@@ -220,6 +259,10 @@ impl Buffer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn filetype(&self) -> &str {
|
||||||
|
&self.filetype
|
||||||
|
}
|
||||||
|
|
||||||
pub fn mode(&self) -> Mode {
|
pub fn mode(&self) -> Mode {
|
||||||
self.mode
|
self.mode
|
||||||
}
|
}
|
||||||
@@ -248,7 +291,7 @@ impl Buffer {
|
|||||||
self.view.cursor_row
|
self.view.cursor_row
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_position(&mut self, config: &Config, px: usize, py: usize) {
|
pub fn set_position(&mut self, config: &EditorConfig, px: usize, py: usize) {
|
||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
|
|
||||||
if self.lines.is_empty() {
|
if self.lines.is_empty() {
|
||||||
@@ -275,7 +318,7 @@ impl Buffer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_line_end(&mut self, config: &Config) {
|
pub fn to_line_end(&mut self, config: &EditorConfig) {
|
||||||
let len = self
|
let len = self
|
||||||
.lines
|
.lines
|
||||||
.get(self.view.cursor_row)
|
.get(self.view.cursor_row)
|
||||||
@@ -285,7 +328,7 @@ impl Buffer {
|
|||||||
self.set_position(config, len, self.view.cursor_row);
|
self.set_position(config, len, self.view.cursor_row);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_first_line(&mut self, config: &Config) {
|
pub fn to_first_line(&mut self, config: &EditorConfig) {
|
||||||
let len = self
|
let len = self
|
||||||
.lines
|
.lines
|
||||||
.get(self.view.cursor_row)
|
.get(self.view.cursor_row)
|
||||||
@@ -295,7 +338,7 @@ impl Buffer {
|
|||||||
self.set_position(config, len, 0);
|
self.set_position(config, len, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_last_line(&mut self, config: &Config) {
|
pub fn to_last_line(&mut self, config: &EditorConfig) {
|
||||||
if self.lines.is_empty() {
|
if self.lines.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -309,18 +352,18 @@ impl Buffer {
|
|||||||
self.set_position(config, len, self.lines.len() - 1);
|
self.set_position(config, len, self.lines.len() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_column(&mut self, config: &Config, x: usize) {
|
pub fn set_column(&mut self, config: &EditorConfig, x: usize) {
|
||||||
self.set_position(config, x, self.view.cursor_row);
|
self.set_position(config, x, self.view.cursor_row);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_cursor(&mut self, config: &Config, dx: isize, dy: isize) {
|
pub fn move_cursor(&mut self, config: &EditorConfig, dx: isize, dy: isize) {
|
||||||
let px = (self.view.cursor_column as isize + dx).max(0) as usize;
|
let px = (self.view.cursor_column as isize + dx).max(0) as usize;
|
||||||
let py = (self.view.cursor_row as isize + dy).max(0) as usize;
|
let py = (self.view.cursor_row as isize + dy).max(0) as usize;
|
||||||
|
|
||||||
self.set_position(config, px, py);
|
self.set_position(config, px, py);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resize(&mut self, config: &Config, offset_x: usize, width: usize, height: usize) {
|
pub fn resize(&mut self, config: &EditorConfig, offset_x: usize, width: usize, height: usize) {
|
||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
self.view.height = height;
|
self.view.height = height;
|
||||||
self.view.width = width;
|
self.view.width = width;
|
||||||
@@ -334,7 +377,7 @@ impl Buffer {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn display_cursor(&self, config: &Config) -> (usize, usize) {
|
pub fn display_cursor(&self, config: &EditorConfig) -> (usize, usize) {
|
||||||
if self.lines.is_empty() {
|
if self.lines.is_empty() {
|
||||||
return (0, 0);
|
return (0, 0);
|
||||||
}
|
}
|
||||||
@@ -374,23 +417,56 @@ impl Buffer {
|
|||||||
|
|
||||||
fn display_line(
|
fn display_line(
|
||||||
&self,
|
&self,
|
||||||
config: &Config,
|
config: &EditorConfig,
|
||||||
|
current_style: &mut Style,
|
||||||
term: &mut Term,
|
term: &mut Term,
|
||||||
row: usize,
|
row: usize,
|
||||||
line: &Line,
|
line: &Line,
|
||||||
|
hi: &Highlighter,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut pos = 0;
|
let mut pos = 0;
|
||||||
term.set_cursor_position(row, self.view.offset_x)
|
term.set_cursor_position(row, self.view.offset_x)
|
||||||
.map_err(Error::TerminalError)?;
|
.map_err(Error::TerminalError)?;
|
||||||
|
|
||||||
let span = line.skip_to_width(self.view.column_offset, config.tab_width);
|
let (span, _skipped_display_cells, skipped_characters) =
|
||||||
|
line.skip_to_width(self.view.column_offset, config.tab_width);
|
||||||
let long_line = span.display_width(config.tab_width) > self.view.width;
|
let long_line = span.display_width(config.tab_width) > self.view.width;
|
||||||
|
|
||||||
for &ch in span.iter() {
|
let mut current_category: Option<&str> = None;
|
||||||
|
|
||||||
|
// Reset style
|
||||||
|
let reset_style = Style::default();
|
||||||
|
reset_style
|
||||||
|
.apply_delta(current_style, term)
|
||||||
|
.map_err(Error::TerminalError)?;
|
||||||
|
*current_style = reset_style;
|
||||||
|
|
||||||
|
for (index, &ch) in span.iter().enumerate() {
|
||||||
if pos >= self.view.width {
|
if pos >= self.view.width {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let character_index = skipped_characters + index;
|
||||||
|
|
||||||
|
let matching_token = line
|
||||||
|
.highlight
|
||||||
|
.tokens
|
||||||
|
.iter()
|
||||||
|
.find(|t| t.range.contains(&character_index));
|
||||||
|
let token_category = matching_token.map(|t| t.category.as_ref());
|
||||||
|
|
||||||
|
if token_category != current_category {
|
||||||
|
current_category = token_category;
|
||||||
|
let style = token_category
|
||||||
|
.and_then(|cat| hi.stylize(&self.filetype, cat, current_style))
|
||||||
|
.unwrap_or_else(Style::default);
|
||||||
|
|
||||||
|
style
|
||||||
|
.apply_delta(current_style, term)
|
||||||
|
.map_err(Error::TerminalError)?;
|
||||||
|
*current_style = style;
|
||||||
|
}
|
||||||
|
|
||||||
if ch == '\t' {
|
if ch == '\t' {
|
||||||
let old_pos = pos;
|
let old_pos = pos;
|
||||||
let new_pos = (pos + config.tab_width) & !(config.tab_width - 1);
|
let new_pos = (pos + config.tab_width) & !(config.tab_width - 1);
|
||||||
@@ -406,6 +482,7 @@ impl Buffer {
|
|||||||
term.write_char('>').map_err(Error::TerminalFmtError)?;
|
term.write_char('>').map_err(Error::TerminalFmtError)?;
|
||||||
term.set_foreground(Color::Default)
|
term.set_foreground(Color::Default)
|
||||||
.map_err(Error::TerminalError)?;
|
.map_err(Error::TerminalError)?;
|
||||||
|
current_style.foreground = Color::Default;
|
||||||
} else {
|
} else {
|
||||||
term.write_char(' ').map_err(Error::TerminalFmtError)?;
|
term.write_char(' ').map_err(Error::TerminalFmtError)?;
|
||||||
}
|
}
|
||||||
@@ -433,7 +510,14 @@ impl Buffer {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn display(&mut self, config: &Config, term: &mut Term) -> Result<(), Error> {
|
pub fn display(
|
||||||
|
&mut self,
|
||||||
|
config: &EditorConfig,
|
||||||
|
term: &mut Term,
|
||||||
|
hi: &Highlighter,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
hi.rehighlight(&mut self.lines, &self.filetype);
|
||||||
|
|
||||||
match self.mode {
|
match self.mode {
|
||||||
Mode::Normal => {
|
Mode::Normal => {
|
||||||
term.set_cursor_style(CursorStyle::Default)
|
term.set_cursor_style(CursorStyle::Default)
|
||||||
@@ -445,6 +529,8 @@ impl Buffer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut current_style = Style::default();
|
||||||
|
|
||||||
for (row, line) in self
|
for (row, line) in self
|
||||||
.lines
|
.lines
|
||||||
.iter()
|
.iter()
|
||||||
@@ -452,13 +538,27 @@ impl Buffer {
|
|||||||
.take(self.view.height)
|
.take(self.view.height)
|
||||||
.enumerate()
|
.enumerate()
|
||||||
{
|
{
|
||||||
self.display_line(config, term, row, line)?;
|
self.display_line(config, &mut current_style, term, row, line, hi)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset to default style
|
||||||
|
Style::DEFAULT
|
||||||
|
.apply_delta(¤t_style, term)
|
||||||
|
.map_err(Error::TerminalError)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_terminal_cursor(&mut self, config: &Config, term: &mut Term) -> Result<(), Error> {
|
pub fn get_terminal_cursor(&self, config: &EditorConfig) -> (usize, usize) {
|
||||||
|
let (x, y) = self.display_cursor(config);
|
||||||
|
(x + self.view.offset_x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_terminal_cursor(
|
||||||
|
&mut self,
|
||||||
|
config: &EditorConfig,
|
||||||
|
term: &mut Term,
|
||||||
|
) -> Result<(), Error> {
|
||||||
let (x, y) = self.display_cursor(config);
|
let (x, y) = self.display_cursor(config);
|
||||||
if self.mode_dirty {
|
if self.mode_dirty {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
@@ -497,7 +597,7 @@ impl Buffer {
|
|||||||
self.lines.insert(self.view.cursor_row + 1, newline);
|
self.lines.insert(self.view.cursor_row + 1, newline);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert(&mut self, config: &Config, ch: char) {
|
pub fn insert(&mut self, config: &EditorConfig, ch: char) {
|
||||||
if self.lines.is_empty() {
|
if self.lines.is_empty() {
|
||||||
assert_eq!(self.view.cursor_row, 0);
|
assert_eq!(self.view.cursor_row, 0);
|
||||||
self.lines.push(Line::new());
|
self.lines.push(Line::new());
|
||||||
@@ -509,7 +609,7 @@ impl Buffer {
|
|||||||
self.modified = true;
|
self.modified = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn erase_backward(&mut self, config: &Config) {
|
pub fn erase_backward(&mut self, config: &EditorConfig) {
|
||||||
if self.lines.is_empty() {
|
if self.lines.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -547,12 +647,15 @@ impl Buffer {
|
|||||||
self.modified = true;
|
self.modified = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn kill_line(&mut self, config: &Config) {
|
pub fn kill_line(&mut self, config: &EditorConfig) {
|
||||||
if self.lines.is_empty() {
|
if self.lines.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.lines.remove(self.view.cursor_row);
|
self.lines.remove(self.view.cursor_row);
|
||||||
|
if self.view.cursor_row < self.lines.len() {
|
||||||
|
self.lines[self.view.cursor_row].highlight_dirty = true;
|
||||||
|
}
|
||||||
self.move_cursor(config, 0, 1);
|
self.move_cursor(config, 0, 1);
|
||||||
self.modified = true;
|
self.modified = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
use std::io;
|
||||||
|
|
||||||
|
use libterm::{Color, Term};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct Style {
|
||||||
|
pub foreground: Color,
|
||||||
|
pub background: Color,
|
||||||
|
pub bold: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Style {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::DEFAULT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Style {
|
||||||
|
pub const DEFAULT: Self = Self {
|
||||||
|
foreground: Color::White,
|
||||||
|
background: Color::Black,
|
||||||
|
bold: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn apply(&self, term: &mut Term) -> Result<(), io::Error> {
|
||||||
|
self.apply_delta(&Self::DEFAULT, term)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_delta(&self, original: &Self, term: &mut Term) -> Result<(), io::Error> {
|
||||||
|
// TODO libterm Color PartialEq
|
||||||
|
if self.foreground as u32 != original.foreground as u32 {
|
||||||
|
// Apply foreground
|
||||||
|
term.set_foreground(self.foreground)?;
|
||||||
|
}
|
||||||
|
if self.background as u32 != original.background as u32 {
|
||||||
|
// Apply background
|
||||||
|
term.set_background(self.background)?;
|
||||||
|
}
|
||||||
|
if self.bold != original.bold {
|
||||||
|
// Apply bold
|
||||||
|
term.set_bright(self.bold)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,144 +1,20 @@
|
|||||||
use std::ops::RangeInclusive;
|
use lysp::vm::value::convert::AnyFunction;
|
||||||
|
|
||||||
use crate::{
|
use crate::{State, error::Error};
|
||||||
State,
|
|
||||||
buffer::{Buffer, SetMode},
|
|
||||||
config::Config,
|
|
||||||
error::Error,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub type CommandFn = fn(&mut State, &[&str]) -> Result<(), Error>;
|
pub type CommandFn = fn(&mut State, &[&str]) -> Result<(), Error>;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
// Editing
|
Handler(AnyFunction),
|
||||||
EraseBackward,
|
Command(String),
|
||||||
InsertBefore,
|
None,
|
||||||
InsertAfter,
|
|
||||||
NewlineBefore,
|
|
||||||
NewlineAfter,
|
|
||||||
BreakLine,
|
|
||||||
KillLine,
|
|
||||||
|
|
||||||
// Movement
|
|
||||||
MoveFirstLine,
|
|
||||||
MoveLastLine,
|
|
||||||
MoveCharPrev,
|
|
||||||
MoveCharNext,
|
|
||||||
MoveLineBack(usize),
|
|
||||||
MoveLineForward(usize),
|
|
||||||
MoveLineStart,
|
|
||||||
MoveLineEnd,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static COMMANDS: &[(&str, RangeInclusive<usize>, CommandFn)] = &[
|
impl From<Option<AnyFunction>> for Action {
|
||||||
("w", 0..=1, cmd_write),
|
fn from(value: Option<AnyFunction>) -> Self {
|
||||||
("w!", 0..=1, cmd_force_write),
|
match value {
|
||||||
("q", 0..=0, cmd_exit),
|
Some(handler) => Self::Handler(handler),
|
||||||
("q!", 0..=0, cmd_force_exit),
|
None => Self::None,
|
||||||
("e", 1..=1, cmd_edit),
|
|
||||||
("e!", 0..=1, cmd_force_edit),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Commands
|
|
||||||
fn cmd_write(state: &mut State, args: &[&str]) -> Result<(), Error> {
|
|
||||||
if args.len() == 1 && state.buffer().is_modified() && state.buffer().path().is_some() {
|
|
||||||
return Err(Error::UnsavedBuffer(
|
|
||||||
"Use :w! FILE to force write to another file",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd_force_write(state, args)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cmd_force_write(state: &mut State, args: &[&str]) -> Result<(), Error> {
|
|
||||||
let buffer = state.buffer_mut();
|
|
||||||
if let Some(&path) = args.first() {
|
|
||||||
buffer.set_path(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.save()?;
|
|
||||||
|
|
||||||
if let Some(name) = buffer.name() {
|
|
||||||
let status = format!("{:?} written", name);
|
|
||||||
state.set_status(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cmd_edit(state: &mut State, args: &[&str]) -> Result<(), Error> {
|
|
||||||
if state.buffer().is_modified() {
|
|
||||||
return Err(Error::UnsavedBuffer("Use :e! [FILE] to open another file"));
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd_force_edit(state, args)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cmd_force_edit(state: &mut State, args: &[&str]) -> Result<(), Error> {
|
|
||||||
if let Some(&path) = args.first() {
|
|
||||||
state.buffer_mut().reopen(path).map_err(Error::OpenError)
|
|
||||||
} else if let Some(path) = state.buffer().path().cloned() {
|
|
||||||
state.buffer_mut().reopen(path).map_err(Error::OpenError)
|
|
||||||
} else {
|
|
||||||
Err(Error::NoPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cmd_exit(state: &mut State, _args: &[&str]) -> Result<(), Error> {
|
|
||||||
let buffer = state.buffer();
|
|
||||||
|
|
||||||
if buffer.is_modified() {
|
|
||||||
return Err(Error::UnsavedBuffer("Use :q! to force exit"));
|
|
||||||
}
|
|
||||||
|
|
||||||
state.exit();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cmd_force_exit(state: &mut State, _args: &[&str]) -> Result<(), Error> {
|
|
||||||
state.exit();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn execute(state: &mut State, command: String) -> Result<(), Error> {
|
|
||||||
let words = command.split(' ').collect::<Vec<_>>();
|
|
||||||
let Some((&cmd, args)) = words.split_first() else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
for (name, nargs, f) in COMMANDS.iter() {
|
|
||||||
if *name == cmd {
|
|
||||||
if !nargs.contains(&args.len()) {
|
|
||||||
todo!();
|
|
||||||
}
|
|
||||||
|
|
||||||
return f(state, args);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(Error::UnknownCommand(cmd.into()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn perform(buffer: &mut Buffer, config: &Config, action: Action) -> Result<(), Error> {
|
|
||||||
match action {
|
|
||||||
// Editing
|
|
||||||
Action::EraseBackward => buffer.erase_backward(config),
|
|
||||||
Action::InsertBefore => buffer.set_mode(config, SetMode::InsertBefore),
|
|
||||||
Action::InsertAfter => buffer.set_mode(config, SetMode::InsertAfter),
|
|
||||||
Action::NewlineBefore => buffer.newline_before(),
|
|
||||||
Action::NewlineAfter => buffer.newline_after(false),
|
|
||||||
Action::BreakLine => buffer.newline_after(true),
|
|
||||||
Action::KillLine => buffer.kill_line(config),
|
|
||||||
// Movement
|
|
||||||
Action::MoveCharPrev => buffer.move_cursor(config, -1, 0),
|
|
||||||
Action::MoveCharNext => buffer.move_cursor(config, 1, 0),
|
|
||||||
Action::MoveLineBack(count) => buffer.move_cursor(config, 0, -(count as isize)),
|
|
||||||
Action::MoveLineForward(count) => buffer.move_cursor(config, 0, count as isize),
|
|
||||||
Action::MoveLineStart => buffer.set_column(config, 0),
|
|
||||||
Action::MoveLineEnd => buffer.to_line_end(config),
|
|
||||||
Action::MoveFirstLine => buffer.to_first_line(config),
|
|
||||||
Action::MoveLastLine => buffer.to_last_line(config),
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,80 +1,127 @@
|
|||||||
use libterm::TermKey;
|
use std::{
|
||||||
|
mem,
|
||||||
use crate::{
|
ops::{Deref, DerefMut},
|
||||||
buffer::Mode,
|
|
||||||
command::Action,
|
|
||||||
keymap::{KeyMap, KeySeq, PrefixNode, bind1, bindn},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Config {
|
pub struct EditorConfig {
|
||||||
// TODO must be a power of 2, lol
|
|
||||||
pub tab_width: usize,
|
pub tab_width: usize,
|
||||||
pub number: bool,
|
pub number: Dirty<bool>,
|
||||||
|
|
||||||
pub nmap: KeyMap,
|
|
||||||
pub imap: KeyMap,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
pub struct Dirty<T>(T, bool);
|
||||||
|
|
||||||
|
impl Default for EditorConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
use Action::*;
|
|
||||||
|
|
||||||
let nmap = KeyMap::from_iter([
|
|
||||||
bind1('i', [InsertBefore]),
|
|
||||||
bind1('a', [InsertAfter]),
|
|
||||||
bind1('h', [MoveCharPrev]),
|
|
||||||
bind1('l', [MoveCharNext]),
|
|
||||||
bind1('j', [MoveLineForward(1)]),
|
|
||||||
bind1('J', [MoveLineForward(25)]),
|
|
||||||
bind1('k', [MoveLineBack(1)]),
|
|
||||||
bind1('K', [MoveLineBack(25)]),
|
|
||||||
bind1(TermKey::Left, [MoveCharPrev]),
|
|
||||||
bind1(TermKey::Right, [MoveCharNext]),
|
|
||||||
bind1(TermKey::Up, [MoveLineBack(1)]),
|
|
||||||
bind1(TermKey::Down, [MoveLineForward(1)]),
|
|
||||||
bind1(TermKey::Home, [MoveLineStart]),
|
|
||||||
bind1(TermKey::End, [MoveLineEnd]),
|
|
||||||
bindn(['g', 'g'], [MoveFirstLine]),
|
|
||||||
bind1('G', [MoveLastLine]),
|
|
||||||
bind1('I', [MoveLineStart, InsertBefore]),
|
|
||||||
bind1('A', [MoveLineEnd, InsertAfter]),
|
|
||||||
bind1('o', [NewlineAfter, MoveLineForward(1), InsertBefore]),
|
|
||||||
bind1('O', [NewlineBefore, MoveLineBack(1), InsertBefore]),
|
|
||||||
bindn(['d', 'd'], [KillLine]),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let imap = KeyMap::from_iter([
|
|
||||||
bind1('\x7F', [EraseBackward]),
|
|
||||||
bind1(TermKey::Left, [MoveCharPrev]),
|
|
||||||
bind1(TermKey::Right, [MoveCharNext]),
|
|
||||||
bind1(TermKey::Up, [MoveLineBack(1)]),
|
|
||||||
bind1(TermKey::Down, [MoveLineForward(1)]),
|
|
||||||
bind1(TermKey::Home, [MoveLineStart]),
|
|
||||||
bind1(TermKey::End, [MoveLineEnd]),
|
|
||||||
bind1(
|
|
||||||
'\n',
|
|
||||||
[BreakLine, MoveLineForward(1), MoveLineStart, InsertBefore],
|
|
||||||
),
|
|
||||||
bind1(
|
|
||||||
'\x0D',
|
|
||||||
[BreakLine, MoveLineForward(1), MoveLineStart, InsertBefore],
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
tab_width: 4,
|
tab_width: 4,
|
||||||
number: true,
|
number: Dirty::new(false),
|
||||||
nmap,
|
|
||||||
imap,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl<T> Dirty<T> {
|
||||||
pub fn key_seq(&self, mode: Mode, seq: &KeySeq) -> Option<&PrefixNode<KeySeq, Vec<Action>>> {
|
pub const fn new(value: T) -> Self {
|
||||||
match mode {
|
Self(value, true)
|
||||||
Mode::Normal => self.nmap.get(seq),
|
}
|
||||||
Mode::Insert => self.imap.get(seq),
|
|
||||||
}
|
pub fn clean(&mut self) -> bool {
|
||||||
|
mem::replace(&mut self.1, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> Deref for Dirty<T> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> DerefMut for Dirty<T> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
self.1 = true;
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// use libterm::TermKey;
|
||||||
|
//
|
||||||
|
// use crate::{
|
||||||
|
// buffer::Mode,
|
||||||
|
// command::Action,
|
||||||
|
// keymap::{KeyMap, KeySeq, PrefixNode, bind1, bindn},
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// pub struct Config {
|
||||||
|
// // TODO must be a power of 2, lol
|
||||||
|
// pub tab_width: usize,
|
||||||
|
// pub number: bool,
|
||||||
|
//
|
||||||
|
// pub nmap: KeyMap,
|
||||||
|
// pub imap: KeyMap,
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// impl Default for Config {
|
||||||
|
// fn default() -> Self {
|
||||||
|
// use Action::*;
|
||||||
|
//
|
||||||
|
// let nmap = KeyMap::from_iter([
|
||||||
|
// bind1('i', [InsertBefore]),
|
||||||
|
// bind1('a', [InsertAfter]),
|
||||||
|
// bind1('h', [MoveCharPrev]),
|
||||||
|
// bind1('l', [MoveCharNext]),
|
||||||
|
// bind1('j', [MoveLineForward(1)]),
|
||||||
|
// bind1('J', [MoveLineForward(25)]),
|
||||||
|
// bind1('k', [MoveLineBack(1)]),
|
||||||
|
// bind1('K', [MoveLineBack(25)]),
|
||||||
|
// bind1(TermKey::Left, [MoveCharPrev]),
|
||||||
|
// bind1(TermKey::Right, [MoveCharNext]),
|
||||||
|
// bind1(TermKey::Up, [MoveLineBack(1)]),
|
||||||
|
// bind1(TermKey::Down, [MoveLineForward(1)]),
|
||||||
|
// bind1(TermKey::Home, [MoveLineStart]),
|
||||||
|
// bind1(TermKey::End, [MoveLineEnd]),
|
||||||
|
// bindn(['g', 'g'], [MoveFirstLine]),
|
||||||
|
// bind1('G', [MoveLastLine]),
|
||||||
|
// bind1('I', [MoveLineStart, InsertBefore]),
|
||||||
|
// bind1('A', [MoveLineEnd, InsertAfter]),
|
||||||
|
// bind1('o', [NewlineAfter, MoveLineForward(1), InsertBefore]),
|
||||||
|
// bind1('O', [NewlineBefore, MoveLineBack(1), InsertBefore]),
|
||||||
|
// bindn(['d', 'd'], [KillLine]),
|
||||||
|
// ]);
|
||||||
|
//
|
||||||
|
// let imap = KeyMap::from_iter([
|
||||||
|
// bind1('\x7F', [EraseBackward]),
|
||||||
|
// bind1(TermKey::Left, [MoveCharPrev]),
|
||||||
|
// bind1(TermKey::Right, [MoveCharNext]),
|
||||||
|
// bind1(TermKey::Up, [MoveLineBack(1)]),
|
||||||
|
// bind1(TermKey::Down, [MoveLineForward(1)]),
|
||||||
|
// bind1(TermKey::Home, [MoveLineStart]),
|
||||||
|
// bind1(TermKey::End, [MoveLineEnd]),
|
||||||
|
// bind1(
|
||||||
|
// '\n',
|
||||||
|
// [BreakLine, MoveLineForward(1), MoveLineStart, InsertBefore],
|
||||||
|
// ),
|
||||||
|
// bind1(
|
||||||
|
// '\x0D',
|
||||||
|
// [BreakLine, MoveLineForward(1), MoveLineStart, InsertBefore],
|
||||||
|
// ),
|
||||||
|
// ]);
|
||||||
|
//
|
||||||
|
// Self {
|
||||||
|
// tab_width: 4,
|
||||||
|
// number: true,
|
||||||
|
// nmap,
|
||||||
|
// imap,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// impl Config {
|
||||||
|
// pub fn key_seq(&self, mode: Mode, seq: &KeySeq) -> Option<&PrefixNode<KeySeq, Vec<Action>>> {
|
||||||
|
// match mode {
|
||||||
|
// Mode::Normal => self.nmap.get(seq),
|
||||||
|
// Mode::Insert => self.imap.get(seq),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use std::{fmt, io};
|
use std::{fmt, io};
|
||||||
|
|
||||||
|
use lysp::error::{MachineError, ValueConversionError};
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
// I/O errors
|
// I/O errors
|
||||||
@@ -19,4 +21,13 @@ pub enum Error {
|
|||||||
TerminalError(io::Error),
|
TerminalError(io::Error),
|
||||||
#[error("Terminal error: {0:?}")]
|
#[error("Terminal error: {0:?}")]
|
||||||
TerminalFmtError(fmt::Error),
|
TerminalFmtError(fmt::Error),
|
||||||
|
|
||||||
|
#[error("Scripting error: {0:?}")]
|
||||||
|
Script(#[from] MachineError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ValueConversionError> for Error {
|
||||||
|
fn from(value: ValueConversionError) -> Self {
|
||||||
|
Self::Script(value.into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,269 @@
|
|||||||
|
use std::{collections::HashMap, ops::Range, rc::Rc};
|
||||||
|
|
||||||
|
use libterm::Color;
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
|
use crate::buffer::{line::Line, style::Style};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Highlighter {
|
||||||
|
syntaxes: HashMap<Rc<str>, Syntax>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TokenSpan {
|
||||||
|
pub range: Range<usize>,
|
||||||
|
pub category: Rc<str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct LineHighlight {
|
||||||
|
pub start_state: u32,
|
||||||
|
pub end_state: u32,
|
||||||
|
pub tokens: Vec<TokenSpan>,
|
||||||
|
pub dirty: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SyntaxPattern {
|
||||||
|
Keyword(Rc<str>),
|
||||||
|
Regex(Regex),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SyntaxRule {
|
||||||
|
pattern: SyntaxPattern,
|
||||||
|
next_state: Option<u32>,
|
||||||
|
category: Rc<str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct SyntaxStyle {
|
||||||
|
pub foreground: Option<Color>,
|
||||||
|
pub background: Option<Color>,
|
||||||
|
pub bold: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Syntax {
|
||||||
|
state_rules: HashMap<u32, Vec<SyntaxRule>>,
|
||||||
|
style_rules: HashMap<Rc<str>, SyntaxStyle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SyntaxStyle {
|
||||||
|
pub fn apply(&self, base: &Style) -> Style {
|
||||||
|
let mut result = *base;
|
||||||
|
if let Some(foreground) = self.foreground {
|
||||||
|
result.foreground = foreground;
|
||||||
|
}
|
||||||
|
if let Some(background) = self.background {
|
||||||
|
result.background = background;
|
||||||
|
}
|
||||||
|
if let Some(bold) = self.bold {
|
||||||
|
result.bold = bold;
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SyntaxRule {
|
||||||
|
pub fn keyword<S: Into<Rc<str>>, C: Into<Rc<str>>>(
|
||||||
|
keyword: S,
|
||||||
|
next_state: Option<u32>,
|
||||||
|
category: C,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
pattern: SyntaxPattern::Keyword(keyword.into()),
|
||||||
|
next_state,
|
||||||
|
category: category.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn regex<C: Into<Rc<str>>>(
|
||||||
|
regex: &str,
|
||||||
|
next_state: Option<u32>,
|
||||||
|
category: C,
|
||||||
|
) -> Option<Self> {
|
||||||
|
Some(Self {
|
||||||
|
pattern: SyntaxPattern::Regex(Regex::new(regex).ok()?),
|
||||||
|
next_state,
|
||||||
|
category: category.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Highlighter {
|
||||||
|
pub fn define_rule(&mut self, filetype: Rc<str>, entry_state: u32, rule: SyntaxRule) {
|
||||||
|
self.syntaxes
|
||||||
|
.entry(filetype)
|
||||||
|
.or_default()
|
||||||
|
.define_rule(entry_state, rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn define_category(&mut self, filetype: Rc<str>, category: Rc<str>, style: SyntaxStyle) {
|
||||||
|
self.syntaxes
|
||||||
|
.entry(filetype)
|
||||||
|
.or_default()
|
||||||
|
.define_style(category, style);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stylize(&self, filetype: &str, category: &str, base: &Style) -> Option<Style> {
|
||||||
|
self.syntaxes
|
||||||
|
.get(filetype)
|
||||||
|
.and_then(|syntax| syntax.stylize(category, base))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rehighlight(&self, lines: &mut [Line], filetype: &str) {
|
||||||
|
// Ignore if no syntax is defined
|
||||||
|
let Some(syntax) = self.syntaxes.get(filetype) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut dirty_start = None;
|
||||||
|
let mut dirty_end = None;
|
||||||
|
for (i, line) in lines.iter().enumerate() {
|
||||||
|
if line.highlight_dirty {
|
||||||
|
if dirty_start.is_none() {
|
||||||
|
dirty_start = Some(i);
|
||||||
|
}
|
||||||
|
dirty_end = Some(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some(dirty_start) = dirty_start else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let dirty_end = dirty_end.unwrap();
|
||||||
|
|
||||||
|
let mut index = dirty_start;
|
||||||
|
let mut expected_start_state = if index == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
lines[index - 1].highlight.end_state
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keep rehighlighting lines until either both !dirty and in the same state or
|
||||||
|
// until the end of the file is reached
|
||||||
|
while index < lines.len() {
|
||||||
|
let is_dirty = lines[index].highlight_dirty;
|
||||||
|
let state_mismatch = lines[index].highlight.start_state != expected_start_state;
|
||||||
|
|
||||||
|
if is_dirty || state_mismatch {
|
||||||
|
lines[index].highlight.start_state = expected_start_state;
|
||||||
|
syntax.highlight(&mut lines[index]);
|
||||||
|
lines[index].highlight_dirty = false;
|
||||||
|
} else if index > dirty_end {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
expected_start_state = lines[index].highlight.end_state;
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Syntax {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
state_rules: HashMap::new(),
|
||||||
|
style_rules: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn define_style<C: Into<Rc<str>>>(&mut self, category: C, style: SyntaxStyle) {
|
||||||
|
self.style_rules.insert(category.into(), style);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn define_rule(&mut self, state: u32, rule: SyntaxRule) {
|
||||||
|
self.state_rules.entry(state).or_default().push(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub fn highlight(&self, line: &Text, state: &mut LineHighlight) {
|
||||||
|
|
||||||
|
fn is_word_char(c: char) -> bool {
|
||||||
|
!c.is_whitespace() && !"()[]{},<>".contains(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stylize(&self, category: &str, base: &Style) -> Option<Style> {
|
||||||
|
self.style_rules
|
||||||
|
.get(category)
|
||||||
|
.map(|style| style.apply(base))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn highlight(&self, line: &mut Line) {
|
||||||
|
line.highlight.tokens.clear();
|
||||||
|
let text = line.to_string();
|
||||||
|
let mut cursor = 0;
|
||||||
|
let mut current_state = line.highlight.start_state;
|
||||||
|
|
||||||
|
while cursor < text.len() {
|
||||||
|
let Some(rule_set) = self.state_rules.get(¤t_state) else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
let current_slice = &text[cursor..];
|
||||||
|
|
||||||
|
let mut matched = false;
|
||||||
|
for rule in rule_set {
|
||||||
|
match &rule.pattern {
|
||||||
|
SyntaxPattern::Regex(regex) => {
|
||||||
|
// TODO this is inefficient
|
||||||
|
if let Some(regex_match) = regex.find(current_slice)
|
||||||
|
&& regex_match.start() == 0
|
||||||
|
{
|
||||||
|
let match_len = regex_match.end();
|
||||||
|
|
||||||
|
line.highlight.tokens.push(TokenSpan {
|
||||||
|
range: cursor..cursor + match_len,
|
||||||
|
category: rule.category.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
cursor += match_len;
|
||||||
|
if let Some(next) = rule.next_state {
|
||||||
|
current_state = next;
|
||||||
|
}
|
||||||
|
matched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SyntaxPattern::Keyword(keyword) => {
|
||||||
|
if current_slice.starts_with(keyword.as_ref()) {
|
||||||
|
let pattern_len = keyword.len();
|
||||||
|
let pattern_starts_with_word =
|
||||||
|
keyword.chars().next().is_none_or(Self::is_word_char);
|
||||||
|
let pattern_ends_with_word =
|
||||||
|
keyword.chars().next_back().is_none_or(Self::is_word_char);
|
||||||
|
|
||||||
|
let leading_boundary_match = !pattern_starts_with_word || {
|
||||||
|
text[..cursor]
|
||||||
|
.chars()
|
||||||
|
.next_back()
|
||||||
|
.is_none_or(|c| !Self::is_word_char(c))
|
||||||
|
};
|
||||||
|
let trailing_boundary_match = !pattern_ends_with_word || {
|
||||||
|
text[cursor + pattern_len..]
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.is_none_or(|c| !Self::is_word_char(c))
|
||||||
|
};
|
||||||
|
|
||||||
|
if leading_boundary_match && trailing_boundary_match {
|
||||||
|
line.highlight.tokens.push(TokenSpan {
|
||||||
|
range: cursor..cursor + pattern_len,
|
||||||
|
category: rule.category.clone(),
|
||||||
|
});
|
||||||
|
cursor += pattern_len;
|
||||||
|
if let Some(next) = rule.next_state {
|
||||||
|
current_state = next;
|
||||||
|
}
|
||||||
|
matched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !matched {
|
||||||
|
cursor = text.ceil_char_boundary(cursor + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
line.highlight.end_state = current_state;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
use std::{borrow::Borrow, hash::Hash};
|
use std::{borrow::Borrow, hash::Hash};
|
||||||
|
|
||||||
use crate::command::Action;
|
|
||||||
|
|
||||||
use self::map::PrefixMap;
|
use self::map::PrefixMap;
|
||||||
pub use self::map::PrefixNode;
|
pub use self::map::PrefixNode;
|
||||||
|
|
||||||
@@ -9,21 +7,24 @@ mod key;
|
|||||||
mod map;
|
mod map;
|
||||||
|
|
||||||
pub use key::KeySeq;
|
pub use key::KeySeq;
|
||||||
use libterm::TermKey;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct KeyMap {
|
pub struct KeyMap<T> {
|
||||||
map: PrefixMap<KeySeq, Vec<Action>>,
|
map: PrefixMap<KeySeq, T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KeyMap {
|
impl<T> KeyMap<T> {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
map: PrefixMap::new(),
|
map: PrefixMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get<N>(&self, key: &N) -> Option<&PrefixNode<KeySeq, Vec<Action>>>
|
pub fn set(&mut self, key: KeySeq, value: T) {
|
||||||
|
self.map.insert(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get<N>(&self, key: &N) -> Option<&PrefixNode<KeySeq, T>>
|
||||||
where
|
where
|
||||||
KeySeq: Borrow<N>,
|
KeySeq: Borrow<N>,
|
||||||
N: Eq + Hash + ?Sized,
|
N: Eq + Hash + ?Sized,
|
||||||
@@ -32,30 +33,10 @@ impl KeyMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromIterator<(KeySeq, Vec<Action>)> for KeyMap {
|
impl<A> FromIterator<(KeySeq, A)> for KeyMap<A> {
|
||||||
fn from_iter<T: IntoIterator<Item = (KeySeq, Vec<Action>)>>(iter: T) -> Self {
|
fn from_iter<T: IntoIterator<Item = (KeySeq, A)>>(iter: T) -> Self {
|
||||||
Self {
|
Self {
|
||||||
map: PrefixMap::from_iter(iter),
|
map: PrefixMap::from_iter(iter),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bindn<I: Into<KeySeq>, V: IntoIterator<Item = Action>>(
|
|
||||||
key: I,
|
|
||||||
actions: V,
|
|
||||||
) -> (KeySeq, Vec<Action>) {
|
|
||||||
(key.into(), actions.into_iter().collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bind1<I: Into<TermKey>, V: IntoIterator<Item = Action>>(
|
|
||||||
key: I,
|
|
||||||
actions: V,
|
|
||||||
) -> (KeySeq, Vec<Action>) {
|
|
||||||
(KeySeq::one(key), actions.into_iter().collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
#[test]
|
|
||||||
fn from_iter() {}
|
|
||||||
}
|
|
||||||
|
|||||||
+161
-336
@@ -1,27 +1,41 @@
|
|||||||
#![feature(rustc_private)]
|
#![feature(rustc_private)]
|
||||||
#![cfg_attr(target_os = "yggdrasil", feature(yggdrasil_os))]
|
// #![cfg_attr(target_os = "yggdrasil", feature(yggdrasil_os))]
|
||||||
#![allow(clippy::new_without_default)]
|
#![allow(clippy::new_without_default)]
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
env,
|
env,
|
||||||
fmt::Write,
|
|
||||||
io::{self, IsTerminal},
|
io::{self, IsTerminal},
|
||||||
path::Path,
|
path::Path,
|
||||||
|
rc::Rc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use libterm::{Clear, Color, Term, TermKey};
|
use libterm::TermKey;
|
||||||
|
|
||||||
use buffer::{Buffer, Mode, SetMode};
|
// use config::Config;
|
||||||
use config::Config;
|
|
||||||
use error::Error;
|
use error::Error;
|
||||||
use keymap::{KeySeq, PrefixNode};
|
use lysp::{
|
||||||
|
error::MachineError,
|
||||||
|
vm::{
|
||||||
|
Value,
|
||||||
|
env::{Environment, EnvironmentAccessHook},
|
||||||
|
machine::Machine,
|
||||||
|
value::{IdentifierValue, convert::AnyFunction},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{command::Action, config::EditorConfig, state::State};
|
||||||
|
|
||||||
pub mod buffer;
|
pub mod buffer;
|
||||||
pub mod command;
|
pub mod command;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod highlight;
|
||||||
pub mod keymap;
|
pub mod keymap;
|
||||||
pub mod line;
|
pub mod state;
|
||||||
|
pub mod text;
|
||||||
|
|
||||||
|
mod script;
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
pub enum TopMode {
|
pub enum TopMode {
|
||||||
@@ -29,352 +43,171 @@ pub enum TopMode {
|
|||||||
Command,
|
Command,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct State {
|
pub struct Editor {
|
||||||
term: Term,
|
machine: Machine,
|
||||||
buffer: Buffer,
|
state: Rc<RefCell<State>>,
|
||||||
command: String,
|
env: Rc<Environment>,
|
||||||
message: Option<String>,
|
|
||||||
status: Option<String>,
|
|
||||||
key_seq: KeySeq,
|
|
||||||
top_mode: TopMode,
|
|
||||||
config: Config,
|
|
||||||
running: bool,
|
|
||||||
number_width: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl Editor {
|
||||||
pub fn open<P: AsRef<Path>>(path: Option<P>) -> Result<Self, Error> {
|
pub fn new<P: AsRef<Path>>(path: Option<P>) -> Result<Self, Error> {
|
||||||
let config = Config::default();
|
#[allow(unused)]
|
||||||
let mut buffer = match path {
|
struct ConfigAccessHook(Rc<RefCell<State>>, Rc<RefCell<EditorConfig>>);
|
||||||
Some(path) => Buffer::open(path).unwrap(),
|
|
||||||
None => Buffer::empty(),
|
|
||||||
};
|
|
||||||
let term = Term::open().map_err(Error::TerminalError)?;
|
|
||||||
|
|
||||||
let (w, h) = term.size().map_err(Error::TerminalError)?;
|
impl EnvironmentAccessHook for ConfigAccessHook {
|
||||||
if config.number {
|
fn read_variable(&mut self, name: &str) -> Option<Value> {
|
||||||
let nw = buffer.number_width() + 2;
|
let config = self.1.borrow();
|
||||||
buffer.resize(&config, nw, w - nw - 1, h - 2);
|
|
||||||
} else {
|
match name {
|
||||||
buffer.resize(&config, 0, w - 1, h - 2);
|
"red/number" => Some((*config.number).into()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn write_variable(&mut self, name: IdentifierValue, value: Value) -> bool {
|
||||||
|
match name.as_ref() {
|
||||||
|
"red/number" => {
|
||||||
|
let mut config = self.1.borrow_mut();
|
||||||
|
*config.number = value.is_trueish();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let config = Rc::new(RefCell::new(EditorConfig::default()));
|
||||||
|
let state = Rc::new(RefCell::new(State::open(path, config.clone())?));
|
||||||
|
let machine = Machine::default();
|
||||||
|
let env = Rc::new(Environment::default());
|
||||||
|
env.set_access_hook(Some(Box::new(ConfigAccessHook(state.clone(), config))));
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
number_width: buffer.number_width(),
|
state,
|
||||||
top_mode: TopMode::Normal,
|
machine,
|
||||||
message: None,
|
env,
|
||||||
status: None,
|
|
||||||
command: String::new(),
|
|
||||||
key_seq: KeySeq::empty(),
|
|
||||||
running: true,
|
|
||||||
buffer,
|
|
||||||
term,
|
|
||||||
config,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn buffer(&self) -> &Buffer {
|
pub fn defun<S, F>(&self, name: S, function: F)
|
||||||
&self.buffer
|
where
|
||||||
}
|
S: Into<IdentifierValue>,
|
||||||
|
F: Fn(
|
||||||
pub fn buffer_mut(&mut self) -> &mut Buffer {
|
&mut Machine,
|
||||||
&mut self.buffer
|
&Rc<Environment>,
|
||||||
}
|
&Rc<RefCell<State>>,
|
||||||
|
&[Value],
|
||||||
pub fn exit(&mut self) {
|
) -> Result<Value, Error>
|
||||||
self.running = false;
|
+ 'static,
|
||||||
}
|
{
|
||||||
|
let state = self.state.clone();
|
||||||
pub fn set_status<S: Into<String>>(&mut self, status: S) {
|
self.env.defun_native(name, "", move |vm, env, args| {
|
||||||
self.status.replace(status.into());
|
match function(vm, env, &state, args) {
|
||||||
}
|
Ok(value) => Ok(value),
|
||||||
|
Err(error) => Err(MachineError::Custom(error.into())),
|
||||||
fn display_number(&mut self) -> Result<(), Error> {
|
|
||||||
let start = self.buffer.row_offset();
|
|
||||||
let end = self.buffer.len();
|
|
||||||
|
|
||||||
for i in 0.. {
|
|
||||||
self.term
|
|
||||||
.set_cursor_position(i, 0)
|
|
||||||
.map_err(Error::TerminalError)?;
|
|
||||||
|
|
||||||
if i + start == self.buffer.cursor_row() {
|
|
||||||
self.term.set_bright(true).map_err(Error::TerminalError)?;
|
|
||||||
self.term
|
|
||||||
.set_foreground(Color::Yellow)
|
|
||||||
.map_err(Error::TerminalError)?;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
if i + start < end {
|
}
|
||||||
write!(self.term, " {0:1$} ", i + start + 1, self.number_width)
|
pub fn defmacro<S, F>(&self, name: S, function: F)
|
||||||
.map_err(Error::TerminalFmtError)?;
|
where
|
||||||
|
S: Into<IdentifierValue>,
|
||||||
|
F: Fn(
|
||||||
|
&mut Machine,
|
||||||
|
&Rc<Environment>,
|
||||||
|
&Rc<RefCell<State>>,
|
||||||
|
&[Value],
|
||||||
|
) -> Result<Value, Error>
|
||||||
|
+ 'static,
|
||||||
|
{
|
||||||
|
let state = self.state.clone();
|
||||||
|
self.env.defmacro_native(name, "", move |vm, env, args| {
|
||||||
|
match function(vm, env, &state, args) {
|
||||||
|
Ok(value) => Ok(value),
|
||||||
|
Err(error) => Err(MachineError::Custom(error.into())),
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if i == self.buffer.height() {
|
pub fn evaluate_file<P: AsRef<Path>>(&mut self, path: P) {
|
||||||
|
let path = path.as_ref();
|
||||||
|
if let Err(error) = self.machine.load_file(Default::default(), &self.env, path) {
|
||||||
|
self.state
|
||||||
|
.borrow_mut()
|
||||||
|
.set_message(format!("{}: {}", path.display(), error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn evaluate_callback(&mut self, function: &AnyFunction, args: &[Value]) {
|
||||||
|
let name = function.name();
|
||||||
|
if let Err(error) = function.invoke(&mut self.machine, &self.env, args) {
|
||||||
|
self.state
|
||||||
|
.borrow_mut()
|
||||||
|
.set_message(format!("{name}: {error}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(&mut self) -> Result<(), Error> {
|
||||||
|
script::setup_env(self);
|
||||||
|
|
||||||
|
#[cfg(any(feature = "runtime", rust_analyzer))]
|
||||||
|
{
|
||||||
|
self.evaluate_file("runtime/core.lysp");
|
||||||
|
}
|
||||||
|
#[cfg(any(not(feature = "runtime"), rust_analyzer))]
|
||||||
|
{
|
||||||
|
self.evaluate_file("/usr/share/red/runtime/core.lysp");
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let exited = self.state.borrow().exited();
|
||||||
|
|
||||||
|
if exited {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if i + start == self.buffer.cursor_row() {
|
// TODO pre-render hook
|
||||||
self.term.reset_style().map_err(Error::TerminalError)?;
|
let (width, height) = self.state.borrow_mut().display()?;
|
||||||
|
|
||||||
|
let post_render_hook = self.state.borrow().post_render_hook.clone();
|
||||||
|
if let Some(post_render_hook) = post_render_hook {
|
||||||
|
self.evaluate_callback(&post_render_hook, &[width.into(), height.into()]);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
self.term.reset_style().map_err(Error::TerminalError)?;
|
self.state.borrow_mut().finish_display()?;
|
||||||
|
|
||||||
Ok(())
|
let key = self.state.borrow_mut().wait_for_events()?;
|
||||||
}
|
|
||||||
|
|
||||||
fn display_modeline(&mut self) -> Result<(), Error> {
|
if let TermKey::Char('Q') = key {
|
||||||
self.term
|
break;
|
||||||
.set_cursor_position(self.buffer.height(), 0)
|
}
|
||||||
.map_err(Error::TerminalError)?;
|
|
||||||
|
|
||||||
let bg = match (self.top_mode, self.buffer.mode()) {
|
let action = self.state.borrow_mut().handle_key(key);
|
||||||
(TopMode::Normal, Mode::Normal) => Color::Yellow,
|
|
||||||
(TopMode::Normal, Mode::Insert) => Color::Cyan,
|
|
||||||
(TopMode::Command, _) => Color::Green,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.term.set_background(bg).map_err(Error::TerminalError)?;
|
match action {
|
||||||
self.term
|
Action::Handler(handler) => {
|
||||||
.set_foreground(Color::Black)
|
self.evaluate_callback(&handler, &[]);
|
||||||
.map_err(Error::TerminalError)?;
|
|
||||||
|
|
||||||
match self.top_mode {
|
|
||||||
TopMode::Normal => {
|
|
||||||
write!(self.term, " {} ", self.buffer.mode().as_str())
|
|
||||||
.map_err(Error::TerminalFmtError)?;
|
|
||||||
|
|
||||||
if self.buffer.is_modified() {
|
|
||||||
self.term
|
|
||||||
.set_background(Color::Magenta)
|
|
||||||
.map_err(Error::TerminalError)?;
|
|
||||||
self.term
|
|
||||||
.set_foreground(Color::Default)
|
|
||||||
.map_err(Error::TerminalError)?;
|
|
||||||
} else {
|
|
||||||
self.term
|
|
||||||
.set_foreground(Color::Green)
|
|
||||||
.map_err(Error::TerminalError)?;
|
|
||||||
self.term
|
|
||||||
.set_background(Color::Default)
|
|
||||||
.map_err(Error::TerminalError)?;
|
|
||||||
}
|
}
|
||||||
}
|
Action::Command(command) => {
|
||||||
TopMode::Command => {
|
let hook = self.state.borrow().command_hook.clone();
|
||||||
write!(self.term, " COMMAND ").map_err(Error::TerminalFmtError)?;
|
if let Some(hook) = hook {
|
||||||
|
let words = command
|
||||||
|
.split(' ')
|
||||||
|
.map(Into::into)
|
||||||
|
.map(Value::String)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
self.term
|
self.evaluate_callback(&hook, &words);
|
||||||
.set_foreground(Color::Green)
|
}
|
||||||
.map_err(Error::TerminalError)?;
|
|
||||||
self.term
|
|
||||||
.set_background(Color::Default)
|
|
||||||
.map_err(Error::TerminalError)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = self
|
|
||||||
.buffer
|
|
||||||
.name()
|
|
||||||
.map(String::as_str)
|
|
||||||
.unwrap_or("<unnamed>");
|
|
||||||
write!(self.term, " {}", name).map_err(Error::TerminalFmtError)?;
|
|
||||||
self.term
|
|
||||||
.clear(Clear::LineToEnd)
|
|
||||||
.map_err(Error::TerminalError)?;
|
|
||||||
self.term
|
|
||||||
.set_cursor_position(self.buffer.height(), self.buffer.width() - 10)
|
|
||||||
.map_err(Error::TerminalError)?;
|
|
||||||
self.term
|
|
||||||
.set_foreground(Color::White)
|
|
||||||
.map_err(Error::TerminalError)?;
|
|
||||||
write!(self.term, "{}", self.key_seq).map_err(Error::TerminalFmtError)?;
|
|
||||||
|
|
||||||
self.term.reset_style().map_err(Error::TerminalError)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn display(&mut self) -> Result<(), Error> {
|
|
||||||
if self.buffer.is_dirty() {
|
|
||||||
self.term.clear(Clear::All).map_err(Error::TerminalError)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.config.number && self.buffer.is_dirty() {
|
|
||||||
self.display_number()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.buffer.display(&self.config, &mut self.term)?;
|
|
||||||
|
|
||||||
if self.top_mode != TopMode::Command {
|
|
||||||
if let Some(status) = &self.status {
|
|
||||||
self.term
|
|
||||||
.set_cursor_position(self.buffer().height() + 1, 0)
|
|
||||||
.map_err(Error::TerminalError)?;
|
|
||||||
self.term
|
|
||||||
.write_str(status.as_str())
|
|
||||||
.map_err(Error::TerminalFmtError)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(msg) = &self.message {
|
|
||||||
self.term
|
|
||||||
.set_cursor_position(self.buffer.height(), 0)
|
|
||||||
.map_err(Error::TerminalError)?;
|
|
||||||
self.term.write_str(msg).map_err(Error::TerminalFmtError)?;
|
|
||||||
self.term.flush().map_err(Error::TerminalError)?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
self.display_modeline()?;
|
|
||||||
|
|
||||||
match self.top_mode {
|
|
||||||
TopMode::Normal => {
|
|
||||||
self.buffer
|
|
||||||
.set_terminal_cursor(&self.config, &mut self.term)?;
|
|
||||||
}
|
|
||||||
TopMode::Command => {
|
|
||||||
self.term
|
|
||||||
.set_cursor_position(self.buffer.height() + 1, 0)
|
|
||||||
.map_err(Error::TerminalError)?;
|
|
||||||
write!(self.term, ":{}", self.command.as_str()).map_err(Error::TerminalFmtError)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.term.flush().map_err(Error::TerminalError)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_command(&mut self) -> Result<(), Error> {
|
|
||||||
let cmd = self.command.clone();
|
|
||||||
command::execute(self, cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_command_key(&mut self, key: TermKey) -> Result<(), Error> {
|
|
||||||
match key {
|
|
||||||
TermKey::Char('\n') | TermKey::Char('\x0D') => {
|
|
||||||
self.top_mode = TopMode::Normal;
|
|
||||||
self.handle_command()?;
|
|
||||||
}
|
|
||||||
TermKey::Char('\x7F') => {
|
|
||||||
if self.command.is_empty() {
|
|
||||||
self.top_mode = TopMode::Normal;
|
|
||||||
} else {
|
|
||||||
self.command.pop();
|
|
||||||
}
|
}
|
||||||
}
|
Action::None => (),
|
||||||
TermKey::Escape => {
|
};
|
||||||
self.top_mode = TopMode::Normal;
|
|
||||||
}
|
|
||||||
TermKey::Char(c) if c.is_ascii_graphic() || c == ' ' => self.command.push(c),
|
|
||||||
_ => (),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_mode_key(&mut self, mode: Mode, key: TermKey) -> Result<(), Error> {
|
|
||||||
let buffer = &mut self.buffer;
|
|
||||||
|
|
||||||
self.key_seq.push(key);
|
|
||||||
|
|
||||||
match self.config.key_seq(mode, &self.key_seq) {
|
|
||||||
Some(PrefixNode::Leaf(actions)) => {
|
|
||||||
self.key_seq.clear();
|
|
||||||
|
|
||||||
for &action in actions {
|
|
||||||
command::perform(buffer, &self.config, action)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(PrefixNode::Prefix(_)) => {}
|
|
||||||
None => {
|
|
||||||
self.key_seq.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.buffer().mode() != Mode::Normal {
|
|
||||||
self.status = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_normal_key(&mut self, key: TermKey) -> Result<(), Error> {
|
|
||||||
match key {
|
|
||||||
TermKey::Escape => {
|
|
||||||
self.key_seq.clear();
|
|
||||||
self.buffer.set_mode(&self.config, SetMode::Normal);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
TermKey::Char(':') => {
|
|
||||||
self.key_seq.clear();
|
|
||||||
self.command.clear();
|
|
||||||
self.status = None;
|
|
||||||
self.top_mode = TopMode::Command;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
_ => self.handle_mode_key(Mode::Normal, key),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_insert_key(&mut self, key: TermKey) -> Result<(), Error> {
|
|
||||||
match key {
|
|
||||||
TermKey::Escape => {
|
|
||||||
self.buffer.set_mode(&self.config, SetMode::Normal);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
TermKey::Char(key)
|
|
||||||
if !key.is_ascii() || key == ' ' || key == '\t' || key.is_ascii_graphic() =>
|
|
||||||
{
|
|
||||||
self.buffer.insert(&self.config, key);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
_ => self.handle_mode_key(Mode::Insert, key),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update(&mut self) -> Result<(), Error> {
|
|
||||||
if self.config.number {
|
|
||||||
let nw = self.buffer.number_width();
|
|
||||||
if nw != self.number_width {
|
|
||||||
self.number_width = nw;
|
|
||||||
let nw = nw + 2;
|
|
||||||
let (w, h) = self.term.size().map_err(Error::TerminalError)?;
|
|
||||||
self.buffer.resize(&self.config, nw, w - nw - 1, h - 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.display()?;
|
|
||||||
|
|
||||||
let key = self.term.read_key().map_err(Error::TerminalError)?;
|
|
||||||
|
|
||||||
if self.message.is_some() {
|
|
||||||
self.message = None;
|
|
||||||
if key != TermKey::Char(':') {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = match (self.top_mode, self.buffer.mode()) {
|
|
||||||
(TopMode::Normal, Mode::Normal) => self.handle_normal_key(key),
|
|
||||||
(TopMode::Normal, Mode::Insert) => self.handle_insert_key(key),
|
|
||||||
(TopMode::Command, _) => self.handle_command_key(key),
|
|
||||||
};
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(()) => Ok(()),
|
|
||||||
Err(e) => {
|
|
||||||
self.message = Some(format!("Error: {}", e));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cleanup(&mut self) {
|
|
||||||
self.term.clear(Clear::All).ok();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
logsink::setup_logging(false);
|
||||||
|
|
||||||
let args = env::args().collect::<Vec<_>>();
|
let args = env::args().collect::<Vec<_>>();
|
||||||
if args.len() > 2 {
|
if args.len() > 2 {
|
||||||
eprintln!("Usage: red [FILE]");
|
eprintln!("Usage: red [FILE]");
|
||||||
@@ -387,19 +220,11 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let path = args.get(1);
|
let path = args.get(1);
|
||||||
let mut state = State::open(path).unwrap();
|
let mut editor = Editor::new(path).unwrap();
|
||||||
let error = loop {
|
let result = editor.run();
|
||||||
if !state.running {
|
editor.state.borrow_mut().cleanup();
|
||||||
break None;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(error) = state.update() {
|
if let Err(error) = result {
|
||||||
break Some(error);
|
eprintln!("Error: {error}");
|
||||||
}
|
|
||||||
};
|
|
||||||
state.cleanup();
|
|
||||||
|
|
||||||
if let Some(error) = error {
|
|
||||||
eprintln!("Error: {:?}", error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
use libterm::{Color, TermKey};
|
||||||
|
use lysp::{
|
||||||
|
error::{MachineError, ValueConversionError},
|
||||||
|
vm::{
|
||||||
|
Value,
|
||||||
|
value::{IdentifierValue, convert::TryFromValue},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
TopMode,
|
||||||
|
buffer::{Mode, SetMode},
|
||||||
|
error::Error,
|
||||||
|
keymap::KeySeq,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub enum Movement {
|
||||||
|
LineStart,
|
||||||
|
LineEnd,
|
||||||
|
NextLine,
|
||||||
|
PrevLine,
|
||||||
|
NextPage,
|
||||||
|
PrevPage,
|
||||||
|
FirstLine,
|
||||||
|
LastLine,
|
||||||
|
NextChar,
|
||||||
|
PrevChar,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromValue for Movement {
|
||||||
|
fn from_value(value: &Value) -> Result<Self, Error> {
|
||||||
|
let value = IdentifierValue::try_from_value(value)?;
|
||||||
|
match value.as_ref() {
|
||||||
|
"line-start" => Ok(Self::LineStart),
|
||||||
|
"line-end" => Ok(Self::LineEnd),
|
||||||
|
"next-line" => Ok(Self::NextLine),
|
||||||
|
"prev-line" => Ok(Self::PrevLine),
|
||||||
|
"next-page" => Ok(Self::NextPage),
|
||||||
|
"prev-page" => Ok(Self::PrevPage),
|
||||||
|
"first-line" => Ok(Self::FirstLine),
|
||||||
|
"last-line" => Ok(Self::LastLine),
|
||||||
|
"next-char" => Ok(Self::NextChar),
|
||||||
|
"prev-char" => Ok(Self::PrevChar),
|
||||||
|
_ => Err(Error::NoPath),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait AsValue {
|
||||||
|
fn as_value(&self) -> impl Into<Value>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait FromValue: Sized {
|
||||||
|
fn from_value(value: &Value) -> Result<Self, Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsValue for TopMode {
|
||||||
|
fn as_value(&self) -> impl Into<Value> {
|
||||||
|
IdentifierValue::from(match self {
|
||||||
|
Self::Normal => "normal",
|
||||||
|
Self::Command => "command",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsValue for Mode {
|
||||||
|
fn as_value(&self) -> impl Into<Value> {
|
||||||
|
IdentifierValue::from(match self {
|
||||||
|
Self::Normal => "normal",
|
||||||
|
Self::Insert => "insert",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromValue for SetMode {
|
||||||
|
fn from_value(value: &Value) -> Result<Self, Error> {
|
||||||
|
let value = IdentifierValue::try_from_value(value)?;
|
||||||
|
match value.as_ref() {
|
||||||
|
"normal" => Ok(Self::Normal),
|
||||||
|
"insert" | "insert-before" => Ok(Self::InsertBefore),
|
||||||
|
"insert-after" => Ok(Self::InsertAfter),
|
||||||
|
_ => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsValue for TermKey {
|
||||||
|
fn as_value(&self) -> impl Into<Value> {
|
||||||
|
IdentifierValue::from(match self {
|
||||||
|
TermKey::Insert => "insert".to_owned(),
|
||||||
|
TermKey::Up => "up".to_owned(),
|
||||||
|
TermKey::Down => "down".to_owned(),
|
||||||
|
TermKey::Left => "left".to_owned(),
|
||||||
|
TermKey::Right => "right".to_owned(),
|
||||||
|
TermKey::Delete => "delete".to_owned(),
|
||||||
|
TermKey::Char(char) => format!("{char}"),
|
||||||
|
_ => "".to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsValue for Color {
|
||||||
|
fn as_value(&self) -> impl Into<Value> {
|
||||||
|
IdentifierValue::from(match self {
|
||||||
|
Self::Green => "green",
|
||||||
|
Self::Red => "red",
|
||||||
|
Self::Cyan => "cyan",
|
||||||
|
Self::Blue => "blue",
|
||||||
|
Self::Default => "default",
|
||||||
|
Self::Black => "black",
|
||||||
|
Self::White => "white",
|
||||||
|
Self::Yellow => "yellow",
|
||||||
|
Self::Magenta => "magenta",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromValue for Color {
|
||||||
|
fn from_value(value: &Value) -> Result<Self, Error> {
|
||||||
|
let name = IdentifierValue::try_from_value(value)?;
|
||||||
|
match name.as_ref() {
|
||||||
|
"red" => Ok(Self::Red),
|
||||||
|
"green" => Ok(Self::Green),
|
||||||
|
"blue" => Ok(Self::Blue),
|
||||||
|
"yellow" => Ok(Self::Yellow),
|
||||||
|
"magenta" => Ok(Self::Magenta),
|
||||||
|
"cyan" => Ok(Self::Cyan),
|
||||||
|
"black" => Ok(Self::Black),
|
||||||
|
"white" => Ok(Self::White),
|
||||||
|
"default" => Ok(Self::Default),
|
||||||
|
_ => Err(MachineError::ValueConversion(ValueConversionError {
|
||||||
|
expected: "color symbol".into(),
|
||||||
|
got: value.clone(),
|
||||||
|
})
|
||||||
|
.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromValue for TermKey {
|
||||||
|
fn from_value(value: &Value) -> Result<Self, Error> {
|
||||||
|
let identifier = IdentifierValue::try_from_value(value)?;
|
||||||
|
let identifier = identifier.as_ref();
|
||||||
|
if identifier.len() == 1
|
||||||
|
&& let Some(char) = identifier.chars().next()
|
||||||
|
{
|
||||||
|
match char {
|
||||||
|
ch if ch.is_alphanumeric() => return Ok(Self::Char(ch)),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match identifier {
|
||||||
|
"left" => Ok(Self::Left),
|
||||||
|
"right" => Ok(Self::Right),
|
||||||
|
"up" => Ok(Self::Up),
|
||||||
|
"down" => Ok(Self::Down),
|
||||||
|
"end" => Ok(Self::End),
|
||||||
|
"home" => Ok(Self::Home),
|
||||||
|
"newline" => Ok(Self::Char('\r')),
|
||||||
|
"backspace" => Ok(Self::Char('\x7F')),
|
||||||
|
_ => Err(Error::NoPath),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromValue for KeySeq {
|
||||||
|
fn from_value(mut value: &Value) -> Result<Self, Error> {
|
||||||
|
match value {
|
||||||
|
Value::Cons(_) => {
|
||||||
|
let mut seq = KeySeq::empty();
|
||||||
|
while !value.is_nil() {
|
||||||
|
let Value::Cons(cons) = value else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let key = TermKey::from_value(&cons.0)?;
|
||||||
|
seq.push(key);
|
||||||
|
value = &cons.1;
|
||||||
|
}
|
||||||
|
Ok(seq)
|
||||||
|
}
|
||||||
|
Value::Identifier(_) => {
|
||||||
|
let key = TermKey::from_value(value)?;
|
||||||
|
Ok(KeySeq::one(key))
|
||||||
|
}
|
||||||
|
_ => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use libterm::TermKey;
|
||||||
|
use lysp::vm::Value;
|
||||||
|
|
||||||
|
use crate::{keymap::KeySeq, script::FromValue};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_value_for_keyseq() {
|
||||||
|
let v = Value::list_or_nil([Value::Identifier("a".into()), Value::Identifier("a".into())]);
|
||||||
|
let v = KeySeq::from_value(&v).unwrap();
|
||||||
|
let mut s = KeySeq::empty();
|
||||||
|
s.push(TermKey::Char('a'));
|
||||||
|
s.push(TermKey::Char('a'));
|
||||||
|
assert_eq!(v, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,333 @@
|
|||||||
|
use crate::{
|
||||||
|
Editor,
|
||||||
|
buffer::SetMode,
|
||||||
|
highlight::{SyntaxRule, SyntaxStyle},
|
||||||
|
keymap::KeySeq,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod convert;
|
||||||
|
pub use convert::{AsValue, FromValue, Movement};
|
||||||
|
use libterm::Color;
|
||||||
|
use lysp::{
|
||||||
|
error::MachineError,
|
||||||
|
vm::{
|
||||||
|
Value, prelude,
|
||||||
|
value::{
|
||||||
|
StringValue,
|
||||||
|
convert::{AnyFunction, TryFromValue},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn editor_api(editor: &mut Editor) {
|
||||||
|
editor.defun("red/bind-normal-hook", |_, _, state, args| {
|
||||||
|
let [seq, handler] = args else {
|
||||||
|
return Err(MachineError::InvalidArgumentCount.into());
|
||||||
|
};
|
||||||
|
let seq = KeySeq::from_value(seq)?;
|
||||||
|
let handler = AnyFunction::try_from_value(handler)?;
|
||||||
|
|
||||||
|
let mut state = state.borrow_mut();
|
||||||
|
state.normal_map.set(seq, handler);
|
||||||
|
|
||||||
|
Ok(Value::Nil)
|
||||||
|
});
|
||||||
|
editor.defun("red/bind-insert-hook", |_, _, state, args| {
|
||||||
|
let [seq, handler] = args else {
|
||||||
|
return Err(MachineError::InvalidArgumentCount.into());
|
||||||
|
};
|
||||||
|
let seq = KeySeq::from_value(seq)?;
|
||||||
|
let handler = AnyFunction::try_from_value(handler)?;
|
||||||
|
|
||||||
|
let mut state = state.borrow_mut();
|
||||||
|
state.insert_map.set(seq, handler);
|
||||||
|
|
||||||
|
Ok(Value::Nil)
|
||||||
|
});
|
||||||
|
editor.defun("red/bind-command-hook", |_, _, state, args| {
|
||||||
|
let [hook] = args else {
|
||||||
|
return Err(MachineError::InvalidArgumentCount.into());
|
||||||
|
};
|
||||||
|
let hook = AnyFunction::try_from_value(hook)?;
|
||||||
|
state.borrow_mut().command_hook = Some(hook);
|
||||||
|
Ok(Value::Nil)
|
||||||
|
});
|
||||||
|
editor.defun("red/bind-post-render-hook", |_, _, state, args| {
|
||||||
|
let [hook] = args else {
|
||||||
|
return Err(MachineError::InvalidArgumentCount.into());
|
||||||
|
};
|
||||||
|
let hook = AnyFunction::try_from_value(hook)?;
|
||||||
|
state.borrow_mut().post_render_hook = Some(hook);
|
||||||
|
Ok(Value::Nil)
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.defun("red/quit", |_, _, state, args| {
|
||||||
|
let force = match args {
|
||||||
|
[] => false,
|
||||||
|
[arg, ..] => arg.is_trueish(),
|
||||||
|
};
|
||||||
|
state.borrow_mut().exit(force);
|
||||||
|
Ok(Value::Nil)
|
||||||
|
});
|
||||||
|
editor.defun("red/message", |_, _, state, args| {
|
||||||
|
let mut message = String::new();
|
||||||
|
for (i, arg) in args.iter().enumerate() {
|
||||||
|
if i != 0 {
|
||||||
|
message.push(' ');
|
||||||
|
}
|
||||||
|
message.push_str(&format!("{arg}"));
|
||||||
|
}
|
||||||
|
state.borrow_mut().set_message(message);
|
||||||
|
Ok(Value::Nil)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn syntax_api(editor: &mut Editor) {
|
||||||
|
editor.defun("red/syntax/define-regex-rule", |_, _, state, args| {
|
||||||
|
// ( <SYNTAX> <STATE> <PATTERN> <CATEGORY> &optional <NEXT-STATE> )
|
||||||
|
let (filetype, entry_state, pattern, category, next_state) = match args {
|
||||||
|
[a, b, c, d] => (a, b, c, d, &Value::Nil),
|
||||||
|
[a, b, c, d, e] => (a, b, c, d, e),
|
||||||
|
_ => return Err(MachineError::InvalidArgumentCount.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let filetype = StringValue::try_from_value(filetype)?;
|
||||||
|
let entry_state = usize::try_from_value(entry_state)? as u32;
|
||||||
|
let pattern = StringValue::try_from_value(pattern)?;
|
||||||
|
let category = StringValue::try_from_value(category)?;
|
||||||
|
let next_state = if next_state.is_nil() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(usize::try_from_value(next_state)? as u32)
|
||||||
|
};
|
||||||
|
|
||||||
|
let rule = match SyntaxRule::regex(&pattern, next_state, category.as_rc_str().clone()) {
|
||||||
|
Some(rule) => rule,
|
||||||
|
None => return Err(MachineError::InstructionFetch.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut state = state.borrow_mut();
|
||||||
|
state
|
||||||
|
.highlight
|
||||||
|
.define_rule(filetype.as_rc_str().clone(), entry_state, rule);
|
||||||
|
|
||||||
|
Ok(Value::Nil)
|
||||||
|
});
|
||||||
|
editor.defun("red/syntax/define-keyword-rule", |_, _, state, args| {
|
||||||
|
// ( <SYNTAX> <STATE> <PATTERN> <CATEGORY> &optional <NEXT-STATE> )
|
||||||
|
let (filetype, entry_state, pattern, category, next_state) = match args {
|
||||||
|
[a, b, c, d] => (a, b, c, d, &Value::Nil),
|
||||||
|
[a, b, c, d, e] => (a, b, c, d, e),
|
||||||
|
_ => return Err(MachineError::InvalidArgumentCount.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let filetype = StringValue::try_from_value(filetype)?;
|
||||||
|
let entry_state = usize::try_from_value(entry_state)? as u32;
|
||||||
|
let pattern = StringValue::try_from_value(pattern)?;
|
||||||
|
let category = StringValue::try_from_value(category)?;
|
||||||
|
let next_state = if next_state.is_nil() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(usize::try_from_value(next_state)? as u32)
|
||||||
|
};
|
||||||
|
|
||||||
|
let rule = SyntaxRule::keyword(
|
||||||
|
pattern.as_rc_str().clone(),
|
||||||
|
next_state,
|
||||||
|
category.as_rc_str().clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut state = state.borrow_mut();
|
||||||
|
state
|
||||||
|
.highlight
|
||||||
|
.define_rule(filetype.as_rc_str().clone(), entry_state, rule);
|
||||||
|
|
||||||
|
Ok(Value::Nil)
|
||||||
|
});
|
||||||
|
editor.defun("red/syntax/define-category-style", |_, _, state, args| {
|
||||||
|
// ( <SYNTAX> <CATEGORY> <FOREGROUND> <BACKGROUND> <BOLD> )
|
||||||
|
let (filetype, category, foreground, background, bold) = match args {
|
||||||
|
[a, b, c] => (a, b, c, &Value::Nil, &Value::Nil),
|
||||||
|
[a, b, c, d] => (a, b, c, d, &Value::Nil),
|
||||||
|
[a, b, c, d, e] => (a, b, c, d, e),
|
||||||
|
_ => return Err(MachineError::InvalidArgumentCount.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let filetype = StringValue::try_from_value(filetype)?;
|
||||||
|
let category = StringValue::try_from_value(category)?;
|
||||||
|
let foreground = if foreground.is_nil() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(Color::from_value(foreground)?)
|
||||||
|
};
|
||||||
|
let background = if background.is_nil() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(Color::from_value(background)?)
|
||||||
|
};
|
||||||
|
let bold = if bold.is_nil() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(bold.is_trueish())
|
||||||
|
};
|
||||||
|
|
||||||
|
let style = SyntaxStyle {
|
||||||
|
foreground,
|
||||||
|
background,
|
||||||
|
bold,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut state = state.borrow_mut();
|
||||||
|
state.highlight.define_category(
|
||||||
|
filetype.as_rc_str().clone(),
|
||||||
|
category.as_rc_str().clone(),
|
||||||
|
style,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Value::Nil)
|
||||||
|
});
|
||||||
|
editor.defun("red/syntax/reset", |_, _, state, _| {
|
||||||
|
let mut state = state.borrow_mut();
|
||||||
|
state.buffer_mut().reset_highlight();
|
||||||
|
Ok(Value::Nil)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buffer_api(editor: &mut Editor) {
|
||||||
|
editor.defun("red/buffer/path", |_, _, state, _| {
|
||||||
|
let state = state.borrow();
|
||||||
|
let path = state.buffer().path();
|
||||||
|
if let Some(path) = path {
|
||||||
|
Ok(Value::String(format!("{}", path.display()).into()))
|
||||||
|
} else {
|
||||||
|
Ok(Value::Nil)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
editor.defun("red/buffer/mode", |_, _, state, _| {
|
||||||
|
let state = state.borrow();
|
||||||
|
let (top_mode, buffer_mode) = state.mode();
|
||||||
|
let top_mode = top_mode.as_value();
|
||||||
|
let buffer_mode = buffer_mode.as_value();
|
||||||
|
Ok(Value::list_or_nil([top_mode.into(), buffer_mode.into()]))
|
||||||
|
});
|
||||||
|
editor.defun("red/buffer/set-mode", |_, _, state, args| {
|
||||||
|
let [mode] = args else {
|
||||||
|
return Err(MachineError::InvalidArgumentCount.into());
|
||||||
|
};
|
||||||
|
let target_mode = SetMode::from_value(mode)?;
|
||||||
|
let mut state = state.borrow_mut();
|
||||||
|
state.set_mode(target_mode);
|
||||||
|
Ok(Value::Nil)
|
||||||
|
});
|
||||||
|
editor.defun("red/buffer/write", |_, _, state, args| {
|
||||||
|
let path = match args {
|
||||||
|
[] => None,
|
||||||
|
[path] => Some(StringValue::try_from_value(path)?),
|
||||||
|
_ => return Err(MachineError::InvalidArgumentCount.into()),
|
||||||
|
};
|
||||||
|
let mut state = state.borrow_mut();
|
||||||
|
state.write_buffer(path.as_deref())?;
|
||||||
|
Ok(Value::Nil)
|
||||||
|
});
|
||||||
|
editor.defun("red/buffer/open", |_, _, state, args| {
|
||||||
|
let (path, force) = match args {
|
||||||
|
[] => return Ok(Value::Nil),
|
||||||
|
[path] => (StringValue::try_from_value(path)?, false),
|
||||||
|
[path, force] => (StringValue::try_from_value(path)?, force.is_trueish()),
|
||||||
|
_ => return Err(MachineError::InvalidArgumentCount.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut state = state.borrow_mut();
|
||||||
|
state.open_buffer(&*path, force)?;
|
||||||
|
Ok(Value::Nil)
|
||||||
|
});
|
||||||
|
editor.defun("red/buffer/move", |_, _, state, args| {
|
||||||
|
let [movement] = args else {
|
||||||
|
return Err(MachineError::InvalidArgumentCount.into());
|
||||||
|
};
|
||||||
|
let movement = Movement::from_value(movement)?;
|
||||||
|
let mut state = state.borrow_mut();
|
||||||
|
state.move_cursor(movement);
|
||||||
|
Ok(Value::Nil)
|
||||||
|
});
|
||||||
|
editor.defun("red/buffer/insert-line-before", |_, _, state, _| {
|
||||||
|
let mut state = state.borrow_mut();
|
||||||
|
state.buffer_mut().newline_before();
|
||||||
|
Ok(Value::Nil)
|
||||||
|
});
|
||||||
|
editor.defun("red/buffer/insert-line-after", |_, _, state, args| {
|
||||||
|
let break_line = match args {
|
||||||
|
[] => false,
|
||||||
|
[arg, ..] => arg.is_trueish(),
|
||||||
|
};
|
||||||
|
let mut state = state.borrow_mut();
|
||||||
|
state.buffer_mut().newline_after(break_line);
|
||||||
|
Ok(Value::Nil)
|
||||||
|
});
|
||||||
|
editor.defun("red/buffer/kill-line", |_, _, state, _| {
|
||||||
|
let mut state = state.borrow_mut();
|
||||||
|
state.kill_current_line();
|
||||||
|
Ok(Value::Nil)
|
||||||
|
});
|
||||||
|
editor.defun("red/buffer/erase-backward", |_, _, state, _| {
|
||||||
|
let mut state = state.borrow_mut();
|
||||||
|
state.erase_backward();
|
||||||
|
Ok(Value::Nil)
|
||||||
|
});
|
||||||
|
editor.defun("red/buffer/term-cursor", |_, _, state, _| {
|
||||||
|
let state = state.borrow();
|
||||||
|
match state.buffer_terminal_cursor() {
|
||||||
|
Some((x, y)) => Ok(Value::list_or_nil([x.into(), y.into()])),
|
||||||
|
None => Ok(Value::Nil),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn term_api(editor: &mut Editor) {
|
||||||
|
editor.defun("term/fg-color", |_, _, state, args| {
|
||||||
|
let [color] = args else {
|
||||||
|
return Err(MachineError::InvalidArgumentCount.into());
|
||||||
|
};
|
||||||
|
let mut state = state.borrow_mut();
|
||||||
|
let color = Color::from_value(color)?;
|
||||||
|
state.term.set_foreground(color).ok();
|
||||||
|
Ok(Value::Nil)
|
||||||
|
});
|
||||||
|
editor.defun("term/bg-color", |_, _, state, args| {
|
||||||
|
let [color] = args else {
|
||||||
|
return Err(MachineError::InvalidArgumentCount.into());
|
||||||
|
};
|
||||||
|
let mut state = state.borrow_mut();
|
||||||
|
let color = Color::from_value(color)?;
|
||||||
|
state.term.set_background(color).ok();
|
||||||
|
Ok(Value::Nil)
|
||||||
|
});
|
||||||
|
editor.defun("term/write", |_, _, state, args| {
|
||||||
|
use std::fmt::Write;
|
||||||
|
let [message] = args else {
|
||||||
|
return Err(MachineError::InvalidArgumentCount.into());
|
||||||
|
};
|
||||||
|
let mut state = state.borrow_mut();
|
||||||
|
write!(state.term, "{message}").ok();
|
||||||
|
Ok(Value::Nil)
|
||||||
|
});
|
||||||
|
editor.defun("term/set-cursor", |_, _, state, args| {
|
||||||
|
let [row, column] = args else {
|
||||||
|
return Err(MachineError::InvalidArgumentCount.into());
|
||||||
|
};
|
||||||
|
let row = usize::try_from_value(row)?;
|
||||||
|
let column = usize::try_from_value(column)?;
|
||||||
|
let mut state = state.borrow_mut();
|
||||||
|
state.term.set_cursor_position(row, column).ok();
|
||||||
|
Ok(Value::Nil)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup_env(editor: &mut Editor) {
|
||||||
|
prelude::load(&editor.env);
|
||||||
|
|
||||||
|
editor_api(editor);
|
||||||
|
buffer_api(editor);
|
||||||
|
term_api(editor);
|
||||||
|
syntax_api(editor);
|
||||||
|
}
|
||||||
@@ -0,0 +1,446 @@
|
|||||||
|
use std::{cell::RefCell, fmt::Write as FmtWrite, mem, path::Path, rc::Rc};
|
||||||
|
|
||||||
|
use libterm::{Clear, Color, Term, TermKey};
|
||||||
|
use lysp::vm::value::convert::AnyFunction;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
TopMode,
|
||||||
|
buffer::{Buffer, Mode, SetMode},
|
||||||
|
command::Action,
|
||||||
|
config::EditorConfig,
|
||||||
|
error::Error,
|
||||||
|
highlight::Highlighter,
|
||||||
|
keymap::{KeyMap, KeySeq, PrefixNode},
|
||||||
|
script::Movement,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct State {
|
||||||
|
pub(super) term: Term,
|
||||||
|
buffer: Buffer,
|
||||||
|
command: String,
|
||||||
|
message: Option<String>,
|
||||||
|
status: Option<String>,
|
||||||
|
key_seq: KeySeq,
|
||||||
|
top_mode: TopMode,
|
||||||
|
config: Rc<RefCell<EditorConfig>>,
|
||||||
|
// config: Config,
|
||||||
|
running: bool,
|
||||||
|
number_width: usize,
|
||||||
|
pub(super) highlight: Highlighter,
|
||||||
|
|
||||||
|
// Scripting hooks
|
||||||
|
pub(super) normal_map: KeyMap<AnyFunction>,
|
||||||
|
pub(super) insert_map: KeyMap<AnyFunction>,
|
||||||
|
pub(super) command_hook: Option<AnyFunction>,
|
||||||
|
pub(super) post_render_hook: Option<AnyFunction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
pub fn open<P: AsRef<Path>>(
|
||||||
|
path: Option<P>,
|
||||||
|
config: Rc<RefCell<EditorConfig>>,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
let mut buffer = match path {
|
||||||
|
Some(path) => Buffer::open(path).unwrap(),
|
||||||
|
None => Buffer::empty(),
|
||||||
|
};
|
||||||
|
let term = Term::open().map_err(Error::TerminalError)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
number_width: buffer.number_width(),
|
||||||
|
top_mode: TopMode::Normal,
|
||||||
|
message: None,
|
||||||
|
status: None,
|
||||||
|
command: String::new(),
|
||||||
|
key_seq: KeySeq::empty(),
|
||||||
|
running: true,
|
||||||
|
buffer,
|
||||||
|
term,
|
||||||
|
config,
|
||||||
|
highlight: Highlighter::default(),
|
||||||
|
|
||||||
|
normal_map: KeyMap::new(),
|
||||||
|
insert_map: KeyMap::new(),
|
||||||
|
command_hook: None,
|
||||||
|
post_render_hook: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn buffer(&self) -> &Buffer {
|
||||||
|
&self.buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn buffer_mut(&mut self) -> &mut Buffer {
|
||||||
|
&mut self.buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mode(&self) -> (TopMode, Mode) {
|
||||||
|
(self.top_mode, self.buffer.mode())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn buffer_terminal_cursor(&self) -> Option<(usize, usize)> {
|
||||||
|
if self.top_mode == TopMode::Command {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(self.buffer.get_terminal_cursor(&self.config.borrow()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exit(&mut self, force: bool) {
|
||||||
|
if self.buffer.is_modified() && !force {
|
||||||
|
self.message = Some("Buffer has unsaved changes. Use :q! to force-exit".into());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exited(&self) -> bool {
|
||||||
|
!self.running
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_status<S: Into<String>>(&mut self, status: S) {
|
||||||
|
self.status.replace(status.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_message<S: Into<String>>(&mut self, message: S) {
|
||||||
|
self.message.replace(message.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_number(&mut self) -> Result<(), Error> {
|
||||||
|
let start = self.buffer.row_offset();
|
||||||
|
let end = self.buffer.len();
|
||||||
|
|
||||||
|
for i in 0.. {
|
||||||
|
self.term
|
||||||
|
.set_cursor_position(i, 0)
|
||||||
|
.map_err(Error::TerminalError)?;
|
||||||
|
|
||||||
|
if i + start == self.buffer.cursor_row() {
|
||||||
|
self.term.set_bright(true).map_err(Error::TerminalError)?;
|
||||||
|
self.term
|
||||||
|
.set_foreground(Color::Yellow)
|
||||||
|
.map_err(Error::TerminalError)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if i + start < end {
|
||||||
|
write!(self.term, " {0:1$} ", i + start + 1, self.number_width)
|
||||||
|
.map_err(Error::TerminalFmtError)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == self.buffer.height() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if i + start == self.buffer.cursor_row() {
|
||||||
|
self.term.reset_style().map_err(Error::TerminalError)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.term.reset_style().map_err(Error::TerminalError)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_modeline(&mut self) -> Result<(), Error> {
|
||||||
|
self.term
|
||||||
|
.set_cursor_position(self.buffer.height(), 0)
|
||||||
|
.map_err(Error::TerminalError)?;
|
||||||
|
|
||||||
|
let bg = match (self.top_mode, self.buffer.mode()) {
|
||||||
|
(TopMode::Normal, Mode::Normal) => Color::Yellow,
|
||||||
|
(TopMode::Normal, Mode::Insert) => Color::Cyan,
|
||||||
|
(TopMode::Command, _) => Color::Green,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.term.set_background(bg).map_err(Error::TerminalError)?;
|
||||||
|
self.term
|
||||||
|
.set_foreground(Color::Black)
|
||||||
|
.map_err(Error::TerminalError)?;
|
||||||
|
|
||||||
|
match self.top_mode {
|
||||||
|
TopMode::Normal => {
|
||||||
|
write!(self.term, " {} ", self.buffer.mode().as_str())
|
||||||
|
.map_err(Error::TerminalFmtError)?;
|
||||||
|
|
||||||
|
if self.buffer.is_modified() {
|
||||||
|
self.term
|
||||||
|
.set_background(Color::Magenta)
|
||||||
|
.map_err(Error::TerminalError)?;
|
||||||
|
self.term
|
||||||
|
.set_foreground(Color::Default)
|
||||||
|
.map_err(Error::TerminalError)?;
|
||||||
|
} else {
|
||||||
|
self.term
|
||||||
|
.set_foreground(Color::Green)
|
||||||
|
.map_err(Error::TerminalError)?;
|
||||||
|
self.term
|
||||||
|
.set_background(Color::Default)
|
||||||
|
.map_err(Error::TerminalError)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TopMode::Command => {
|
||||||
|
write!(self.term, " COMMAND ").map_err(Error::TerminalFmtError)?;
|
||||||
|
|
||||||
|
self.term
|
||||||
|
.set_foreground(Color::Green)
|
||||||
|
.map_err(Error::TerminalError)?;
|
||||||
|
self.term
|
||||||
|
.set_background(Color::Default)
|
||||||
|
.map_err(Error::TerminalError)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = self
|
||||||
|
.buffer
|
||||||
|
.name()
|
||||||
|
.map(String::as_str)
|
||||||
|
.unwrap_or("<unnamed>");
|
||||||
|
write!(self.term, " {}", name).map_err(Error::TerminalFmtError)?;
|
||||||
|
self.term
|
||||||
|
.clear(Clear::LineToEnd)
|
||||||
|
.map_err(Error::TerminalError)?;
|
||||||
|
self.term
|
||||||
|
.set_cursor_position(self.buffer.height(), self.buffer.width() - 10)
|
||||||
|
.map_err(Error::TerminalError)?;
|
||||||
|
self.term
|
||||||
|
.set_foreground(Color::White)
|
||||||
|
.map_err(Error::TerminalError)?;
|
||||||
|
write!(self.term, "{} {}", self.buffer.filetype(), self.key_seq)
|
||||||
|
.map_err(Error::TerminalFmtError)?;
|
||||||
|
|
||||||
|
self.term.reset_style().map_err(Error::TerminalError)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finish_display(&mut self) -> Result<(), Error> {
|
||||||
|
let config = self.config.borrow();
|
||||||
|
|
||||||
|
match self.top_mode {
|
||||||
|
TopMode::Normal => {
|
||||||
|
self.buffer.set_terminal_cursor(&config, &mut self.term)?;
|
||||||
|
}
|
||||||
|
TopMode::Command => {
|
||||||
|
self.term
|
||||||
|
.set_cursor_position(self.buffer.height() + 1, 0)
|
||||||
|
.map_err(Error::TerminalError)?;
|
||||||
|
write!(self.term, ":{}", self.command.as_str()).map_err(Error::TerminalFmtError)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.term.flush().map_err(Error::TerminalError)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn display(&mut self) -> Result<(usize, usize), Error> {
|
||||||
|
let config = self.config.clone();
|
||||||
|
let mut config = config.borrow_mut();
|
||||||
|
|
||||||
|
if self.buffer.is_dirty() {
|
||||||
|
self.term.clear(Clear::All).map_err(Error::TerminalError)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (w, h) = self.term.size().map_err(Error::TerminalError)?;
|
||||||
|
|
||||||
|
if config.number.clean() {
|
||||||
|
if *config.number {
|
||||||
|
let nw = self.buffer.number_width() + 3;
|
||||||
|
self.buffer.resize(&config, nw, w - nw - 1, h - 2);
|
||||||
|
} else {
|
||||||
|
self.buffer.resize(&config, 0, w - 1, h - 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if *config.number && self.buffer.is_dirty() {
|
||||||
|
self.display_number()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.buffer
|
||||||
|
.display(&config, &mut self.term, &self.highlight)?;
|
||||||
|
|
||||||
|
if self.top_mode != TopMode::Command
|
||||||
|
&& let Some(status) = &self.status
|
||||||
|
{
|
||||||
|
self.term
|
||||||
|
.set_cursor_position(self.buffer().height() + 1, 0)
|
||||||
|
.map_err(Error::TerminalError)?;
|
||||||
|
self.term
|
||||||
|
.write_str(status.as_str())
|
||||||
|
.map_err(Error::TerminalFmtError)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(msg) = &self.message {
|
||||||
|
self.term
|
||||||
|
.set_cursor_position(self.buffer.height(), 0)
|
||||||
|
.map_err(Error::TerminalError)?;
|
||||||
|
self.term.write_str(msg).map_err(Error::TerminalFmtError)?;
|
||||||
|
self.term.flush().map_err(Error::TerminalError)?;
|
||||||
|
return Ok((w, h));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.display_modeline()?;
|
||||||
|
|
||||||
|
Ok((w, h))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_mode(&mut self, target: SetMode) {
|
||||||
|
self.top_mode = TopMode::Normal;
|
||||||
|
self.message = None;
|
||||||
|
self.key_seq.clear();
|
||||||
|
self.buffer.set_mode(&self.config.borrow(), target);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key_seq(&self, mode: Mode) -> Option<&PrefixNode<KeySeq, AnyFunction>> {
|
||||||
|
match mode {
|
||||||
|
Mode::Normal => self.normal_map.get(&self.key_seq),
|
||||||
|
Mode::Insert => self.insert_map.get(&self.key_seq),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_mode_key(&mut self, mode: Mode, key: TermKey) -> Option<AnyFunction> {
|
||||||
|
self.key_seq.push(key);
|
||||||
|
match self.key_seq(mode) {
|
||||||
|
Some(PrefixNode::Leaf(action)) => {
|
||||||
|
let action = action.clone();
|
||||||
|
self.key_seq.clear();
|
||||||
|
return Some(action);
|
||||||
|
}
|
||||||
|
Some(PrefixNode::Prefix(_)) => {}
|
||||||
|
None => self.key_seq.clear(),
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_normal_key(&mut self, key: TermKey) -> Option<AnyFunction> {
|
||||||
|
match key {
|
||||||
|
TermKey::Escape => {
|
||||||
|
self.key_seq.clear();
|
||||||
|
self.buffer.set_mode(&self.config.borrow(), SetMode::Normal);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
TermKey::Char(':') => {
|
||||||
|
self.key_seq.clear();
|
||||||
|
self.command.clear();
|
||||||
|
self.status = None;
|
||||||
|
self.top_mode = TopMode::Command;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
_ => self.handle_mode_key(Mode::Normal, key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_insert_key(&mut self, key: TermKey) -> Option<AnyFunction> {
|
||||||
|
match key {
|
||||||
|
TermKey::Escape => {
|
||||||
|
self.buffer.set_mode(&self.config.borrow(), SetMode::Normal);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
TermKey::Char(key)
|
||||||
|
if !key.is_ascii() || key == ' ' || key == '\t' || key.is_ascii_graphic() =>
|
||||||
|
{
|
||||||
|
self.buffer.insert(&self.config.borrow(), key);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
_ => self.handle_mode_key(Mode::Insert, key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_command_key(&mut self, key: TermKey) -> Action {
|
||||||
|
match key {
|
||||||
|
TermKey::Char('\n') | TermKey::Char('\x0D') => {
|
||||||
|
self.top_mode = TopMode::Normal;
|
||||||
|
let command = mem::take(&mut self.command);
|
||||||
|
return Action::Command(command);
|
||||||
|
}
|
||||||
|
TermKey::Char('\x7F') => {
|
||||||
|
if self.command.is_empty() {
|
||||||
|
self.top_mode = TopMode::Normal;
|
||||||
|
} else {
|
||||||
|
self.command.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TermKey::Escape => {
|
||||||
|
self.top_mode = TopMode::Normal;
|
||||||
|
}
|
||||||
|
TermKey::Char(c) if c.is_ascii_graphic() || c == ' ' => self.command.push(c),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
Action::None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_key(&mut self, key: TermKey) -> Action {
|
||||||
|
if self.message.is_some() {
|
||||||
|
self.message = None;
|
||||||
|
if key != TermKey::Char(':') {
|
||||||
|
return Action::None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match (self.top_mode, self.buffer.mode()) {
|
||||||
|
(TopMode::Normal, Mode::Normal) => self.handle_normal_key(key).into(),
|
||||||
|
(TopMode::Normal, Mode::Insert) => self.handle_insert_key(key).into(),
|
||||||
|
(TopMode::Command, _) => self.handle_command_key(key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_cursor(&mut self, movement: Movement) {
|
||||||
|
let config = self.config.borrow();
|
||||||
|
match movement {
|
||||||
|
Movement::LineStart => self.buffer.set_column(&config, 0),
|
||||||
|
Movement::LineEnd => self.buffer.to_line_end(&config),
|
||||||
|
Movement::NextLine => self.buffer.move_cursor(&config, 0, 1),
|
||||||
|
Movement::PrevLine => self.buffer.move_cursor(&config, 0, -1),
|
||||||
|
Movement::NextPage => self.buffer.move_cursor(&config, 0, 25),
|
||||||
|
Movement::PrevPage => self.buffer.move_cursor(&config, 0, -25),
|
||||||
|
Movement::FirstLine => self.buffer.to_first_line(&config),
|
||||||
|
Movement::LastLine => self.buffer.to_last_line(&config),
|
||||||
|
Movement::NextChar => self.buffer.move_cursor(&config, 1, 0),
|
||||||
|
Movement::PrevChar => self.buffer.move_cursor(&config, -1, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn kill_current_line(&mut self) {
|
||||||
|
let config = self.config.borrow();
|
||||||
|
self.buffer.kill_line(&config);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn erase_backward(&mut self) {
|
||||||
|
let config = self.config.borrow();
|
||||||
|
self.buffer.erase_backward(&config);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_buffer<P: AsRef<Path>>(&mut self, path: P, force: bool) -> Result<(), Error> {
|
||||||
|
if self.buffer.is_modified() && !force {
|
||||||
|
return Err(Error::UnsavedBuffer("Use :e! [FILE] to open another file"));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.buffer.reopen(path).map_err(Error::OpenError)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_buffer<P: AsRef<Path>>(&mut self, path: Option<P>) -> Result<(), Error> {
|
||||||
|
if path.is_some() && self.buffer.is_modified() && self.buffer.path().is_some() {
|
||||||
|
return Err(Error::UnsavedBuffer(
|
||||||
|
"Use :w! FILE to force write to another file",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(path) = path {
|
||||||
|
self.buffer.set_path(path);
|
||||||
|
}
|
||||||
|
self.buffer.save()?;
|
||||||
|
|
||||||
|
if let Some(name) = self.buffer.name() {
|
||||||
|
let status = format!("{name:?} written");
|
||||||
|
self.set_status(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wait_for_events(&mut self) -> Result<TermKey, Error> {
|
||||||
|
self.term.read_key().map_err(Error::TerminalError)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cleanup(&mut self) {
|
||||||
|
self.term.clear(Clear::All).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
use std::{fmt, ops::Index, slice::SliceIndex};
|
||||||
|
|
||||||
|
use unicode_width::UnicodeWidthChar;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Default)]
|
||||||
|
pub struct Text {
|
||||||
|
data: Vec<char>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct Span<'a>(&'a [char]);
|
||||||
|
|
||||||
|
pub trait TextLike: Index<usize, Output = char> + ToString {
|
||||||
|
type Iter<'a>: Iterator<Item = &'a char>
|
||||||
|
where
|
||||||
|
Self: 'a;
|
||||||
|
type Span<'a>: TextLike + 'a
|
||||||
|
where
|
||||||
|
Self: 'a;
|
||||||
|
|
||||||
|
fn display_width(&self, tab_width: usize) -> usize;
|
||||||
|
fn span<R: SliceIndex<[char], Output = [char]>>(&self, range: R) -> Self::Span<'_>;
|
||||||
|
fn skip_to_width(&self, offset: usize, tab_width: usize) -> (Self::Span<'_>, usize, usize);
|
||||||
|
|
||||||
|
fn iter(&self) -> Self::Iter<'_>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait TextLikeMut: TextLike {
|
||||||
|
fn split_off(&mut self, at: usize) -> Self;
|
||||||
|
fn insert(&mut self, at: usize, ch: char);
|
||||||
|
fn remove(&mut self, at: usize);
|
||||||
|
fn extend(&mut self, other: Self);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Line
|
||||||
|
impl Text {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { data: vec![] }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::should_implement_trait)]
|
||||||
|
pub fn from_str<S: AsRef<str>>(s: S) -> Self {
|
||||||
|
let chars = s.as_ref().chars();
|
||||||
|
Self {
|
||||||
|
data: Vec::from_iter(chars),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_span(&self) -> Span<'_> {
|
||||||
|
Span(self.data.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.data.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.data.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoIterator for Text {
|
||||||
|
type Item = char;
|
||||||
|
type IntoIter = std::vec::IntoIter<char>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.data.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextLike for Text {
|
||||||
|
type Span<'a> = Span<'a>;
|
||||||
|
type Iter<'a> = std::slice::Iter<'a, char>;
|
||||||
|
|
||||||
|
fn display_width(&self, tab_width: usize) -> usize {
|
||||||
|
self.as_span().display_width(tab_width)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn span<R: SliceIndex<[char], Output = [char]>>(&self, range: R) -> Self::Span<'_> {
|
||||||
|
self.as_span().span(range)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skip_to_width(&self, offset: usize, tab_width: usize) -> (Self::Span<'_>, usize, usize) {
|
||||||
|
self.as_span().skip_to_width(offset, tab_width)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn iter(&self) -> Self::Iter<'_> {
|
||||||
|
self.data.iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextLikeMut for Text {
|
||||||
|
fn insert(&mut self, at: usize, ch: char) {
|
||||||
|
self.data.insert(at, ch);
|
||||||
|
}
|
||||||
|
fn remove(&mut self, at: usize) {
|
||||||
|
self.data.remove(at);
|
||||||
|
}
|
||||||
|
fn extend(&mut self, other: Self) {
|
||||||
|
self.data.extend(other.data);
|
||||||
|
}
|
||||||
|
fn split_off(&mut self, at: usize) -> Self {
|
||||||
|
let data = self.data.split_off(at);
|
||||||
|
Text { data }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Index<usize> for Text {
|
||||||
|
type Output = char;
|
||||||
|
|
||||||
|
fn index(&self, index: usize) -> &Self::Output {
|
||||||
|
&self.data[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Text {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
use fmt::Write;
|
||||||
|
self.data.iter().try_for_each(|i| f.write_char(*i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Span
|
||||||
|
impl<'s> TextLike for Span<'s> {
|
||||||
|
type Iter<'a>
|
||||||
|
= std::slice::Iter<'a, char>
|
||||||
|
where
|
||||||
|
's: 'a;
|
||||||
|
type Span<'a>
|
||||||
|
= Span<'s>
|
||||||
|
where
|
||||||
|
's: 'a;
|
||||||
|
|
||||||
|
fn display_width(&self, tab_width: usize) -> usize {
|
||||||
|
self.0.iter().fold(0, |pos, &ch| match ch {
|
||||||
|
'\t' => (pos + tab_width) & !(tab_width - 1),
|
||||||
|
_ => pos + ch.width().unwrap_or(1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn span<R: SliceIndex<[char], Output = [char]>>(&self, range: R) -> Self::Span<'_> {
|
||||||
|
Span(&self.0[range])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skip_to_width(&self, offset: usize, tab_width: usize) -> (Self::Span<'_>, usize, usize) {
|
||||||
|
let mut index = 0;
|
||||||
|
let mut pos = 0;
|
||||||
|
for &ch in self.0.iter() {
|
||||||
|
if pos >= offset {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
match ch {
|
||||||
|
'\t' => pos = (pos + tab_width) & !(tab_width - 1),
|
||||||
|
_ => pos += ch.width().unwrap_or(1),
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
(self.span(index..), pos, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn iter(&self) -> Self::Iter<'_> {
|
||||||
|
self.0.iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Index<usize> for Span<'_> {
|
||||||
|
type Output = char;
|
||||||
|
|
||||||
|
fn index(&self, index: usize) -> &Self::Output {
|
||||||
|
&self.0[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Span<'_> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
use fmt::Write;
|
||||||
|
self.0.iter().try_for_each(|i| f.write_char(*i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::text::{Span, TextLike};
|
||||||
|
|
||||||
|
use super::Text;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn line_from_str() {
|
||||||
|
// pure ASCII
|
||||||
|
let text = "abc123\n\t xyz";
|
||||||
|
let line = Text::from_str(text);
|
||||||
|
assert_eq!(
|
||||||
|
line.data,
|
||||||
|
vec!['a', 'b', 'c', '1', '2', '3', '\n', '\t', ' ', 'x', 'y', 'z']
|
||||||
|
);
|
||||||
|
|
||||||
|
// cyrillic unicode
|
||||||
|
let text = "це тест123";
|
||||||
|
let line = Text::from_str(text);
|
||||||
|
assert_eq!(
|
||||||
|
line.data,
|
||||||
|
vec!['ц', 'е', ' ', 'т', 'е', 'с', 'т', '1', '2', '3']
|
||||||
|
);
|
||||||
|
|
||||||
|
// japanese unicode
|
||||||
|
let text = "1日本2";
|
||||||
|
let line = Text::from_str(text);
|
||||||
|
assert_eq!(line.data, vec!['1', '日', '本', '2']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn line_to_string() {
|
||||||
|
let line = Text {
|
||||||
|
data: vec!['a', 'b', 'c', 'т', 'е', 'с', 'т', '1', '2', '3', '\n'],
|
||||||
|
};
|
||||||
|
assert_eq!(line.to_string().as_str(), "abcтест123\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn line_span() {
|
||||||
|
// All span
|
||||||
|
let line = Text::from_str("abcdef");
|
||||||
|
assert_eq!(line.as_span(), Span(&['a', 'b', 'c', 'd', 'e', 'f']));
|
||||||
|
|
||||||
|
assert_eq!(line.span(..3), Span(&['a', 'b', 'c']));
|
||||||
|
assert_eq!(line.span(..=3), Span(&['a', 'b', 'c', 'd']));
|
||||||
|
|
||||||
|
assert_eq!(line.span(..=3).span(2..), Span(&['c', 'd']));
|
||||||
|
assert_eq!(line.span(2..=3), Span(&['c', 'd']));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn line_width() {
|
||||||
|
// No tabs
|
||||||
|
let line = Text::from_str("abcdef");
|
||||||
|
assert_eq!(line.display_width(4), line.len());
|
||||||
|
|
||||||
|
// Tabs
|
||||||
|
let line = Text::from_str("\ta\tbcdef");
|
||||||
|
assert_eq!(line.display_width(4), 8 + 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -177,6 +177,13 @@ fn build_rootfs<S: AsRef<Path>, D: AsRef<Path>>(
|
|||||||
// Copy /etc
|
// Copy /etc
|
||||||
util::copy_dir_recursive(user_dir.join("etc"), rootfs_dir.join("etc"))?;
|
util::copy_dir_recursive(user_dir.join("etc"), rootfs_dir.join("etc"))?;
|
||||||
|
|
||||||
|
// Create /usr/share/red
|
||||||
|
fs::create_dir_all(rootfs_dir.join("usr/share/red"))?;
|
||||||
|
util::copy_dir_recursive(
|
||||||
|
env.workspace_root.join("userspace/tools/red/runtime"),
|
||||||
|
rootfs_dir.join("usr/share/red/runtime"),
|
||||||
|
)?;
|
||||||
|
|
||||||
// Copy architecture-specific directories
|
// Copy architecture-specific directories
|
||||||
let arch_dir = user_dir.join("arch").join(env.arch.name());
|
let arch_dir = user_dir.join("arch").join(env.arch.name());
|
||||||
let arch_rc_d = arch_dir.join("rc.d");
|
let arch_rc_d = arch_dir.join("rc.d");
|
||||||
|
|||||||
Reference in New Issue
Block a user