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::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
}
#[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",
);
}
}
}