use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::config::{Action, KeyMap};
use crate::editor::{EditorState, Mode};
use super::diff_keys;
use super::ModeHandler;
pub struct DefaultHandler<'k> {
keymap: &'k KeyMap,
}
impl<'k> DefaultHandler<'k> {
pub fn new(keymap: &'k KeyMap) -> Self {
Self { keymap }
}
}
impl<'k> ModeHandler for DefaultHandler<'k> {
fn handle(&mut self, event: KeyEvent, state: &EditorState) -> Option<Action> {
if state.mode == Mode::Diff {
if let Some(action) = diff_review_handle(&event) {
return Some(action);
}
}
if let Some(search) = state.search.as_ref() {
if let Some(action) = crate::search::search_action_for(&event) {
let capturing = search.is_replace_flow();
if capturing
|| matches!(
action,
Action::SearchNext | Action::SearchPrev | Action::SearchExit
)
{
return Some(action);
}
}
}
if let Some(action) = self.keymap.action_for(&event) {
if state.mode == Mode::Preview
&& event.modifiers.contains(KeyModifiers::CONTROL)
&& !preview_safe_action(action)
{
return None;
}
return Some(action.clone());
}
if is_ctrl_backspace(&event) {
if state.mode == Mode::Preview {
return None;
}
return Some(Action::DeleteWordBack);
}
if let KeyCode::Char(ch) = event.code {
let only_shift =
event.modifiers == KeyModifiers::NONE || event.modifiers == KeyModifiers::SHIFT;
if only_shift {
return Some(Action::InsertChar(ch));
}
}
None
}
}
fn preview_safe_action(action: &Action) -> bool {
matches!(
action,
Action::Quit
| Action::Copy
| Action::SelectAll
| Action::Save
| Action::Open
| Action::ToggleRawMode
| Action::EnterEditMode
| Action::ExitToPreview
| Action::ScrollUp
| Action::ScrollDown
| Action::ScrollPageUp
| Action::ScrollPageDown
| Action::ScrollToTop
| Action::ScrollToBottom
| Action::ShowCommandPalette
| Action::ShowMarkdownCheatSheet
| Action::BrowseTips
| Action::OpenDoc(_)
| Action::ShowAbout
| Action::CheckForUpdates
| Action::OpenSettings
| Action::OpenWelcome
| Action::OpenKeybinds
| Action::SwitchTheme
| Action::CreateCustomTheme
| Action::ExportHtml
| Action::OpenConfigFolder
| Action::OpenInExternalEditor
| Action::ToggleTableButtons
| Action::ToggleBigH1
| Action::ToggleLineNumbers
| Action::ToggleBlinkCursor
| Action::ToggleAutosave
| Action::ToggleVisualLineNav
| Action::ToggleVimMode
| Action::ToggleLimitWidth
| Action::ToggleDiffOnChange
| Action::InsertTable
| Action::SaveAs
| Action::GoToSection
| Action::OpenSearch
)
}
fn diff_review_handle(event: &KeyEvent) -> Option<Action> {
diff_keys::diff_action_for(event)
}
pub(crate) fn is_ctrl_backspace(event: &KeyEvent) -> bool {
let has_ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
match event.code {
KeyCode::Backspace if has_ctrl => true,
KeyCode::Char('\x08') => true,
KeyCode::Char('\x7f') if has_ctrl => true,
KeyCode::Char('h') | KeyCode::Char('H') if has_ctrl => true,
_ => false,
}
}
pub(crate) fn is_ctrl_delete(event: &KeyEvent) -> bool {
matches!(event.code, KeyCode::Delete) && event.modifiers.contains(KeyModifiers::CONTROL)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{KeyBindingOverrides, KeyMap};
use crate::document::Buffer;
use crate::editor::{EditorState, Mode};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
fn keymap() -> KeyMap {
KeyMap::build(&KeyBindingOverrides::default()).unwrap()
}
fn state(mode: Mode) -> EditorState {
let theme = Box::leak(Box::new(crate::config::Theme::default()));
let mut s = EditorState::new(Buffer::new(), theme);
s.mode = mode;
s
}
fn readonly_state(mode: Mode) -> EditorState {
let mut s = state(mode);
s.readonly = true;
s
}
#[test]
fn a_read_only_page_resolves_keys_through_the_ordinary_keymap() {
let km = keymap();
let mut h = DefaultHandler::new(&km);
let readonly = readonly_state(Mode::Preview);
let editable = state(Mode::Preview);
for (code, mods) in [
(KeyCode::Char('j'), KeyModifiers::NONE),
(KeyCode::Char('k'), KeyModifiers::NONE),
(KeyCode::Char('g'), KeyModifiers::NONE),
(KeyCode::Char(' '), KeyModifiers::NONE),
(KeyCode::Char('d'), KeyModifiers::CONTROL),
(KeyCode::Char('u'), KeyModifiers::CONTROL),
(KeyCode::Home, KeyModifiers::NONE),
(KeyCode::End, KeyModifiers::NONE),
] {
let ev = KeyEvent::new(code, mods);
assert_eq!(
h.handle(ev, &readonly),
h.handle(ev, &editable),
"{code:?} must resolve the same on a read-only page"
);
}
}
#[test]
fn an_ordinary_preview_still_edits_on_a_letter_key() {
let km = keymap();
let mut h = DefaultHandler::new(&km);
let st = state(Mode::Preview);
assert_eq!(
h.handle(KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE), &st),
Some(Action::InsertChar('j'))
);
}
#[test]
fn unbound_keys_still_reach_the_keymap_on_a_read_only_page() {
let km = keymap();
let mut h = DefaultHandler::new(&km);
let st = readonly_state(Mode::Preview);
assert_eq!(
h.handle(
KeyEvent::new(KeyCode::Char('q'), KeyModifiers::CONTROL),
&st
),
Some(Action::Quit)
);
}
#[test]
fn a_read_only_page_no_longer_suppresses_the_keymap_bypassing_arms() {
let km = keymap();
let mut h = DefaultHandler::new(&km);
let st = readonly_state(Mode::Preview);
assert_eq!(
h.handle(KeyEvent::new(KeyCode::Char('z'), KeyModifiers::NONE), &st),
Some(Action::InsertChar('z'))
);
let ev = KeyEvent::new(
KeyCode::Backspace,
KeyModifiers::CONTROL | KeyModifiers::SHIFT,
);
assert_eq!(h.handle(ev, &st), None);
assert_eq!(
h.handle(ev, &readonly_state(Mode::Rendered)),
Some(Action::DeleteWordBack)
);
}
#[test]
fn bound_chords_still_resolve_on_a_read_only_page() {
let km = keymap();
let mut h = DefaultHandler::new(&km);
let st = readonly_state(Mode::Rendered);
let ev = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::CONTROL);
assert_eq!(h.handle(ev, &st), Some(Action::Quit));
}
#[test]
fn an_ordinary_document_still_types() {
let km = keymap();
let mut h = DefaultHandler::new(&km);
let st = state(Mode::Rendered);
assert_eq!(
h.handle(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE), &st),
Some(Action::InsertChar('a'))
);
}
#[test]
fn ctrl_q_returns_quit() {
let km = keymap();
let mut handler = DefaultHandler::new(&km);
let event = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::CONTROL);
assert_eq!(
handler.handle(event, &state(Mode::Preview)),
Some(Action::Quit)
);
}
#[test]
fn printable_char_returns_insert() {
let km = keymap();
let mut handler = DefaultHandler::new(&km);
let event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
assert_eq!(
handler.handle(event, &state(Mode::Rendered)),
Some(Action::InsertChar('a'))
);
}
#[test]
fn ctrl_char_not_insert() {
let km = keymap();
let mut handler = DefaultHandler::new(&km);
let event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL);
let action = handler.handle(event, &state(Mode::Rendered));
assert_ne!(action, Some(Action::InsertChar('a')));
}
#[test]
fn ctrl_c_returns_copy() {
let km = keymap();
let mut handler = DefaultHandler::new(&km);
let event = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
assert_eq!(
handler.handle(event, &state(Mode::Rendered)),
Some(Action::Copy)
);
}
#[test]
fn preview_ctrl_c_and_ctrl_a_still_fire() {
let km = keymap();
let mut handler = DefaultHandler::new(&km);
assert_eq!(
handler.handle(
KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL),
&state(Mode::Preview)
),
Some(Action::Copy)
);
assert_eq!(
handler.handle(
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL),
&state(Mode::Preview)
),
Some(Action::SelectAll)
);
assert_eq!(
handler.handle(
KeyEvent::new(KeyCode::Char('q'), KeyModifiers::CONTROL),
&state(Mode::Preview)
),
Some(Action::Quit)
);
}
#[test]
fn preview_suppresses_non_safelisted_ctrl_chords() {
let km = keymap();
let mut handler = DefaultHandler::new(&km);
for ch in ['z', 'd', 'x', 'v'] {
let event = KeyEvent::new(KeyCode::Char(ch), KeyModifiers::CONTROL);
assert_eq!(
handler.handle(event, &state(Mode::Preview)),
None,
"ctrl+{ch} should be suppressed in Preview mode",
);
}
}
}