1#![allow(clippy::enum_glob_use)]
2
3use crate::{
4 cursor::{Cursor, CursorEdit, CursorMove, CursorState},
5 editor::Editor,
6 key::{KeyCode, KeyModifiers},
7 keymap::{Keymap, KeymapResult, keymap},
8 mode::{Mode, normal},
9 text::Text,
10};
11use ropey::{Rope, RopeSlice};
12use std::sync::{Arc, LazyLock, Mutex};
13
14#[cfg(feature = "arbitrary")]
15use arbitrary::Arbitrary;
16
17#[derive(Clone)]
18pub struct State {
19 prompt: &'static str,
20 text: Text,
21 cursor: CursorState,
22 #[expect(clippy::type_complexity)]
25 handler: Option<Arc<Mutex<dyn FnMut(&mut Editor, &str) + Send>>>,
26}
27
28impl State {
29 #[must_use]
30 pub fn new(
31 prompt: &'static str,
32 handler: impl FnMut(&mut Editor, &str) + Send + 'static,
33 ) -> Self {
34 Self {
35 prompt,
36 text: Text::default(),
37 cursor: CursorState::default(),
38 handler: Some(Arc::new(Mutex::new(handler))),
39 }
40 }
41
42 #[must_use]
43 pub fn prompt(&self) -> &str {
44 self.prompt
45 }
46
47 #[must_use]
48 pub fn rope(&self) -> &Rope {
49 self.text.rope()
50 }
51
52 #[must_use]
54 pub fn content(&self) -> RopeSlice<'_> {
55 let rope = self.text.rope();
56 assert_eq!(
57 rope.byte(rope.len() - 1),
58 b'\n',
59 "Prompt text keeps its trailing newline"
60 );
61 rope.slice(..rope.len() - 1)
62 }
63
64 pub fn cursor(&self) -> Cursor<'_> {
65 let cursor = Cursor::new(&self.text, &self.cursor)
66 .expect("Command mode text and cursor state are always kept valid");
67 cursor.assert_invariants().unwrap();
68 cursor
69 }
70
71 pub fn cursor_move(&mut self) -> CursorMove<'_> {
72 CursorMove::new(&self.text, &mut self.cursor)
73 .expect("Command mode text and cursor state are always kept valid")
74 .on_drop(|cursor| cursor.assert_invariants().unwrap())
75 }
76
77 pub fn cursor_edit(&mut self) -> CursorEdit<'_> {
78 CursorEdit::new(&mut self.text, &mut self.cursor)
79 .expect("Command mode text and cursor state are always kept valid")
80 .on_drop(|cursor| cursor.assert_invariants().unwrap())
81 }
82
83 pub fn assert_invariants(&self) -> anyhow::Result<()> {
84 let _ = Cursor::new(&self.text, &self.cursor)?;
85 Ok(())
86 }
87}
88
89#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
90#[derive(Debug)]
91pub enum Action {
92 EnterNormalMode,
93 MoveLeft,
94 MoveRight,
95 MoveToStart,
96 MoveToEnd,
97 DeleteToStart,
98 DeleteToEnd,
99 DeleteBefore,
100 InsertChar(char),
101 Exec,
102}
103
104pub static KEYMAP: LazyLock<Keymap<Vec<Action>>> = LazyLock::new(|| {
105 use Action::*;
106 keymap! { Vec<Action>;
107 "<esc>" => vec![EnterNormalMode], "<ret>" => vec![Exec], "<c-b>" => vec![MoveLeft], "<c-f>" => vec![MoveRight], "<c-a>" => vec![MoveToStart], "<c-e>" => vec![MoveToEnd], "<c-u>" => vec![DeleteToStart], "<c-k>" => vec![DeleteToEnd], "<bs>" => vec![DeleteBefore], keys => {
117 if let [key] = keys
118 && let KeyCode::Char(c) = key.code
119 && key.modifiers.difference(KeyModifiers::SHIFT).is_empty()
120 {
121 KeymapResult::Fallback(vec![InsertChar(char::from(c))])
122 } else {
123 KeymapResult::Unmapped
124 }
125 } }
127});
128
129pub fn handle_actions(editor: &mut Editor, actions: &[Action]) {
130 for action in actions {
131 handle_action(editor, action);
132 }
133}
134
135pub fn handle_action(editor: &mut Editor, action: &Action) {
136 match action {
137 Action::EnterNormalMode => normal::enter(editor),
138 Action::MoveLeft => move_left(editor),
139 Action::MoveRight => move_right(editor),
140 Action::MoveToStart => move_to_start(editor),
141 Action::MoveToEnd => move_to_end(editor),
142 Action::DeleteToStart => delete_to_start(editor),
143 Action::DeleteToEnd => delete_to_end(editor),
144 Action::DeleteBefore => delete_before(editor),
145 Action::InsertChar(c) => insert_char(editor, *c),
146 Action::Exec => exec(editor),
147 }
148}
149
150pub fn handle_keys(editor: &mut Editor) -> bool {
151 match KEYMAP.get_keys(&editor.pending_keys) {
152 KeymapResult::Mapped(actions) => {
153 editor.pending_keys.clear();
154 handle_actions(editor, actions);
155 true
156 }
157 KeymapResult::Fallback(actions) => {
158 editor.pending_keys.clear();
159 handle_actions(editor, &actions);
160 true
161 }
162 KeymapResult::Unmapped => {
163 editor.pending_keys.clear();
164 false
165 }
166 KeymapResult::Pending => true,
167 }
168}
169
170pub fn enter(
171 editor: &mut Editor,
172 prompt: &'static str,
173 handler: impl FnMut(&mut Editor, &str) + Send + 'static,
174) {
175 editor.mode = Mode::Prompt(State::new(prompt, handler));
176 editor.count = None;
177}
178
179fn move_left(editor: &mut Editor) {
180 let Mode::Prompt(prompt_mode) = &mut editor.mode else {
181 panic!("Not in prompt mode")
182 };
183 prompt_mode.cursor_move().move_left(1);
184}
185
186fn move_right(editor: &mut Editor) {
187 let Mode::Prompt(prompt_mode) = &mut editor.mode else {
188 panic!("Not in prompt mode")
189 };
190 prompt_mode.cursor_move().move_right(1);
191}
192
193fn move_to_start(editor: &mut Editor) {
194 let Mode::Prompt(prompt_mode) = &mut editor.mode else {
195 panic!("Not in prompt mode")
196 };
197 prompt_mode.cursor_move().move_to_start();
198}
199
200fn move_to_end(editor: &mut Editor) {
201 let Mode::Prompt(prompt_mode) = &mut editor.mode else {
202 panic!("Not in prompt mode")
203 };
204 prompt_mode.cursor_move().move_to_end();
205}
206
207fn delete_to_start(editor: &mut Editor) {
208 let Mode::Prompt(prompt_mode) = &mut editor.mode else {
209 panic!("Not in prompt mode")
210 };
211 let mut cursor = prompt_mode.cursor_edit();
212 while cursor.delete_before().is_some() {}
213}
214
215fn delete_to_end(editor: &mut Editor) {
216 let Mode::Prompt(prompt_mode) = &mut editor.mode else {
217 panic!("Not in prompt mode")
218 };
219 let mut cursor = prompt_mode.cursor_edit();
220 while cursor.delete_after().is_some() {}
221}
222
223fn insert_char(editor: &mut Editor, char: char) {
224 let Mode::Prompt(prompt_mode) = &mut editor.mode else {
225 panic!("Not in prompt mode")
226 };
227 prompt_mode.cursor_edit().insert_char(char);
228}
229
230pub fn paste(editor: &mut Editor, text: &str) {
231 let Mode::Prompt(prompt_mode) = &mut editor.mode else {
232 panic!("Not in prompt mode")
233 };
234 prompt_mode.cursor_edit().insert(text);
235}
236
237fn delete_before(editor: &mut Editor) {
238 let Mode::Prompt(prompt_mode) = &mut editor.mode else {
239 panic!("Not in prompt mode")
240 };
241 if prompt_mode.cursor().is_at_start() {
242 normal::enter(editor);
243 } else {
244 prompt_mode.cursor_edit().delete_before();
245 }
246}
247
248fn exec(editor: &mut Editor) {
249 let Mode::Prompt(prompt_mode) = &mut editor.mode else {
250 panic!("Not in prompt mode")
251 };
252 let text = prompt_mode.content().to_string();
253 let handler = prompt_mode
254 .handler
255 .take()
256 .expect("Handler is always present");
257 handler.lock().unwrap()(editor, &text);
258 normal::enter(editor);
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn content_strips_trailing_newline() {
267 let mut state = State::new("test", |_, _| {});
268 assert_eq!(state.content().to_string(), "");
270 assert!(state.cursor().is_at_end());
271
272 state.cursor_edit().insert("abc");
273 assert_eq!(state.rope().to_string(), "abc\n");
274 assert_eq!(state.content().to_string(), "abc");
275
276 state.cursor_move().move_to_end();
278 assert!(state.cursor().is_at_end());
279 state.cursor_edit().insert_char('!');
280 assert_eq!(state.content().to_string(), "abc!");
281
282 state.cursor_move().move_to_start();
284 let mut cursor = state.cursor_edit();
285 while cursor.delete_after().is_some() {}
286 drop(cursor);
287 assert_eq!(state.content().to_string(), "");
288 }
289}