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"
|
||||
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]]
|
||||
name = "crypt"
|
||||
version = "0.1.0"
|
||||
@@ -1057,15 +1032,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
@@ -1349,17 +1315,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "http"
|
||||
version = "1.3.1"
|
||||
@@ -1812,12 +1767,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "md2txt"
|
||||
version = "0.1.0"
|
||||
@@ -1859,18 +1808,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "ndk"
|
||||
version = "0.9.0"
|
||||
@@ -2029,15 +1966,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_threads"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc-sys"
|
||||
version = "0.3.5"
|
||||
@@ -2310,29 +2238,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "pci-ids"
|
||||
version = "0.2.5"
|
||||
@@ -2764,10 +2669,11 @@ name = "red"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cross",
|
||||
"crossterm",
|
||||
"libc",
|
||||
"libterm",
|
||||
"syslog",
|
||||
"log",
|
||||
"logsink",
|
||||
"lysp",
|
||||
"regex",
|
||||
"thiserror 1.0.69",
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
@@ -2792,9 +2698,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
version = "1.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -2804,9 +2710,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.9"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -3119,36 +3025,6 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "signature"
|
||||
version = "2.2.0"
|
||||
@@ -3365,19 +3241,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "sysutils"
|
||||
version = "0.1.0"
|
||||
@@ -3481,14 +3344,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"libc",
|
||||
"num-conv",
|
||||
"num_threads",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3497,16 +3355,6 @@ version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "tiny-skia"
|
||||
version = "0.11.4"
|
||||
@@ -3961,22 +3809,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.9"
|
||||
@@ -3986,12 +3818,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
@@ -4001,15 +3827,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
|
||||
@@ -1 +1 @@
|
||||
(import "examples/io.lysp")
|
||||
(import "io.lysp")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ops::{Deref, DerefMut},
|
||||
path::Path,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
@@ -31,6 +32,7 @@ pub struct FunctionBlock {
|
||||
identifier: Option<IdentifierValue>,
|
||||
docstring: Option<StringValue>,
|
||||
signature: FunctionSignature,
|
||||
script_path: Option<Rc<Path>>,
|
||||
|
||||
// Data
|
||||
pub(crate) constants: Vec<Value>,
|
||||
@@ -81,6 +83,7 @@ pub struct CompileContext {
|
||||
pub function_blocks: Vec<FunctionBlock>,
|
||||
pub(crate) current: usize,
|
||||
pub(crate) options: CompileOptions,
|
||||
script_path: Option<Rc<Path>>,
|
||||
}
|
||||
|
||||
pub trait Compile {
|
||||
@@ -88,11 +91,16 @@ pub trait Compile {
|
||||
}
|
||||
|
||||
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 {
|
||||
function_blocks: vec![FunctionBlock::root(root_name)],
|
||||
function_blocks: vec![FunctionBlock::root(root_name, script_path.clone())],
|
||||
current: 0,
|
||||
options,
|
||||
script_path,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,8 +108,9 @@ impl CompileContext {
|
||||
options: CompileOptions,
|
||||
chunk_name: Option<IdentifierValue>,
|
||||
value: &Value,
|
||||
script_path: Option<Rc<Path>>,
|
||||
) -> 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 value = expression.compile(&mut cx)?;
|
||||
cx.compile_return_value(value)?;
|
||||
@@ -141,7 +150,13 @@ impl CompileContext {
|
||||
if self.options.trace_compile {
|
||||
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();
|
||||
self.function_blocks.push(block);
|
||||
self.current = index;
|
||||
@@ -384,6 +399,7 @@ impl FunctionBlock {
|
||||
identifier: Option<IdentifierValue>,
|
||||
docstring: Option<StringValue>,
|
||||
signature: &FunctionSignature,
|
||||
script_path: Option<Rc<Path>>,
|
||||
) -> Self {
|
||||
let mut block = Self {
|
||||
parent,
|
||||
@@ -397,6 +413,7 @@ impl FunctionBlock {
|
||||
instructions: vec![],
|
||||
labels: vec![],
|
||||
loop_stack: vec![],
|
||||
script_path,
|
||||
};
|
||||
for required in signature.required_arguments.iter() {
|
||||
block
|
||||
@@ -416,7 +433,7 @@ impl FunctionBlock {
|
||||
block
|
||||
}
|
||||
|
||||
fn root(identifier: Option<IdentifierValue>) -> Self {
|
||||
fn root(identifier: Option<IdentifierValue>, script_path: Option<Rc<Path>>) -> Self {
|
||||
Self::new(
|
||||
None,
|
||||
identifier,
|
||||
@@ -426,6 +443,7 @@ impl FunctionBlock {
|
||||
optional_arguments: vec![],
|
||||
rest_argument: None,
|
||||
},
|
||||
script_path,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -531,6 +549,7 @@ impl FunctionBlock {
|
||||
required_count: self.signature.required_arguments.len(),
|
||||
optional_count: self.signature.optional_arguments.len(),
|
||||
has_rest: self.signature.rest_argument.is_some(),
|
||||
script: self.script_path.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -631,7 +650,7 @@ impl FunctionBlock {
|
||||
pub fn test_compile(
|
||||
expression: &Expression,
|
||||
) -> 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)?;
|
||||
Ok((cx, value))
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ fn run_module<P: AsRef<Path>>(
|
||||
let path = path.as_ref();
|
||||
let name = format!("{}", path.display());
|
||||
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) {
|
||||
Ok(function) => function,
|
||||
Err(error) => return Err(handle_module_error(error)),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
io::{BufRead, Write, stdin, stdout},
|
||||
path::PathBuf,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
@@ -39,6 +40,7 @@ pub struct FileReader<R: BufRead> {
|
||||
|
||||
pub struct ModuleReader<R: BufRead> {
|
||||
reader: FileReader<R>,
|
||||
path: PathBuf,
|
||||
macro_machine: Machine,
|
||||
}
|
||||
|
||||
@@ -93,11 +95,12 @@ impl<R: BufRead> Reader for FileReader<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();
|
||||
macro_machine.trace_macros = trace_macros;
|
||||
Self {
|
||||
reader: FileReader::new(reader),
|
||||
path: path.into(),
|
||||
macro_machine,
|
||||
}
|
||||
}
|
||||
@@ -130,7 +133,8 @@ impl<R: BufRead> ModuleReader<R> {
|
||||
options: &CompileOptions,
|
||||
env: &Rc<Environment>,
|
||||
) -> 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 {
|
||||
head: vec![],
|
||||
tail: Rc::new(Expression::Nil),
|
||||
|
||||
@@ -26,10 +26,11 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
struct CallFrame {
|
||||
closure: ClosureValue,
|
||||
ip: usize,
|
||||
base_pointer: usize,
|
||||
#[derive(Debug)]
|
||||
pub struct CallFrame {
|
||||
pub closure: ClosureValue,
|
||||
pub ip: usize,
|
||||
pub base_pointer: usize,
|
||||
}
|
||||
|
||||
pub struct Machine {
|
||||
@@ -95,6 +96,10 @@ impl Machine {
|
||||
.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> {
|
||||
self.call_stack.head().map(|frame| MachineErrorLocation {
|
||||
function: frame.closure.function.clone(),
|
||||
@@ -691,9 +696,10 @@ impl Machine {
|
||||
value: Value,
|
||||
) -> Result<Value, MachineErrorAt> {
|
||||
let value_expanded = self.macro_expand(env, &value)?;
|
||||
let function = CompileContext::compile_value(compile_options, chunk_name, &value_expanded)
|
||||
.map_err(MachineError::Compile)
|
||||
.map_err(MachineErrorAt::at_unknown)?;
|
||||
let function =
|
||||
CompileContext::compile_value(compile_options, chunk_name, &value_expanded, None)
|
||||
.map_err(MachineError::Compile)
|
||||
.map_err(MachineErrorAt::at_unknown)?;
|
||||
|
||||
let closure = ClosureValue {
|
||||
function,
|
||||
@@ -718,7 +724,7 @@ impl Machine {
|
||||
.map_err(MachineError::Read)
|
||||
.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) {
|
||||
Ok(function) => function,
|
||||
Err(error) => todo!("Handle error: {error:?}"),
|
||||
@@ -799,6 +805,7 @@ mod tests {
|
||||
required_count: 0,
|
||||
optional_count: 0,
|
||||
has_rest: false,
|
||||
script: None,
|
||||
}),
|
||||
};
|
||||
let mut machine = Machine::default();
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use std::rc::Rc;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::MachineError,
|
||||
@@ -18,11 +21,31 @@ pub fn load(env: &Rc<Environment>) {
|
||||
return Err(MachineError::InvalidArgumentCount);
|
||||
}
|
||||
|
||||
for arg in args {
|
||||
let path = StringValue::try_from_value(arg)?;
|
||||
let caller = vm
|
||||
.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)
|
||||
.map_err(|error| MachineError::LoadError(path, error.into()))?;
|
||||
for arg in args {
|
||||
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)
|
||||
|
||||
@@ -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> {
|
||||
if index < self.pointer {
|
||||
Some(unsafe { self.data[index].assume_init_ref() })
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::fmt;
|
||||
use std::{fmt, path::Path, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
compile::UpvalueDef,
|
||||
@@ -16,6 +16,7 @@ pub struct BytecodeFunction {
|
||||
pub instructions: Box<[u8]>,
|
||||
pub constants: Box<[Value]>,
|
||||
pub upvalues: Box<[UpvalueDef]>,
|
||||
pub script: Option<Rc<Path>>,
|
||||
|
||||
pub required_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 {
|
||||
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"
|
||||
authors = ["Mark Poliakov <mark@alnyan.me>"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
libterm.workspace = true
|
||||
cross.workspace = true
|
||||
lysp.workspace = true
|
||||
logsink.workspace = true
|
||||
|
||||
thiserror.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
unicode-width = "0.1.11"
|
||||
regex = "1.12.3"
|
||||
|
||||
[target.'cfg(not(target_os = "yggdrasil"))'.dependencies]
|
||||
libc = "0.2.150"
|
||||
crossterm = "0.27.0"
|
||||
syslog = "6.1.0"
|
||||
[features]
|
||||
default = []
|
||||
runtime = []
|
||||
|
||||
[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,
|
||||
io::{self, BufRead, BufReader, BufWriter, Write as IoWrite},
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use libterm::{Color, CursorStyle, Term};
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
buffer::{line::Line, style::Style},
|
||||
config::EditorConfig,
|
||||
error::Error,
|
||||
line::{Line, TextLike},
|
||||
highlight::Highlighter,
|
||||
text::{Text, TextLike, TextLikeMut},
|
||||
};
|
||||
|
||||
pub mod line;
|
||||
pub mod style;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct View {
|
||||
cursor_column: usize,
|
||||
@@ -47,6 +53,7 @@ pub struct Buffer {
|
||||
name: Option<String>,
|
||||
path: Option<PathBuf>,
|
||||
modified: bool,
|
||||
filetype: Rc<str>,
|
||||
}
|
||||
|
||||
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 {
|
||||
self.column_offset = 0;
|
||||
self.cursor_column = 0;
|
||||
@@ -121,6 +128,7 @@ impl Buffer {
|
||||
name: None,
|
||||
path: None,
|
||||
modified: false,
|
||||
filetype: "text".into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +137,8 @@ impl Buffer {
|
||||
let lines = input.lines().collect::<Result<Vec<_>, _>>()?;
|
||||
let lines = lines
|
||||
.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();
|
||||
Ok(lines)
|
||||
}
|
||||
@@ -143,7 +152,7 @@ impl Buffer {
|
||||
vec![]
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
let mut this = Self {
|
||||
lines,
|
||||
name,
|
||||
path: Some(path.into()),
|
||||
@@ -152,7 +161,13 @@ impl Buffer {
|
||||
mode_dirty: true,
|
||||
view: View::default(),
|
||||
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<()> {
|
||||
@@ -173,10 +188,34 @@ impl Buffer {
|
||||
|
||||
self.path = Some(path.into());
|
||||
self.name = name;
|
||||
self.update_filetype();
|
||||
self.reset_highlight();
|
||||
|
||||
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> {
|
||||
let path = self.path.as_ref().ok_or(Error::NoPath)?;
|
||||
let mut writer = BufWriter::new(File::create(path).map_err(Error::WriteError)?);
|
||||
@@ -201,7 +240,7 @@ impl Buffer {
|
||||
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 {
|
||||
SetMode::Normal => Mode::Normal,
|
||||
SetMode::InsertAfter | SetMode::InsertBefore => Mode::Insert,
|
||||
@@ -220,6 +259,10 @@ impl Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn filetype(&self) -> &str {
|
||||
&self.filetype
|
||||
}
|
||||
|
||||
pub fn mode(&self) -> Mode {
|
||||
self.mode
|
||||
}
|
||||
@@ -248,7 +291,7 @@ impl Buffer {
|
||||
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;
|
||||
|
||||
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
|
||||
.lines
|
||||
.get(self.view.cursor_row)
|
||||
@@ -285,7 +328,7 @@ impl Buffer {
|
||||
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
|
||||
.lines
|
||||
.get(self.view.cursor_row)
|
||||
@@ -295,7 +338,7 @@ impl Buffer {
|
||||
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() {
|
||||
return;
|
||||
}
|
||||
@@ -309,18 +352,18 @@ impl Buffer {
|
||||
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);
|
||||
}
|
||||
|
||||
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 py = (self.view.cursor_row as isize + dy).max(0) as usize;
|
||||
|
||||
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.view.height = height;
|
||||
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() {
|
||||
return (0, 0);
|
||||
}
|
||||
@@ -374,23 +417,56 @@ impl Buffer {
|
||||
|
||||
fn display_line(
|
||||
&self,
|
||||
config: &Config,
|
||||
config: &EditorConfig,
|
||||
current_style: &mut Style,
|
||||
term: &mut Term,
|
||||
row: usize,
|
||||
line: &Line,
|
||||
hi: &Highlighter,
|
||||
) -> Result<(), Error> {
|
||||
let mut pos = 0;
|
||||
term.set_cursor_position(row, self.view.offset_x)
|
||||
.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;
|
||||
|
||||
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 {
|
||||
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' {
|
||||
let old_pos = pos;
|
||||
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.set_foreground(Color::Default)
|
||||
.map_err(Error::TerminalError)?;
|
||||
current_style.foreground = Color::Default;
|
||||
} else {
|
||||
term.write_char(' ').map_err(Error::TerminalFmtError)?;
|
||||
}
|
||||
@@ -433,7 +510,14 @@ impl Buffer {
|
||||
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 {
|
||||
Mode::Normal => {
|
||||
term.set_cursor_style(CursorStyle::Default)
|
||||
@@ -445,6 +529,8 @@ impl Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
let mut current_style = Style::default();
|
||||
|
||||
for (row, line) in self
|
||||
.lines
|
||||
.iter()
|
||||
@@ -452,13 +538,27 @@ impl Buffer {
|
||||
.take(self.view.height)
|
||||
.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(())
|
||||
}
|
||||
|
||||
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);
|
||||
if self.mode_dirty {
|
||||
match self.mode {
|
||||
@@ -497,7 +597,7 @@ impl Buffer {
|
||||
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() {
|
||||
assert_eq!(self.view.cursor_row, 0);
|
||||
self.lines.push(Line::new());
|
||||
@@ -509,7 +609,7 @@ impl Buffer {
|
||||
self.modified = true;
|
||||
}
|
||||
|
||||
pub fn erase_backward(&mut self, config: &Config) {
|
||||
pub fn erase_backward(&mut self, config: &EditorConfig) {
|
||||
if self.lines.is_empty() {
|
||||
return;
|
||||
}
|
||||
@@ -547,12 +647,15 @@ impl Buffer {
|
||||
self.modified = true;
|
||||
}
|
||||
|
||||
pub fn kill_line(&mut self, config: &Config) {
|
||||
pub fn kill_line(&mut self, config: &EditorConfig) {
|
||||
if self.lines.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
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.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::{
|
||||
State,
|
||||
buffer::{Buffer, SetMode},
|
||||
config::Config,
|
||||
error::Error,
|
||||
};
|
||||
use crate::{State, error::Error};
|
||||
|
||||
pub type CommandFn = fn(&mut State, &[&str]) -> Result<(), Error>;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Action {
|
||||
// Editing
|
||||
EraseBackward,
|
||||
InsertBefore,
|
||||
InsertAfter,
|
||||
NewlineBefore,
|
||||
NewlineAfter,
|
||||
BreakLine,
|
||||
KillLine,
|
||||
|
||||
// Movement
|
||||
MoveFirstLine,
|
||||
MoveLastLine,
|
||||
MoveCharPrev,
|
||||
MoveCharNext,
|
||||
MoveLineBack(usize),
|
||||
MoveLineForward(usize),
|
||||
MoveLineStart,
|
||||
MoveLineEnd,
|
||||
Handler(AnyFunction),
|
||||
Command(String),
|
||||
None,
|
||||
}
|
||||
|
||||
static COMMANDS: &[(&str, RangeInclusive<usize>, CommandFn)] = &[
|
||||
("w", 0..=1, cmd_write),
|
||||
("w!", 0..=1, cmd_force_write),
|
||||
("q", 0..=0, cmd_exit),
|
||||
("q!", 0..=0, cmd_force_exit),
|
||||
("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);
|
||||
impl From<Option<AnyFunction>> for Action {
|
||||
fn from(value: Option<AnyFunction>) -> Self {
|
||||
match value {
|
||||
Some(handler) => Self::Handler(handler),
|
||||
None => Self::None,
|
||||
}
|
||||
}
|
||||
|
||||
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 crate::{
|
||||
buffer::Mode,
|
||||
command::Action,
|
||||
keymap::{KeyMap, KeySeq, PrefixNode, bind1, bindn},
|
||||
use std::{
|
||||
mem,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
|
||||
pub struct Config {
|
||||
// TODO must be a power of 2, lol
|
||||
pub struct EditorConfig {
|
||||
pub tab_width: usize,
|
||||
pub number: bool,
|
||||
|
||||
pub nmap: KeyMap,
|
||||
pub imap: KeyMap,
|
||||
pub number: Dirty<bool>,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
pub struct Dirty<T>(T, bool);
|
||||
|
||||
impl Default for EditorConfig {
|
||||
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,
|
||||
number: Dirty::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
impl<T> Dirty<T> {
|
||||
pub const fn new(value: T) -> Self {
|
||||
Self(value, true)
|
||||
}
|
||||
|
||||
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 lysp::error::{MachineError, ValueConversionError};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
// I/O errors
|
||||
@@ -19,4 +21,13 @@ pub enum Error {
|
||||
TerminalError(io::Error),
|
||||
#[error("Terminal error: {0:?}")]
|
||||
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 crate::command::Action;
|
||||
|
||||
use self::map::PrefixMap;
|
||||
pub use self::map::PrefixNode;
|
||||
|
||||
@@ -9,21 +7,24 @@ mod key;
|
||||
mod map;
|
||||
|
||||
pub use key::KeySeq;
|
||||
use libterm::TermKey;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct KeyMap {
|
||||
map: PrefixMap<KeySeq, Vec<Action>>,
|
||||
pub struct KeyMap<T> {
|
||||
map: PrefixMap<KeySeq, T>,
|
||||
}
|
||||
|
||||
impl KeyMap {
|
||||
impl<T> KeyMap<T> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
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
|
||||
KeySeq: Borrow<N>,
|
||||
N: Eq + Hash + ?Sized,
|
||||
@@ -32,30 +33,10 @@ impl KeyMap {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<(KeySeq, Vec<Action>)> for KeyMap {
|
||||
fn from_iter<T: IntoIterator<Item = (KeySeq, Vec<Action>)>>(iter: T) -> Self {
|
||||
impl<A> FromIterator<(KeySeq, A)> for KeyMap<A> {
|
||||
fn from_iter<T: IntoIterator<Item = (KeySeq, A)>>(iter: T) -> Self {
|
||||
Self {
|
||||
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)]
|
||||
#![cfg_attr(target_os = "yggdrasil", feature(yggdrasil_os))]
|
||||
// #![cfg_attr(target_os = "yggdrasil", feature(yggdrasil_os))]
|
||||
#![allow(clippy::new_without_default)]
|
||||
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
env,
|
||||
fmt::Write,
|
||||
io::{self, IsTerminal},
|
||||
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 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 command;
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod highlight;
|
||||
pub mod keymap;
|
||||
pub mod line;
|
||||
pub mod state;
|
||||
pub mod text;
|
||||
|
||||
mod script;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum TopMode {
|
||||
@@ -29,352 +43,171 @@ pub enum TopMode {
|
||||
Command,
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
term: Term,
|
||||
buffer: Buffer,
|
||||
command: String,
|
||||
message: Option<String>,
|
||||
status: Option<String>,
|
||||
key_seq: KeySeq,
|
||||
top_mode: TopMode,
|
||||
config: Config,
|
||||
running: bool,
|
||||
number_width: usize,
|
||||
pub struct Editor {
|
||||
machine: Machine,
|
||||
state: Rc<RefCell<State>>,
|
||||
env: Rc<Environment>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn open<P: AsRef<Path>>(path: Option<P>) -> Result<Self, Error> {
|
||||
let config = Config::default();
|
||||
let mut buffer = match path {
|
||||
Some(path) => Buffer::open(path).unwrap(),
|
||||
None => Buffer::empty(),
|
||||
};
|
||||
let term = Term::open().map_err(Error::TerminalError)?;
|
||||
impl Editor {
|
||||
pub fn new<P: AsRef<Path>>(path: Option<P>) -> Result<Self, Error> {
|
||||
#[allow(unused)]
|
||||
struct ConfigAccessHook(Rc<RefCell<State>>, Rc<RefCell<EditorConfig>>);
|
||||
|
||||
let (w, h) = term.size().map_err(Error::TerminalError)?;
|
||||
if config.number {
|
||||
let nw = buffer.number_width() + 2;
|
||||
buffer.resize(&config, nw, w - nw - 1, h - 2);
|
||||
} else {
|
||||
buffer.resize(&config, 0, w - 1, h - 2);
|
||||
impl EnvironmentAccessHook for ConfigAccessHook {
|
||||
fn read_variable(&mut self, name: &str) -> Option<Value> {
|
||||
let config = self.1.borrow();
|
||||
|
||||
match name {
|
||||
"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 {
|
||||
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,
|
||||
state,
|
||||
machine,
|
||||
env,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn buffer(&self) -> &Buffer {
|
||||
&self.buffer
|
||||
}
|
||||
|
||||
pub fn buffer_mut(&mut self) -> &mut Buffer {
|
||||
&mut self.buffer
|
||||
}
|
||||
|
||||
pub fn exit(&mut self) {
|
||||
self.running = false;
|
||||
}
|
||||
|
||||
pub fn set_status<S: Into<String>>(&mut self, status: S) {
|
||||
self.status.replace(status.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)?;
|
||||
pub fn defun<S, F>(&self, name: S, function: F)
|
||||
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.defun_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 + start < end {
|
||||
write!(self.term, " {0:1$} ", i + start + 1, self.number_width)
|
||||
.map_err(Error::TerminalFmtError)?;
|
||||
});
|
||||
}
|
||||
pub fn defmacro<S, F>(&self, name: S, function: F)
|
||||
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;
|
||||
}
|
||||
|
||||
if i + start == self.buffer.cursor_row() {
|
||||
self.term.reset_style().map_err(Error::TerminalError)?;
|
||||
// TODO pre-render hook
|
||||
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> {
|
||||
self.term
|
||||
.set_cursor_position(self.buffer.height(), 0)
|
||||
.map_err(Error::TerminalError)?;
|
||||
if let TermKey::Char('Q') = key {
|
||||
break;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
let action = self.state.borrow_mut().handle_key(key);
|
||||
|
||||
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)?;
|
||||
match action {
|
||||
Action::Handler(handler) => {
|
||||
self.evaluate_callback(&handler, &[]);
|
||||
}
|
||||
}
|
||||
TopMode::Command => {
|
||||
write!(self.term, " COMMAND ").map_err(Error::TerminalFmtError)?;
|
||||
Action::Command(command) => {
|
||||
let hook = self.state.borrow().command_hook.clone();
|
||||
if let Some(hook) = hook {
|
||||
let words = command
|
||||
.split(' ')
|
||||
.map(Into::into)
|
||||
.map(Value::String)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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.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();
|
||||
self.evaluate_callback(&hook, &words);
|
||||
}
|
||||
}
|
||||
}
|
||||
TermKey::Escape => {
|
||||
self.top_mode = TopMode::Normal;
|
||||
}
|
||||
TermKey::Char(c) if c.is_ascii_graphic() || c == ' ' => self.command.push(c),
|
||||
_ => (),
|
||||
Action::None => (),
|
||||
};
|
||||
}
|
||||
|
||||
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() {
|
||||
logsink::setup_logging(false);
|
||||
|
||||
let args = env::args().collect::<Vec<_>>();
|
||||
if args.len() > 2 {
|
||||
eprintln!("Usage: red [FILE]");
|
||||
@@ -387,19 +220,11 @@ fn main() {
|
||||
}
|
||||
|
||||
let path = args.get(1);
|
||||
let mut state = State::open(path).unwrap();
|
||||
let error = loop {
|
||||
if !state.running {
|
||||
break None;
|
||||
}
|
||||
let mut editor = Editor::new(path).unwrap();
|
||||
let result = editor.run();
|
||||
editor.state.borrow_mut().cleanup();
|
||||
|
||||
if let Err(error) = state.update() {
|
||||
break Some(error);
|
||||
}
|
||||
};
|
||||
state.cleanup();
|
||||
|
||||
if let Some(error) = error {
|
||||
eprintln!("Error: {:?}", error);
|
||||
if let Err(error) = result {
|
||||
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
|
||||
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
|
||||
let arch_dir = user_dir.join("arch").join(env.arch.name());
|
||||
let arch_rc_d = arch_dir.join("rc.d");
|
||||
|
||||
Reference in New Issue
Block a user