red: multi-character keybinds

This commit is contained in:
Mark Poliakov 2023-11-20 13:03:02 +02:00
parent d030e0d6f1
commit 6c8ae720d0
7 changed files with 369 additions and 103 deletions

View File

@ -78,7 +78,7 @@ impl View {
self.cursor_column = col;
if line.display_width(config.tab_width) + 1 <= self.width {
if line.display_width(config.tab_width) < self.width {
self.column_offset = 0;
return;
}
@ -240,6 +240,10 @@ impl Buffer {
self.lines.len()
}
pub fn is_empty(&self) -> bool {
self.lines.is_empty()
}
pub fn cursor_row(&self) -> usize {
self.view.cursor_row
}
@ -277,6 +281,30 @@ impl Buffer {
self.set_position(config, len, self.view.cursor_row);
}
pub fn to_first_line(&mut self, config: &Config) {
let len = self
.lines
.get(self.view.cursor_row)
.map(Line::len)
.unwrap_or(0);
self.set_position(config, len, 0);
}
pub fn to_last_line(&mut self, config: &Config) {
if self.lines.is_empty() {
return;
}
let len = self
.lines
.get(self.view.cursor_row)
.map(Line::len)
.unwrap_or(0);
self.set_position(config, len, self.lines.len() - 1);
}
pub fn set_column(&mut self, config: &Config, x: usize) {
self.set_position(config, x, self.view.cursor_row);
}
@ -324,6 +352,10 @@ impl Buffer {
)
}
pub fn width(&self) -> usize {
self.view.width
}
pub fn height(&self) -> usize {
self.view.height
}
@ -481,8 +513,18 @@ impl Buffer {
self.modified = true;
}
pub fn kill_line(&mut self, config: &Config) {
if self.lines.is_empty() {
return;
}
self.lines.remove(self.view.cursor_row);
self.move_cursor(config, 0, 1);
self.modified = true;
}
pub fn number_width(&mut self) -> usize {
if self.lines.len() == 0 {
if self.lines.is_empty() {
1
} else {
self.lines.len().ilog10() as usize + 1

View File

@ -13,12 +13,15 @@ pub enum Action {
NewlineBefore,
NewlineAfter,
BreakLine,
KillLine,
// Movement
MoveFirstLine,
MoveLastLine,
MoveCharPrev,
MoveCharNext,
MoveLinePrev,
MoveLineNext,
MoveLineBack(usize),
MoveLineForward(usize),
MoveLineStart,
MoveLineEnd,
}
@ -119,13 +122,16 @@ pub fn perform(buffer: &mut Buffer, config: &Config, action: Action) -> Result<(
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::MoveLinePrev => buffer.move_cursor(config, 0, -1),
Action::MoveLineNext => buffer.move_cursor(config, 0, 1),
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(())
}

View File

@ -1,54 +1,66 @@
use std::collections::HashMap;
use crate::{buffer::Mode, command::Action};
use crate::{
buffer::Mode,
command::Action,
keymap::{bind, KeyMap, PrefixNode},
};
pub struct Config {
// TODO must be a power of 2, lol
pub tab_width: usize,
pub number: bool,
pub nmap: HashMap<char, Vec<Action>>,
pub imap: HashMap<char, Vec<Action>>,
}
fn bind<I: IntoIterator<Item = Action>>(key: char, items: I) -> (char, Vec<Action>) {
(key, items.into_iter().map(Into::into).collect())
pub nmap: KeyMap,
pub imap: KeyMap,
}
impl Default for Config {
fn default() -> Self {
use Action::*;
let nmap = KeyMap::from_iter([
bind('i', [InsertBefore]),
bind('a', [InsertAfter]),
bind('h', [MoveCharPrev]),
bind('l', [MoveCharNext]),
bind('j', [MoveLineForward(1)]),
bind("J", [MoveLineForward(25)]),
bind('k', [MoveLineBack(1)]),
bind("K", [MoveLineBack(25)]),
bind("gg", [MoveFirstLine]),
bind("G", [MoveLastLine]),
bind('I', [MoveLineStart, InsertBefore]),
bind('A', [MoveLineEnd, InsertAfter]),
bind('o', [NewlineAfter, MoveLineForward(1), InsertBefore]),
bind('O', [NewlineBefore, MoveLineBack(1), InsertBefore]),
bind("dd", [KillLine]),
]);
let imap = KeyMap::from_iter([
bind('\x7F', [EraseBackward]),
bind(
'\n',
[BreakLine, MoveLineForward(1), MoveLineStart, InsertBefore],
),
bind(
'\x0D',
[BreakLine, MoveLineForward(1), MoveLineStart, InsertBefore],
),
]);
Self {
tab_width: 4,
number: true,
nmap: HashMap::from_iter([
bind('i', [InsertBefore]),
bind('a', [InsertAfter]),
bind('h', [MoveCharPrev]),
bind('l', [MoveCharNext]),
bind('j', [MoveLineNext]),
bind('k', [MoveLinePrev]),
bind('I', [MoveLineStart, InsertBefore]),
bind('A', [MoveLineEnd, InsertAfter]),
bind('o', [NewlineAfter, MoveLineNext, InsertBefore]),
bind('O', [NewlineBefore, MoveLinePrev, InsertBefore]),
]),
imap: HashMap::from_iter([
bind('\x7F', [EraseBackward]),
bind('\n', [BreakLine, MoveLineNext, MoveLineStart, InsertBefore]),
bind('\x0D', [BreakLine, MoveLineNext, MoveLineStart, InsertBefore]),
])
nmap,
imap,
}
}
}
impl Config {
pub fn key(&self, mode: Mode, key: char) -> Option<&[Action]> {
pub fn key(&self, mode: Mode, key: &str) -> Option<&PrefixNode<String, Vec<Action>>> {
match mode {
Mode::Normal => self.nmap.get(&key),
Mode::Insert => self.imap.get(&key)
}.map(AsRef::as_ref)
Mode::Normal => self.nmap.get(key),
Mode::Insert => self.imap.get(key),
}
}
}

100
red/src/keymap/map.rs Normal file
View File

@ -0,0 +1,100 @@
use std::{
borrow::Borrow,
collections::{HashMap, HashSet},
fmt,
hash::Hash,
};
pub trait Prefix: Sized + Eq + Hash + Clone {
fn prefix(&self) -> Option<Self>;
}
#[derive(PartialEq)]
pub enum PrefixNode<K: Prefix, V> {
Prefix(HashSet<K>),
Leaf(V),
}
#[derive(Default)]
pub struct PrefixMap<K: Prefix, V> {
map: HashMap<K, PrefixNode<K, V>>,
}
impl<K: Prefix, V> PrefixNode<K, V> {
pub fn empty_prefix() -> Self {
Self::Prefix(HashSet::new())
}
pub fn add_suffix(&mut self, suffix: K) {
if let Self::Prefix(set) = self {
set.insert(suffix);
} else {
*self = Self::Prefix(HashSet::from_iter([suffix]));
}
}
}
impl<K: Prefix + fmt::Debug, V: fmt::Debug> fmt::Debug for PrefixNode<K, V> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Leaf(leaf) => f.debug_struct("Leaf").field("value", leaf).finish(),
Self::Prefix(suffixes) => f.debug_struct("Prefix").field("suffixes", suffixes).finish(),
}
}
}
impl<K: Prefix, V> PrefixMap<K, V> {
pub fn new() -> Self {
Self {
map: HashMap::new(),
}
}
pub fn insert(&mut self, key: K, value: V) {
if let Some(prefix) = key.prefix() {
self.map
.entry(prefix)
.or_insert_with(PrefixNode::empty_prefix)
.add_suffix(key.clone());
}
// TODO remove all suffixes of `key`, if those exist
self.map.insert(key, PrefixNode::Leaf(value));
}
pub fn get<N>(&self, key: &N) -> Option<&PrefixNode<K, V>>
where
K: Borrow<N>,
N: Eq + Hash + ?Sized,
{
self.map.get(key)
}
}
impl<K: Prefix, V> FromIterator<(K, V)> for PrefixMap<K, V> {
fn from_iter<T: IntoIterator<Item = (K, V)>>(iter: T) -> Self {
let mut this = Self::new();
for (k, v) in iter {
this.insert(k, v);
}
this
}
}
impl<K: Prefix + fmt::Debug, V: fmt::Debug> fmt::Debug for PrefixMap<K, V> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut dm = f.debug_map();
for (k, v) in self.map.iter().filter_map(|(k, v)| {
if let PrefixNode::Leaf(leaf) = v {
Some((k, leaf))
} else {
None
}
}) {
dm.entry(k, v);
}
dm.finish()
}
}

