use crate::caret::CaretShape;
use crate::edit::Buffer;
use crate::event::{Key, KeyCode, Modifiers};
use crate::keymap::{Keymap, keys};
pub const CONTEXT: &str = "text_area";
const CTRL_SHIFT_Z: Key = Key::with(
KeyCode::Char('z'),
Modifiers {
shift: true,
ctrl: true,
alt: false,
},
);
const ACTIONS: &[(&str, &str)] = &[
("left", "move left"),
("right", "move right"),
("up", "line up"),
("down", "line down"),
("word-left", "word left"),
("word-right", "word right"),
("home", "start of the line"),
("end", "end of the line"),
("top", "start of the text"),
("bottom", "end of the text"),
("page-up", "page up"),
("page-down", "page down"),
("paragraph-up", "paragraph up"),
("paragraph-down", "paragraph down"),
("select-all", "select everything"),
("set-mark", "set the mark: moves select"),
("deselect", "drop the selection"),
("copy", "copy the selection"),
("cut", "cut the selection"),
("undo", "undo"),
("redo", "redo"),
("newline", "new line"),
("indent", "indent"),
("outdent", "outdent"),
("backspace", "delete back"),
("delete", "delete forward"),
("delete-word", "delete a word back"),
("delete-next-word", "delete a word forward"),
("delete-to-start", "delete to the line's start"),
("delete-to-end", "delete to the line's end"),
("kill-to-end", "kill to the line's end"),
("kill-to-start", "kill to the line's start"),
("kill-word", "kill the selection or a word back"),
("kill-next-word", "kill a word forward"),
("kill-previous-word", "kill to the word's start"),
("yank", "yank the last kill"),
("yank-pop", "swap the yank for an older kill"),
("find-next", "next match"),
("find-previous", "previous match"),
];
fn keymap_of(bound: &[(&str, Vec<Key>)]) -> Keymap {
let mut keymap = Keymap::new(CONTEXT);
for (action, description) in ACTIONS {
let keys = bound
.iter()
.find(|(name, _)| name == action)
.map(|(_, keys)| keys.clone())
.unwrap_or_default();
keymap = keymap.bind(*action, keys, *description);
}
keymap
}
pub fn editor_keymap() -> Keymap {
let mut redo = keys("ctrl+y ctrl+shift+z");
redo.push(CTRL_SHIFT_Z);
keymap_of(&[
("left", keys("left")),
("right", keys("right")),
("up", keys("up")),
("down", keys("down")),
("word-left", keys("ctrl+left")),
("word-right", keys("ctrl+right")),
("home", keys("home")),
("end", keys("end")),
("top", keys("ctrl+home")),
("bottom", keys("ctrl+end")),
("page-up", keys("pageup")),
("page-down", keys("pagedown")),
("select-all", keys("ctrl+a")),
("deselect", keys("escape")),
("copy", keys("ctrl+c")),
("cut", keys("ctrl+x")),
("undo", keys("ctrl+z")),
("redo", redo),
("newline", keys("enter")),
("indent", keys("tab")),
("outdent", keys("backtab")),
("backspace", keys("backspace")),
("delete", keys("delete")),
("delete-word", keys("ctrl+w")),
("delete-to-start", keys("ctrl+u")),
("delete-to-end", keys("ctrl+k")),
("find-next", keys("f3")),
("find-previous", keys("shift+f3")),
])
}
pub fn emacs_keymap() -> Keymap {
keymap_of(&[
("left", keys("left ctrl+b")),
("right", keys("right ctrl+f")),
("up", keys("up ctrl+p")),
("down", keys("down ctrl+n")),
("word-left", keys("ctrl+left alt+b")),
("word-right", keys("ctrl+right alt+f")),
("home", keys("home ctrl+a")),
("end", keys("end ctrl+e")),
("top", keys("ctrl+home alt+<")),
("bottom", keys("ctrl+end alt+>")),
("page-up", keys("pageup alt+v")),
("page-down", keys("pagedown ctrl+v")),
("paragraph-up", keys("alt+{")),
("paragraph-down", keys("alt+}")),
("set-mark", keys("ctrl+space")),
("deselect", keys("escape ctrl+g")),
("copy", keys("ctrl+c alt+w")),
("cut", keys("ctrl+x")),
("undo", keys("ctrl+z ctrl+/ ctrl+_ ctrl+7")),
(
"redo",
vec![Key::parse("ctrl+shift+z").expect("a key"), CTRL_SHIFT_Z],
),
("newline", keys("enter ctrl+j")),
("indent", keys("tab")),
("outdent", keys("backtab")),
("backspace", keys("backspace ctrl+h")),
("delete", keys("delete ctrl+d")),
("kill-to-end", keys("ctrl+k")),
("kill-to-start", keys("ctrl+u")),
("kill-word", keys("ctrl+w")),
("kill-next-word", keys("alt+d")),
("kill-previous-word", keys("alt+backspace")),
("yank", keys("ctrl+y")),
("yank-pop", keys("alt+y")),
("find-next", keys("f3 ctrl+s")),
("find-previous", keys("shift+f3 ctrl+r")),
])
}
pub fn resolve(keymap: &Keymap, key: Key) -> Option<(String, bool)> {
let bindings = keymap.bindings();
let find = |key: Key| {
bindings
.iter()
.find(|binding| !binding.sequence && binding.keys.contains(&key))
.map(|binding| binding.action.clone())
};
let m = key.modifiers;
let is_char = matches!(key.code, KeyCode::Char(_));
let mut tries = vec![(key, false)];
if m.shift {
let plain = Modifiers { shift: false, ..m };
tries.push((Key::with(key.code, plain), !is_char));
}
let ctrl_tab = key.code == KeyCode::Tab && m.ctrl;
if !ctrl_tab && (!is_char || m.ctrl) {
if m.alt {
let ctrl = Modifiers {
ctrl: m.ctrl,
..Modifiers::NONE
};
tries.push((Key::with(key.code, ctrl), m.shift && !is_char));
}
if !is_char && m.ctrl {
tries.push((Key::new(key.code), m.shift));
}
}
tries
.into_iter()
.find_map(|(key, select)| find(key).map(|action| (action, select)))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Act {
pub select: bool,
pub editable: bool,
pub single_line: bool,
pub page: usize,
}
impl Default for Act {
fn default() -> Act {
Act {
select: false,
editable: true,
single_line: false,
page: 10,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Outcome {
Used,
Unused,
Unknown,
}
pub fn is_edit(action: &str) -> bool {
matches!(
action,
"undo"
| "redo"
| "newline"
| "indent"
| "outdent"
| "backspace"
| "delete"
| "cut"
| "yank"
| "yank-pop"
) || action.starts_with("delete-")
|| action.starts_with("kill-")
}
pub fn perform(buffer: &mut Buffer, action: &str, act: Act) -> Outcome {
let select = act.select;
let vertical = matches!(
action,
"up" | "down"
| "page-up"
| "page-down"
| "paragraph-up"
| "paragraph-down"
| "newline"
| "indent"
| "outdent"
);
if act.single_line && vertical || !act.editable && is_edit(action) {
return if ACTIONS.iter().any(|(name, _)| *name == action) {
Outcome::Unused
} else {
Outcome::Unknown
};
}
let page = act.page.max(1) as isize;
match action {
"left" => buffer.left(select),
"right" => buffer.right(select),
"up" => buffer.vertical(-1, select),
"down" => buffer.vertical(1, select),
"word-left" => buffer.word_left(select),
"word-right" => buffer.word_right(select),
"home" => buffer.home(select),
"end" => buffer.end(select),
"top" => buffer.top(select),
"bottom" => buffer.bottom(select),
"page-up" => buffer.vertical(-page, select),
"page-down" => buffer.vertical(page, select),
"paragraph-up" => buffer.paragraph_up(select),
"paragraph-down" => buffer.paragraph_down(select),
"select-all" => buffer.select_all(),
"set-mark" => buffer.set_mark(),
"deselect" => {
if buffer.selection().is_none() && !buffer.marking() {
return Outcome::Unused;
}
buffer.clear_mark();
}
"undo" => {
buffer.undo();
}
"redo" => {
buffer.redo();
}
"newline" => buffer.insert("\n"),
"indent" => buffer.indent(),
"outdent" => buffer.outdent(),
"backspace" => buffer.backspace(),
"delete" => buffer.delete(),
"delete-word" => buffer.delete_word(),
"delete-next-word" => buffer.delete_next_word(),
"delete-to-start" => buffer.delete_to_start(),
"delete-to-end" => buffer.delete_to_end(),
"kill-to-end" => buffer.kill_to_end(),
"kill-to-start" => buffer.kill_to_start(),
"kill-word" => buffer.kill_word(),
"kill-next-word" => buffer.kill_next_word(),
"kill-previous-word" => buffer.kill_previous_word(),
"yank" => {
buffer.yank();
}
"yank-pop" => {
buffer.yank_pop();
}
"find-next" | "find-previous" => {
if buffer.search().is_none_or(|s| s.pattern().is_empty()) {
return Outcome::Unused;
}
buffer.select_match(action == "find-next");
}
_ => return Outcome::Unknown,
}
Outcome::Used
}
pub struct ModeCx<'a> {
pub buffer: &'a mut Buffer,
pub editable: bool,
pub single_line: bool,
pub page: usize,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ModeKey {
Used,
Pass,
Unused,
Copy(String),
}
pub trait KeyMode {
fn key(&mut self, key: Key, cx: &mut ModeCx<'_>) -> ModeKey;
fn caret_shape(&self) -> Option<CaretShape> {
None
}
fn label(&self) -> Option<String> {
None
}
fn pending(&self) -> Option<String> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::edit::Pos;
fn run(keymap: &Keymap, buffer: &mut Buffer, keys: &str) {
for name in keys.split(' ') {
let key = Key::parse(name).expect("a key");
let (action, select) = resolve(keymap, key).unwrap_or_else(|| panic!("{name}"));
let act = Act {
select,
..Act::default()
};
perform(buffer, &action, act);
}
}
#[test]
fn modifiers_fall_back_as_the_hard_coded_keys_did() {
let keymap = editor_keymap();
let key = |name: &str| resolve(&keymap, Key::parse(name).unwrap());
let is = |name: &str, action: &str, select: bool| {
assert_eq!(key(name), Some((action.to_string(), select)), "{name}");
};
is("shift+left", "left", true);
is("ctrl+shift+left", "word-left", true);
is("ctrl+alt+left", "word-left", false);
is("alt+left", "left", false);
is("alt+up", "up", false);
is("ctrl+shift+home", "top", true);
is("ctrl+enter", "newline", false);
is("ctrl+backspace", "backspace", false);
is("ctrl+alt+w", "delete-word", false);
let ctrl_shift_y = Key::with(
KeyCode::Char('y'),
Modifiers {
shift: true,
..Modifiers::CTRL
},
);
assert_eq!(resolve(&keymap, ctrl_shift_y), Some(("redo".into(), false)));
is("ctrl+shift+z", "redo", false);
assert_eq!(resolve(&keymap, CTRL_SHIFT_Z), Some(("redo".into(), false)));
assert_eq!(key("ctrl+tab"), None, "Ctrl+Tab moves the focus");
assert_eq!(key("alt+x"), None);
assert_eq!(key("x"), None, "typed");
assert_eq!(key("ctrl+h"), None, "Ctrl+H is not Backspace by default");
}
#[test]
fn rebinding_moves_a_key() {
let mut keymap = editor_keymap();
keymap.rebind("yank", keys("ctrl+y"));
let key = Key::parse("ctrl+y").unwrap();
assert_eq!(resolve(&keymap, key), Some(("yank".into(), false)));
assert_eq!(
keymap.keys("redo"),
keys("ctrl+shift+z")
.into_iter()
.chain([CTRL_SHIFT_Z])
.collect::<Vec<_>>()
);
}
#[test]
fn emacs_kills_and_yanks() {
let keymap = emacs_keymap();
let mut b = Buffer::new("one two three");
run(&keymap, &mut b, "alt+f alt+d alt+d ctrl+e ctrl+y");
assert_eq!(b.text(), "one two three");
run(&keymap, &mut b, "ctrl+a ctrl+k ctrl+y ctrl+y");
assert_eq!(b.text(), "one two threeone two three");
run(&keymap, &mut b, "ctrl+a ctrl+f ctrl+space alt+f");
assert_eq!(b.selected_text().as_deref(), Some("ne"));
run(&keymap, &mut b, "ctrl+g ctrl+b ctrl+h");
assert_eq!(b.caret(), Pos::new(0, 1));
assert!(b.text().starts_with("oe two"));
}
#[test]
fn read_only_and_single_line_leave_keys_to_the_app() {
let mut b = Buffer::new("x");
let read_only = Act {
editable: false,
..Act::default()
};
assert_eq!(perform(&mut b, "backspace", read_only), Outcome::Unused);
assert_eq!(perform(&mut b, "end", read_only), Outcome::Used);
let line = Act {
single_line: true,
..Act::default()
};
assert_eq!(perform(&mut b, "up", line), Outcome::Unused);
assert_eq!(perform(&mut b, "deselect", line), Outcome::Unused);
assert_eq!(perform(&mut b, "copy", line), Outcome::Unknown);
assert_eq!(perform(&mut b, "find-next", line), Outcome::Unused);
assert_eq!(perform(&mut b, "no-such", line), Outcome::Unknown);
}
}