1use crate::host::BuffrHost;
12use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
13use hjkl_engine::{
14 Editor, KeybindingMode, Modifiers, PlannedInput, SpecialKey, VimMode, types::Options,
15};
16use std::sync::Arc;
17
18fn key_event_to_planned(key: KeyEvent) -> PlannedInput {
23 let mods = Modifiers {
24 ctrl: key.modifiers.contains(KeyModifiers::CONTROL),
25 shift: key.modifiers.contains(KeyModifiers::SHIFT),
26 alt: key.modifiers.contains(KeyModifiers::ALT),
27 super_: key.modifiers.contains(KeyModifiers::SUPER),
28 };
29 match key.code {
30 KeyCode::Char(c) => PlannedInput::Char(c, mods),
31 KeyCode::Esc => PlannedInput::Key(SpecialKey::Esc, mods),
32 KeyCode::Enter => PlannedInput::Key(SpecialKey::Enter, mods),
33 KeyCode::Backspace => PlannedInput::Key(SpecialKey::Backspace, mods),
34 KeyCode::Tab => PlannedInput::Key(SpecialKey::Tab, mods),
35 KeyCode::BackTab => PlannedInput::Key(SpecialKey::BackTab, mods),
36 KeyCode::Up => PlannedInput::Key(SpecialKey::Up, mods),
37 KeyCode::Down => PlannedInput::Key(SpecialKey::Down, mods),
38 KeyCode::Left => PlannedInput::Key(SpecialKey::Left, mods),
39 KeyCode::Right => PlannedInput::Key(SpecialKey::Right, mods),
40 KeyCode::Home => PlannedInput::Key(SpecialKey::Home, mods),
41 KeyCode::End => PlannedInput::Key(SpecialKey::End, mods),
42 KeyCode::PageUp => PlannedInput::Key(SpecialKey::PageUp, mods),
43 KeyCode::PageDown => PlannedInput::Key(SpecialKey::PageDown, mods),
44 KeyCode::Insert => PlannedInput::Key(SpecialKey::Insert, mods),
45 KeyCode::Delete => PlannedInput::Key(SpecialKey::Delete, mods),
46 KeyCode::F(n) => PlannedInput::Key(SpecialKey::F(n), mods),
47 _ => PlannedInput::Key(SpecialKey::Insert, mods),
50 }
51}
52
53pub struct EditSession {
61 editor: Editor<hjkl_buffer::Buffer, BuffrHost>,
62}
63
64impl EditSession {
65 pub fn new(initial: &str) -> Self {
67 let mut editor = Editor::new(
68 hjkl_buffer::Buffer::new(),
69 BuffrHost::new(),
70 Options::default(),
71 );
72 editor.keybinding_mode = KeybindingMode::Vim;
76 editor.set_content(initial);
77 Self { editor }
78 }
79
80 pub fn handle_key(&mut self, key: KeyEvent) -> bool {
84 self.editor.feed_input(key_event_to_planned(key))
85 }
86
87 pub fn feed_planned(&mut self, input: hjkl_engine::PlannedInput) -> bool {
92 self.editor.feed_input(input)
93 }
94
95 pub fn type_char(&mut self, ch: char) -> bool {
99 self.handle_key(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE))
100 }
101
102 pub fn press(&mut self, code: KeyCode) -> bool {
105 self.handle_key(KeyEvent::new(code, KeyModifiers::NONE))
106 }
107
108 pub fn type_str(&mut self, s: &str) {
112 debug_assert_eq!(self.editor.vim_mode(), VimMode::Insert);
113 for ch in s.chars() {
114 self.type_char(ch);
115 }
116 }
117
118 pub fn take_content_change(&mut self) -> Option<Arc<String>> {
122 self.editor.take_content_change()
123 }
124
125 pub fn content(&self) -> String {
127 self.editor.content()
128 }
129
130 pub fn vim_mode(&self) -> VimMode {
132 self.editor.vim_mode()
133 }
134
135 pub fn drain_clipboard_outbox(&mut self) -> Vec<String> {
138 self.editor.host_mut().drain_clipboard_outbox()
139 }
140
141 pub fn drain_intents(&mut self) -> Vec<crate::host::BuffrEditIntent> {
144 self.editor.host_mut().drain_intents()
145 }
146
147 pub fn host_mut(&mut self) -> &mut BuffrHost {
150 self.editor.host_mut()
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn empty_session_starts_in_normal_mode() {
160 let s = EditSession::new("");
161 assert_eq!(s.vim_mode(), VimMode::Normal);
162 assert!(matches!(s.content().as_str(), "" | "\n"));
164 }
165
166 #[test]
167 fn type_hello_in_insert_then_esc() {
168 let mut s = EditSession::new("");
169 s.type_char('i');
171 assert_eq!(s.vim_mode(), VimMode::Insert);
172 s.type_str("hello");
173 s.press(KeyCode::Esc);
174 assert_eq!(s.vim_mode(), VimMode::Normal);
175 assert!(s.content().starts_with("hello"));
176 }
177
178 #[test]
179 fn take_content_change_drains_after_first_call() {
180 let mut s = EditSession::new("foo");
181 assert!(s.take_content_change().is_some());
183 assert!(s.take_content_change().is_none());
185 s.type_char('i');
187 s.type_char('X');
188 s.press(KeyCode::Esc);
189 let after = s.take_content_change();
190 assert!(after.is_some());
191 assert!(after.unwrap().contains('X'));
192 }
193
194 #[test]
195 fn dd_clears_only_line() {
196 let mut s = EditSession::new("hello world");
197 s.type_char('d');
199 s.type_char('d');
200 let content = s.content();
202 assert!(
203 content.is_empty() || content == "\n",
204 "expected empty or \\n, got {content:?}"
205 );
206 }
207
208 #[test]
209 fn esc_from_normal_stays_normal() {
210 let mut s = EditSession::new("hello");
214 s.press(KeyCode::Esc);
215 s.press(KeyCode::Esc);
216 s.press(KeyCode::Esc);
217 assert_eq!(s.vim_mode(), VimMode::Normal);
218 }
219
220 #[test]
221 fn feed_planned_round_trip() {
222 use hjkl_engine::{Modifiers, PlannedInput, SpecialKey};
225 let empty_mods = Modifiers::default();
226 let mut s = EditSession::new("");
227 s.feed_planned(PlannedInput::Char('i', empty_mods));
229 assert_eq!(s.vim_mode(), VimMode::Insert);
230 s.feed_planned(PlannedInput::Char('H', empty_mods));
231 s.feed_planned(PlannedInput::Char('i', empty_mods));
232 s.feed_planned(PlannedInput::Key(SpecialKey::Esc, empty_mods));
233 assert_eq!(s.vim_mode(), VimMode::Normal);
234 assert!(
235 s.content().starts_with("Hi"),
236 "expected content to start with 'Hi', got {:?}",
237 s.content()
238 );
239 }
240
241 #[test]
242 fn yank_then_paste_via_clipboard() {
243 let mut s = EditSession::new("alpha");
247 s.type_char('y');
248 s.type_char('y');
249 s.type_char('p');
250 let content = s.content();
251 assert!(
253 content.matches("alpha").count() >= 2,
254 "expected two 'alpha' lines, got {content:?}"
255 );
256 }
257}