red: add syntax highlighting and lysp support

This commit is contained in:
2026-05-28 15:51:32 +03:00
parent 37ad3702d0
commit 4b98ec1ce2
31 changed files with 3210 additions and 1021 deletions
+8 -191
View File
@@ -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
View File
@@ -1 +1 @@
(import "examples/io.lysp")
(import "io.lysp")
+25 -6
View File
@@ -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))
}
+1 -1
View File
@@ -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)),
+6 -2
View File
@@ -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),
+15 -8
View File
@@ -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();
+28 -5
View File
@@ -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)
+8
View File
@@ -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() })
+6 -1
View File
@@ -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
}
+614 -211
View File
File diff suppressed because it is too large Load Diff
+15 -6
View File
@@ -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"
+30
View File
@@ -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)
)
+57
View File
@@ -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"))
+169
View File
@@ -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)
+60
View File
@@ -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")
)
+104
View File
@@ -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)
}
}
+127 -24
View File
@@ -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(&current_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;
}
+45
View File
@@ -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(())
}
}
+10 -134
View File
@@ -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(())
}
+113 -66
View File
@@ -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),
// }
// }
// }
+11
View File
@@ -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())
}
}
+269
View File
@@ -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(&current_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;
}
}
+10 -29
View File
@@ -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
View File
@@ -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}");
}
}
+206
View File
@@ -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);
}
}
+333
View File
@@ -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);
}
+446
View File
@@ -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();
}
}
+243
View File
@@ -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);
}
}
+7
View File
@@ -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");