diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index d33df02747..fca38a45a4 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -664,7 +664,8 @@ "shift-up": "terminal::ScrollLineUp", "shift-down": "terminal::ScrollLineDown", "shift-home": "terminal::ScrollToTop", - "shift-end": "terminal::ScrollToBottom" + "shift-end": "terminal::ScrollToBottom", + "ctrl-shift-space": "terminal::ToggleViMode" } }, { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index de929b8dd1..3de9a1495e 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -678,7 +678,8 @@ "cmd-home": "terminal::ScrollToTop", "cmd-end": "terminal::ScrollToBottom", "shift-home": "terminal::ScrollToTop", - "shift-end": "terminal::ScrollToBottom" + "shift-end": "terminal::ScrollToBottom", + "ctrl-shift-space": "terminal::ToggleViMode" } } ] diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 9412999cbf..76c2fa892c 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -18,6 +18,7 @@ use alacritty_terminal::{ Config, RenderableCursor, TermMode, }, tty::{self}, + vi_mode::{ViModeCursor, ViMotion}, vte::ansi::{ ClearMode, CursorStyle as AlacCursorStyle, Handler, NamedPrivateMode, PrivateMode, }, @@ -78,6 +79,7 @@ actions!( ScrollPageDown, ScrollToTop, ScrollToBottom, + ToggleViMode, ] ); @@ -139,6 +141,9 @@ enum InternalEvent { // Adjusted mouse position, should open FindHyperlink(Point, bool), Copy, + // Vi mode events + ToggleViMode, + ViMotion(ViMotion), } ///A translation struct for Alacritty to communicate with us from their event loop @@ -447,6 +452,7 @@ impl TerminalBuilder { hovered_word: false, url_regex, word_regex, + vi_mode_enabled: false, }; Ok(TerminalBuilder { @@ -602,6 +608,7 @@ pub struct Terminal { url_regex: RegexSearch, word_regex: RegexSearch, task: Option, + vi_mode_enabled: bool, } pub struct TaskState { @@ -767,6 +774,43 @@ impl Terminal { InternalEvent::Scroll(scroll) => { term.scroll_display(*scroll); self.refresh_hovered_word(); + + if self.vi_mode_enabled { + match *scroll { + AlacScroll::Delta(delta) => { + term.vi_mode_cursor = term.vi_mode_cursor.scroll(&term, delta); + } + AlacScroll::PageUp => { + let lines = term.screen_lines() as i32; + term.vi_mode_cursor = term.vi_mode_cursor.scroll(&term, lines); + } + AlacScroll::PageDown => { + let lines = -(term.screen_lines() as i32); + term.vi_mode_cursor = term.vi_mode_cursor.scroll(&term, lines); + } + AlacScroll::Top => { + let point = AlacPoint::new(term.topmost_line(), Column(0)); + term.vi_mode_cursor = ViModeCursor::new(point); + } + AlacScroll::Bottom => { + let point = AlacPoint::new(term.bottommost_line(), Column(0)); + term.vi_mode_cursor = ViModeCursor::new(point); + } + } + if let Some(mut selection) = term.selection.take() { + let point = term.vi_mode_cursor.point; + selection.update(point, AlacDirection::Right); + term.selection = Some(selection); + + #[cfg(target_os = "linux")] + if let Some(selection_text) = term.selection_to_string() { + cx.write_to_primary(ClipboardItem::new_string(selection_text)); + } + + self.selection_head = Some(point); + cx.emit(Event::SelectionsChanged) + } + } } InternalEvent::SetSelection(selection) => { term.selection = selection.as_ref().map(|(sel, _)| sel.clone()); @@ -811,6 +855,13 @@ impl Terminal { term.scroll_to_point(*point); self.refresh_hovered_word(); } + InternalEvent::ToggleViMode => { + self.vi_mode_enabled = !self.vi_mode_enabled; + term.toggle_vi_mode(); + } + InternalEvent::ViMotion(motion) => { + term.vi_motion(*motion); + } InternalEvent::FindHyperlink(position, open) => { let prev_hovered_word = self.last_content.last_hovered_word.take(); @@ -1092,7 +1143,109 @@ impl Terminal { self.write_bytes_to_pty(input); } + pub fn toggle_vi_mode(&mut self) { + self.events.push_back(InternalEvent::ToggleViMode); + } + + pub fn vi_motion(&mut self, keystroke: &Keystroke) { + if !self.vi_mode_enabled { + return; + } + + let mut key = keystroke.key.clone(); + if keystroke.modifiers.shift { + key = key.to_uppercase(); + } + + let motion: Option = match key.as_str() { + "h" => Some(ViMotion::Left), + "j" => Some(ViMotion::Down), + "k" => Some(ViMotion::Up), + "l" => Some(ViMotion::Right), + "w" => Some(ViMotion::WordRight), + "b" if !keystroke.modifiers.control => Some(ViMotion::WordLeft), + "e" => Some(ViMotion::WordRightEnd), + "%" => Some(ViMotion::Bracket), + "$" => Some(ViMotion::Last), + "0" => Some(ViMotion::First), + "^" => Some(ViMotion::FirstOccupied), + "H" => Some(ViMotion::High), + "M" => Some(ViMotion::Middle), + "L" => Some(ViMotion::Low), + _ => None, + }; + + if let Some(motion) = motion { + let cursor = self.last_content.cursor.point; + let cursor_pos = Point { + x: cursor.column.0 as f32 * self.last_content.size.cell_width, + y: cursor.line.0 as f32 * self.last_content.size.line_height, + }; + self.events + .push_back(InternalEvent::UpdateSelection(cursor_pos)); + self.events.push_back(InternalEvent::ViMotion(motion)); + return; + } + + let scroll_motion = match key.as_str() { + "g" => Some(AlacScroll::Top), + "G" => Some(AlacScroll::Bottom), + "b" if keystroke.modifiers.control => Some(AlacScroll::PageUp), + "f" if keystroke.modifiers.control => Some(AlacScroll::PageDown), + "d" if keystroke.modifiers.control => { + let amount = self.last_content.size.line_height().to_f64() as i32 / 2; + Some(AlacScroll::Delta(-amount)) + } + "u" if keystroke.modifiers.control => { + let amount = self.last_content.size.line_height().to_f64() as i32 / 2; + Some(AlacScroll::Delta(amount)) + } + _ => None, + }; + + if let Some(scroll_motion) = scroll_motion { + self.events.push_back(InternalEvent::Scroll(scroll_motion)); + return; + } + + match key.as_str() { + "v" => { + let point = self.last_content.cursor.point; + let selection_type = SelectionType::Simple; + let side = AlacDirection::Right; + let selection = Selection::new(selection_type, point, side); + self.events + .push_back(InternalEvent::SetSelection(Some((selection, point)))); + return; + } + + "escape" => { + self.events.push_back(InternalEvent::SetSelection(None)); + return; + } + + "y" => { + self.events.push_back(InternalEvent::Copy); + self.events.push_back(InternalEvent::SetSelection(None)); + return; + } + + "i" => { + self.scroll_to_bottom(); + self.toggle_vi_mode(); + return; + } + _ => {} + } + } + pub fn try_keystroke(&mut self, keystroke: &Keystroke, alt_is_meta: bool) -> bool { + if self.vi_mode_enabled { + self.vi_motion(keystroke); + return true; + } + + // Keep default terminal behavior let esc = to_esc_str(keystroke, &self.last_content.mode, alt_is_meta); if let Some(esc) = esc { self.input(esc); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index ce65be30c6..28a3d61d65 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -22,7 +22,7 @@ use terminal::{ terminal_settings::{CursorShape, TerminalBlink, TerminalSettings, WorkingDirectory}, Clear, Copy, Event, MaybeNavigationTarget, Paste, ScrollLineDown, ScrollLineUp, ScrollPageDown, ScrollPageUp, ScrollToBottom, ScrollToTop, ShowCharacterPalette, TaskStatus, Terminal, - TerminalSize, + TerminalSize, ToggleViMode, }; use terminal_element::{is_blank, TerminalElement}; use terminal_panel::TerminalPanel; @@ -431,6 +431,11 @@ impl TerminalView { cx.notify(); } + fn toggle_vi_mode(&mut self, _: &ToggleViMode, cx: &mut ViewContext) { + self.terminal.update(cx, |term, _| term.toggle_vi_mode()); + cx.notify(); + } + pub fn should_show_cursor(&self, focused: bool, cx: &mut gpui::ViewContext) -> bool { //Don't blink the cursor when not focused, blinking is disabled, or paused if !focused @@ -968,6 +973,7 @@ impl Render for TerminalView { .on_action(cx.listener(TerminalView::scroll_page_down)) .on_action(cx.listener(TerminalView::scroll_to_top)) .on_action(cx.listener(TerminalView::scroll_to_bottom)) + .on_action(cx.listener(TerminalView::toggle_vi_mode)) .on_action(cx.listener(TerminalView::show_character_palette)) .on_action(cx.listener(TerminalView::select_all)) .on_key_down(cx.listener(Self::key_down))