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.
86///
87/// [`MoveBlockUp`], [`MoveBlockDown`], [`DuplicateBlock`] and [`RemoveBlock`]
88/// have no chord.
89pub fn bindings() -> Vec<KeyBinding> {
90    let ctx = Some(CONTEXT);
91    let mut bindings = Vec::new();
92    bindings.extend([
93        KeyBinding::new("backspace", Backspace, ctx),
94        KeyBinding::new("delete", Delete, ctx),
95        KeyBinding::new("left", Left, ctx),
96        KeyBinding::new("right", Right, ctx),
97        KeyBinding::new("up", Up, ctx),
98        KeyBinding::new("down", Down, ctx),
99        KeyBinding::new("home", Home, ctx),
100        KeyBinding::new("end", End, ctx),
101        KeyBinding::new("shift-left", SelectLeft, ctx),
102        KeyBinding::new("shift-right", SelectRight, ctx),
103        KeyBinding::new("shift-up", SelectUp, ctx),
104        KeyBinding::new("shift-down", SelectDown, ctx),
105        KeyBinding::new("shift-home", SelectHome, ctx),
106        KeyBinding::new("shift-end", SelectEnd, ctx),
107        KeyBinding::new("enter", SplitBlock, ctx),
108        KeyBinding::new("shift-enter", SoftBreak, ctx),
109        KeyBinding::new("ctrl-enter", InsertParagraph, ctx),
110        KeyBinding::new("tab", Indent, ctx),
111        KeyBinding::new("shift-tab", Outdent, ctx),
112        KeyBinding::new("escape", Dismiss, ctx),
113    ]);
114
115    // The URL prompt's own context, because the field holds focus while it is
116    // open and the document behind it must not answer the same two keys.
117    let prompt = Some(image::PROMPT_CONTEXT);
118    bindings.extend([
119        KeyBinding::new("enter", ConfirmUrl, prompt),
120        KeyBinding::new("escape", CancelUrl, prompt),
121    ]);
122
123    // `MoveBlockUp`, `MoveBlockDown`, `DuplicateBlock` and `RemoveBlock` are
124    // deliberately unbound. Every chord that fits is already taken by something
125    // standard — `cmd-shift-up`/`down` select to the ends of a document on
126    // macOS, `alt-up`/`down` move by paragraph — and shadowing one of those in
127    // a text surface is worse than reaching the block menu for it. They are
128    // actions so an app can bind what suits its own keymap.
129    //
130    // The emacs kill ring is unbound for the same reason and stays unbuilt with
131    // it: `ctrl-w`, `alt-w` and `ctrl-y` collide with `cmd-x`, `cmd-c`, `cmd-v`
132    // and `cmd-w`, so a kill deletes and `cmd-x` is how text travels. Off
133    // macOS `ctrl-y` is redo, which is the chord Windows reaches for.
134
135    #[cfg(target_os = "macos")]
136    bindings.extend([
137        KeyBinding::new("cmd-a", SelectAll, ctx),
138        KeyBinding::new("cmd-c", Copy, ctx),
139        KeyBinding::new("cmd-x", Cut, ctx),
140        KeyBinding::new("cmd-v", Paste, ctx),
141        KeyBinding::new("cmd-z", Undo, ctx),
142        KeyBinding::new("cmd-shift-z", Redo, ctx),
143        KeyBinding::new("cmd-b", ToggleBold, ctx),
144        KeyBinding::new("cmd-i", ToggleItalic, ctx),
145        KeyBinding::new("cmd-e", ToggleCode, ctx),
146        KeyBinding::new("cmd-shift-x", ToggleStrike, ctx),
147        KeyBinding::new("cmd-shift-h", ToggleHighlight, ctx),
148        // Three chords for one key: `cmd-+` is `cmd-shift-=` on the keyboards
149        // that have no `+` of their own, and which of the two a platform
150        // reports is not ours to guess.
151        KeyBinding::new("cmd-=", IncreaseTextSize, ctx),
152        KeyBinding::new("cmd-+", IncreaseTextSize, ctx),
153        KeyBinding::new("cmd-shift-=", IncreaseTextSize, ctx),
154        KeyBinding::new("cmd--", DecreaseTextSize, ctx),
155        KeyBinding::new("cmd-0", ResetTextSize, ctx),
156        // cmd = line, option = word: the macOS convention.
157        KeyBinding::new("cmd-left", Home, ctx),
158        KeyBinding::new("cmd-right", End, ctx),
159        KeyBinding::new("cmd-up", DocumentStart, ctx),
160        KeyBinding::new("cmd-down", DocumentEnd, ctx),
161        KeyBinding::new("cmd-shift-left", SelectHome, ctx),
162        KeyBinding::new("cmd-shift-right", SelectEnd, ctx),
163        KeyBinding::new("cmd-shift-up", SelectDocumentStart, ctx),
164        KeyBinding::new("cmd-shift-down", SelectDocumentEnd, ctx),
165        KeyBinding::new("alt-left", WordLeft, ctx),
166        KeyBinding::new("alt-right", WordRight, ctx),
167        KeyBinding::new("alt-shift-left", SelectWordLeft, ctx),
168        KeyBinding::new("alt-shift-right", SelectWordRight, ctx),
169        // The emacs bindings macOS honours in every native text field.
170        KeyBinding::new("ctrl-a", Home, ctx),
171        KeyBinding::new("ctrl-e", End, ctx),
172        KeyBinding::new("ctrl-b", Left, ctx),
173        KeyBinding::new("ctrl-f", Right, ctx),
174        KeyBinding::new("ctrl-n", Down, ctx),
175        KeyBinding::new("ctrl-p", Up, ctx),
176        KeyBinding::new("ctrl-h", Backspace, ctx),
177        KeyBinding::new("ctrl-d", Delete, ctx),
178        // `ctrl-k` is the one chord emacs and AppKit spell the same way; the
179        // rest are what option and cmd already mean for motion, deleting.
180        KeyBinding::new("ctrl-k", KillLine, ctx),
181        KeyBinding::new("alt-backspace", DeleteWordLeft, ctx),
182        KeyBinding::new("alt-delete", DeleteWordRight, ctx),
183        KeyBinding::new("cmd-backspace", DeleteToHome, ctx),
184    ]);
185
186    #[cfg(not(target_os = "macos"))]
187    bindings.extend([
188        KeyBinding::new("ctrl-a", SelectAll, ctx),
189        KeyBinding::new("ctrl-c", Copy, ctx),
190        KeyBinding::new("ctrl-x", Cut, ctx),
191        KeyBinding::new("ctrl-v", Paste, ctx),
192        KeyBinding::new("ctrl-z", Undo, ctx),
193        KeyBinding::new("ctrl-shift-z", Redo, ctx),
194        KeyBinding::new("ctrl-y", Redo, ctx),
195        KeyBinding::new("ctrl-b", ToggleBold, ctx),
196        KeyBinding::new("ctrl-i", ToggleItalic, ctx),
197        KeyBinding::new("ctrl-e", ToggleCode, ctx),
198        KeyBinding::new("ctrl-shift-x", ToggleStrike, ctx),
199        KeyBinding::new("ctrl-shift-h", ToggleHighlight, ctx),
200        KeyBinding::new("ctrl-=", IncreaseTextSize, ctx),
201        KeyBinding::new("ctrl-+", IncreaseTextSize, ctx),
202        KeyBinding::new("ctrl-shift-=", IncreaseTextSize, ctx),
203        KeyBinding::new("ctrl--", DecreaseTextSize, ctx),
204        KeyBinding::new("ctrl-0", ResetTextSize, ctx),
205        // ctrl = word on Windows/Linux, where there is no line modifier.
206        KeyBinding::new("ctrl-left", WordLeft, ctx),
207        KeyBinding::new("ctrl-right", WordRight, ctx),
208        KeyBinding::new("ctrl-shift-left", SelectWordLeft, ctx),
209        KeyBinding::new("ctrl-shift-right", SelectWordRight, ctx),
210        KeyBinding::new("ctrl-home", DocumentStart, ctx),
211        KeyBinding::new("ctrl-end", DocumentEnd, ctx),
212        KeyBinding::new("ctrl-shift-home", SelectDocumentStart, ctx),
213        KeyBinding::new("ctrl-shift-end", SelectDocumentEnd, ctx),
214        KeyBinding::new("ctrl-backspace", DeleteWordLeft, ctx),
215        KeyBinding::new("ctrl-delete", DeleteWordRight, ctx),
216        // `ctrl-k` stays free here: GTK entries kill the line with it, Windows
217        // reads it as "insert link", and a chord with two meanings is one this
218        // library does not get to claim.
219    ]);
220
221    // Inside a painted fence every chord above is taken away, so a control the
222    // block paints keeps what it does not bind itself, and escape leaves it.
223    let painted = Some(markdown::PAINTED_CONTEXT);
224    let mut taken: Vec<String> = Vec::new();
225    for binding in &bindings {
226        let chord = binding
227            .keystrokes()
228            .iter()
229            .map(|keystroke| keystroke.unparse())
230            .collect::<Vec<_>>()
231            .join(" ");
232        if chord != "escape" && !taken.contains(&chord) {
233            taken.push(chord);
234        }
235    }
236    bindings.extend(
237        taken
238            .iter()
239            .map(|chord| KeyBinding::new(chord, gpui::NoAction, painted)),
240    );
241    bindings.push(KeyBinding::new("escape", markdown::LeaveBlock, painted));
242
243    bindings
244}