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_and_clamp_cursor(1, false, select);
163                    for hook in &mut self.hooks {
164                        hook.on_selection_change(&self.buffer, self.selection.as_ref());
165                    }
166                    self.flush_effects();
167                    self.sync_search_state();
168                    cx.notify();
169                    return;
170                }
171                KeyCode::Down => {
172                    self.scroll_and_clamp_cursor(1, true, select);
173                    for hook in &mut self.hooks {
174                        hook.on_selection_change(&self.buffer, self.selection.as_ref());
175                    }
176                    self.flush_effects();
177                    self.sync_search_state();
178                    cx.notify();
179                    return;
180                }
181                _ => {}
182            }
183        } else {
184            match &key_event.code {
185                KeyCode::Backspace => {
186                    if !self.delete_selection() {
187                        self.buffer.backspace();
188                    }
189                    self.selection = None;
190                    edited = true;
191                }
192                KeyCode::Delete => {
193                    if !self.delete_selection() {
194                        self.buffer.delete();
195                    }
196                    self.selection = None;
197                    edited = true;
198                }
199                KeyCode::Enter => {
200                    self.replace_selection_or_insert("\n");
201                    self.selection = None;
202                    edited = true;
203                }
204                KeyCode::Tab => {
205                    self.replace_selection_or_insert(&" ".repeat(self.config.tab_size));
206                    self.selection = None;
207                    edited = true;
208                }
209                KeyCode::Char(' ') => {
210                    self.replace_selection_or_insert(" ");
211                    self.selection = None;
212                    edited = true;
213                }
214                KeyCode::Left => {
215                    if !select && self.selection.is_some() {
216                        let sel = self.selection.take().unwrap();
217                        self.buffer.set_cursor_offset(sel.byte_range().start);
218                    } else {
219                        let target = if self.buffer.cursor_offset() > 0 {
220                            let char_idx =
221                                self.buffer.text().byte_to_char(self.buffer.cursor_offset());
222                            self.buffer.text().char_to_byte(char_idx - 1)
223                        } else {
224                            0
225                        };
226                        self.move_cursor_to(target, select);
227                    }
228                }
229                KeyCode::Right => {
230                    if !select && self.selection.is_some() {
231                        let sel = self.selection.take().unwrap();
232                        self.buffer.set_cursor_offset(sel.byte_range().end);
233                    } else {
234                        let target = if self.buffer.cursor_offset() < self.buffer.len_bytes() {
235                            let char_idx =
236                                self.buffer.text().byte_to_char(self.buffer.cursor_offset());
237                            self.buffer
238                                .text()
239                                .char_to_byte((char_idx + 1).min(self.buffer.text().len_chars()))
240                        } else {
241                            self.buffer.len_bytes()
242                        };
243                        self.move_cursor_to(target, select);
244                    }
245                }
246                KeyCode::Up => {
247                    let point = self.buffer.cursor_point();
248                    if point.row > 0 {
249                        let target = self
250                            .buffer
251                            .point_to_offset(BufferPoint::new(point.row - 1, point.column));
252                        self.move_cursor_to(target, select);
253                    } else {
254                        self.move_cursor_to(0, select);
255                    }
256                }
257                KeyCode::Down => {
258                    let point = self.buffer.cursor_point();
259                    let total_lines = self.buffer.len_lines();
260                    if point.row + 1 < total_lines {
261                        let target = self
262                            .buffer
263                            .point_to_offset(BufferPoint::new(point.row + 1, point.column));
264                        self.move_cursor_to(target, select);
265                    } else {
266                        self.move_cursor_to(self.buffer.len_bytes(), select);
267                    }
268                }
269                KeyCode::Home => {
270                    let target = self.buffer.line_start_offset();
271                    self.move_cursor_to(target, select);
272                }
273                KeyCode::End => {
274                    let target = self.buffer.line_end_offset();
275                    self.move_cursor_to(target, select);
276                }
277                KeyCode::Char(c)
278                    if !key_event.modifiers.alt
279                        && !key_event.modifiers.ctrl
280                        && !key_event.modifiers.meta =>
281                {
282                    let mut insert_consumed = false;
283                    let mut hook_idx = 0;
284                    while hook_idx < self.hooks.len() {
285                        let mut ctx = HookContext::new(
286                            &mut self.buffer,
287                            &mut self.selection,
288                            &mut self.cursor_style,
289                            &mut self.prompt,
290                            &mut self.pending_effects,
291                        );
292                        if self.hooks[hook_idx].before_insert(&mut ctx, *c) == HookOutcome::Consumed
293                        {
294                            insert_consumed = true;
295                            break;
296                        }
297                        hook_idx += 1;
298                    }
299
300                    if !insert_consumed {
301                        let mut buf = [0u8; 4];
302                        self.replace_selection_or_insert(c.encode_utf8(&mut buf));
303                        self.selection = None;
304                        edited = true;
305                    }
306                }
307                _ => {}
308            }
309        }
310
311        if edited {
312            for hook in &mut self.hooks {
313                hook.after_edit(&mut self.buffer);
314            }
315        }
316
317        for hook in &mut self.hooks {
318            hook.on_selection_change(&self.buffer, self.selection.as_ref());
319        }
320
321        self.scroll_to_cursor(window);
322        self.flush_effects();
323        self.sync_search_state();
324        cx.notify();
325    }
326
327    /// Feeds a synthetic key through the hook chain with full post-processing.
328    ///
329    /// Used by prompt-bar chips and arrow buttons so clicks share the exact
330    /// keyboard path (e.g. `Alt+C` toggles Match Case, plain `Down`
331    /// walks to the next match).
332    pub fn press_search_key(
333        &mut self,
334        code: KeyCode,
335        ctrl: bool,
336        alt: bool,
337        shift: bool,
338        window: Option<&Window>,
339        cx: &mut Context<Self>,
340    ) {
341        let key_event = KeyEvent {
342            code,
343            modifiers: Modifiers {
344                ctrl,
345                alt,
346                shift,
347                meta: false,
348            },
349        };
350        self.dispatch_key(&key_event, window, cx);
351    }
352
353    /// Feeds a synthetic search-panel action (prompt-bar clicks) through the
354    /// hook chain with the same post-processing as consumed keys.
355    ///
356    /// Pointer chrome cannot produce a [`KeyCode`], so actions travel on
357    /// [`EditorHook::on_search_action`] instead of through [`KeyEvent`].
358    pub fn press_search_action(
359        &mut self,
360        action: SearchAction,
361        window: Option<&Window>,
362        cx: &mut Context<Self>,
363    ) {
364        self.reset_blink_cursor(cx);
365        let initial_version = self.buffer.version();
366
367        let mut hook_idx = 0;
368        while hook_idx < self.hooks.len() {
369            let mut ctx = HookContext::new(
370                &mut self.buffer,
371                &mut self.selection,
372                &mut self.cursor_style,
373                &mut self.prompt,
374                &mut self.pending_effects,
375            );
376            if self.hooks[hook_idx].on_search_action(&mut ctx, action) == HookOutcome::Consumed {
377                break;
378            }
379            hook_idx += 1;
380        }
381
382        self.finish_consumed_action(window, cx, initial_version);
383    }
384
385    /// Post-processing shared by consumed keys and synthetic actions:
386    /// selection callbacks, scrolling, effect + search sync, notify.
387    fn finish_consumed_action(
388        &mut self,
389        window: Option<&Window>,
390        cx: &mut Context<Self>,
391        initial_version: usize,
392    ) {
393        if self.buffer.version() != initial_version {
394            for hook in &mut self.hooks {
395                hook.after_edit(&mut self.buffer);
396            }
397        }
398        for hook in &mut self.hooks {
399            hook.on_selection_change(&self.buffer, self.selection.as_ref());
400        }
401        self.scroll_to_cursor(window);
402        self.flush_effects();
403        self.sync_search_state();
404        cx.notify();
405    }
406}