Skip to main content

floem/views/editor/keypress/
mod.rs

1pub mod key;
2pub mod press;
3
4use std::{collections::HashMap, str::FromStr};
5
6use crate::reactive::RwSignal;
7use floem_editor_core::{
8    command::{EditCommand, MoveCommand, MultiSelectionCommand, ScrollCommand},
9    mode::Mode,
10};
11use floem_reactive::{SignalGet, SignalWith};
12use ui_events::keyboard::{Key, Modifiers};
13
14use super::{
15    Editor,
16    command::{Command, CommandExecuted},
17};
18
19#[derive(Clone, PartialEq, Eq, Hash)]
20pub struct KeypressKey {
21    pub key: Key,
22    pub modifiers: Modifiers,
23}
24
25/// The default keymap handler does not have modal-mode specific
26/// keybindings.
27#[derive(Clone)]
28pub struct KeypressMap {
29    pub keymaps: HashMap<KeypressKey, Command>,
30}
31impl KeypressMap {
32    pub fn default_windows() -> Self {
33        let mut keymaps = HashMap::new();
34        add_default_common(&mut keymaps);
35        add_default_windows(&mut keymaps);
36        Self { keymaps }
37    }
38
39    pub fn default_macos() -> Self {
40        let mut keymaps = HashMap::new();
41        add_default_common(&mut keymaps);
42        add_default_macos(&mut keymaps);
43        Self { keymaps }
44    }
45
46    pub fn default_linux() -> Self {
47        let mut keymaps = HashMap::new();
48        add_default_common(&mut keymaps);
49        add_default_linux(&mut keymaps);
50        Self { keymaps }
51    }
52
53    /// Look up a command for the given keypress, accounting for modal editing.
54    ///
55    /// In insert mode, a keypress with Shift held (e.g. Shift+Home) falls back
56    /// to the unshifted binding (Home) if no exact match exists. In other modes,
57    /// Shift+key is a distinct command and no fallback occurs.
58    fn resolve_command(
59        &self,
60        editor: RwSignal<Editor>,
61        keypress: &KeypressKey,
62    ) -> Option<&Command> {
63        if let Some(cmd) = self.keymaps.get(keypress) {
64            return Some(cmd);
65        }
66
67        let mode = editor.get_untracked().cursor.get_untracked().get_mode();
68        if mode != Mode::Insert {
69            return None;
70        }
71
72        // Insert mode: Shift is a text modifier (uppercase), not a command
73        // modifier. Try the lookup again without it.
74        let mut unshifted = keypress.modifiers;
75        unshifted.set(Modifiers::SHIFT, false);
76        self.keymaps.get(&KeypressKey {
77            key: keypress.key.clone(),
78            modifiers: unshifted,
79        })
80    }
81
82    /// Dispatch a key combination against this keymap.
83    ///
84    /// Looks up the key combination in the keymap. If no exact match is
85    /// found, retries the lookup with the Shift modifier removed. This
86    /// allows movement commands like Home and End to support selection
87    /// extension (Shift+Home, Shift+End) without needing separate keymap
88    /// entries — the Shift modifier is passed through to the command,
89    /// which uses it to decide between moving the cursor and extending
90    /// the selection.
91    ///
92    /// This applies to any binding in the map, including custom ones.
93    /// A binding for Ctrl+Home will automatically handle Ctrl+Shift+Home
94    /// as the same command with selection extension.
95    ///
96    /// Returns [`CommandExecuted::Yes`] if a command was found and executed,
97    /// [`CommandExecuted::No`] otherwise.
98    ///
99    /// See the [`custom_keymap`](https://github.com/lapce/floem/tree/main/examples/custom_keymap)
100    /// example for a complete usage demonstration.
101    pub fn handle_keypress(
102        &self,
103        editor: RwSignal<Editor>,
104        keypress: &KeypressKey,
105    ) -> CommandExecuted {
106        let Some(command) = self.resolve_command(editor, keypress) else {
107            return CommandExecuted::No;
108        };
109
110        editor.with_untracked(|editor| {
111            editor
112                .doc()
113                .run_command(editor, command, Some(1), keypress.modifiers)
114        })
115    }
116}
117
118impl Default for KeypressMap {
119    fn default() -> Self {
120        match std::env::consts::OS {
121            "macos" => Self::default_macos(),
122            "windows" => Self::default_windows(),
123            _ => Self::default_linux(),
124        }
125    }
126}
127
128fn key(s: &str, m: Modifiers) -> KeypressKey {
129    KeypressKey {
130        key: Key::from_str(s).unwrap(),
131        modifiers: m,
132    }
133}
134
135fn key_d(s: &str) -> KeypressKey {
136    key(s, Modifiers::default())
137}
138
139fn add_default_common(c: &mut HashMap<KeypressKey, Command>) {
140    // Note: this should typically be kept in sync with Lapce's
141    // `defaults/keymaps-common.toml`
142
143    // --- Basic editing ---
144
145    c.insert(
146        key("ArrowUp", Modifiers::ALT),
147        Command::Edit(EditCommand::MoveLineUp),
148    );
149    c.insert(
150        key("ArrowDown", Modifiers::ALT),
151        Command::Edit(EditCommand::MoveLineDown),
152    );
153
154    c.insert(key_d("Delete"), Command::Edit(EditCommand::DeleteForward));
155    c.insert(
156        key_d("Backspace"),
157        Command::Edit(EditCommand::DeleteBackward),
158    );
159    c.insert(
160        key("Backspace", Modifiers::SHIFT),
161        Command::Edit(EditCommand::DeleteForward),
162    );
163
164    c.insert(key_d("Home"), Command::Move(MoveCommand::LineStartNonBlank));
165    c.insert(key_d("End"), Command::Move(MoveCommand::LineEnd));
166
167    c.insert(key_d("PageUp"), Command::Scroll(ScrollCommand::PageUp));
168    c.insert(key_d("PageDown"), Command::Scroll(ScrollCommand::PageDown));
169    c.insert(
170        key("PageUp", Modifiers::CONTROL),
171        Command::Scroll(ScrollCommand::ScrollUp),
172    );
173    c.insert(
174        key("PageDown", Modifiers::CONTROL),
175        Command::Scroll(ScrollCommand::ScrollDown),
176    );
177
178    // --- Multi cursor ---
179
180    c.insert(
181        key("i", Modifiers::ALT | Modifiers::SHIFT),
182        Command::MultiSelection(MultiSelectionCommand::InsertCursorEndOfLine),
183    );
184
185    // TODO: should we have jump location backward/forward?
186
187    // TODO: jump to snippet positions?
188
189    // --- ---- ---
190    c.insert(key_d("ArrowRight"), Command::Move(MoveCommand::Right));
191    c.insert(key_d("ArrowLeft"), Command::Move(MoveCommand::Left));
192    c.insert(key_d("ArrowUp"), Command::Move(MoveCommand::Up));
193    c.insert(key_d("ArrowDown"), Command::Move(MoveCommand::Down));
194
195    c.insert(key_d("Enter"), Command::Edit(EditCommand::InsertNewLine));
196
197    c.insert(key_d("Tab"), Command::Edit(EditCommand::InsertTab));
198
199    c.insert(
200        key("ArrowUp", Modifiers::ALT | Modifiers::SHIFT),
201        Command::Edit(EditCommand::DuplicateLineUp),
202    );
203    c.insert(
204        key("ArrowDown", Modifiers::ALT | Modifiers::SHIFT),
205        Command::Edit(EditCommand::DuplicateLineDown),
206    );
207}
208
209fn add_default_windows(c: &mut HashMap<KeypressKey, Command>) {
210    add_default_nonmacos(c);
211}
212
213fn add_default_macos(c: &mut HashMap<KeypressKey, Command>) {
214    // Note: this should typically be kept in sync with Lapce's
215    // `defaults/keymaps-macos.toml`
216
217    // --- Basic editing ---
218    c.insert(key("z", Modifiers::META), Command::Edit(EditCommand::Undo));
219    c.insert(
220        key("z", Modifiers::META | Modifiers::SHIFT),
221        Command::Edit(EditCommand::Redo),
222    );
223    c.insert(key("y", Modifiers::META), Command::Edit(EditCommand::Redo));
224    c.insert(
225        key("x", Modifiers::META),
226        Command::Edit(EditCommand::ClipboardCut),
227    );
228    c.insert(
229        key("c", Modifiers::META),
230        Command::Edit(EditCommand::ClipboardCopy),
231    );
232    c.insert(
233        key("v", Modifiers::META),
234        Command::Edit(EditCommand::ClipboardPaste),
235    );
236
237    c.insert(
238        key("ArrowRight", Modifiers::ALT),
239        Command::Move(MoveCommand::WordEndForward),
240    );
241    c.insert(
242        key("ArrowLeft", Modifiers::ALT),
243        Command::Move(MoveCommand::WordBackward),
244    );
245    c.insert(
246        key("ArrowLeft", Modifiers::META),
247        Command::Move(MoveCommand::LineStartNonBlank),
248    );
249    c.insert(
250        key("ArrowRight", Modifiers::META),
251        Command::Move(MoveCommand::LineEnd),
252    );
253
254    c.insert(
255        key("a", Modifiers::CONTROL),
256        Command::Move(MoveCommand::LineStartNonBlank),
257    );
258    c.insert(
259        key("e", Modifiers::CONTROL),
260        Command::Move(MoveCommand::LineEnd),
261    );
262
263    c.insert(
264        key("k", Modifiers::META | Modifiers::SHIFT),
265        Command::Edit(EditCommand::DeleteLine),
266    );
267
268    c.insert(
269        key("Backspace", Modifiers::ALT),
270        Command::Edit(EditCommand::DeleteWordBackward),
271    );
272    c.insert(
273        key("Backspace", Modifiers::META),
274        Command::Edit(EditCommand::DeleteToBeginningOfLine),
275    );
276    c.insert(
277        key("k", Modifiers::CONTROL),
278        Command::Edit(EditCommand::DeleteToEndOfLine),
279    );
280    c.insert(
281        key("Delete", Modifiers::ALT),
282        Command::Edit(EditCommand::DeleteWordForward),
283    );
284
285    // TODO: match pairs?
286    // TODO: indent/outdent line?
287
288    c.insert(
289        key("a", Modifiers::META),
290        Command::MultiSelection(MultiSelectionCommand::SelectAll),
291    );
292
293    c.insert(
294        key("Enter", Modifiers::META),
295        Command::Edit(EditCommand::NewLineBelow),
296    );
297    c.insert(
298        key("Enter", Modifiers::META | Modifiers::SHIFT),
299        Command::Edit(EditCommand::NewLineAbove),
300    );
301
302    // --- Multi cursor ---
303    c.insert(
304        key("ArrowUp", Modifiers::ALT | Modifiers::META),
305        Command::MultiSelection(MultiSelectionCommand::InsertCursorAbove),
306    );
307    c.insert(
308        key("ArrowDown", Modifiers::ALT | Modifiers::META),
309        Command::MultiSelection(MultiSelectionCommand::InsertCursorBelow),
310    );
311
312    c.insert(
313        key("l", Modifiers::META),
314        Command::MultiSelection(MultiSelectionCommand::SelectCurrentLine),
315    );
316    c.insert(
317        key("l", Modifiers::META | Modifiers::SHIFT),
318        Command::MultiSelection(MultiSelectionCommand::SelectAllCurrent),
319    );
320
321    c.insert(
322        key("u", Modifiers::META),
323        Command::MultiSelection(MultiSelectionCommand::SelectUndo),
324    );
325
326    // --- ---- ---
327    c.insert(
328        key("ArrowUp", Modifiers::META),
329        Command::Move(MoveCommand::DocumentStart),
330    );
331    c.insert(
332        key("ArrowDown", Modifiers::META),
333        Command::Move(MoveCommand::DocumentEnd),
334    );
335}
336
337fn add_default_linux(c: &mut HashMap<KeypressKey, Command>) {
338    add_default_nonmacos(c);
339}
340
341fn add_default_nonmacos(c: &mut HashMap<KeypressKey, Command>) {
342    // Note: this should typically be kept in sync with Lapce's
343    // `defaults/keymaps-nonmacos.toml`
344
345    // --- Basic editing ---
346    c.insert(
347        key("z", Modifiers::CONTROL),
348        Command::Edit(EditCommand::Undo),
349    );
350    c.insert(
351        key("z", Modifiers::CONTROL | Modifiers::SHIFT),
352        Command::Edit(EditCommand::Redo),
353    );
354    c.insert(
355        key("y", Modifiers::CONTROL),
356        Command::Edit(EditCommand::Redo),
357    );
358    c.insert(
359        key("x", Modifiers::CONTROL),
360        Command::Edit(EditCommand::ClipboardCut),
361    );
362    c.insert(
363        key("Delete", Modifiers::SHIFT),
364        Command::Edit(EditCommand::ClipboardCut),
365    );
366    c.insert(
367        key("c", Modifiers::CONTROL),
368        Command::Edit(EditCommand::ClipboardCopy),
369    );
370    c.insert(
371        key("Insert", Modifiers::CONTROL),
372        Command::Edit(EditCommand::ClipboardCopy),
373    );
374    c.insert(
375        key("v", Modifiers::CONTROL),
376        Command::Edit(EditCommand::ClipboardPaste),
377    );
378    c.insert(
379        key("Insert", Modifiers::SHIFT),
380        Command::Edit(EditCommand::ClipboardPaste),
381    );
382
383    c.insert(
384        key("ArrowRight", Modifiers::CONTROL),
385        Command::Move(MoveCommand::WordEndForward),
386    );
387    c.insert(
388        key("ArrowLeft", Modifiers::CONTROL),
389        Command::Move(MoveCommand::WordBackward),
390    );
391
392    c.insert(
393        key("Backspace", Modifiers::CONTROL),
394        Command::Edit(EditCommand::DeleteWordBackward),
395    );
396    c.insert(
397        key("Delete", Modifiers::CONTROL),
398        Command::Edit(EditCommand::DeleteWordForward),
399    );
400
401    // TODO: match pairs?
402
403    // TODO: indent/outdent line?
404
405    c.insert(
406        key("a", Modifiers::CONTROL),
407        Command::MultiSelection(MultiSelectionCommand::SelectAll),
408    );
409
410    c.insert(
411        key("Enter", Modifiers::CONTROL),
412        Command::Edit(EditCommand::NewLineAbove),
413    );
414
415    // --- Multi cursor ---
416    c.insert(
417        key("ArrowUp", Modifiers::CONTROL | Modifiers::ALT),
418        Command::MultiSelection(MultiSelectionCommand::InsertCursorAbove),
419    );
420    c.insert(
421        key("ArrowDown", Modifiers::CONTROL | Modifiers::ALT),
422        Command::MultiSelection(MultiSelectionCommand::InsertCursorBelow),
423    );
424
425    c.insert(
426        key("l", Modifiers::CONTROL),
427        Command::MultiSelection(MultiSelectionCommand::SelectCurrentLine),
428    );
429    c.insert(
430        key("l", Modifiers::CONTROL | Modifiers::SHIFT),
431        Command::MultiSelection(MultiSelectionCommand::SelectAllCurrent),
432    );
433
434    c.insert(
435        key("u", Modifiers::CONTROL),
436        Command::MultiSelection(MultiSelectionCommand::SelectUndo),
437    );
438
439    // --- Navigation ---
440    c.insert(
441        key("Home", Modifiers::CONTROL),
442        Command::Move(MoveCommand::DocumentStart),
443    );
444    c.insert(
445        key("End", Modifiers::CONTROL),
446        Command::Move(MoveCommand::DocumentEnd),
447    );
448}
449
450/// Returns a key handler closure that dispatches against the default keymap.
451///
452/// This is used internally by [`text_editor`](super::text_editor::text_editor)
453/// to provide standard editing keybindings. If you need the default keymap with
454/// additional custom shortcuts, use [`KeypressMap::handle_keypress`] directly
455/// instead.
456pub fn default_key_handler(
457    editor: RwSignal<Editor>,
458) -> impl Fn(KeypressKey) -> CommandExecuted + 'static {
459    let keypress_map = KeypressMap::default();
460    move |keypress| keypress_map.handle_keypress(editor, &keypress)
461}