Skip to main content

twrite_gpui/editor/
keyboard.rs

1use gpui::{Context, KeyDownEvent, Window};
2use twrite_core::{
3    HookContext, HookOutcome, KeyCode, KeyEvent, Modifiers, Point as BufferPoint, SearchAction,
4    Selection,
5};
6
7use super::Editor;
8
9impl Editor {
10    pub(crate) fn handle_key_down(
11        &mut self,
12        event: &KeyDownEvent,
13        window: &mut Window,
14        cx: &mut Context<Self>,
15    ) {
16        if let Some(key_event) = crate::input::translate_key_down(event) {
17            if self.context_menu.is_open() {
18                let plain = !key_event.modifiers.ctrl
19                    && !key_event.modifiers.alt
20                    && !key_event.modifiers.meta;
21                match &key_event.code {
22                    KeyCode::Escape => {
23                        self.dismiss_context_menu(cx);
24                        return;
25                    }
26                    KeyCode::Up if plain => {
27                        self.move_context_menu_selection(false, cx);
28                        return;
29                    }
30                    KeyCode::Down if plain => {
31                        self.move_context_menu_selection(true, cx);
32                        return;
33                    }
34                    KeyCode::Enter if plain => {
35                        if self.context_menu_selected.is_some() {
36                            self.activate_context_menu_selected(window, cx);
37                        } else {
38                            self.dismiss_context_menu(cx);
39                        }
40                        return;
41                    }
42                    // Any other key dismisses the menu and falls through
43                    // so typing still lands in the buffer.
44                    _ => self.dismiss_context_menu(cx),
45                }
46            }
47            self.dispatch_key(&key_event, Some(window), cx);
48        }
49    }
50
51    /// Feeds a translated key through the hook chain with full post-processing
52    /// (selection callbacks, scrolling, effect + search sync, notify).
53    ///
54    /// Used by [`Self::handle_key_down`] and by synthetic prompt-bar clicks
55    /// via [`Self::press_search_key`].
56    pub(crate) fn dispatch_key(
57        &mut self,
58        key_event: &KeyEvent,
59        window: Option<&Window>,
60        cx: &mut Context<Self>,
61    ) {
62        self.reset_blink_cursor(cx);
63        let initial_version = self.buffer.version();
64        let mut consumed = false;
65
66        let mut hook_idx = 0;
67        while hook_idx < self.hooks.len() {
68            let mut ctx = HookContext::new(
69                &mut self.buffer,
70                &mut self.selection,
71                &mut self.cursor_style,
72                &mut self.prompt,
73                &mut self.pending_effects,
74            );
75            let outcome = self.hooks[hook_idx].on_key(&mut ctx, key_event);
76            if outcome == HookOutcome::Consumed {
77                consumed = true;
78                break;
79            }
80            hook_idx += 1;
81        }
82
83        if consumed {
84            self.finish_consumed_action(window, cx, initial_version);
85            return;
86        }
87
88        if self.prompt.is_open() {
89            for hook in &mut self.hooks {
90                hook.on_selection_change(&self.buffer, self.selection.as_ref());
91            }
92            self.flush_effects();
93            self.sync_search_state();
94            cx.notify();
95            return;
96        }
97
98        let mut edited = false;
99        let select = key_event.modifiers.shift;
100
101        if key_event.modifiers.ctrl || key_event.modifiers.meta {
102            // Physical fallbacks from translation are already lowercase, so
103            // `Char` codes match directly (the old string path lowercased).
104            match &key_event.code {
105                KeyCode::Char('z') => {
106                    if key_event.modifiers.shift {
107                        self.buffer.redo();
108                    } else {
109                        self.buffer.undo();
110                    }
111                    self.selection = None;
112                    edited = true;
113                }
114                KeyCode::Char('y') => {
115                    self.buffer.redo();
116                    self.selection = None;
117                    edited = true;
118                }
119                KeyCode::Char('a') => {
120                    self.selection = Some(Selection::range(0, self.buffer.len_bytes()));
121                }
122                KeyCode::Char('c') => {
123                    self.copy(cx);
124                }
125                KeyCode::Char('x') => {
126                    edited = self.cut(cx);
127                }
128                KeyCode::Char('v') => {
129                    edited = self.paste(cx);
130                }
131                KeyCode::Backspace => {
132                    if !self.delete_selection() {
133                        edited = self.buffer.delete_prev_word();
134                    } else {
135                        edited = true;
136                    }
137                    self.selection = None;
138                }
139                KeyCode::Delete => {
140                    if !self.delete_selection() {
141                        edited = self.buffer.delete_next_word();
142                    } else {
143                        edited = true;
144                    }
145                    self.selection = None;
146                }
147                KeyCode::Left => {
148                    let target = self.buffer.prev_word_offset();
149                    self.move_cursor_to(target, select);
150                }
151                KeyCode::Right => {
152                    let target = self.buffer.next_word_offset();
153                    self.move_cursor_to(target, select);
154                }
155                KeyCode::Home => {
156                    self.move_cursor_to(0, select);
157                }
158                KeyCode::End => {
159                    self.move_cursor_to(self.buffer.len_bytes(), select);
160                }
161                KeyCode::Up => {
162                    self.scroll_up(1);
163                    self.flush_effects();
164                    cx.notify();
165                    return;
166                }
167                KeyCode::Down => {
168                    self.scroll_down(1);
169                    self.flush_effects();
170                    cx.notify();
171                    return;
172                }
173                _ => {}
174            }
175        } else {
176            match &key_event.code {
177                KeyCode::Backspace => {
178                    if !self.delete_selection() {
179                        self.buffer.backspace();
180                    }
181                    self.selection = None;
182                    edited = true;
183                }
184                KeyCode::Delete => {
185                    if !self.delete_selection() {
186                        self.buffer.delete();
187                    }
188                    self.selection = None;
189                    edited = true;
190                }
191                KeyCode::Enter => {
192                    self.replace_selection_or_insert("\n");
193                    self.selection = None;
194                    edited = true;
195                }
196                KeyCode::Tab => {
197                    self.replace_selection_or_insert(&" ".repeat(self.config.tab_size));
198                    self.selection = None;
199                    edited = true;
200                }
201                KeyCode::Char(' ') => {
202                    self.replace_selection_or_insert(" ");
203                    self.selection = None;
204                    edited = true;
205                }
206                KeyCode::Left => {
207                    if !select && self.selection.is_some() {
208                        let sel = self.selection.take().unwrap();
209                        self.buffer.set_cursor_offset(sel.byte_range().start);
210                    } else {
211                        let target = if self.buffer.cursor_offset() > 0 {
212                            let char_idx =
213                                self.buffer.text().byte_to_char(self.buffer.cursor_offset());
214                            self.buffer.text().char_to_byte(char_idx - 1)
215                        } else {
216                            0
217                        };
218                        self.move_cursor_to(target, select);
219                    }
220                }
221                KeyCode::Right => {
222                    if !select && self.selection.is_some() {
223                        let sel = self.selection.take().unwrap();
224                        self.buffer.set_cursor_offset(sel.byte_range().end);
225                    } else {
226                        let target = if self.buffer.cursor_offset() < self.buffer.len_bytes() {
227                            let char_idx =
228                                self.buffer.text().byte_to_char(self.buffer.cursor_offset());
229                            self.buffer
230                                .text()
231                                .char_to_byte((char_idx + 1).min(self.buffer.text().len_chars()))
232                        } else {
233                            self.buffer.len_bytes()
234                        };
235                        self.move_cursor_to(target, select);
236                    }
237                }
238                KeyCode::Up => {
239                    let point = self.buffer.cursor_point();
240                    if point.row > 0 {
241                        let target = self
242                            .buffer
243                            .point_to_offset(BufferPoint::new(point.row - 1, point.column));
244                        self.move_cursor_to(target, select);
245                    } else {
246                        self.move_cursor_to(0, select);
247                    }
248                }
249                KeyCode::Down => {
250                    let point = self.buffer.cursor_point();
251                    let total_lines = self.buffer.len_lines();
252                    if point.row + 1 < total_lines {
253                        let target = self
254                            .buffer
255                            .point_to_offset(BufferPoint::new(point.row + 1, point.column));
256                        self.move_cursor_to(target, select);
257                    } else {
258                        self.move_cursor_to(self.buffer.len_bytes(), select);
259                    }
260                }
261                KeyCode::Home => {
262                    let target = self.buffer.line_start_offset();
263                    self.move_cursor_to(target, select);
264                }
265                KeyCode::End => {
266                    let target = self.buffer.line_end_offset();
267                    self.move_cursor_to(target, select);
268                }
269                KeyCode::Char(c)
270                    if !key_event.modifiers.alt
271                        && !key_event.modifiers.ctrl
272                        && !key_event.modifiers.meta =>
273                {
274                    let mut insert_consumed = false;
275                    let mut hook_idx = 0;
276                    while hook_idx < self.hooks.len() {
277                        let mut ctx = HookContext::new(
278                            &mut self.buffer,
279                            &mut self.selection,
280                            &mut self.cursor_style,
281                            &mut self.prompt,
282                            &mut self.pending_effects,
283                        );
284                        if self.hooks[hook_idx].before_insert(&mut ctx, *c) == HookOutcome::Consumed
285                        {
286                            insert_consumed = true;
287                            break;
288                        }
289                        hook_idx += 1;
290                    }
291
292                    if !insert_consumed {
293                        let mut buf = [0u8; 4];
294                        self.replace_selection_or_insert(c.encode_utf8(&mut buf));
295                        self.selection = None;
296                        edited = true;
297                    }
298                }
299                _ => {}
300            }
301        }
302
303        if edited {
304            for hook in &mut self.hooks {
305                hook.after_edit(&mut self.buffer);
306            }
307        }
308
309        for hook in &mut self.hooks {
310            hook.on_selection_change(&self.buffer, self.selection.as_ref());
311        }
312
313        self.scroll_to_cursor(window);
314        self.flush_effects();
315        self.sync_search_state();
316        cx.notify();
317    }
318
319    /// Feeds a synthetic key through the hook chain with full post-processing.
320    ///
321    /// Used by prompt-bar chips and arrow buttons so clicks share the exact
322    /// keyboard path (e.g. `Alt+C` toggles Match Case, plain `Down`
323    /// walks to the next match).
324    pub fn press_search_key(
325        &mut self,
326        code: KeyCode,
327        ctrl: bool,
328        alt: bool,
329        shift: bool,
330        window: Option<&Window>,
331        cx: &mut Context<Self>,
332    ) {
333        let key_event = KeyEvent {
334            code,
335            modifiers: Modifiers {
336                ctrl,
337                alt,
338                shift,
339                meta: false,
340            },
341        };
342        self.dispatch_key(&key_event, window, cx);
343    }
344
345    /// Feeds a synthetic search-panel action (prompt-bar clicks) through the
346    /// hook chain with the same post-processing as consumed keys.
347    ///
348    /// Pointer chrome cannot produce a [`KeyCode`], so actions travel on
349    /// [`EditorHook::on_search_action`] instead of through [`KeyEvent`].
350    pub fn press_search_action(
351        &mut self,
352        action: SearchAction,
353        window: Option<&Window>,
354        cx: &mut Context<Self>,
355    ) {
356        self.reset_blink_cursor(cx);
357        let initial_version = self.buffer.version();
358
359        let mut hook_idx = 0;
360        while hook_idx < self.hooks.len() {
361            let mut ctx = HookContext::new(
362                &mut self.buffer,
363                &mut self.selection,
364                &mut self.cursor_style,
365                &mut self.prompt,
366                &mut self.pending_effects,
367            );
368            if self.hooks[hook_idx].on_search_action(&mut ctx, action) == HookOutcome::Consumed {
369                break;
370            }
371            hook_idx += 1;
372        }
373
374        self.finish_consumed_action(window, cx, initial_version);
375    }
376
377    /// Post-processing shared by consumed keys and synthetic actions:
378    /// selection callbacks, scrolling, effect + search sync, notify.
379    fn finish_consumed_action(
380        &mut self,
381        window: Option<&Window>,
382        cx: &mut Context<Self>,
383        initial_version: usize,
384    ) {
385        if self.buffer.version() != initial_version {
386            for hook in &mut self.hooks {
387                hook.after_edit(&mut self.buffer);
388            }
389        }
390        for hook in &mut self.hooks {
391            hook.on_selection_change(&self.buffer, self.selection.as_ref());
392        }
393        self.scroll_to_cursor(window);
394        self.flush_effects();
395        self.sync_search_state();
396        cx.notify();
397    }
398}