85
red/src/keymap/mod.rs Normal file
View File

@ -0,0 +1,85 @@
use std::{
borrow::Borrow,
hash::Hash,
};
use crate::command::Action;
use self::map::{PrefixMap, Prefix};
pub use self::map::PrefixNode;
mod map;
#[derive(Debug, Default)]
pub struct KeyMap {
map: PrefixMap<String, Vec<Action>>,
}
impl KeyMap {
pub fn new() -> Self {
Self {
map: PrefixMap::new(),
}
}
pub fn get<N>(&self, key: &N) -> Option<&PrefixNode<String, Vec<Action>>>
where
String: Borrow<N>,
N: Eq + Hash + ?Sized,
{
self.map.get(key)
}
}
impl FromIterator<(String, Vec<Action>)> for KeyMap {
fn from_iter<T: IntoIterator<Item = (String, Vec<Action>)>>(iter: T) -> Self {
Self {
map: PrefixMap::from_iter(iter),
}
}
}
impl Prefix for String {
fn prefix(&self) -> Option<Self> {
if self.is_empty() {
None
} else {
let mut res = self.clone();
res.remove(res.len() - 1);
Some(res)
}
}
}
pub fn bind<I: Into<String>, V: IntoIterator<Item = Action>>(
key: I,
actions: V,
) -> (String, Vec<Action>) {
(key.into(), actions.into_iter().collect())
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use crate::{command::Action, keymap::PrefixNode};
use super::{bind, KeyMap};
#[test]
fn from_iter() {
let map = KeyMap::from_iter([
bind("aa", [Action::InsertBefore, Action::MoveLineEnd]),
]);
assert_eq!(
map.get("a"),
Some(&PrefixNode::Prefix(HashSet::from_iter(["aa".to_owned()])))
);
assert_eq!(
map.get("aa"),
Some(&PrefixNode::Leaf([Action::InsertBefore, Action::MoveLineEnd].to_vec()))
);
}
}

