use crate::host::BuffrHost;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use hjkl_engine::{
Editor, KeybindingMode, Modifiers, PlannedInput, SpecialKey, VimMode, types::Options,
};
use std::sync::Arc;
fn key_event_to_planned(key: KeyEvent) -> PlannedInput {
let mods = Modifiers {
ctrl: key.modifiers.contains(KeyModifiers::CONTROL),
shift: key.modifiers.contains(KeyModifiers::SHIFT),
alt: key.modifiers.contains(KeyModifiers::ALT),
super_: key.modifiers.contains(KeyModifiers::SUPER),
};
match key.code {
KeyCode::Char(c) => PlannedInput::Char(c, mods),
KeyCode::Esc => PlannedInput::Key(SpecialKey::Esc, mods),
KeyCode::Enter => PlannedInput::Key(SpecialKey::Enter, mods),
KeyCode::Backspace => PlannedInput::Key(SpecialKey::Backspace, mods),
KeyCode::Tab => PlannedInput::Key(SpecialKey::Tab, mods),
KeyCode::BackTab => PlannedInput::Key(SpecialKey::BackTab, mods),
KeyCode::Up => PlannedInput::Key(SpecialKey::Up, mods),
KeyCode::Down => PlannedInput::Key(SpecialKey::Down, mods),
KeyCode::Left => PlannedInput::Key(SpecialKey::Left, mods),
KeyCode::Right => PlannedInput::Key(SpecialKey::Right, mods),
KeyCode::Home => PlannedInput::Key(SpecialKey::Home, mods),
KeyCode::End => PlannedInput::Key(SpecialKey::End, mods),
KeyCode::PageUp => PlannedInput::Key(SpecialKey::PageUp, mods),
KeyCode::PageDown => PlannedInput::Key(SpecialKey::PageDown, mods),
KeyCode::Insert => PlannedInput::Key(SpecialKey::Insert, mods),
KeyCode::Delete => PlannedInput::Key(SpecialKey::Delete, mods),
KeyCode::F(n) => PlannedInput::Key(SpecialKey::F(n), mods),
_ => PlannedInput::Key(SpecialKey::Insert, mods),
}
}
pub struct EditSession {
editor: Editor<hjkl_buffer::Buffer, BuffrHost>,
}
impl EditSession {
pub fn new(initial: &str) -> Self {
let mut editor = Editor::new(
hjkl_buffer::Buffer::new(),
BuffrHost::new(),
Options::default(),
);
editor.keybinding_mode = KeybindingMode::Vim;
editor.set_content(initial);
Self { editor }
}
pub fn handle_key(&mut self, key: KeyEvent) -> bool {
self.editor.feed_input(key_event_to_planned(key))
}
pub fn feed_planned(&mut self, input: hjkl_engine::PlannedInput) -> bool {
self.editor.feed_input(input)
}
pub fn type_char(&mut self, ch: char) -> bool {
self.handle_key(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE))
}
pub fn press(&mut self, code: KeyCode) -> bool {
self.handle_key(KeyEvent::new(code, KeyModifiers::NONE))
}
pub fn type_str(&mut self, s: &str) {
debug_assert_eq!(self.editor.vim_mode(), VimMode::Insert);
for ch in s.chars() {
self.type_char(ch);
}
}
pub fn take_content_change(&mut self) -> Option<Arc<String>> {
self.editor.take_content_change()
}
pub fn content(&self) -> String {
self.editor.content()
}
pub fn vim_mode(&self) -> VimMode {
self.editor.vim_mode()
}
pub fn drain_clipboard_outbox(&mut self) -> Vec<String> {
self.editor.host_mut().drain_clipboard_outbox()
}
pub fn drain_intents(&mut self) -> Vec<crate::host::BuffrEditIntent> {
self.editor.host_mut().drain_intents()
}
pub fn host_mut(&mut self) -> &mut BuffrHost {
self.editor.host_mut()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_session_starts_in_normal_mode() {
let s = EditSession::new("");
assert_eq!(s.vim_mode(), VimMode::Normal);
assert!(matches!(s.content().as_str(), "" | "\n"));
}
#[test]
fn type_hello_in_insert_then_esc() {
let mut s = EditSession::new("");
s.type_char('i');
assert_eq!(s.vim_mode(), VimMode::Insert);
s.type_str("hello");
s.press(KeyCode::Esc);
assert_eq!(s.vim_mode(), VimMode::Normal);
assert!(s.content().starts_with("hello"));
}
#[test]
fn take_content_change_drains_after_first_call() {
let mut s = EditSession::new("foo");
assert!(s.take_content_change().is_some());
assert!(s.take_content_change().is_none());
s.type_char('i');
s.type_char('X');
s.press(KeyCode::Esc);
let after = s.take_content_change();
assert!(after.is_some());
assert!(after.unwrap().contains('X'));
}
#[test]
fn dd_clears_only_line() {
let mut s = EditSession::new("hello world");
s.type_char('d');
s.type_char('d');
let content = s.content();
assert!(
content.is_empty() || content == "\n",
"expected empty or \\n, got {content:?}"
);
}
#[test]
fn esc_from_normal_stays_normal() {
let mut s = EditSession::new("hello");
s.press(KeyCode::Esc);
s.press(KeyCode::Esc);
s.press(KeyCode::Esc);
assert_eq!(s.vim_mode(), VimMode::Normal);
}
#[test]
fn feed_planned_round_trip() {
use hjkl_engine::{Modifiers, PlannedInput, SpecialKey};
let empty_mods = Modifiers::default();
let mut s = EditSession::new("");
s.feed_planned(PlannedInput::Char('i', empty_mods));
assert_eq!(s.vim_mode(), VimMode::Insert);
s.feed_planned(PlannedInput::Char('H', empty_mods));
s.feed_planned(PlannedInput::Char('i', empty_mods));
s.feed_planned(PlannedInput::Key(SpecialKey::Esc, empty_mods));
assert_eq!(s.vim_mode(), VimMode::Normal);
assert!(
s.content().starts_with("Hi"),
"expected content to start with 'Hi', got {:?}",
s.content()
);
}
#[test]
fn yank_then_paste_via_clipboard() {
let mut s = EditSession::new("alpha");
s.type_char('y');
s.type_char('y');
s.type_char('p');
let content = s.content();
assert!(
content.matches("alpha").count() >= 2,
"expected two 'alpha' lines, got {content:?}"
);
}
}