Skip to main content

indigo_core/mode/
prompt.rs

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    // TODO: Consider changing this back to `Box<dyn FnOnce(..)>`. Either write a weird `Clone` impl
23    // that makes `handler = None` or add a new trait for deriving view state?
24    #[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    /// The prompt's content, without the `Text` invariant's trailing newline.
53    #[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], /// Cancel
108        "<ret>" => vec![Exec], /// Submit
109        "<c-b>" => vec![MoveLeft], /// Move left
110        "<c-f>" => vec![MoveRight], /// Move right
111        "<c-a>" => vec![MoveToStart], /// Move to start
112        "<c-e>" => vec![MoveToEnd], /// Move to end
113        "<c-u>" => vec![DeleteToStart], /// Delete to start
114        "<c-k>" => vec![DeleteToEnd], /// Delete to end
115        "<bs>" => vec![DeleteBefore], /// Delete before cursor
116        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        } /// Insert character
126    }
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        // The empty prompt is "\n" internally, but its content is empty.
269        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        // Typing at the "end" inserts before the invariant newline.
277        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        // Kill-to-end deletes everything after the cursor except the invariant newline.
283        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}