use crate::editor::{events::AppEvent, Editor};
use strop_core::frontend_input::Key;
use strop_core::frontend_input::{Input, KeyCode, KeyEvent, KeyKind};
impl Editor {
pub(crate) fn handle_frontend_input(&mut self, input: Input) {
if matches!(&input, Input::Key(key) if key.kind != KeyKind::Press) {
self.terminals.keyboard = strop_terminal::model::SUPPORTED_KEYBOARD_FLAGS;
}
if self.terminal_owns_input() {
self.feed_terminal(input);
return;
}
match input {
Input::Key(key) => {
for event in editor_events(key).into_iter().flatten() {
self.handle_app_event(event);
}
}
Input::Paste(text) => self.handle_app_event(AppEvent::Paste(text)),
Input::Text(text) => {
for ch in text.chars() {
self.feed(Key::Char(ch));
}
}
}
}
}
fn editor_events(mut key: KeyEvent) -> [Option<AppEvent>; 2] {
if key.kind == KeyKind::Release {
return [None, None];
}
let alt = key.modifiers.alt;
key.modifiers.alt = false;
let base = editor_key(key).map(AppEvent::EditorKey);
if alt {
[Some(AppEvent::EditorKey(Key::Esc)), base]
} else {
[base, None]
}
}
fn editor_key(key: KeyEvent) -> Option<Key> {
let control = key.modifiers.control;
Some(match key.code {
KeyCode::Char('c') if control => Key::CtrlC,
KeyCode::Char('\x03') => Key::CtrlC,
KeyCode::Escape => Key::Esc,
KeyCode::Enter => Key::Enter,
KeyCode::Char('d') if control => Key::CtrlD,
KeyCode::Char('u') if control => Key::CtrlU,
KeyCode::Char('f') if control => Key::CtrlF,
KeyCode::Char('b') if control => Key::CtrlB,
KeyCode::Char('6') if control => Key::CtrlCaret,
KeyCode::Char('\x1e') => Key::CtrlCaret,
KeyCode::Char('\x04') => Key::CtrlD,
KeyCode::Char(' ') if control => Key::CtrlSpace,
KeyCode::Null | KeyCode::Char('\0') => Key::CtrlSpace,
KeyCode::Char('o') if control => Key::CtrlO,
KeyCode::Char('\x0f') => Key::CtrlO,
KeyCode::Backspace => Key::Backspace,
KeyCode::Tab if key.modifiers.shift => Key::Backtab,
KeyCode::Tab => Key::Tab,
KeyCode::Char('\x12') => Key::CtrlR,
KeyCode::Char('\x17') => Key::CtrlW,
KeyCode::Char('\x18') => Key::CtrlX,
KeyCode::BackTab => Key::Backtab,
KeyCode::Up => Key::Up,
KeyCode::Down => Key::Down,
KeyCode::Left => Key::Left,
KeyCode::Right => Key::Right,
KeyCode::Char('i') if control => Key::Tab,
KeyCode::Char('m') if control => Key::Enter,
KeyCode::Char('[') if control => Key::Esc,
KeyCode::Char('n') if control => Key::CtrlN,
KeyCode::Char('p') if control => Key::CtrlP,
KeyCode::Char('y') if control => Key::CtrlY,
KeyCode::Char('e') if control => Key::CtrlE,
KeyCode::Char('\x0e') => Key::CtrlN,
KeyCode::Char('\x10') => Key::CtrlP,
KeyCode::Char('\x19') => Key::CtrlY,
KeyCode::Char('\x05') => Key::CtrlE,
KeyCode::Char('r') if control => Key::CtrlR,
KeyCode::Char('x') if control => Key::CtrlX,
KeyCode::Char('l') if control => Key::CtrlL,
KeyCode::Char('\x0c') => Key::CtrlL,
KeyCode::Char('v') if control => Key::CtrlV,
KeyCode::Char('\x16') => Key::CtrlV,
KeyCode::Char('w') if control => Key::CtrlW,
KeyCode::Char(ch) => Key::Char(if key.modifiers.shift && !control {
let mut uppercase = ch.to_uppercase();
match (uppercase.next(), uppercase.next()) {
(Some(shifted), None) => shifted,
_ => ch,
}
} else {
ch
}),
_ => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::editor::Mode;
fn send(editor: &mut Editor, code: KeyCode, alt: bool, control: bool) {
let mut event = KeyEvent::press(code);
event.modifiers.alt = alt;
event.modifiers.control = control;
editor.handle_app_event(AppEvent::Input(Input::Key(event)));
}
fn send_shifted(editor: &mut Editor, ch: char) {
let mut event = KeyEvent::press(KeyCode::Char(ch));
event.modifiers.shift = true;
editor.handle_app_event(AppEvent::Input(Input::Key(event)));
}
#[test]
fn coalesced_escape_leaves_insert_before_applying_motion() {
let mut editor = Editor::new(strop_core::Buffer::from_text(""));
editor.feed_text("iwritewrite");
send(&mut editor, KeyCode::Char('h'), true, false);
assert_eq!(editor.mode, Mode::Normal);
assert_eq!(editor.buf().text().to_string(), "writewrite");
assert_eq!(editor.head(), 8);
}
#[test]
fn coalesced_escape_is_not_motion_specific() {
let mut editor = Editor::new(strop_core::Buffer::from_text(""));
editor.feed_text("iwritewrite");
send(&mut editor, KeyCode::Char('x'), true, false);
assert_eq!(editor.mode, Mode::Normal);
assert_eq!(editor.buf().text().to_string(), "writewrit");
}
#[test]
fn releases_do_not_edit_and_unmapped_alt_still_escapes() {
let mut editor = Editor::new(strop_core::Buffer::from_text(""));
editor.feed_text("iabc");
let mut event = KeyEvent::press(KeyCode::Char('x'));
event.kind = KeyKind::Release;
event.modifiers.alt = true;
editor.handle_app_event(AppEvent::Input(Input::Key(event)));
assert_eq!(editor.mode, Mode::Insert);
assert_eq!(editor.buf().text().to_string(), "abc");
send(&mut editor, KeyCode::Function(7), true, false);
assert_eq!(editor.mode, Mode::Normal);
assert_eq!(editor.buf().text().to_string(), "abc");
}
#[test]
fn shifted_letters_reach_prompts_pickers_and_normal_motions() {
let mut prompt = Editor::new(strop_core::Buffer::from_text("Needle\nneedle\n"));
prompt.feed_text("/");
send_shifted(&mut prompt, 'n');
prompt.feed_text("eedle\r");
assert_eq!(
prompt.last_search().map(|search| search.query.source()),
Some("Needle")
);
let directory = tempfile::tempdir().unwrap();
let mut picker = Editor::new_in(
strop_core::Buffer::from_text(""),
directory.path().to_owned(),
);
picker.open_picker(strop_picker::Kind::Files);
send_shifted(&mut picker, 'g');
assert_eq!(picker.picker.as_ref().unwrap().picker.input.text(), "G");
let mut normal = Editor::new(strop_core::Buffer::from_text("one\ntwo\nthree\n"));
send_shifted(&mut normal, 'g');
assert_eq!(normal.buf().line_of(normal.head()), 2);
}
}