Skip to main content

editor/editor/
keys.rs

1//! The keymap: every action the editor answers to, and the chords bound to it.
2//!
3//! Separate from the surface because it is configuration rather than behaviour
4//! — an app that wants a different keymap replaces this call, not the editor.
5
6use gpui::{App, KeyBinding, actions};
7
8use crate::editor::{CONTEXT, image};
9
10actions!(
11    bezel_editor,
12    [
13        Backspace,
14        Delete,
15        KillLine,
16        DeleteWordLeft,
17        DeleteWordRight,
18        DeleteToHome,
19        Left,
20        Right,
21        Up,
22        Down,
23        Home,
24        End,
25        DocumentStart,
26        DocumentEnd,
27        SelectLeft,
28        SelectRight,
29        SelectUp,
30        SelectDown,
31        SelectHome,
32        SelectEnd,
33        SelectDocumentStart,
34        SelectDocumentEnd,
35        SelectAll,
36        WordLeft,
37        WordRight,
38        SelectWordLeft,
39        SelectWordRight,
40        SplitBlock,
41        SoftBreak,
42        InsertParagraph,
43        Indent,
44        Outdent,
45        Dismiss,
46        Copy,
47        Cut,
48        Paste,
49        Undo,
50        Redo,
51        ToggleBold,
52        ToggleItalic,
53        ToggleStrike,
54        ToggleCode,
55        ToggleHighlight,
56        MoveBlockUp,
57        MoveBlockDown,
58        DuplicateBlock,
59        RemoveBlock,
60        ConfirmUrl,
61        CancelUrl,
62        IncreaseTextSize,
63        DecreaseTextSize,
64        ResetTextSize,
65    ]
66);
67
68/// Install the editor's key bindings — [`bindings`], bound.
69pub fn init(cx: &mut App) {
70    cx.bind_keys(bindings());
71}
72
73/// The editor's default keymap, as data, so an app can have it without having
74/// to take it. Scoped to the editor's own key context, so binding `tab` here
75/// does not make `tab` mean "indent" for the whole app.
76///
77/// The chords are [`ui::input::TextField`]'s, because a document is not the place to
78/// invent a second set: what `alt-left` does in a search box is what a reader
79/// expects it to do here.
80///
81/// An app with a keymap of its own has three ways in, none of which is copying
82/// this list: bind over it (a later binding wins), bind `gpui::NoAction` to a
83/// chord to take it away, or skip [`init`] and bind a filtered `bindings()`.
84/// Either way the actions are public and [`CONTEXT`] names the scope, so what
85/// the editor answers to is the app's to say.
86pub fn bindings() -> Vec<KeyBinding> {
87    let ctx = Some(CONTEXT);
88    let mut bindings = Vec::new();
89    bindings.extend([
90        KeyBinding::new("backspace", Backspace, ctx),
91        KeyBinding::new("delete", Delete, ctx),
92        KeyBinding::new("left", Left, ctx),
93        KeyBinding::new("right", Right, ctx),
94        KeyBinding::new("up", Up, ctx),
95        KeyBinding::new("down", Down, ctx),
96        KeyBinding::new("home", Home, ctx),
97        KeyBinding::new("end", End, ctx),
98        KeyBinding::new("shift-left", SelectLeft, ctx),
99        KeyBinding::new("shift-right", SelectRight, ctx),
100        KeyBinding::new("shift-up", SelectUp, ctx),
101        KeyBinding::new("shift-down", SelectDown, ctx),
102        KeyBinding::new("shift-home", SelectHome, ctx),
103        KeyBinding::new("shift-end", SelectEnd, ctx),
104        KeyBinding::new("enter", SplitBlock, ctx),
105        KeyBinding::new("shift-enter", SoftBreak, ctx),
106        KeyBinding::new("ctrl-enter", InsertParagraph, ctx),
107        KeyBinding::new("tab", Indent, ctx),
108        KeyBinding::new("shift-tab", Outdent, ctx),
109        KeyBinding::new("escape", Dismiss, ctx),
110    ]);
111
112    // The URL prompt's own context, because the field holds focus while it is
113    // open and the document behind it must not answer the same two keys.
114    let prompt = Some(image::PROMPT_CONTEXT);
115    bindings.extend([
116        KeyBinding::new("enter", ConfirmUrl, prompt),
117        KeyBinding::new("escape", CancelUrl, prompt),
118    ]);
119
120    // `MoveBlockUp`, `MoveBlockDown`, `DuplicateBlock` and `RemoveBlock` are
121    // deliberately unbound. Every chord that fits is already taken by something
122    // standard — `cmd-shift-up`/`down` select to the ends of a document on
123    // macOS, `alt-up`/`down` move by paragraph — and shadowing one of those in
124    // a text surface is worse than reaching the block menu for it. They are
125    // actions so an app can bind what suits its own keymap.
126    //
127    // The emacs kill ring is unbound for the same reason and stays unbuilt with
128    // it: `ctrl-w`, `alt-w` and `ctrl-y` collide with `cmd-x`, `cmd-c`, `cmd-v`
129    // and `cmd-w`, so a kill deletes and `cmd-x` is how text travels. Off
130    // macOS `ctrl-y` is redo, which is the chord Windows reaches for.
131
132    #[cfg(target_os = "macos")]
133    bindings.extend([
134        KeyBinding::new("cmd-a", SelectAll, ctx),
135        KeyBinding::new("cmd-c", Copy, ctx),
136        KeyBinding::new("cmd-x", Cut, ctx),
137        KeyBinding::new("cmd-v", Paste, ctx),
138        KeyBinding::new("cmd-z", Undo, ctx),
139        KeyBinding::new("cmd-shift-z", Redo, ctx),
140        KeyBinding::new("cmd-b", ToggleBold, ctx),
141        KeyBinding::new("cmd-i", ToggleItalic, ctx),
142        KeyBinding::new("cmd-e", ToggleCode, ctx),
143        KeyBinding::new("cmd-shift-x", ToggleStrike, ctx),
144        KeyBinding::new("cmd-shift-h", ToggleHighlight, ctx),
145        // Three chords for one key: `cmd-+` is `cmd-shift-=` on the keyboards
146        // that have no `+` of their own, and which of the two a platform
147        // reports is not ours to guess.
148        KeyBinding::new("cmd-=", IncreaseTextSize, ctx),
149        KeyBinding::new("cmd-+", IncreaseTextSize, ctx),
150        KeyBinding::new("cmd-shift-=", IncreaseTextSize, ctx),
151        KeyBinding::new("cmd--", DecreaseTextSize, ctx),
152        KeyBinding::new("cmd-0", ResetTextSize, ctx),
153        // cmd = line, option = word: the macOS convention.
154        KeyBinding::new("cmd-left", Home, ctx),
155        KeyBinding::new("cmd-right", End, ctx),
156        KeyBinding::new("cmd-up", DocumentStart, ctx),
157        KeyBinding::new("cmd-down", DocumentEnd, ctx),
158        KeyBinding::new("cmd-shift-left", SelectHome, ctx),
159        KeyBinding::new("cmd-shift-right", SelectEnd, ctx),
160        KeyBinding::new("cmd-shift-up", SelectDocumentStart, ctx),
161        KeyBinding::new("cmd-shift-down", SelectDocumentEnd, ctx),
162        KeyBinding::new("alt-left", WordLeft, ctx),
163        KeyBinding::new("alt-right", WordRight, ctx),
164        KeyBinding::new("alt-shift-left", SelectWordLeft, ctx),
165        KeyBinding::new("alt-shift-right", SelectWordRight, ctx),
166        // The emacs bindings macOS honours in every native text field.
167        KeyBinding::new("ctrl-a", Home, ctx),
168        KeyBinding::new("ctrl-e", End, ctx),
169        KeyBinding::new("ctrl-b", Left, ctx),
170        KeyBinding::new("ctrl-f", Right, ctx),
171        KeyBinding::new("ctrl-n", Down, ctx),
172        KeyBinding::new("ctrl-p", Up, ctx),
173        KeyBinding::new("ctrl-h", Backspace, ctx),
174        KeyBinding::new("ctrl-d", Delete, ctx),
175        // `ctrl-k` is the one chord emacs and AppKit spell the same way; the
176        // rest are what option and cmd already mean for motion, deleting.
177        KeyBinding::new("ctrl-k", KillLine, ctx),
178        KeyBinding::new("alt-backspace", DeleteWordLeft, ctx),
179        KeyBinding::new("alt-delete", DeleteWordRight, ctx),
180        KeyBinding::new("cmd-backspace", DeleteToHome, ctx),
181    ]);
182
183    #[cfg(not(target_os = "macos"))]
184    bindings.extend([
185        KeyBinding::new("ctrl-a", SelectAll, ctx),
186        KeyBinding::new("ctrl-c", Copy, ctx),
187        KeyBinding::new("ctrl-x", Cut, ctx),
188        KeyBinding::new("ctrl-v", Paste, ctx),
189        KeyBinding::new("ctrl-z", Undo, ctx),
190        KeyBinding::new("ctrl-shift-z", Redo, ctx),
191        KeyBinding::new("ctrl-y", Redo, ctx),
192        KeyBinding::new("ctrl-b", ToggleBold, ctx),
193        KeyBinding::new("ctrl-i", ToggleItalic, ctx),
194        KeyBinding::new("ctrl-e", ToggleCode, ctx),
195        KeyBinding::new("ctrl-shift-x", ToggleStrike, ctx),
196        KeyBinding::new("ctrl-shift-h", ToggleHighlight, ctx),
197        KeyBinding::new("ctrl-=", IncreaseTextSize, ctx),
198        KeyBinding::new("ctrl-+", IncreaseTextSize, ctx),
199        KeyBinding::new("ctrl-shift-=", IncreaseTextSize, ctx),
200        KeyBinding::new("ctrl--", DecreaseTextSize, ctx),
201        KeyBinding::new("ctrl-0", ResetTextSize, ctx),
202        // ctrl = word on Windows/Linux, where there is no line modifier.
203        KeyBinding::new("ctrl-left", WordLeft, ctx),
204        KeyBinding::new("ctrl-right", WordRight, ctx),
205        KeyBinding::new("ctrl-shift-left", SelectWordLeft, ctx),
206        KeyBinding::new("ctrl-shift-right", SelectWordRight, ctx),
207        KeyBinding::new("ctrl-home", DocumentStart, ctx),
208        KeyBinding::new("ctrl-end", DocumentEnd, ctx),
209        KeyBinding::new("ctrl-shift-home", SelectDocumentStart, ctx),
210        KeyBinding::new("ctrl-shift-end", SelectDocumentEnd, ctx),
211        KeyBinding::new("ctrl-backspace", DeleteWordLeft, ctx),
212        KeyBinding::new("ctrl-delete", DeleteWordRight, ctx),
213        // `ctrl-k` stays free here: GTK entries kill the line with it, Windows
214        // reads it as "insert link", and a chord with two meanings is one this
215        // library does not get to claim.
216    ]);
217
218    // Inside a painted fence every chord above is taken away, so a control the
219    // block paints keeps what it does not bind itself, and escape leaves it.
220    let painted = Some(markdown::PAINTED_CONTEXT);
221    let mut taken: Vec<String> = Vec::new();
222    for binding in &bindings {
223        let chord = binding
224            .keystrokes()
225            .iter()
226            .map(|keystroke| keystroke.unparse())
227            .collect::<Vec<_>>()
228            .join(" ");
229        if chord != "escape" && !taken.contains(&chord) {
230            taken.push(chord);
231        }
232    }
233    bindings.extend(
234        taken
235            .iter()
236            .map(|chord| KeyBinding::new(chord, gpui::NoAction, painted)),
237    );
238    bindings.push(KeyBinding::new("escape", markdown::LeaveBlock, painted));
239
240    bindings
241}