@@ -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<Pixels>, 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<TaskState>,
+ 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<ViMotion> = 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);
@@ -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>) {
+ self.terminal.update(cx, |term, _| term.toggle_vi_mode());
+ cx.notify();
+ }
+
pub fn should_show_cursor(&self, focused: bool, cx: &mut gpui::ViewContext<Self>) -> 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))