View File

@ -6,12 +6,14 @@ use std::{env, fmt::Write, path::Path};
use buffer::{Buffer, Mode, SetMode};
use config::Config;
use error::Error;
use keymap::PrefixNode;
use term::{Clear, Color, Term};
pub mod buffer;
pub mod command;
pub mod config;
pub mod error;
pub mod keymap;
pub mod line;
pub mod term;
@ -27,52 +29,13 @@ pub struct State {
command: String,
message: Option<String>,
status: Option<String>,
key: String,
top_mode: TopMode,
config: Config,
running: bool,
number_width: usize,
}
fn display_modeline(term: &mut Term, top_mode: TopMode, buf: &Buffer) -> Result<(), Error> {
term.set_cursor_position(buf.height(), 0)?;
let bg = match (top_mode, buf.mode()) {
(TopMode::Normal, Mode::Normal) => Color::Yellow,
(TopMode::Normal, Mode::Insert) => Color::Cyan,
(TopMode::Command, _) => Color::Green,
};
term.set_background(bg)?;
term.set_foreground(Color::Black)?;
match top_mode {
TopMode::Normal => {
write!(term, " {} ", buf.mode().as_str()).map_err(Error::TerminalFmtError)?;
if buf.is_modified() {
term.set_background(Color::Magenta)?;
term.set_foreground(Color::Default)?;
} else {
term.set_foreground(Color::Green)?;
term.set_background(Color::Default)?;
}
}
TopMode::Command => {
write!(term, " COMMAND ").map_err(Error::TerminalFmtError)?;
term.set_foreground(Color::Green)?;
term.set_background(Color::Default)?;
}
}
let name = buf.name().map(String::as_str).unwrap_or("<unnamed>");
write!(term, " {}", name).map_err(Error::TerminalFmtError)?;
term.clear(Clear::LineToEnd)?;
term.reset_style()?;
Ok(())
}
impl State {
pub fn open<P: AsRef<Path>>(path: Option<P>) -> Result<Self, Error> {
let config = Config::default();
@ -96,6 +59,7 @@ impl State {
message: None,
status: None,
command: String::new(),
key: String::new(),
running: true,
buffer,
term,
@ -150,6 +114,56 @@ impl State {
Ok(())
}
fn display_modeline(&mut self) -> Result<(), Error> {
self.term.set_cursor_position(self.buffer.height(), 0)?;
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)?;
self.term.set_foreground(Color::Black)?;
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)?;
self.term.set_foreground(Color::Default)?;
} else {
self.term.set_foreground(Color::Green)?;
self.term.set_background(Color::Default)?;
}
}
TopMode::Command => {
write!(self.term, " COMMAND ").map_err(Error::TerminalFmtError)?;
self.term.set_foreground(Color::Green)?;
self.term.set_background(Color::Default)?;
}
}
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)?;
self.term
.set_cursor_position(self.buffer.height(), self.buffer.width() - 10)?;
self.term.set_foreground(Color::White)?;
write!(self.term, "{}", self.key).map_err(Error::TerminalFmtError)?;
self.term.reset_style()?;
Ok(())
}
fn display(&mut self) -> Result<(), Error> {
if self.buffer.is_dirty() {
self.term.clear(Clear::All)?;
@ -178,7 +192,8 @@ impl State {
return Ok(());
}
display_modeline(&mut self.term, self.top_mode, &self.buffer)?;
self.display_modeline()?;
match self.top_mode {
TopMode::Normal => {
self.buffer
@ -222,32 +237,47 @@ impl State {
Ok(())
}
fn handle_mode_key(&mut self, mode: Mode, key: char) -> Result<(), Error> {
let buffer = &mut self.buffer;
self.key.push(key);
match self.config.key(mode, &self.key) {
Some(PrefixNode::Leaf(actions)) => {
self.key.clear();
for &action in actions {
command::perform(buffer, &self.config, action)?;
}
}
Some(PrefixNode::Prefix(_)) => {}
None => {
self.key.clear();
}
}
if self.buffer().mode() != Mode::Normal {
self.status = None;
}
Ok(())
}
fn handle_normal_key(&mut self, key: char) -> Result<(), Error> {
match key {
'\x1B' => {
self.key.clear();
self.buffer.set_mode(&self.config, SetMode::Normal);
Ok(())
}
':' => {
self.key.clear();
self.command.clear();
self.status = None;
self.top_mode = TopMode::Command;
Ok(())
}
_ => {
let buffer = &mut self.buffer;
if let Some(actions) = self.config.key(Mode::Normal, key) {
for &action in actions {
command::perform(buffer, &self.config, action)?;
}
}
if self.buffer().mode() != Mode::Normal {
self.status = None;
}
Ok(())
}
_ => self.handle_mode_key(Mode::Normal, key),
}
}
@ -261,16 +291,7 @@ impl State {
self.buffer.insert(&self.config, key);
Ok(())
}
_ => {
let buffer = &mut self.buffer;
if let Some(actions) = self.config.key(Mode::Insert, key) {
for &action in actions {
command::perform(buffer, &self.config, action)?;
}
}
Ok(())
}
_ => self.handle_mode_key(Mode::Insert, key),
}
}

View File

@ -172,7 +172,7 @@ impl Term {
impl fmt::Write for Term {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.stdout.write_all(s.as_bytes()).map_err(|_| fmt::Error::default())
self.stdout.write_all(s.as_bytes()).map_err(|_| fmt::Error)
}
}