diff --git a/.travis.yml b/.travis.yml index f645450..71100b1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ os: - osx rust: - - 1.37.0 + - 1.39.0 - stable - nightly @@ -30,22 +30,22 @@ matrix: - name: "Clippy Linux" os: linux env: CLIPPY=true - rust: 1.37.0 + rust: 1.39.0 - name: "Clippy OSX" os: osx env: CLIPPY=true - rust: 1.37.0 + rust: 1.39.0 - name: "Clippy Windows" os: windows env: CLIPPY=true - rust: 1.37.0-x86_64-pc-windows-msvc + rust: 1.39.0-x86_64-pc-windows-msvc - name: "Rustfmt" os: linux env: RUSTFMT=true rust: nightly - - name: "Windows 1.37.0" + - name: "Windows 1.39.0" os: windows - rust: 1.37.0-x86_64-pc-windows-msvc + rust: 1.39.0-x86_64-pc-windows-msvc - name: "Windows Stable" os: windows rust: stable-x86_64-pc-windows-msvc diff --git a/CHANGELOG.md b/CHANGELOG.md index 009faad..be8b62f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Default Command+N keybinding for SpawnNewInstance on macOS +- Vi mode for copying text and opening links + +### Changed + +- Block cursor is no longer inverted at the start/end of a selection ## 0.4.2-dev diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7b97743..1ffb880 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,7 +42,7 @@ and [easy](https://github.com/alacritty/alacritty/issues?q=is%3Aopen+is%3Aissue+label%3A%22D+-+easy%22) issues. -Please note that the minimum supported version of Alacritty is Rust 1.37.0. All patches are expected +Please note that the minimum supported version of Alacritty is Rust 1.39.0. All patches are expected to work with the minimum supported version. ### Testing diff --git a/alacritty.yml b/alacritty.yml index 1fcf46b..744fd11 100644 --- a/alacritty.yml +++ b/alacritty.yml @@ -187,12 +187,20 @@ # Cursor colors # - # Colors which should be used to draw the terminal cursor. If these are unset, - # the cursor color will be the inverse of the cell color. + # Colors which should be used to draw the terminal cursor. If these are + # unset, the cursor color will be the inverse of the cell color. #cursor: # text: '#000000' # cursor: '#ffffff' + # Vi mode cursor colors + # + # Colors for the cursor when the vi mode is active. If these are unset, the + # cursor color will be the inverse of the cell color. + #vi_mode_cursor: + # text: '#000000' + # cursor: '#ffffff' + # Selection colors # # Colors which should be used to draw the selection area. If selection @@ -298,6 +306,14 @@ # - | Beam #style: Block + # Vi mode cursor style + # + # If the vi mode cursor style is `None` or not specified, it will fall back to + # the style of the active value of the normal cursor. + # + # See `cursor.style` for available options. + #vi_mode_style: None + # If this is `true`, the cursor will be rendered as a hollow box when the # window is not focused. #unfocused_hollow: true @@ -435,6 +451,7 @@ # # - `action`: Execute a predefined action # +# - ToggleViMode # - Copy # - Paste # - PasteSelection @@ -454,9 +471,36 @@ # - ToggleFullscreen # - SpawnNewInstance # - ClearLogNotice +# - ClearSelection # - ReceiveChar # - None # +# (`mode: Vi` only): +# - Open +# - Up +# - Down +# - Left +# - Right +# - First +# - Last +# - FirstOccupied +# - High +# - Middle +# - Low +# - SemanticLeft +# - SemanticRight +# - SemanticLeftEnd +# - SemanticRightEnd +# - WordRight +# - WordLeft +# - WordRightEnd +# - WordLeftEnd +# - Bracket +# - ToggleNormalSelection +# - ToggleLineSelection +# - ToggleBlockSelection +# - ToggleSemanticSelection +# # (macOS only): # - ToggleSimpleFullscreen: Enters fullscreen without occupying another space # @@ -501,6 +545,57 @@ # If the same trigger is assigned to multiple actions, all of them are executed # at once. #key_bindings: + #- { key: Paste, action: Paste } + #- { key: Copy, action: Copy } + #- { key: L, mods: Control, action: ClearLogNotice } + #- { key: L, mods: Control, chars: "\x0c" } + #- { key: PageUp, mods: Shift, action: ScrollPageUp, mode: ~Alt } + #- { key: PageDown, mods: Shift, action: ScrollPageDown, mode: ~Alt } + #- { key: Home, mods: Shift, action: ScrollToTop, mode: ~Alt } + #- { key: End, mods: Shift, action: ScrollToBottom, mode: ~Alt } + + # Vi Mode + #- { key: Space, mods: Shift|Control, mode: Vi, action: ScrollToBottom } + #- { key: Space, mods: Shift|Control, action: ToggleViMode } + #- { key: Escape, mode: Vi, action: ClearSelection } + #- { key: I, mode: Vi, action: ScrollToBottom } + #- { key: I, mode: Vi, action: ToggleViMode } + #- { key: Y, mods: Control, mode: Vi, action: ScrollLineUp } + #- { key: E, mods: Control, mode: Vi, action: ScrollLineDown } + #- { key: G, mode: Vi, action: ScrollToTop } + #- { key: G, mods: Shift, mode: Vi, action: ScrollToBottom } + #- { key: B, mods: Control, mode: Vi, action: ScrollPageUp } + #- { key: F, mods: Control, mode: Vi, action: ScrollPageDown } + #- { key: U, mods: Control, mode: Vi, action: ScrollHalfPageUp } + #- { key: D, mods: Control, mode: Vi, action: ScrollHalfPageDown } + #- { key: Y, mode: Vi, action: Copy } + #- { key: V, mode: Vi, action: ToggleNormalSelection } + #- { key: V, mods: Shift, mode: Vi, action: ToggleLineSelection } + #- { key: V, mods: Control, mode: Vi, action: ToggleBlockSelection } + #- { key: V, mods: Alt, mode: Vi, action: ToggleSemanticSelection } + #- { key: Return, mode: Vi, action: Open } + #- { key: K, mode: Vi, action: Up } + #- { key: J, mode: Vi, action: Down } + #- { key: H, mode: Vi, action: Left } + #- { key: L, mode: Vi, action: Right } + #- { key: Up, mode: Vi, action: Up } + #- { key: Down, mode: Vi, action: Down } + #- { key: Left, mode: Vi, action: Left } + #- { key: Right, mode: Vi, action: Right } + #- { key: Key0, mode: Vi, action: First } + #- { key: Key4, mods: Shift, mode: Vi, action: Last } + #- { key: Key6, mods: Shift, mode: Vi, action: FirstOccupied } + #- { key: H, mods: Shift, mode: Vi, action: High } + #- { key: M, mods: Shift, mode: Vi, action: Middle } + #- { key: L, mods: Shift, mode: Vi, action: Low } + #- { key: B, mode: Vi, action: SemanticLeft } + #- { key: W, mode: Vi, action: SemanticRight } + #- { key: E, mode: Vi, action: SemanticRightEnd } + #- { key: B, mods: Shift, mode: Vi, action: WordLeft } + #- { key: W, mods: Shift, mode: Vi, action: WordRight } + #- { key: E, mods: Shift, mode: Vi, action: WordRightEnd } + #- { key: Key5, mods: Shift, mode: Vi, action: Bracket } + # (Windows, Linux, and BSD only) #- { key: V, mods: Control|Shift, action: Paste } #- { key: C, mods: Control|Shift, action: Copy } @@ -530,14 +625,14 @@ #- { key: N, mods: Command, action: SpawnNewInstance } #- { key: F, mods: Command|Control, action: ToggleFullscreen } - #- { key: Paste, action: Paste } - #- { key: Copy, action: Copy } - #- { key: L, mods: Control, action: ClearLogNotice } - #- { key: L, mods: Control, chars: "\x0c" } - #- { key: PageUp, mods: Shift, action: ScrollPageUp, mode: ~Alt } - #- { key: PageDown, mods: Shift, action: ScrollPageDown, mode: ~Alt } - #- { key: Home, mods: Shift, action: ScrollToTop, mode: ~Alt } - #- { key: End, mods: Shift, action: ScrollToBottom, mode: ~Alt } + #- { key: Paste, action: Paste } + #- { key: Copy, action: Copy } + #- { key: L, mods: Control, action: ClearLogNotice } + #- { key: L, mods: Control, chars: "\x0c" } + #- { key: PageUp, mods: Shift, action: ScrollPageUp, mode: ~Alt } + #- { key: PageDown, mods: Shift, action: ScrollPageDown, mode: ~Alt } + #- { key: Home, mods: Shift, action: ScrollToTop, mode: ~Alt } + #- { key: End, mods: Shift, action: ScrollToBottom, mode: ~Alt } #debug: # Display the time it takes to redraw each frame. diff --git a/alacritty/src/config/bindings.rs b/alacritty/src/config/bindings.rs index 48a1b9f..7485cd5 100644 --- a/alacritty/src/config/bindings.rs +++ b/alacritty/src/config/bindings.rs @@ -13,18 +13,18 @@ // limitations under the License. #![allow(clippy::enum_glob_use)] -use std::fmt; +use std::fmt::{self, Debug, Display}; use std::str::FromStr; use glutin::event::VirtualKeyCode::*; use glutin::event::{ModifiersState, MouseButton, VirtualKeyCode}; -use log::error; use serde::de::Error as SerdeError; use serde::de::{self, MapAccess, Unexpected, Visitor}; use serde::{Deserialize, Deserializer}; +use serde_yaml::Value as SerdeValue; -use alacritty_terminal::config::LOG_TARGET_CONFIG; use alacritty_terminal::term::TermMode; +use alacritty_terminal::vi_mode::ViMotion; /// Describes a state and action to take in that state /// @@ -55,30 +55,6 @@ pub type KeyBinding = Binding; /// Bindings that are triggered by a mouse button pub type MouseBinding = Binding; -impl Default for KeyBinding { - fn default() -> KeyBinding { - KeyBinding { - mods: Default::default(), - action: Action::Esc(String::new()), - mode: TermMode::NONE, - notmode: TermMode::NONE, - trigger: Key::Keycode(A), - } - } -} - -impl Default for MouseBinding { - fn default() -> MouseBinding { - MouseBinding { - mods: Default::default(), - action: Action::Esc(String::new()), - mode: TermMode::NONE, - notmode: TermMode::NONE, - trigger: MouseButton::Left, - } - } -} - impl Binding { #[inline] pub fn is_triggered_by(&self, mode: TermMode, mods: ModifiersState, input: &T) -> bool { @@ -117,6 +93,18 @@ pub enum Action { #[serde(skip)] Esc(String), + /// Run given command. + #[serde(skip)] + Command(String, Vec), + + /// Move vi mode cursor. + #[serde(skip)] + ViMotion(ViMotion), + + /// Perform vi mode action. + #[serde(skip)] + ViAction(ViAction), + /// Paste contents of system clipboard. Paste, @@ -141,6 +129,12 @@ pub enum Action { /// Scroll exactly one page down. ScrollPageDown, + /// Scroll half a page up. + ScrollHalfPageUp, + + /// Scroll half a page down. + ScrollHalfPageDown, + /// Scroll one line up. ScrollLineUp, @@ -156,10 +150,6 @@ pub enum Action { /// Clear the display buffer(s) to remove history. ClearHistory, - /// Run given command. - #[serde(skip)] - Command(String, Vec), - /// Hide the Alacritty window. Hide, @@ -182,6 +172,12 @@ pub enum Action { #[cfg(target_os = "macos")] ToggleSimpleFullscreen, + /// Clear active selection. + ClearSelection, + + /// Toggle vi mode. + ToggleViMode, + /// Allow receiving char input. ReceiveChar, @@ -189,18 +185,50 @@ pub enum Action { None, } -impl Default for Action { - fn default() -> Action { - Action::None - } -} - impl From<&'static str> for Action { fn from(s: &'static str) -> Action { Action::Esc(s.into()) } } +/// Display trait used for error logging. +impl Display for Action { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Action::ViMotion(motion) => motion.fmt(f), + Action::ViAction(action) => action.fmt(f), + _ => write!(f, "{:?}", self), + } + } +} + +/// Vi mode specific actions. +#[derive(Deserialize, Debug, Copy, Clone, PartialEq, Eq)] +pub enum ViAction { + /// Toggle normal vi selection. + ToggleNormalSelection, + /// Toggle line vi selection. + ToggleLineSelection, + /// Toggle block vi selection. + ToggleBlockSelection, + /// Toggle semantic vi selection. + ToggleSemanticSelection, + /// Launch the URL below the vi mode cursor. + Open, +} + +impl From for Action { + fn from(action: ViAction) -> Self { + Self::ViAction(action) + } +} + +impl From for Action { + fn from(motion: ViMotion) -> Self { + Self::ViMotion(motion) + } +} + macro_rules! bindings { ( KeyBinding; @@ -241,16 +269,16 @@ macro_rules! bindings { let mut _mods = ModifiersState::empty(); $(_mods = $mods;)* let mut _mode = TermMode::empty(); - $(_mode = $mode;)* + $(_mode.insert($mode);)* let mut _notmode = TermMode::empty(); - $(_notmode = $notmode;)* + $(_notmode.insert($notmode);)* v.push($ty { trigger: $key, mods: _mods, mode: _mode, notmode: _notmode, - action: $action, + action: $action.into(), }); )* @@ -261,65 +289,109 @@ macro_rules! bindings { pub fn default_mouse_bindings() -> Vec { bindings!( MouseBinding; - MouseButton::Middle; Action::PasteSelection; + MouseButton::Middle, ~TermMode::VI; Action::PasteSelection; ) } pub fn default_key_bindings() -> Vec { let mut bindings = bindings!( KeyBinding; - Paste; Action::Paste; Copy; Action::Copy; + Paste, ~TermMode::VI; Action::Paste; L, ModifiersState::CTRL; Action::ClearLogNotice; - L, ModifiersState::CTRL; Action::Esc("\x0c".into()); - PageUp, ModifiersState::SHIFT, ~TermMode::ALT_SCREEN; Action::ScrollPageUp; - PageDown, ModifiersState::SHIFT, ~TermMode::ALT_SCREEN; Action::ScrollPageDown; + L, ModifiersState::CTRL, ~TermMode::VI; Action::Esc("\x0c".into()); + Tab, ModifiersState::SHIFT, ~TermMode::VI; Action::Esc("\x1b[Z".into()); + Back, ModifiersState::ALT, ~TermMode::VI; Action::Esc("\x1b\x7f".into()); Home, ModifiersState::SHIFT, ~TermMode::ALT_SCREEN; Action::ScrollToTop; End, ModifiersState::SHIFT, ~TermMode::ALT_SCREEN; Action::ScrollToBottom; - Home, +TermMode::APP_CURSOR; Action::Esc("\x1bOH".into()); - Home, ~TermMode::APP_CURSOR; Action::Esc("\x1b[H".into()); - Home, ModifiersState::SHIFT, +TermMode::ALT_SCREEN; Action::Esc("\x1b[1;2H".into()); - End, +TermMode::APP_CURSOR; Action::Esc("\x1bOF".into()); - End, ~TermMode::APP_CURSOR; Action::Esc("\x1b[F".into()); - End, ModifiersState::SHIFT, +TermMode::ALT_SCREEN; Action::Esc("\x1b[1;2F".into()); - PageUp; Action::Esc("\x1b[5~".into()); - PageUp, ModifiersState::SHIFT, +TermMode::ALT_SCREEN; Action::Esc("\x1b[5;2~".into()); - PageDown; Action::Esc("\x1b[6~".into()); - PageDown, ModifiersState::SHIFT, +TermMode::ALT_SCREEN; Action::Esc("\x1b[6;2~".into()); - Tab, ModifiersState::SHIFT; Action::Esc("\x1b[Z".into()); - Back; Action::Esc("\x7f".into()); - Back, ModifiersState::ALT; Action::Esc("\x1b\x7f".into()); - Insert; Action::Esc("\x1b[2~".into()); - Delete; Action::Esc("\x1b[3~".into()); - Up, +TermMode::APP_CURSOR; Action::Esc("\x1bOA".into()); - Up, ~TermMode::APP_CURSOR; Action::Esc("\x1b[A".into()); - Down, +TermMode::APP_CURSOR; Action::Esc("\x1bOB".into()); - Down, ~TermMode::APP_CURSOR; Action::Esc("\x1b[B".into()); - Right, +TermMode::APP_CURSOR; Action::Esc("\x1bOC".into()); - Right, ~TermMode::APP_CURSOR; Action::Esc("\x1b[C".into()); - Left, +TermMode::APP_CURSOR; Action::Esc("\x1bOD".into()); - Left, ~TermMode::APP_CURSOR; Action::Esc("\x1b[D".into()); - F1; Action::Esc("\x1bOP".into()); - F2; Action::Esc("\x1bOQ".into()); - F3; Action::Esc("\x1bOR".into()); - F4; Action::Esc("\x1bOS".into()); - F5; Action::Esc("\x1b[15~".into()); - F6; Action::Esc("\x1b[17~".into()); - F7; Action::Esc("\x1b[18~".into()); - F8; Action::Esc("\x1b[19~".into()); - F9; Action::Esc("\x1b[20~".into()); - F10; Action::Esc("\x1b[21~".into()); - F11; Action::Esc("\x1b[23~".into()); - F12; Action::Esc("\x1b[24~".into()); - F13; Action::Esc("\x1b[25~".into()); - F14; Action::Esc("\x1b[26~".into()); - F15; Action::Esc("\x1b[28~".into()); - F16; Action::Esc("\x1b[29~".into()); - F17; Action::Esc("\x1b[31~".into()); - F18; Action::Esc("\x1b[32~".into()); - F19; Action::Esc("\x1b[33~".into()); - F20; Action::Esc("\x1b[34~".into()); - NumpadEnter; Action::Esc("\n".into()); + PageUp, ModifiersState::SHIFT, ~TermMode::ALT_SCREEN; Action::ScrollPageUp; + PageDown, ModifiersState::SHIFT, ~TermMode::ALT_SCREEN; Action::ScrollPageDown; + Home, ModifiersState::SHIFT, +TermMode::ALT_SCREEN, ~TermMode::VI; + Action::Esc("\x1b[1;2H".into()); + End, ModifiersState::SHIFT, +TermMode::ALT_SCREEN, ~TermMode::VI; + Action::Esc("\x1b[1;2F".into()); + PageUp, ModifiersState::SHIFT, +TermMode::ALT_SCREEN, ~TermMode::VI; + Action::Esc("\x1b[5;2~".into()); + PageDown, ModifiersState::SHIFT, +TermMode::ALT_SCREEN, ~TermMode::VI; + Action::Esc("\x1b[6;2~".into()); + Home, +TermMode::APP_CURSOR, ~TermMode::VI; Action::Esc("\x1bOH".into()); + Home, ~TermMode::APP_CURSOR, ~TermMode::VI; Action::Esc("\x1b[H".into()); + End, +TermMode::APP_CURSOR, ~TermMode::VI; Action::Esc("\x1bOF".into()); + End, ~TermMode::APP_CURSOR, ~TermMode::VI; Action::Esc("\x1b[F".into()); + Up, +TermMode::APP_CURSOR, ~TermMode::VI; Action::Esc("\x1bOA".into()); + Up, ~TermMode::APP_CURSOR, ~TermMode::VI; Action::Esc("\x1b[A".into()); + Down, +TermMode::APP_CURSOR, ~TermMode::VI; Action::Esc("\x1bOB".into()); + Down, ~TermMode::APP_CURSOR, ~TermMode::VI; Action::Esc("\x1b[B".into()); + Right, +TermMode::APP_CURSOR, ~TermMode::VI; Action::Esc("\x1bOC".into()); + Right, ~TermMode::APP_CURSOR, ~TermMode::VI; Action::Esc("\x1b[C".into()); + Left, +TermMode::APP_CURSOR, ~TermMode::VI; Action::Esc("\x1bOD".into()); + Left, ~TermMode::APP_CURSOR, ~TermMode::VI; Action::Esc("\x1b[D".into()); + Back, ~TermMode::VI; Action::Esc("\x7f".into()); + Insert, ~TermMode::VI; Action::Esc("\x1b[2~".into()); + Delete, ~TermMode::VI; Action::Esc("\x1b[3~".into()); + PageUp, ~TermMode::VI; Action::Esc("\x1b[5~".into()); + PageDown, ~TermMode::VI; Action::Esc("\x1b[6~".into()); + F1, ~TermMode::VI; Action::Esc("\x1bOP".into()); + F2, ~TermMode::VI; Action::Esc("\x1bOQ".into()); + F3, ~TermMode::VI; Action::Esc("\x1bOR".into()); + F4, ~TermMode::VI; Action::Esc("\x1bOS".into()); + F5, ~TermMode::VI; Action::Esc("\x1b[15~".into()); + F6, ~TermMode::VI; Action::Esc("\x1b[17~".into()); + F7, ~TermMode::VI; Action::Esc("\x1b[18~".into()); + F8, ~TermMode::VI; Action::Esc("\x1b[19~".into()); + F9, ~TermMode::VI; Action::Esc("\x1b[20~".into()); + F10, ~TermMode::VI; Action::Esc("\x1b[21~".into()); + F11, ~TermMode::VI; Action::Esc("\x1b[23~".into()); + F12, ~TermMode::VI; Action::Esc("\x1b[24~".into()); + F13, ~TermMode::VI; Action::Esc("\x1b[25~".into()); + F14, ~TermMode::VI; Action::Esc("\x1b[26~".into()); + F15, ~TermMode::VI; Action::Esc("\x1b[28~".into()); + F16, ~TermMode::VI; Action::Esc("\x1b[29~".into()); + F17, ~TermMode::VI; Action::Esc("\x1b[31~".into()); + F18, ~TermMode::VI; Action::Esc("\x1b[32~".into()); + F19, ~TermMode::VI; Action::Esc("\x1b[33~".into()); + F20, ~TermMode::VI; Action::Esc("\x1b[34~".into()); + NumpadEnter, ~TermMode::VI; Action::Esc("\n".into()); + Space, ModifiersState::SHIFT | ModifiersState::CTRL, +TermMode::VI; Action::ScrollToBottom; + Space, ModifiersState::SHIFT | ModifiersState::CTRL; Action::ToggleViMode; + Escape, +TermMode::VI; Action::ClearSelection; + I, +TermMode::VI; Action::ScrollToBottom; + I, +TermMode::VI; Action::ToggleViMode; + Y, ModifiersState::CTRL, +TermMode::VI; Action::ScrollLineUp; + E, ModifiersState::CTRL, +TermMode::VI; Action::ScrollLineDown; + G, +TermMode::VI; Action::ScrollToTop; + G, ModifiersState::SHIFT, +TermMode::VI; Action::ScrollToBottom; + B, ModifiersState::CTRL, +TermMode::VI; Action::ScrollPageUp; + F, ModifiersState::CTRL, +TermMode::VI; Action::ScrollPageDown; + U, ModifiersState::CTRL, +TermMode::VI; Action::ScrollHalfPageUp; + D, ModifiersState::CTRL, +TermMode::VI; Action::ScrollHalfPageDown; + Y, +TermMode::VI; Action::Copy; + V, +TermMode::VI; ViAction::ToggleNormalSelection; + V, ModifiersState::SHIFT, +TermMode::VI; ViAction::ToggleLineSelection; + V, ModifiersState::CTRL, +TermMode::VI; ViAction::ToggleBlockSelection; + V, ModifiersState::ALT, +TermMode::VI; ViAction::ToggleSemanticSelection; + Return, +TermMode::VI; ViAction::Open; + K, +TermMode::VI; ViMotion::Up; + J, +TermMode::VI; ViMotion::Down; + H, +TermMode::VI; ViMotion::Left; + L, +TermMode::VI; ViMotion::Right; + Up, +TermMode::VI; ViMotion::Up; + Down, +TermMode::VI; ViMotion::Down; + Left, +TermMode::VI; ViMotion::Left; + Right, +TermMode::VI; ViMotion::Right; + Key0, +TermMode::VI; ViMotion::First; + Key4, ModifiersState::SHIFT, +TermMode::VI; ViMotion::Last; + Key6, ModifiersState::SHIFT, +TermMode::VI; ViMotion::FirstOccupied; + H, ModifiersState::SHIFT, +TermMode::VI; ViMotion::High; + M, ModifiersState::SHIFT, +TermMode::VI; ViMotion::Middle; + L, ModifiersState::SHIFT, +TermMode::VI; ViMotion::Low; + B, +TermMode::VI; ViMotion::SemanticLeft; + W, +TermMode::VI; ViMotion::SemanticRight; + E, +TermMode::VI; ViMotion::SemanticRightEnd; + B, ModifiersState::SHIFT, +TermMode::VI; ViMotion::WordLeft; + W, ModifiersState::SHIFT, +TermMode::VI; ViMotion::WordRight; + E, ModifiersState::SHIFT, +TermMode::VI; ViMotion::WordRightEnd; + Key5, ModifiersState::SHIFT, +TermMode::VI; ViMotion::Bracket; ); // Code Modifiers @@ -348,31 +420,31 @@ pub fn default_key_bindings() -> Vec { let modifiers_code = index + 2; bindings.extend(bindings!( KeyBinding; - Delete, mods; Action::Esc(format!("\x1b[3;{}~", modifiers_code)); - Up, mods; Action::Esc(format!("\x1b[1;{}A", modifiers_code)); - Down, mods; Action::Esc(format!("\x1b[1;{}B", modifiers_code)); - Right, mods; Action::Esc(format!("\x1b[1;{}C", modifiers_code)); - Left, mods; Action::Esc(format!("\x1b[1;{}D", modifiers_code)); - F1, mods; Action::Esc(format!("\x1b[1;{}P", modifiers_code)); - F2, mods; Action::Esc(format!("\x1b[1;{}Q", modifiers_code)); - F3, mods; Action::Esc(format!("\x1b[1;{}R", modifiers_code)); - F4, mods; Action::Esc(format!("\x1b[1;{}S", modifiers_code)); - F5, mods; Action::Esc(format!("\x1b[15;{}~", modifiers_code)); - F6, mods; Action::Esc(format!("\x1b[17;{}~", modifiers_code)); - F7, mods; Action::Esc(format!("\x1b[18;{}~", modifiers_code)); - F8, mods; Action::Esc(format!("\x1b[19;{}~", modifiers_code)); - F9, mods; Action::Esc(format!("\x1b[20;{}~", modifiers_code)); - F10, mods; Action::Esc(format!("\x1b[21;{}~", modifiers_code)); - F11, mods; Action::Esc(format!("\x1b[23;{}~", modifiers_code)); - F12, mods; Action::Esc(format!("\x1b[24;{}~", modifiers_code)); - F13, mods; Action::Esc(format!("\x1b[25;{}~", modifiers_code)); - F14, mods; Action::Esc(format!("\x1b[26;{}~", modifiers_code)); - F15, mods; Action::Esc(format!("\x1b[28;{}~", modifiers_code)); - F16, mods; Action::Esc(format!("\x1b[29;{}~", modifiers_code)); - F17, mods; Action::Esc(format!("\x1b[31;{}~", modifiers_code)); - F18, mods; Action::Esc(format!("\x1b[32;{}~", modifiers_code)); - F19, mods; Action::Esc(format!("\x1b[33;{}~", modifiers_code)); - F20, mods; Action::Esc(format!("\x1b[34;{}~", modifiers_code)); + Delete, mods, ~TermMode::VI; Action::Esc(format!("\x1b[3;{}~", modifiers_code)); + Up, mods, ~TermMode::VI; Action::Esc(format!("\x1b[1;{}A", modifiers_code)); + Down, mods, ~TermMode::VI; Action::Esc(format!("\x1b[1;{}B", modifiers_code)); + Right, mods, ~TermMode::VI; Action::Esc(format!("\x1b[1;{}C", modifiers_code)); + Left, mods, ~TermMode::VI; Action::Esc(format!("\x1b[1;{}D", modifiers_code)); + F1, mods, ~TermMode::VI; Action::Esc(format!("\x1b[1;{}P", modifiers_code)); + F2, mods, ~TermMode::VI; Action::Esc(format!("\x1b[1;{}Q", modifiers_code)); + F3, mods, ~TermMode::VI; Action::Esc(format!("\x1b[1;{}R", modifiers_code)); + F4, mods, ~TermMode::VI; Action::Esc(format!("\x1b[1;{}S", modifiers_code)); + F5, mods, ~TermMode::VI; Action::Esc(format!("\x1b[15;{}~", modifiers_code)); + F6, mods, ~TermMode::VI; Action::Esc(format!("\x1b[17;{}~", modifiers_code)); + F7, mods, ~TermMode::VI; Action::Esc(format!("\x1b[18;{}~", modifiers_code)); + F8, mods, ~TermMode::VI; Action::Esc(format!("\x1b[19;{}~", modifiers_code)); + F9, mods, ~TermMode::VI; Action::Esc(format!("\x1b[20;{}~", modifiers_code)); + F10, mods, ~TermMode::VI; Action::Esc(format!("\x1b[21;{}~", modifiers_code)); + F11, mods, ~TermMode::VI; Action::Esc(format!("\x1b[23;{}~", modifiers_code)); + F12, mods, ~TermMode::VI; Action::Esc(format!("\x1b[24;{}~", modifiers_code)); + F13, mods, ~TermMode::VI; Action::Esc(format!("\x1b[25;{}~", modifiers_code)); + F14, mods, ~TermMode::VI; Action::Esc(format!("\x1b[26;{}~", modifiers_code)); + F15, mods, ~TermMode::VI; Action::Esc(format!("\x1b[28;{}~", modifiers_code)); + F16, mods, ~TermMode::VI; Action::Esc(format!("\x1b[29;{}~", modifiers_code)); + F17, mods, ~TermMode::VI; Action::Esc(format!("\x1b[31;{}~", modifiers_code)); + F18, mods, ~TermMode::VI; Action::Esc(format!("\x1b[32;{}~", modifiers_code)); + F19, mods, ~TermMode::VI; Action::Esc(format!("\x1b[33;{}~", modifiers_code)); + F20, mods, ~TermMode::VI; Action::Esc(format!("\x1b[34;{}~", modifiers_code)); )); // We're adding the following bindings with `Shift` manually above, so skipping them here @@ -380,11 +452,11 @@ pub fn default_key_bindings() -> Vec { if modifiers_code != 2 { bindings.extend(bindings!( KeyBinding; - Insert, mods; Action::Esc(format!("\x1b[2;{}~", modifiers_code)); - PageUp, mods; Action::Esc(format!("\x1b[5;{}~", modifiers_code)); - PageDown, mods; Action::Esc(format!("\x1b[6;{}~", modifiers_code)); - End, mods; Action::Esc(format!("\x1b[1;{}F", modifiers_code)); - Home, mods; Action::Esc(format!("\x1b[1;{}H", modifiers_code)); + Insert, mods, ~TermMode::VI; Action::Esc(format!("\x1b[2;{}~", modifiers_code)); + PageUp, mods, ~TermMode::VI; Action::Esc(format!("\x1b[5;{}~", modifiers_code)); + PageDown, mods, ~TermMode::VI; Action::Esc(format!("\x1b[6;{}~", modifiers_code)); + End, mods, ~TermMode::VI; Action::Esc(format!("\x1b[1;{}F", modifiers_code)); + Home, mods, ~TermMode::VI; Action::Esc(format!("\x1b[1;{}H", modifiers_code)); )); } } @@ -398,9 +470,9 @@ pub fn default_key_bindings() -> Vec { fn common_keybindings() -> Vec { bindings!( KeyBinding; - V, ModifiersState::CTRL | ModifiersState::SHIFT; Action::Paste; + V, ModifiersState::CTRL | ModifiersState::SHIFT, ~TermMode::VI; Action::Paste; C, ModifiersState::CTRL | ModifiersState::SHIFT; Action::Copy; - Insert, ModifiersState::SHIFT; Action::PasteSelection; + Insert, ModifiersState::SHIFT, ~TermMode::VI; Action::PasteSelection; Key0, ModifiersState::CTRL; Action::ResetFontSize; Equals, ModifiersState::CTRL; Action::IncreaseFontSize; Add, ModifiersState::CTRL; Action::IncreaseFontSize; @@ -428,16 +500,16 @@ pub fn platform_key_bindings() -> Vec { pub fn platform_key_bindings() -> Vec { bindings!( KeyBinding; - Key0, ModifiersState::LOGO; Action::ResetFontSize; - Equals, ModifiersState::LOGO; Action::IncreaseFontSize; - Add, ModifiersState::LOGO; Action::IncreaseFontSize; - Minus, ModifiersState::LOGO; Action::DecreaseFontSize; - Insert, ModifiersState::SHIFT; Action::Esc("\x1b[2;2~".into()); + Key0, ModifiersState::LOGO; Action::ResetFontSize; + Equals, ModifiersState::LOGO; Action::IncreaseFontSize; + Add, ModifiersState::LOGO; Action::IncreaseFontSize; + Minus, ModifiersState::LOGO; Action::DecreaseFontSize; + Insert, ModifiersState::SHIFT, ~TermMode::VI; Action::Esc("\x1b[2;2~".into()); + K, ModifiersState::LOGO, ~TermMode::VI; Action::Esc("\x0c".into()); + V, ModifiersState::LOGO, ~TermMode::VI; Action::Paste; N, ModifiersState::LOGO; Action::SpawnNewInstance; F, ModifiersState::CTRL | ModifiersState::LOGO; Action::ToggleFullscreen; K, ModifiersState::LOGO; Action::ClearHistory; - K, ModifiersState::LOGO; Action::Esc("\x0c".into()); - V, ModifiersState::LOGO; Action::Paste; C, ModifiersState::LOGO; Action::Copy; H, ModifiersState::LOGO; Action::Hide; M, ModifiersState::LOGO; Action::Minimize; @@ -463,7 +535,7 @@ impl<'a> Deserialize<'a> for Key { where D: Deserializer<'a>, { - let value = serde_yaml::Value::deserialize(deserializer)?; + let value = SerdeValue::deserialize(deserializer)?; match u32::deserialize(value.clone()) { Ok(scancode) => Ok(Key::Scancode(scancode)), Err(_) => { @@ -491,7 +563,7 @@ impl<'a> Deserialize<'a> for ModeWrapper { fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str( - "Combination of AppCursor | AppKeypad | Alt, possibly with negation (~)", + "a combination of AppCursor | AppKeypad | Alt | Vi, possibly with negation (~)", ) } @@ -509,7 +581,9 @@ impl<'a> Deserialize<'a> for ModeWrapper { "~appkeypad" => res.not_mode |= TermMode::APP_KEYPAD, "alt" => res.mode |= TermMode::ALT_SCREEN, "~alt" => res.not_mode |= TermMode::ALT_SCREEN, - _ => error!(target: LOG_TARGET_CONFIG, "Unknown mode {:?}", modifier), + "vi" => res.mode |= TermMode::VI, + "~vi" => res.not_mode |= TermMode::VI, + _ => return Err(E::invalid_value(Unexpected::Str(modifier), &self)), } } @@ -612,6 +686,8 @@ impl<'a> Deserialize<'a> for RawBinding { where D: Deserializer<'a>, { + const FIELDS: &[&str] = &["key", "mods", "mode", "action", "chars", "mouse", "command"]; + enum Field { Key, Mods, @@ -629,9 +705,6 @@ impl<'a> Deserialize<'a> for RawBinding { { struct FieldVisitor; - static FIELDS: &[&str] = - &["key", "mods", "mode", "action", "chars", "mouse", "command"]; - impl<'a> Visitor<'a> for FieldVisitor { type Value = Field; @@ -681,7 +754,7 @@ impl<'a> Deserialize<'a> for RawBinding { let mut mouse: Option = None; let mut command: Option = None; - use ::serde::de::Error; + use de::Error; while let Some(struct_key) = map.next_key::()? { match struct_key { @@ -690,10 +763,10 @@ impl<'a> Deserialize<'a> for RawBinding { return Err(::duplicate_field("key")); } - let val = map.next_value::()?; + let val = map.next_value::()?; if val.is_u64() { let scancode = val.as_u64().unwrap(); - if scancode > u64::from(::std::u32::MAX) { + if scancode > u64::from(std::u32::MAX) { return Err(::custom(format!( "Invalid key binding, scancode too big: {}", scancode @@ -726,7 +799,36 @@ impl<'a> Deserialize<'a> for RawBinding { return Err(::duplicate_field("action")); } - action = Some(map.next_value::()?); + let value = map.next_value::()?; + + action = if let Ok(vi_action) = ViAction::deserialize(value.clone()) { + Some(vi_action.into()) + } else if let Ok(vi_motion) = ViMotion::deserialize(value.clone()) { + Some(vi_motion.into()) + } else { + match Action::deserialize(value.clone()).map_err(V::Error::custom) { + Ok(action) => Some(action), + Err(err) => { + let value = match value { + SerdeValue::String(string) => string, + SerdeValue::Mapping(map) if map.len() == 1 => { + match map.into_iter().next() { + Some(( + SerdeValue::String(string), + SerdeValue::Null, + )) => string, + _ => return Err(err), + } + }, + _ => return Err(err), + }; + return Err(V::Error::custom(format!( + "unknown keyboard action `{}`", + value + ))); + }, + } + }; }, Field::Chars => { if chars.is_some() { @@ -752,7 +854,21 @@ impl<'a> Deserialize<'a> for RawBinding { } } + let mode = mode.unwrap_or_else(TermMode::empty); + let not_mode = not_mode.unwrap_or_else(TermMode::empty); + let mods = mods.unwrap_or_else(ModifiersState::default); + let action = match (action, chars, command) { + (Some(action @ Action::ViMotion(_)), None, None) + | (Some(action @ Action::ViAction(_)), None, None) => { + if !mode.intersects(TermMode::VI) || not_mode.intersects(TermMode::VI) { + return Err(V::Error::custom(format!( + "action `{}` is only available in vi mode, try adding `mode: Vi`", + action, + ))); + } + action + }, (Some(action), None, None) => action, (None, Some(chars), None) => Action::Esc(chars), (None, None, Some(cmd)) => match cmd { @@ -761,18 +877,13 @@ impl<'a> Deserialize<'a> for RawBinding { Action::Command(program, args) }, }, - (None, None, None) => { - return Err(V::Error::custom("must specify chars, action or command")); - }, _ => { - return Err(V::Error::custom("must specify only chars, action or command")) + return Err(V::Error::custom( + "must specify exactly one of chars, action or command", + )) }, }; - let mode = mode.unwrap_or_else(TermMode::empty); - let not_mode = not_mode.unwrap_or_else(TermMode::empty); - let mods = mods.unwrap_or_else(ModifiersState::default); - if mouse.is_none() && key.is_none() { return Err(V::Error::custom("bindings require mouse button or key")); } @@ -781,8 +892,6 @@ impl<'a> Deserialize<'a> for RawBinding { } } - const FIELDS: &[&str] = &["key", "mods", "mode", "action", "chars", "mouse", "command"]; - deserializer.deserialize_struct("RawBinding", FIELDS, RawBindingVisitor) } } @@ -793,7 +902,8 @@ impl<'a> Deserialize<'a> for MouseBinding { D: Deserializer<'a>, { let raw = RawBinding::deserialize(deserializer)?; - raw.into_mouse_binding().map_err(|_| D::Error::custom("expected mouse binding")) + raw.into_mouse_binding() + .map_err(|_| D::Error::custom("expected mouse binding, got key binding")) } } @@ -803,7 +913,8 @@ impl<'a> Deserialize<'a> for KeyBinding { D: Deserializer<'a>, { let raw = RawBinding::deserialize(deserializer)?; - raw.into_key_binding().map_err(|_| D::Error::custom("expected key binding")) + raw.into_key_binding() + .map_err(|_| D::Error::custom("expected key binding, got mouse binding")) } } @@ -858,7 +969,7 @@ impl<'a> de::Deserialize<'a> for ModsWrapper { type Value = ModsWrapper; fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("Some subset of Command|Shift|Super|Alt|Option|Control") + f.write_str("a subset of Shift|Control|Super|Command|Alt|Option") } fn visit_str(self, value: &str) -> Result @@ -873,7 +984,7 @@ impl<'a> de::Deserialize<'a> for ModsWrapper { "alt" | "option" => res.insert(ModifiersState::ALT), "control" => res.insert(ModifiersState::CTRL), "none" => (), - _ => error!(target: LOG_TARGET_CONFIG, "Unknown modifier {:?}", modifier), + _ => return Err(E::invalid_value(Unexpected::Str(modifier), &self)), } } @@ -899,7 +1010,7 @@ mod tests { fn default() -> Self { Self { mods: Default::default(), - action: Default::default(), + action: Action::None, mode: TermMode::empty(), notmode: TermMode::empty(), trigger: Default::default(), diff --git a/alacritty/src/config/mod.rs b/alacritty/src/config/mod.rs index 2a59871..bb05d98 100644 --- a/alacritty/src/config/mod.rs +++ b/alacritty/src/config/mod.rs @@ -15,7 +15,7 @@ pub mod monitor; mod mouse; mod ui_config; -pub use crate::config::bindings::{Action, Binding, Key}; +pub use crate::config::bindings::{Action, Binding, Key, ViAction}; #[cfg(test)] pub use crate::config::mouse::{ClickHandler, Mouse}; use crate::config::ui_config::UIConfig; diff --git a/alacritty/src/config/ui_config.rs b/alacritty/src/config/ui_config.rs index d7a477a..13a3b04 100644 --- a/alacritty/src/config/ui_config.rs +++ b/alacritty/src/config/ui_config.rs @@ -1,6 +1,7 @@ +use log::error; use serde::{Deserialize, Deserializer}; -use alacritty_terminal::config::failure_default; +use alacritty_terminal::config::{failure_default, LOG_TARGET_CONFIG}; use crate::config::bindings::{self, Binding, KeyBinding, MouseBinding}; use crate::config::mouse::Mouse; @@ -60,7 +61,18 @@ where T: Copy + Eq, Binding: Deserialize<'a>, { - let mut bindings: Vec> = failure_default(deserializer)?; + let values = Vec::::deserialize(deserializer)?; + + // Skip all invalid values + let mut bindings = Vec::with_capacity(values.len()); + for value in values { + match Binding::::deserialize(value) { + Ok(binding) => bindings.push(binding), + Err(err) => { + error!(target: LOG_TARGET_CONFIG, "Problem with config: {}; ignoring binding", err); + }, + } + } // Remove matching default bindings for binding in bindings.iter() { diff --git a/alacritty/src/display.rs b/alacritty/src/display.rs index 317c875..6d5d810 100644 --- a/alacritty/src/display.rs +++ b/alacritty/src/display.rs @@ -366,6 +366,12 @@ impl Display { let selection = !terminal.selection().as_ref().map(Selection::is_empty).unwrap_or(true); let mouse_mode = terminal.mode().intersects(TermMode::MOUSE_MODE); + let vi_mode_cursor = if terminal.mode().contains(TermMode::VI) { + Some(terminal.vi_mode_cursor) + } else { + None + }; + // Update IME position #[cfg(not(windows))] self.window.update_ime_position(&terminal, &self.size_info); @@ -419,6 +425,13 @@ impl Display { } } + // Highlight URLs at the vi mode cursor position + if let Some(vi_mode_cursor) = vi_mode_cursor { + if let Some(url) = self.urls.find_at(vi_mode_cursor.point) { + rects.append(&mut url.rects(&metrics, &size_info)); + } + } + // Push visual bell after url/underline/strikeout rects if visual_bell_intensity != 0. { let visual_bell_rect = RenderRect::new( diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs index e635283..9757893 100644 --- a/alacritty/src/event.rs +++ b/alacritty/src/event.rs @@ -27,10 +27,10 @@ use alacritty_terminal::event::{Event, EventListener, Notify}; use alacritty_terminal::grid::Scroll; use alacritty_terminal::index::{Column, Line, Point, Side}; use alacritty_terminal::message_bar::{Message, MessageBuffer}; -use alacritty_terminal::selection::Selection; +use alacritty_terminal::selection::{Selection, SelectionType}; use alacritty_terminal::sync::FairMutex; use alacritty_terminal::term::cell::Cell; -use alacritty_terminal::term::{SizeInfo, Term}; +use alacritty_terminal::term::{SizeInfo, Term, TermMode}; #[cfg(not(windows))] use alacritty_terminal::tty; use alacritty_terminal::util::{limit, start_daemon}; @@ -40,6 +40,7 @@ use crate::config; use crate::config::Config; use crate::display::Display; use crate::input::{self, ActionContext as _, FONT_SIZE_STEP}; +use crate::url::{Url, Urls}; use crate::window::Window; #[derive(Default, Clone, Debug, PartialEq)] @@ -68,6 +69,7 @@ pub struct ActionContext<'a, N, T> { pub display_update_pending: &'a mut DisplayUpdate, pub config: &'a mut Config, pub event_loop: &'a EventLoopWindowTarget, + pub urls: &'a Urls, font_size: &'a mut Size, } @@ -83,7 +85,12 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon fn scroll(&mut self, scroll: Scroll) { self.terminal.scroll_display(scroll); - if let ElementState::Pressed = self.mouse().left_button_state { + // Update selection + if self.terminal.mode().contains(TermMode::VI) + && self.terminal.selection().as_ref().map(|s| s.is_empty()) != Some(true) + { + self.update_selection(self.terminal.vi_mode_cursor.point, Side::Right); + } else if ElementState::Pressed == self.mouse().left_button_state { let (x, y) = (self.mouse().x, self.mouse().y); let size_info = self.size_info(); let point = size_info.pixels_to_coords(x, y); @@ -113,35 +120,35 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon let point = self.terminal.visible_to_buffer(point); // Update selection if one exists - if let Some(ref mut selection) = self.terminal.selection_mut() { + let vi_mode = self.terminal.mode().contains(TermMode::VI); + if let Some(selection) = self.terminal.selection_mut() { selection.update(point, side); + + if vi_mode { + selection.include_all(); + } + + self.terminal.dirty = true; } + } + fn start_selection(&mut self, ty: SelectionType, point: Point, side: Side) { + let point = self.terminal.visible_to_buffer(point); + *self.terminal.selection_mut() = Some(Selection::new(ty, point, side)); self.terminal.dirty = true; } - fn simple_selection(&mut self, point: Point, side: Side) { - let point = self.terminal.visible_to_buffer(point); - *self.terminal.selection_mut() = Some(Selection::simple(point, side)); - self.terminal.dirty = true; - } - - fn block_selection(&mut self, point: Point, side: Side) { - let point = self.terminal.visible_to_buffer(point); - *self.terminal.selection_mut() = Some(Selection::block(point, side)); - self.terminal.dirty = true; - } - - fn semantic_selection(&mut self, point: Point) { - let point = self.terminal.visible_to_buffer(point); - *self.terminal.selection_mut() = Some(Selection::semantic(point)); - self.terminal.dirty = true; - } - - fn line_selection(&mut self, point: Point) { - let point = self.terminal.visible_to_buffer(point); - *self.terminal.selection_mut() = Some(Selection::lines(point)); - self.terminal.dirty = true; + fn toggle_selection(&mut self, ty: SelectionType, point: Point, side: Side) { + match self.terminal.selection_mut() { + Some(selection) if selection.ty == ty && !selection.is_empty() => { + self.clear_selection(); + }, + Some(selection) if !selection.is_empty() => { + selection.ty = ty; + self.terminal.dirty = true; + }, + _ => self.start_selection(ty, point, side), + } } fn mouse_coords(&self) -> Option { @@ -155,6 +162,12 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon } } + #[inline] + fn mouse_mode(&self) -> bool { + self.terminal.mode().intersects(TermMode::MOUSE_MODE) + && !self.terminal.mode().contains(TermMode::VI) + } + #[inline] fn mouse_mut(&mut self) -> &mut Mouse { self.mouse @@ -254,8 +267,32 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon fn event_loop(&self) -> &EventLoopWindowTarget { self.event_loop } + + fn urls(&self) -> &Urls { + self.urls + } + + /// Spawn URL launcher when clicking on URLs. + fn launch_url(&self, url: Url) { + if self.mouse.block_url_launcher { + return; + } + + if let Some(ref launcher) = self.config.ui_config.mouse.url.launcher { + let mut args = launcher.args().to_vec(); + let start = self.terminal.visible_to_buffer(url.start()); + let end = self.terminal.visible_to_buffer(url.end()); + args.push(self.terminal.bounds_to_string(start, end)); + + match start_daemon(launcher.program(), &args) { + Ok(_) => debug!("Launched {} with args {:?}", launcher.program(), args), + Err(_) => warn!("Unable to launch {} with args {:?}", launcher.program(), args), + } + } + } } +#[derive(Debug)] pub enum ClickState { None, Click, @@ -264,6 +301,7 @@ pub enum ClickState { } /// State of the mouse +#[derive(Debug)] pub struct Mouse { pub x: usize, pub y: usize, @@ -412,10 +450,10 @@ impl Processor { window: &mut self.display.window, font_size: &mut self.font_size, config: &mut self.config, + urls: &self.display.urls, event_loop, }; - let mut processor = - input::Processor::new(context, &self.display.urls, &self.display.highlighted_url); + let mut processor = input::Processor::new(context, &self.display.highlighted_url); for event in event_queue.drain(..) { Processor::handle_event(event, &mut processor); diff --git a/alacritty/src/input.rs b/alacritty/src/input.rs index 844710d..937457c 100644 --- a/alacritty/src/input.rs +++ b/alacritty/src/input.rs @@ -19,8 +19,7 @@ //! needs to be tracked. Additionally, we need a bit of a state machine to //! determine what to do when a non-modifier key is pressed. use std::borrow::Cow; -use std::cmp::min; -use std::cmp::Ordering; +use std::cmp::{min, Ordering}; use std::marker::PhantomData; use std::time::Instant; @@ -40,12 +39,13 @@ use alacritty_terminal::event::{Event, EventListener}; use alacritty_terminal::grid::Scroll; use alacritty_terminal::index::{Column, Line, Point, Side}; use alacritty_terminal::message_bar::{self, Message}; -use alacritty_terminal::selection::Selection; +use alacritty_terminal::selection::SelectionType; use alacritty_terminal::term::mode::TermMode; use alacritty_terminal::term::{SizeInfo, Term}; use alacritty_terminal::util::start_daemon; +use alacritty_terminal::vi_mode::ViMotion; -use crate::config::{Action, Binding, Config, Key}; +use crate::config::{Action, Binding, Config, Key, ViAction}; use crate::event::{ClickState, Mouse}; use crate::url::{Url, Urls}; use crate::window::Window; @@ -59,21 +59,18 @@ pub const FONT_SIZE_STEP: f32 = 0.5; /// are activated. pub struct Processor<'a, T: EventListener, A: ActionContext> { pub ctx: A, - pub urls: &'a Urls, pub highlighted_url: &'a Option, _phantom: PhantomData, } pub trait ActionContext { - fn write_to_pty>>(&mut self, _: B); + fn write_to_pty>>(&mut self, data: B); fn size_info(&self) -> SizeInfo; - fn copy_selection(&mut self, _: ClipboardType); - fn clear_selection(&mut self); + fn copy_selection(&mut self, ty: ClipboardType); + fn start_selection(&mut self, ty: SelectionType, point: Point, side: Side); + fn toggle_selection(&mut self, ty: SelectionType, point: Point, side: Side); fn update_selection(&mut self, point: Point, side: Side); - fn simple_selection(&mut self, point: Point, side: Side); - fn block_selection(&mut self, point: Point, side: Side); - fn semantic_selection(&mut self, point: Point); - fn line_selection(&mut self, point: Point); + fn clear_selection(&mut self); fn selection_is_empty(&self) -> bool; fn mouse_mut(&mut self) -> &mut Mouse; fn mouse(&self) -> &Mouse; @@ -93,6 +90,9 @@ pub trait ActionContext { fn message(&self) -> Option<&Message>; fn config(&self) -> &Config; fn event_loop(&self) -> &EventLoopWindowTarget; + fn urls(&self) -> &Urls; + fn launch_url(&self, url: Url); + fn mouse_mode(&self) -> bool; } trait Execute { @@ -107,6 +107,22 @@ impl Execute for Binding { } } +impl Action { + fn toggle_selection(ctx: &mut A, ty: SelectionType) + where + T: EventListener, + A: ActionContext, + { + let cursor_point = ctx.terminal().vi_mode_cursor.point; + ctx.toggle_selection(ty, cursor_point, Side::Left); + + // Make sure initial selection is not empty + if let Some(selection) = ctx.terminal_mut().selection_mut() { + selection.include_all(); + } + } +} + impl Execute for Action { #[inline] fn execute>(&self, ctx: &mut A) { @@ -118,6 +134,11 @@ impl Execute for Action { }, Action::Copy => { ctx.copy_selection(ClipboardType::Clipboard); + + // Clear selection in vi mode for better user feedback + if ctx.terminal().mode().contains(TermMode::VI) { + ctx.clear_selection(); + } }, Action::Paste => { let text = ctx.terminal_mut().clipboard().load(ClipboardType::Clipboard); @@ -135,6 +156,27 @@ impl Execute for Action { Err(err) => warn!("Couldn't run command {}", err), } }, + Action::ClearSelection => ctx.clear_selection(), + Action::ToggleViMode => ctx.terminal_mut().toggle_vi_mode(), + Action::ViAction(ViAction::ToggleNormalSelection) => { + Self::toggle_selection(ctx, SelectionType::Simple) + }, + Action::ViAction(ViAction::ToggleLineSelection) => { + Self::toggle_selection(ctx, SelectionType::Lines) + }, + Action::ViAction(ViAction::ToggleBlockSelection) => { + Self::toggle_selection(ctx, SelectionType::Block) + }, + Action::ViAction(ViAction::ToggleSemanticSelection) => { + Self::toggle_selection(ctx, SelectionType::Semantic) + }, + Action::ViAction(ViAction::Open) => { + ctx.mouse_mut().block_url_launcher = false; + if let Some(url) = ctx.urls().find_at(ctx.terminal().vi_mode_cursor.point) { + ctx.launch_url(url); + } + }, + Action::ViMotion(motion) => ctx.terminal_mut().vi_motion(motion), Action::ToggleFullscreen => ctx.window_mut().toggle_fullscreen(), #[cfg(target_os = "macos")] Action::ToggleSimpleFullscreen => ctx.window_mut().toggle_simple_fullscreen(), @@ -147,12 +189,74 @@ impl Execute for Action { Action::IncreaseFontSize => ctx.change_font_size(FONT_SIZE_STEP), Action::DecreaseFontSize => ctx.change_font_size(FONT_SIZE_STEP * -1.), Action::ResetFontSize => ctx.reset_font_size(), - Action::ScrollPageUp => ctx.scroll(Scroll::PageUp), - Action::ScrollPageDown => ctx.scroll(Scroll::PageDown), - Action::ScrollLineUp => ctx.scroll(Scroll::Lines(1)), - Action::ScrollLineDown => ctx.scroll(Scroll::Lines(-1)), - Action::ScrollToTop => ctx.scroll(Scroll::Top), - Action::ScrollToBottom => ctx.scroll(Scroll::Bottom), + Action::ScrollPageUp => { + // Move vi mode cursor + let term = ctx.terminal_mut(); + let scroll_lines = term.grid().num_lines().0 as isize; + term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines); + + ctx.scroll(Scroll::PageUp); + }, + Action::ScrollPageDown => { + // Move vi mode cursor + let term = ctx.terminal_mut(); + let scroll_lines = -(term.grid().num_lines().0 as isize); + term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines); + + ctx.scroll(Scroll::PageDown); + }, + Action::ScrollHalfPageUp => { + // Move vi mode cursor + let term = ctx.terminal_mut(); + let scroll_lines = term.grid().num_lines().0 as isize / 2; + term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines); + + ctx.scroll(Scroll::Lines(scroll_lines)); + }, + Action::ScrollHalfPageDown => { + // Move vi mode cursor + let term = ctx.terminal_mut(); + let scroll_lines = -(term.grid().num_lines().0 as isize / 2); + term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines); + + ctx.scroll(Scroll::Lines(scroll_lines)); + }, + Action::ScrollLineUp => { + // Move vi mode cursor + let term = ctx.terminal(); + if term.grid().display_offset() != term.grid().history_size() + && term.vi_mode_cursor.point.line + 1 != term.grid().num_lines() + { + ctx.terminal_mut().vi_mode_cursor.point.line += 1; + } + + ctx.scroll(Scroll::Lines(1)); + }, + Action::ScrollLineDown => { + // Move vi mode cursor + if ctx.terminal().grid().display_offset() != 0 + && ctx.terminal().vi_mode_cursor.point.line.0 != 0 + { + ctx.terminal_mut().vi_mode_cursor.point.line -= 1; + } + + ctx.scroll(Scroll::Lines(-1)); + }, + Action::ScrollToTop => { + ctx.scroll(Scroll::Top); + + // Move vi mode cursor + ctx.terminal_mut().vi_mode_cursor.point.line = Line(0); + ctx.terminal_mut().vi_motion(ViMotion::FirstOccupied); + }, + Action::ScrollToBottom => { + ctx.scroll(Scroll::Bottom); + + // Move vi mode cursor + let term = ctx.terminal_mut(); + term.vi_mode_cursor.point.line = term.grid().num_lines() - 1; + term.vi_motion(ViMotion::FirstOccupied); + }, Action::ClearHistory => ctx.terminal_mut().clear_screen(ClearMode::Saved), Action::ClearLogNotice => ctx.pop_message(), Action::SpawnNewInstance => ctx.spawn_new_instance(), @@ -197,8 +301,8 @@ impl From for CursorIcon { } impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { - pub fn new(ctx: A, urls: &'a Urls, highlighted_url: &'a Option) -> Self { - Self { ctx, urls, highlighted_url, _phantom: Default::default() } + pub fn new(ctx: A, highlighted_url: &'a Option) -> Self { + Self { ctx, highlighted_url, _phantom: Default::default() } } #[inline] @@ -238,12 +342,16 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { let last_term_line = self.ctx.terminal().grid().num_lines() - 1; if self.ctx.mouse().left_button_state == ElementState::Pressed - && (self.ctx.modifiers().shift() - || !self.ctx.terminal().mode().intersects(TermMode::MOUSE_MODE)) + && (self.ctx.modifiers().shift() || !self.ctx.mouse_mode()) { // Treat motion over message bar like motion over the last line let line = min(point.line, last_term_line); + // Move vi mode cursor to mouse cursor position + if self.ctx.terminal().mode().contains(TermMode::VI) { + self.ctx.terminal_mut().vi_mode_cursor.point = point; + } + self.ctx.update_selection(Point { line, col: point.col }, cell_side); } else if inside_grid && cell_changed @@ -354,13 +462,15 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { fn on_mouse_double_click(&mut self, button: MouseButton, point: Point) { if button == MouseButton::Left { - self.ctx.semantic_selection(point); + let side = self.ctx.mouse().cell_side; + self.ctx.start_selection(SelectionType::Semantic, point, side); } } fn on_mouse_triple_click(&mut self, button: MouseButton, point: Point) { if button == MouseButton::Left { - self.ctx.line_selection(point); + let side = self.ctx.mouse().cell_side; + self.ctx.start_selection(SelectionType::Lines, point, side); } } @@ -402,14 +512,18 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { // Start new empty selection let side = self.ctx.mouse().cell_side; if self.ctx.modifiers().ctrl() { - self.ctx.block_selection(point, side); + self.ctx.start_selection(SelectionType::Block, point, side); } else { - self.ctx.simple_selection(point, side); + self.ctx.start_selection(SelectionType::Simple, point, side); } - if !self.ctx.modifiers().shift() - && self.ctx.terminal().mode().intersects(TermMode::MOUSE_MODE) - { + // Move vi mode cursor to mouse position + if self.ctx.terminal().mode().contains(TermMode::VI) { + // Update vi mode cursor position on click + self.ctx.terminal_mut().vi_mode_cursor.point = point; + } + + if !self.ctx.modifiers().shift() && self.ctx.mouse_mode() { let code = match button { MouseButton::Left => 0, MouseButton::Middle => 1, @@ -427,9 +541,7 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { } fn on_mouse_release(&mut self, button: MouseButton) { - if !self.ctx.modifiers().shift() - && self.ctx.terminal().mode().intersects(TermMode::MOUSE_MODE) - { + if !self.ctx.modifiers().shift() && self.ctx.mouse_mode() { let code = match button { MouseButton::Left => 0, MouseButton::Middle => 1, @@ -440,31 +552,12 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { self.mouse_report(code, ElementState::Released); return; } else if let (MouseButton::Left, MouseState::Url(url)) = (button, self.mouse_state()) { - self.launch_url(url); + self.ctx.launch_url(url); } self.copy_selection(); } - /// Spawn URL launcher when clicking on URLs. - fn launch_url(&self, url: Url) { - if self.ctx.mouse().block_url_launcher { - return; - } - - if let Some(ref launcher) = self.ctx.config().ui_config.mouse.url.launcher { - let mut args = launcher.args().to_vec(); - let start = self.ctx.terminal().visible_to_buffer(url.start()); - let end = self.ctx.terminal().visible_to_buffer(url.end()); - args.push(self.ctx.terminal().bounds_to_string(start, end)); - - match start_daemon(launcher.program(), &args) { - Ok(_) => debug!("Launched {} with args {:?}", launcher.program(), args), - Err(_) => warn!("Unable to launch {} with args {:?}", launcher.program(), args), - } - } - } - pub fn mouse_wheel_input(&mut self, delta: MouseScrollDelta, phase: TouchPhase) { match delta { MouseScrollDelta::LineDelta(_columns, lines) => { @@ -489,7 +582,7 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { fn scroll_terminal(&mut self, new_scroll_px: f64) { let height = f64::from(self.ctx.size_info().cell_height); - if self.ctx.terminal().mode().intersects(TermMode::MOUSE_MODE) { + if self.ctx.mouse_mode() { self.ctx.mouse_mut().scroll_px += new_scroll_px; let code = if new_scroll_px > 0. { 64 } else { 65 }; @@ -530,7 +623,22 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { let lines = self.ctx.mouse().scroll_px / height; + // Store absolute position of vi mode cursor + let term = self.ctx.terminal(); + let absolute = term.visible_to_buffer(term.vi_mode_cursor.point); + self.ctx.scroll(Scroll::Lines(lines as isize)); + + // Try to restore vi mode cursor position, to keep it above its previous content + let term = self.ctx.terminal_mut(); + term.vi_mode_cursor.point = term.grid().clamp_buffer_to_visible(absolute); + term.vi_mode_cursor.point.col = absolute.col; + + // Update selection + let point = term.vi_mode_cursor.point; + if !self.ctx.selection_is_empty() { + self.ctx.update_selection(point, Side::Right); + } } self.ctx.mouse_mut().scroll_px %= height; @@ -560,7 +668,6 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { // Reset cursor when message bar height changed or all messages are gone let size = self.ctx.size_info(); - let mouse_mode = self.ctx.terminal().mode().intersects(TermMode::MOUSE_MODE); let current_lines = (size.lines() - self.ctx.terminal().grid().num_lines()).0; let new_lines = self.ctx.message().map(|m| m.text(&size).len()).unwrap_or(0); @@ -568,7 +675,7 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { Ordering::Less => CursorIcon::Default, Ordering::Equal => CursorIcon::Hand, Ordering::Greater => { - if mouse_mode { + if self.ctx.mouse_mode() { CursorIcon::Default } else { CursorIcon::Text @@ -613,7 +720,7 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { /// Process a received character. pub fn received_char(&mut self, c: char) { - if *self.ctx.suppress_chars() { + if *self.ctx.suppress_chars() || self.ctx.terminal().mode().contains(TermMode::VI) { return; } @@ -685,7 +792,7 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { fn process_mouse_bindings(&mut self, button: MouseButton) { let mods = *self.ctx.modifiers(); let mode = *self.ctx.terminal().mode(); - let mouse_mode = mode.intersects(TermMode::MOUSE_MODE); + let mouse_mode = self.ctx.mouse_mode(); for i in 0..self.ctx.config().ui_config.mouse_bindings.len() { let mut binding = self.ctx.config().ui_config.mouse_bindings[i].clone(); @@ -743,22 +850,24 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { return MouseState::MessageBar; } + let mouse_mode = self.ctx.mouse_mode(); + // Check for URL at mouse cursor let mods = *self.ctx.modifiers(); - let selection = - !self.ctx.terminal().selection().as_ref().map(Selection::is_empty).unwrap_or(true); - let mouse_mode = self.ctx.terminal().mode().intersects(TermMode::MOUSE_MODE); - let highlighted_url = - self.urls.highlighted(self.ctx.config(), self.ctx.mouse(), mods, mouse_mode, selection); + let highlighted_url = self.ctx.urls().highlighted( + self.ctx.config(), + self.ctx.mouse(), + mods, + mouse_mode, + !self.ctx.selection_is_empty(), + ); if let Some(url) = highlighted_url { return MouseState::Url(url); } // Check mouse mode if location is not special - if self.ctx.terminal().mode().intersects(TermMode::MOUSE_MODE) - && !self.ctx.modifiers().shift() - { + if !self.ctx.modifiers().shift() && mouse_mode { MouseState::Mouse } else { MouseState::Text @@ -781,12 +890,12 @@ mod tests { use alacritty_terminal::grid::Scroll; use alacritty_terminal::index::{Point, Side}; use alacritty_terminal::message_bar::{Message, MessageBuffer}; - use alacritty_terminal::selection::Selection; + use alacritty_terminal::selection::{Selection, SelectionType}; use alacritty_terminal::term::{SizeInfo, Term, TermMode}; use crate::config::{ClickHandler, Config}; use crate::event::{ClickState, Mouse}; - use crate::url::Urls; + use crate::url::{Url, Urls}; use crate::window::Window; use super::{Action, Binding, Processor}; @@ -799,7 +908,7 @@ mod tests { fn send_event(&self, _event: TerminalEvent) {} } - #[derive(PartialEq)] + #[derive(Debug, PartialEq)] enum MultiClick { DoubleClick, TripleClick, @@ -824,9 +933,15 @@ mod tests { fn update_selection(&mut self, _point: Point, _side: Side) {} - fn simple_selection(&mut self, _point: Point, _side: Side) {} + fn start_selection(&mut self, ty: SelectionType, _point: Point, _side: Side) { + match ty { + SelectionType::Semantic => self.last_action = MultiClick::DoubleClick, + SelectionType::Lines => self.last_action = MultiClick::TripleClick, + _ => (), + } + } - fn block_selection(&mut self, _point: Point, _side: Side) {} + fn toggle_selection(&mut self, _ty: SelectionType, _point: Point, _side: Side) {} fn copy_selection(&mut self, _: ClipboardType) {} @@ -850,15 +965,6 @@ mod tests { *self.size_info } - fn semantic_selection(&mut self, _point: Point) { - // set something that we can check for here - self.last_action = MultiClick::DoubleClick; - } - - fn line_selection(&mut self, _point: Point) { - self.last_action = MultiClick::TripleClick; - } - fn selection_is_empty(&self) -> bool { true } @@ -878,6 +984,10 @@ mod tests { } } + fn mouse_mode(&self) -> bool { + false + } + #[inline] fn mouse_mut(&mut self) -> &mut Mouse { self.mouse @@ -923,6 +1033,14 @@ mod tests { fn event_loop(&self) -> &EventLoopWindowTarget { unimplemented!(); } + + fn urls(&self) -> &Urls { + unimplemented!(); + } + + fn launch_url(&self, _: Url) { + unimplemented!(); + } } macro_rules! test_clickstate { @@ -981,8 +1099,7 @@ mod tests { config: &cfg, }; - let urls = Urls::new(); - let mut processor = Processor::new(context, &urls, &None); + let mut processor = Processor::new(context, &None); let event: Event::<'_, TerminalEvent> = $input; if let Event::WindowEvent { diff --git a/alacritty/src/url.rs b/alacritty/src/url.rs index e538331..fcdd477 100644 --- a/alacritty/src/url.rs +++ b/alacritty/src/url.rs @@ -147,6 +147,7 @@ impl Urls { url.end_offset = end_offset; } + /// Find URL below the mouse cursor. pub fn highlighted( &self, config: &Config, @@ -171,12 +172,16 @@ impl Urls { return None; } + self.find_at(Point::new(mouse.line, mouse.column)) + } + + /// Find URL at location. + pub fn find_at(&self, point: Point) -> Option { for url in &self.urls { - if (url.start()..=url.end()).contains(&Point::new(mouse.line, mouse.column)) { + if (url.start()..=url.end()).contains(&point) { return Some(url.clone()); } } - None } diff --git a/alacritty_terminal/src/config/colors.rs b/alacritty_terminal/src/config/colors.rs index 35c0368..5c05761 100644 --- a/alacritty_terminal/src/config/colors.rs +++ b/alacritty_terminal/src/config/colors.rs @@ -12,6 +12,8 @@ pub struct Colors { #[serde(deserialize_with = "failure_default")] pub cursor: CursorColors, #[serde(deserialize_with = "failure_default")] + pub vi_mode_cursor: CursorColors, + #[serde(deserialize_with = "failure_default")] pub selection: SelectionColors, #[serde(deserialize_with = "failure_default")] normal: NormalColors, diff --git a/alacritty_terminal/src/config/mod.rs b/alacritty_terminal/src/config/mod.rs index da95391..df8d37b 100644 --- a/alacritty_terminal/src/config/mod.rs +++ b/alacritty_terminal/src/config/mod.rs @@ -28,7 +28,7 @@ mod scrolling; mod visual_bell; mod window; -use crate::ansi::{Color, CursorStyle, NamedColor}; +use crate::ansi::{CursorStyle, NamedColor}; pub use crate::config::colors::Colors; pub use crate::config::debug::Debug; @@ -170,16 +170,28 @@ impl Config { self.dynamic_title.0 } - /// Cursor foreground color + /// Cursor foreground color. #[inline] pub fn cursor_text_color(&self) -> Option { self.colors.cursor.text } - /// Cursor background color + /// Cursor background color. #[inline] - pub fn cursor_cursor_color(&self) -> Option { - self.colors.cursor.cursor.map(|_| Color::Named(NamedColor::Cursor)) + pub fn cursor_cursor_color(&self) -> Option { + self.colors.cursor.cursor.map(|_| NamedColor::Cursor) + } + + /// Vi mode cursor foreground color. + #[inline] + pub fn vi_mode_cursor_text_color(&self) -> Option { + self.colors.vi_mode_cursor.text + } + + /// Vi mode cursor background color. + #[inline] + pub fn vi_mode_cursor_cursor_color(&self) -> Option { + self.colors.vi_mode_cursor.cursor } #[inline] @@ -230,20 +242,16 @@ impl Default for EscapeChars { } #[serde(default)] -#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq)] +#[derive(Deserialize, Copy, Clone, Debug, Default, PartialEq, Eq)] pub struct Cursor { #[serde(deserialize_with = "failure_default")] pub style: CursorStyle, + #[serde(deserialize_with = "option_explicit_none")] + pub vi_mode_style: Option, #[serde(deserialize_with = "failure_default")] unfocused_hollow: DefaultTrueBool, } -impl Default for Cursor { - fn default() -> Self { - Self { style: Default::default(), unfocused_hollow: Default::default() } - } -} - impl Cursor { pub fn unfocused_hollow(self) -> bool { self.unfocused_hollow.0 diff --git a/alacritty_terminal/src/grid/mod.rs b/alacritty_terminal/src/grid/mod.rs index 34d989d..37cf0eb 100644 --- a/alacritty_terminal/src/grid/mod.rs +++ b/alacritty_terminal/src/grid/mod.rs @@ -264,11 +264,9 @@ impl Grid { let mut new_empty_lines = 0; let mut reversed: Vec> = Vec::with_capacity(self.raw.len()); for (i, mut row) in self.raw.drain().enumerate().rev() { - // FIXME: Rust 1.39.0+ allows moving in pattern guard here // Check if reflowing shoud be performed - let mut last_row = reversed.last_mut(); - let last_row = match last_row { - Some(ref mut last_row) if should_reflow(last_row) => last_row, + let last_row = match reversed.last_mut() { + Some(last_row) if should_reflow(last_row) => last_row, _ => { reversed.push(row); continue; @@ -356,11 +354,9 @@ impl Grid { } loop { - // FIXME: Rust 1.39.0+ allows moving in pattern guard here // Check if reflowing shoud be performed - let wrapped = row.shrink(cols); - let mut wrapped = match wrapped { - Some(_) if reflow => wrapped.unwrap(), + let mut wrapped = match row.shrink(cols) { + Some(wrapped) if reflow => wrapped, _ => { new_raw.push(row); break; diff --git a/alacritty_terminal/src/index.rs b/alacritty_terminal/src/index.rs index 56d3200..1334a74 100644 --- a/alacritty_terminal/src/index.rs +++ b/alacritty_terminal/src/index.rs @@ -30,6 +30,15 @@ pub enum Side { Right, } +impl Side { + pub fn opposite(self) -> Self { + match self { + Side::Right => Side::Left, + Side::Left => Side::Right, + } + } +} + /// Index in the grid using row, column notation #[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize, PartialOrd)] pub struct Point { @@ -49,7 +58,7 @@ impl Point { L: Copy + Default + Into + Add + Sub, { let line_changes = - f32::ceil(rhs.saturating_sub(self.col.0) as f32 / num_cols as f32) as usize; + (rhs.saturating_sub(self.col.0) as f32 / num_cols as f32).ceil() as usize; if self.line.into() > Line(line_changes) { self.line = self.line - line_changes; } else { @@ -63,12 +72,40 @@ impl Point { #[must_use = "this returns the result of the operation, without modifying the original"] pub fn add(mut self, num_cols: usize, rhs: usize) -> Point where - L: Add + Sub, + L: Copy + Default + Into + Add + Sub, { self.line = self.line + (rhs + self.col.0) / num_cols; self.col = Column((self.col.0 + rhs) % num_cols); self } + + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn sub_absolute(mut self, num_cols: usize, rhs: usize) -> Point + where + L: Copy + Default + Into + Add + Sub, + { + self.line = + self.line + (rhs.saturating_sub(self.col.0) as f32 / num_cols as f32).ceil() as usize; + self.col = Column((num_cols + self.col.0 - rhs % num_cols) % num_cols); + self + } + + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn add_absolute(mut self, num_cols: usize, rhs: usize) -> Point + where + L: Copy + Default + Into + Add + Sub, + { + let line_changes = (rhs + self.col.0) / num_cols; + if self.line.into() > Line(line_changes) { + self.line = self.line - line_changes; + } else { + self.line = Default::default(); + } + self.col = Column((self.col.0 + rhs) % num_cols); + self + } } impl Ord for Point { diff --git a/alacritty_terminal/src/lib.rs b/alacritty_terminal/src/lib.rs index 039f2b8..6991ffd 100644 --- a/alacritty_terminal/src/lib.rs +++ b/alacritty_terminal/src/lib.rs @@ -37,6 +37,7 @@ pub mod sync; pub mod term; pub mod tty; pub mod util; +pub mod vi_mode; pub use crate::grid::Grid; pub use crate::term::Term; diff --git a/alacritty_terminal/src/selection.rs b/alacritty_terminal/src/selection.rs index f663417..369846c 100644 --- a/alacritty_terminal/src/selection.rs +++ b/alacritty_terminal/src/selection.rs @@ -27,7 +27,7 @@ use crate::term::{Search, Term}; /// A Point and side within that point. #[derive(Debug, Copy, Clone, PartialEq)] -pub struct Anchor { +struct Anchor { point: Point, side: Side, } @@ -67,7 +67,7 @@ impl SelectionRange { /// Different kinds of selection. #[derive(Debug, Copy, Clone, PartialEq)] -enum SelectionType { +pub enum SelectionType { Simple, Block, Semantic, @@ -94,48 +94,20 @@ enum SelectionType { /// [`update`]: enum.Selection.html#method.update #[derive(Debug, Clone, PartialEq)] pub struct Selection { + pub ty: SelectionType, region: Range, - ty: SelectionType, } impl Selection { - pub fn simple(location: Point, side: Side) -> Selection { + pub fn new(ty: SelectionType, location: Point, side: Side) -> Selection { Self { region: Range { start: Anchor::new(location, side), end: Anchor::new(location, side) }, - ty: SelectionType::Simple, + ty, } } - pub fn block(location: Point, side: Side) -> Selection { - Self { - region: Range { start: Anchor::new(location, side), end: Anchor::new(location, side) }, - ty: SelectionType::Block, - } - } - - pub fn semantic(location: Point) -> Selection { - Self { - region: Range { - start: Anchor::new(location, Side::Left), - end: Anchor::new(location, Side::Right), - }, - ty: SelectionType::Semantic, - } - } - - pub fn lines(location: Point) -> Selection { - Self { - region: Range { - start: Anchor::new(location, Side::Left), - end: Anchor::new(location, Side::Right), - }, - ty: SelectionType::Lines, - } - } - - pub fn update(&mut self, location: Point, side: Side) { - self.region.end.point = location; - self.region.end.side = side; + pub fn update(&mut self, point: Point, side: Side) { + self.region.end = Anchor::new(point, side); } pub fn rotate( @@ -233,6 +205,24 @@ impl Selection { } } + /// Expand selection sides to include all cells. + pub fn include_all(&mut self) { + let (start, end) = (self.region.start.point, self.region.end.point); + let (start_side, end_side) = match self.ty { + SelectionType::Block + if start.col > end.col || (start.col == end.col && start.line < end.line) => + { + (Side::Right, Side::Left) + }, + SelectionType::Block => (Side::Left, Side::Right), + _ if Self::points_need_swap(start, end) => (Side::Right, Side::Left), + _ => (Side::Left, Side::Right), + }; + + self.region.start.side = start_side; + self.region.end.side = end_side; + } + /// Convert selection to grid coordinates. pub fn to_range(&self, term: &Term) -> Option { let grid = term.grid(); @@ -392,7 +382,8 @@ impl Selection { /// look like [ B] and [E ]. #[cfg(test)] mod tests { - use super::{Selection, SelectionRange}; + use super::*; + use crate::clipboard::Clipboard; use crate::config::MockConfig; use crate::event::{Event, EventListener}; @@ -425,7 +416,7 @@ mod tests { #[test] fn single_cell_left_to_right() { let location = Point { line: 0, col: Column(0) }; - let mut selection = Selection::simple(location, Side::Left); + let mut selection = Selection::new(SelectionType::Simple, location, Side::Left); selection.update(location, Side::Right); assert_eq!(selection.to_range(&term(1, 1)).unwrap(), SelectionRange { @@ -443,7 +434,7 @@ mod tests { #[test] fn single_cell_right_to_left() { let location = Point { line: 0, col: Column(0) }; - let mut selection = Selection::simple(location, Side::Right); + let mut selection = Selection::new(SelectionType::Simple, location, Side::Right); selection.update(location, Side::Left); assert_eq!(selection.to_range(&term(1, 1)).unwrap(), SelectionRange { @@ -460,7 +451,8 @@ mod tests { /// 3. [ B][E ] #[test] fn between_adjacent_cells_left_to_right() { - let mut selection = Selection::simple(Point::new(0, Column(0)), Side::Right); + let mut selection = + Selection::new(SelectionType::Simple, Point::new(0, Column(0)), Side::Right); selection.update(Point::new(0, Column(1)), Side::Left); assert_eq!(selection.to_range(&term(2, 1)), None); @@ -473,7 +465,8 @@ mod tests { /// 3. [ E][B ] #[test] fn between_adjacent_cells_right_to_left() { - let mut selection = Selection::simple(Point::new(0, Column(1)), Side::Left); + let mut selection = + Selection::new(SelectionType::Simple, Point::new(0, Column(1)), Side::Left); selection.update(Point::new(0, Column(0)), Side::Right); assert_eq!(selection.to_range(&term(2, 1)), None); @@ -489,7 +482,8 @@ mod tests { /// [XX][XE][ ][ ][ ] #[test] fn across_adjacent_lines_upward_final_cell_exclusive() { - let mut selection = Selection::simple(Point::new(1, Column(1)), Side::Right); + let mut selection = + Selection::new(SelectionType::Simple, Point::new(1, Column(1)), Side::Right); selection.update(Point::new(0, Column(1)), Side::Right); assert_eq!(selection.to_range(&term(5, 2)).unwrap(), SelectionRange { @@ -511,7 +505,8 @@ mod tests { /// [XX][XB][ ][ ][ ] #[test] fn selection_bigger_then_smaller() { - let mut selection = Selection::simple(Point::new(0, Column(1)), Side::Right); + let mut selection = + Selection::new(SelectionType::Simple, Point::new(0, Column(1)), Side::Right); selection.update(Point::new(1, Column(1)), Side::Right); selection.update(Point::new(1, Column(0)), Side::Right); @@ -526,7 +521,8 @@ mod tests { fn line_selection() { let num_lines = 10; let num_cols = 5; - let mut selection = Selection::lines(Point::new(0, Column(1))); + let mut selection = + Selection::new(SelectionType::Lines, Point::new(0, Column(1)), Side::Left); selection.update(Point::new(5, Column(1)), Side::Right); selection = selection.rotate(num_lines, num_cols, &(Line(0)..Line(num_lines)), 7).unwrap(); @@ -541,7 +537,8 @@ mod tests { fn semantic_selection() { let num_lines = 10; let num_cols = 5; - let mut selection = Selection::semantic(Point::new(0, Column(3))); + let mut selection = + Selection::new(SelectionType::Semantic, Point::new(0, Column(3)), Side::Left); selection.update(Point::new(5, Column(1)), Side::Right); selection = selection.rotate(num_lines, num_cols, &(Line(0)..Line(num_lines)), 7).unwrap(); @@ -556,7 +553,8 @@ mod tests { fn simple_selection() { let num_lines = 10; let num_cols = 5; - let mut selection = Selection::simple(Point::new(0, Column(3)), Side::Right); + let mut selection = + Selection::new(SelectionType::Simple, Point::new(0, Column(3)), Side::Right); selection.update(Point::new(5, Column(1)), Side::Right); selection = selection.rotate(num_lines, num_cols, &(Line(0)..Line(num_lines)), 7).unwrap(); @@ -571,7 +569,8 @@ mod tests { fn block_selection() { let num_lines = 10; let num_cols = 5; - let mut selection = Selection::block(Point::new(0, Column(3)), Side::Right); + let mut selection = + Selection::new(SelectionType::Block, Point::new(0, Column(3)), Side::Right); selection.update(Point::new(5, Column(1)), Side::Right); selection = selection.rotate(num_lines, num_cols, &(Line(0)..Line(num_lines)), 7).unwrap(); @@ -584,7 +583,8 @@ mod tests { #[test] fn simple_is_empty() { - let mut selection = Selection::simple(Point::new(0, Column(0)), Side::Right); + let mut selection = + Selection::new(SelectionType::Simple, Point::new(0, Column(0)), Side::Right); assert!(selection.is_empty()); selection.update(Point::new(0, Column(1)), Side::Left); assert!(selection.is_empty()); @@ -594,7 +594,8 @@ mod tests { #[test] fn block_is_empty() { - let mut selection = Selection::block(Point::new(0, Column(0)), Side::Right); + let mut selection = + Selection::new(SelectionType::Block, Point::new(0, Column(0)), Side::Right); assert!(selection.is_empty()); selection.update(Point::new(0, Column(1)), Side::Left); assert!(selection.is_empty()); @@ -612,7 +613,8 @@ mod tests { fn rotate_in_region_up() { let num_lines = 10; let num_cols = 5; - let mut selection = Selection::simple(Point::new(2, Column(3)), Side::Right); + let mut selection = + Selection::new(SelectionType::Simple, Point::new(2, Column(3)), Side::Right); selection.update(Point::new(5, Column(1)), Side::Right); selection = selection.rotate(num_lines, num_cols, &(Line(1)..Line(num_lines - 1)), 4).unwrap(); @@ -628,7 +630,8 @@ mod tests { fn rotate_in_region_down() { let num_lines = 10; let num_cols = 5; - let mut selection = Selection::simple(Point::new(5, Column(3)), Side::Right); + let mut selection = + Selection::new(SelectionType::Simple, Point::new(5, Column(3)), Side::Right); selection.update(Point::new(8, Column(1)), Side::Left); selection = selection.rotate(num_lines, num_cols, &(Line(1)..Line(num_lines - 1)), -5).unwrap(); @@ -644,7 +647,8 @@ mod tests { fn rotate_in_region_up_block() { let num_lines = 10; let num_cols = 5; - let mut selection = Selection::block(Point::new(2, Column(3)), Side::Right); + let mut selection = + Selection::new(SelectionType::Block, Point::new(2, Column(3)), Side::Right); selection.update(Point::new(5, Column(1)), Side::Right); selection = selection.rotate(num_lines, num_cols, &(Line(1)..Line(num_lines - 1)), 4).unwrap(); diff --git a/alacritty_terminal/src/term/mod.rs b/alacritty_terminal/src/term/mod.rs index ac5e56b..89c3723 100644 --- a/alacritty_terminal/src/term/mod.rs +++ b/alacritty_terminal/src/term/mod.rs @@ -31,10 +31,11 @@ use crate::event::{Event, EventListener}; use crate::grid::{ BidirectionalIterator, DisplayIter, Grid, GridCell, IndexRegion, Indexed, Scroll, }; -use crate::index::{self, Column, IndexRange, Line, Point}; +use crate::index::{self, Column, IndexRange, Line, Point, Side}; use crate::selection::{Selection, SelectionRange}; use crate::term::cell::{Cell, Flags, LineLength}; use crate::term::color::Rgb; +use crate::vi_mode::{ViModeCursor, ViMotion}; pub mod cell; pub mod color; @@ -180,7 +181,17 @@ impl Search for Term { } } -/// A key for caching cursor glyphs +/// Cursor storing all information relevant for rendering. +#[derive(Debug, Eq, PartialEq, Copy, Clone, Deserialize)] +struct RenderableCursor { + text_color: Option, + cursor_color: Option, + key: CursorKey, + point: Point, + rendered: bool, +} + +/// A key for caching cursor glyphs. #[derive(Debug, Eq, PartialEq, Copy, Clone, Hash, Deserialize)] pub struct CursorKey { pub style: CursorStyle, @@ -198,10 +209,7 @@ pub struct CursorKey { pub struct RenderableCellsIter<'a, C> { inner: DisplayIter<'a, Cell>, grid: &'a Grid, - cursor: &'a Point, - cursor_offset: usize, - cursor_key: Option, - cursor_style: CursorStyle, + cursor: RenderableCursor, config: &'a Config, colors: &'a color::List, selection: Option>, @@ -216,12 +224,10 @@ impl<'a, C> RenderableCellsIter<'a, C> { term: &'b Term, config: &'b Config, selection: Option, - mut cursor_style: CursorStyle, ) -> RenderableCellsIter<'b, C> { let grid = &term.grid; let num_cols = grid.num_cols(); - let cursor_offset = grid.num_lines().0 - term.cursor.point.line.0 - 1; let inner = grid.display_iter(); let selection_range = selection.and_then(|span| { @@ -242,29 +248,13 @@ impl<'a, C> RenderableCellsIter<'a, C> { Some(SelectionRange::new(start, end, span.is_block)) }); - // Load cursor glyph - let cursor = &term.cursor.point; - let cursor_visible = term.mode.contains(TermMode::SHOW_CURSOR) && grid.contains(cursor); - let cursor_key = if cursor_visible { - let is_wide = - grid[cursor].flags.contains(Flags::WIDE_CHAR) && (cursor.col + 1) < num_cols; - Some(CursorKey { style: cursor_style, is_wide }) - } else { - // Use hidden cursor so text will not get inverted - cursor_style = CursorStyle::Hidden; - None - }; - RenderableCellsIter { - cursor, - cursor_offset, + cursor: term.renderable_cursor(config), grid, inner, selection: selection_range, config, colors: &term.colors, - cursor_key, - cursor_style, } } @@ -275,6 +265,18 @@ impl<'a, C> RenderableCellsIter<'a, C> { None => return false, }; + // Do not invert block cursor at selection boundaries + if self.cursor.key.style == CursorStyle::Block + && self.cursor.point == point + && (selection.start == point + || selection.end == point + || (selection.is_block + && ((selection.start.line == point.line && selection.end.col == point.col) + || (selection.end.line == point.line && selection.start.col == point.col)))) + { + return false; + } + // Point itself is selected if selection.contains(point.col, point.line) { return true; @@ -442,43 +444,46 @@ impl<'a, C> Iterator for RenderableCellsIter<'a, C> { #[inline] fn next(&mut self) -> Option { loop { - if self.cursor_offset == self.inner.offset() && self.inner.column() == self.cursor.col { - let selected = self.is_selected(Point::new(self.cursor.line, self.cursor.col)); + if self.cursor.point.line == self.inner.line() + && self.cursor.point.col == self.inner.column() + { + let selected = self.is_selected(self.cursor.point); - // Handle cursor - if let Some(cursor_key) = self.cursor_key.take() { - let cell = Indexed { - inner: self.grid[self.cursor], - column: self.cursor.col, - // Using `self.cursor.line` leads to inconsitent cursor position when - // scrolling. See https://github.com/alacritty/alacritty/issues/2570 for more - // info. - line: self.inner.line(), - }; - - let mut renderable_cell = - RenderableCell::new(self.config, self.colors, cell, selected); - - renderable_cell.inner = RenderableCellContent::Cursor(cursor_key); - - if let Some(color) = self.config.cursor_cursor_color() { - renderable_cell.fg = RenderableCell::compute_bg_rgb(self.colors, color); - } - - return Some(renderable_cell); - } else { + // Handle cell below cursor + if self.cursor.rendered { let mut cell = RenderableCell::new(self.config, self.colors, self.inner.next()?, selected); - if self.cursor_style == CursorStyle::Block { - std::mem::swap(&mut cell.bg, &mut cell.fg); + if self.cursor.key.style == CursorStyle::Block { + mem::swap(&mut cell.bg, &mut cell.fg); - if let Some(color) = self.config.cursor_text_color() { + if let Some(color) = self.cursor.text_color { cell.fg = color; } } return Some(cell); + } else { + // Handle cursor + self.cursor.rendered = true; + + let buffer_point = self.grid.visible_to_buffer(self.cursor.point); + let cell = Indexed { + inner: self.grid[buffer_point.line][buffer_point.col], + column: self.cursor.point.col, + line: self.cursor.point.line, + }; + + let mut renderable_cell = + RenderableCell::new(self.config, self.colors, cell, selected); + + renderable_cell.inner = RenderableCellContent::Cursor(self.cursor.key); + + if let Some(color) = self.cursor.cursor_color { + renderable_cell.fg = color; + } + + return Some(renderable_cell); } } else { let cell = self.inner.next()?; @@ -497,26 +502,27 @@ pub mod mode { use bitflags::bitflags; bitflags! { - pub struct TermMode: u16 { - const SHOW_CURSOR = 0b0000_0000_0000_0001; - const APP_CURSOR = 0b0000_0000_0000_0010; - const APP_KEYPAD = 0b0000_0000_0000_0100; - const MOUSE_REPORT_CLICK = 0b0000_0000_0000_1000; - const BRACKETED_PASTE = 0b0000_0000_0001_0000; - const SGR_MOUSE = 0b0000_0000_0010_0000; - const MOUSE_MOTION = 0b0000_0000_0100_0000; - const LINE_WRAP = 0b0000_0000_1000_0000; - const LINE_FEED_NEW_LINE = 0b0000_0001_0000_0000; - const ORIGIN = 0b0000_0010_0000_0000; - const INSERT = 0b0000_0100_0000_0000; - const FOCUS_IN_OUT = 0b0000_1000_0000_0000; - const ALT_SCREEN = 0b0001_0000_0000_0000; - const MOUSE_DRAG = 0b0010_0000_0000_0000; - const MOUSE_MODE = 0b0010_0000_0100_1000; - const UTF8_MOUSE = 0b0100_0000_0000_0000; - const ALTERNATE_SCROLL = 0b1000_0000_0000_0000; - const ANY = 0b1111_1111_1111_1111; + pub struct TermMode: u32 { const NONE = 0; + const SHOW_CURSOR = 0b0000_0000_0000_0000_0001; + const APP_CURSOR = 0b0000_0000_0000_0000_0010; + const APP_KEYPAD = 0b0000_0000_0000_0000_0100; + const MOUSE_REPORT_CLICK = 0b0000_0000_0000_0000_1000; + const BRACKETED_PASTE = 0b0000_0000_0000_0001_0000; + const SGR_MOUSE = 0b0000_0000_0000_0010_0000; + const MOUSE_MOTION = 0b0000_0000_0000_0100_0000; + const LINE_WRAP = 0b0000_0000_0000_1000_0000; + const LINE_FEED_NEW_LINE = 0b0000_0000_0001_0000_0000; + const ORIGIN = 0b0000_0000_0010_0000_0000; + const INSERT = 0b0000_0000_0100_0000_0000; + const FOCUS_IN_OUT = 0b0000_0000_1000_0000_0000; + const ALT_SCREEN = 0b0000_0001_0000_0000_0000; + const MOUSE_DRAG = 0b0000_0010_0000_0000_0000; + const MOUSE_MODE = 0b0000_0010_0000_0100_1000; + const UTF8_MOUSE = 0b0000_0100_0000_0000_0000; + const ALTERNATE_SCROLL = 0b0000_1000_0000_0000_0000; + const VI = 0b0001_0000_0000_0000_0000; + const ANY = std::u32::MAX; } } @@ -730,113 +736,28 @@ impl VisualBell { } } -pub struct Term { - /// Terminal focus - pub is_focused: bool, - - /// The grid - grid: Grid, - - /// Tracks if the next call to input will need to first handle wrapping. - /// This is true after the last column is set with the input function. Any function that - /// implicitly sets the line or column needs to set this to false to avoid wrapping twice. - /// input_needs_wrap ensures that cursor.col is always valid for use into indexing into - /// arrays. Without it we would have to sanitize cursor.col every time we used it. - input_needs_wrap: bool, - - /// Alternate grid - alt_grid: Grid, - - /// Alt is active - alt: bool, - - /// The cursor - cursor: Cursor, - - /// The graphic character set, out of `charsets`, which ASCII is currently - /// being mapped to - active_charset: CharsetIndex, - - /// Tabstops - tabs: TabStops, - - /// Mode flags - mode: TermMode, - - /// Scroll region. - /// - /// Range going from top to bottom of the terminal, indexed from the top of the viewport. - scroll_region: Range, - - pub dirty: bool, - - pub visual_bell: VisualBell, - - /// Saved cursor from main grid - cursor_save: Cursor, - - /// Saved cursor from alt grid - cursor_save_alt: Cursor, - - semantic_escape_chars: String, - - /// Colors used for rendering - colors: color::List, - - /// Is color in `colors` modified or not - color_modified: [bool; color::COUNT], - - /// Original colors from config - original_colors: color::List, - - /// Current style of the cursor - cursor_style: Option, - - /// Default style for resetting the cursor - default_cursor_style: CursorStyle, - - /// Clipboard access coupled to the active window - clipboard: Clipboard, - - /// Proxy for sending events to the event loop - event_proxy: T, - - /// Current title of the window. - title: Option, - - /// Default title for resetting it. - default_title: String, - - /// Whether to permit updating the terminal title. - dynamic_title: bool, - - /// Stack of saved window titles. When a title is popped from this stack, the `title` for the - /// term is set, and the Glutin window's title attribute is changed through the event listener. - title_stack: Vec>, -} - -/// Terminal size info +/// Terminal size info. #[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq)] pub struct SizeInfo { - /// Terminal window width + /// Terminal window width. pub width: f32, - /// Terminal window height + /// Terminal window height. pub height: f32, - /// Width of individual cell + /// Width of individual cell. pub cell_width: f32, - /// Height of individual cell + /// Height of individual cell. pub cell_height: f32, - /// Horizontal window padding + /// Horizontal window padding. pub padding_x: f32, - /// Horizontal window padding + /// Horizontal window padding. pub padding_y: f32, - /// DPI factor of the current window + /// DPI factor of the current window. #[serde(default)] pub dpr: f64, } @@ -873,6 +794,96 @@ impl SizeInfo { } } +pub struct Term { + /// Terminal focus. + pub is_focused: bool, + + /// The grid. + grid: Grid, + + /// Tracks if the next call to input will need to first handle wrapping. + /// This is true after the last column is set with the input function. Any function that + /// implicitly sets the line or column needs to set this to false to avoid wrapping twice. + /// input_needs_wrap ensures that cursor.col is always valid for use into indexing into + /// arrays. Without it we would have to sanitize cursor.col every time we used it. + input_needs_wrap: bool, + + /// Alternate grid. + alt_grid: Grid, + + /// Alt is active. + alt: bool, + + /// The cursor. + cursor: Cursor, + + /// Cursor location for vi mode. + pub vi_mode_cursor: ViModeCursor, + + /// Index into `charsets`, pointing to what ASCII is currently being mapped to. + active_charset: CharsetIndex, + + /// Tabstops. + tabs: TabStops, + + /// Mode flags. + mode: TermMode, + + /// Scroll region. + /// + /// Range going from top to bottom of the terminal, indexed from the top of the viewport. + scroll_region: Range, + + pub dirty: bool, + + pub visual_bell: VisualBell, + + /// Saved cursor from main grid. + cursor_save: Cursor, + + /// Saved cursor from alt grid. + cursor_save_alt: Cursor, + + semantic_escape_chars: String, + + /// Colors used for rendering. + colors: color::List, + + /// Is color in `colors` modified or not. + color_modified: [bool; color::COUNT], + + /// Original colors from config. + original_colors: color::List, + + /// Current style of the cursor. + cursor_style: Option, + + /// Default style for resetting the cursor. + default_cursor_style: CursorStyle, + + /// Style of the vi mode cursor. + vi_mode_cursor_style: Option, + + /// Clipboard access coupled to the active window + clipboard: Clipboard, + + /// Proxy for sending events to the event loop. + event_proxy: T, + + /// Current title of the window. + title: Option, + + /// Default title for resetting it. + default_title: String, + + /// Whether to permit updating the terminal title. + dynamic_title: bool, + + /// Stack of saved window titles. When a title is popped from this stack, the `title` for the + /// term is set, and the Glutin window's title attribute is changed through the event listener. + title_stack: Vec>, +} + impl Term { pub fn selection(&self) -> &Option { &self.grid.selection @@ -920,6 +931,7 @@ impl Term { alt: false, active_charset: Default::default(), cursor: Default::default(), + vi_mode_cursor: Default::default(), cursor_save: Default::default(), cursor_save_alt: Default::default(), tabs, @@ -931,6 +943,7 @@ impl Term { semantic_escape_chars: config.selection.semantic_escape_chars().to_owned(), cursor_style: None, default_cursor_style: config.cursor.style, + vi_mode_cursor_style: config.cursor.vi_mode_style, dynamic_title: config.dynamic_title(), clipboard, event_proxy, @@ -959,6 +972,7 @@ impl Term { self.mode.remove(TermMode::ALTERNATE_SCROLL); } self.default_cursor_style = config.cursor.style; + self.vi_mode_cursor_style = config.cursor.vi_mode_style; self.default_title = config.window.title.clone(); self.dynamic_title = config.dynamic_title(); @@ -1105,13 +1119,7 @@ impl Term { pub fn renderable_cells<'b, C>(&'b self, config: &'b Config) -> RenderableCellsIter<'_, C> { let selection = self.grid.selection.as_ref().and_then(|s| s.to_range(self)); - let cursor = if self.is_focused || !config.cursor.unfocused_hollow() { - self.cursor_style.unwrap_or(self.default_cursor_style) - } else { - CursorStyle::HollowBlock - }; - - RenderableCellsIter::new(&self, config, selection, cursor) + RenderableCellsIter::new(&self, config, selection) } /// Resize terminal to new dimensions @@ -1129,12 +1137,12 @@ impl Term { self.grid.selection = None; self.alt_grid.selection = None; - // Should not allow less than 1 col, causes all sorts of checks to be required. + // Should not allow less than 2 cols, causes all sorts of checks to be required. if num_cols <= Column(1) { num_cols = Column(2); } - // Should not allow less than 1 line, causes all sorts of checks to be required. + // Should not allow less than 2 lines, causes all sorts of checks to be required. if num_lines <= Line(1) { num_lines = Line(2); } @@ -1178,6 +1186,8 @@ impl Term { self.cursor_save.point.line = min(self.cursor_save.point.line, num_lines - 1); self.cursor_save_alt.point.col = min(self.cursor_save_alt.point.col, num_cols - 1); self.cursor_save_alt.point.line = min(self.cursor_save_alt.point.line, num_lines - 1); + self.vi_mode_cursor.point.col = min(self.vi_mode_cursor.point.col, num_cols - 1); + self.vi_mode_cursor.point.line = min(self.vi_mode_cursor.point.line, num_lines - 1); // Recreate tabs list self.tabs.resize(self.grid.num_cols()); @@ -1200,7 +1210,7 @@ impl Term { } self.alt = !self.alt; - std::mem::swap(&mut self.grid, &mut self.alt_grid); + mem::swap(&mut self.grid, &mut self.alt_grid); } /// Scroll screen down @@ -1258,10 +1268,58 @@ impl Term { self.event_proxy.send_event(Event::Exit); } + #[inline] pub fn clipboard(&mut self) -> &mut Clipboard { &mut self.clipboard } + /// Toggle the vi mode. + #[inline] + pub fn toggle_vi_mode(&mut self) { + self.mode ^= TermMode::VI; + self.grid.selection = None; + + // Reset vi mode cursor position to match primary cursor + if self.mode.contains(TermMode::VI) { + let line = min(self.cursor.point.line + self.grid.display_offset(), self.lines() - 1); + self.vi_mode_cursor = ViModeCursor::new(Point::new(line, self.cursor.point.col)); + } + + self.dirty = true; + } + + /// Move vi mode cursor. + #[inline] + pub fn vi_motion(&mut self, motion: ViMotion) + where + T: EventListener, + { + // Require vi mode to be active + if !self.mode.contains(TermMode::VI) { + return; + } + + // Move cursor + self.vi_mode_cursor = self.vi_mode_cursor.motion(self, motion); + + // Update selection if one is active + let viewport_point = self.visible_to_buffer(self.vi_mode_cursor.point); + if let Some(selection) = &mut self.grid.selection { + // Do not extend empty selections started by single mouse click + if !selection.is_empty() { + selection.update(viewport_point, Side::Left); + selection.include_all(); + } + } + + self.dirty = true; + } + + #[inline] + pub fn semantic_escape_chars(&self) -> &str { + &self.semantic_escape_chars + } + /// Insert a linebreak at the current cursor position. #[inline] fn wrapline(&mut self) @@ -1297,6 +1355,65 @@ impl Term { cell.c = self.cursor.charsets[self.active_charset].map(c); cell } + + /// Get rendering information about the active cursor. + fn renderable_cursor(&self, config: &Config) -> RenderableCursor { + let vi_mode = self.mode.contains(TermMode::VI); + + // Cursor position + let mut point = if vi_mode { + self.vi_mode_cursor.point + } else { + let mut point = self.cursor.point; + point.line += self.grid.display_offset(); + point + }; + + // Cursor shape + let hidden = !self.mode.contains(TermMode::SHOW_CURSOR) || point.line >= self.lines(); + let cursor_style = if hidden && !vi_mode { + point.line = Line(0); + CursorStyle::Hidden + } else if !self.is_focused && config.cursor.unfocused_hollow() { + CursorStyle::HollowBlock + } else { + let cursor_style = self.cursor_style.unwrap_or(self.default_cursor_style); + + if vi_mode { + self.vi_mode_cursor_style.unwrap_or(cursor_style) + } else { + cursor_style + } + }; + + // Cursor colors + let (text_color, cursor_color) = if vi_mode { + (config.vi_mode_cursor_text_color(), config.vi_mode_cursor_cursor_color()) + } else { + let cursor_cursor_color = config.cursor_cursor_color().map(|c| self.colors[c]); + (config.cursor_text_color(), cursor_cursor_color) + }; + + // Expand across wide cell when inside wide char or spacer + let buffer_point = self.visible_to_buffer(point); + let cell = self.grid[buffer_point.line][buffer_point.col]; + let is_wide = if cell.flags.contains(Flags::WIDE_CHAR_SPACER) + && self.grid[buffer_point.line][buffer_point.col - 1].flags.contains(Flags::WIDE_CHAR) + { + point.col -= 1; + true + } else { + cell.flags.contains(Flags::WIDE_CHAR) + }; + + RenderableCursor { + text_color, + cursor_color, + key: CursorKey { style: cursor_style, is_wide }, + point, + rendered: false, + } + } } impl TermInfo for Term { @@ -2184,7 +2301,7 @@ mod tests { use crate::event::{Event, EventListener}; use crate::grid::{Grid, Scroll}; use crate::index::{Column, Line, Point, Side}; - use crate::selection::Selection; + use crate::selection::{Selection, SelectionType}; use crate::term::cell::{Cell, Flags}; use crate::term::{SizeInfo, Term}; @@ -2222,17 +2339,29 @@ mod tests { mem::swap(&mut term.semantic_escape_chars, &mut escape_chars); { - *term.selection_mut() = Some(Selection::semantic(Point { line: 2, col: Column(1) })); + *term.selection_mut() = Some(Selection::new( + SelectionType::Semantic, + Point { line: 2, col: Column(1) }, + Side::Left, + )); assert_eq!(term.selection_to_string(), Some(String::from("aa"))); } { - *term.selection_mut() = Some(Selection::semantic(Point { line: 2, col: Column(4) })); + *term.selection_mut() = Some(Selection::new( + SelectionType::Semantic, + Point { line: 2, col: Column(4) }, + Side::Left, + )); assert_eq!(term.selection_to_string(), Some(String::from("aaa"))); } { - *term.selection_mut() = Some(Selection::semantic(Point { line: 1, col: Column(1) })); + *term.selection_mut() = Some(Selection::new( + SelectionType::Semantic, + Point { line: 1, col: Column(1) }, + Side::Left, + )); assert_eq!(term.selection_to_string(), Some(String::from("aaa"))); } } @@ -2258,7 +2387,11 @@ mod tests { mem::swap(&mut term.grid, &mut grid); - *term.selection_mut() = Some(Selection::lines(Point { line: 0, col: Column(3) })); + *term.selection_mut() = Some(Selection::new( + SelectionType::Lines, + Point { line: 0, col: Column(3) }, + Side::Left, + )); assert_eq!(term.selection_to_string(), Some(String::from("\"aa\"a\n"))); } @@ -2285,7 +2418,8 @@ mod tests { mem::swap(&mut term.grid, &mut grid); - let mut selection = Selection::simple(Point { line: 2, col: Column(0) }, Side::Left); + let mut selection = + Selection::new(SelectionType::Simple, Point { line: 2, col: Column(0) }, Side::Left); selection.update(Point { line: 0, col: Column(2) }, Side::Right); *term.selection_mut() = Some(selection); assert_eq!(term.selection_to_string(), Some("aaa\n\naaa\n".into())); diff --git a/alacritty_terminal/src/vi_mode.rs b/alacritty_terminal/src/vi_mode.rs new file mode 100644 index 0000000..196193e --- /dev/null +++ b/alacritty_terminal/src/vi_mode.rs @@ -0,0 +1,799 @@ +use std::cmp::{max, min}; + +use serde::Deserialize; + +use crate::event::EventListener; +use crate::grid::{GridCell, Scroll}; +use crate::index::{Column, Line, Point}; +use crate::term::cell::Flags; +use crate::term::{Search, Term}; + +/// Possible vi mode motion movements. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)] +pub enum ViMotion { + /// Move up. + Up, + /// Move down. + Down, + /// Move left. + Left, + /// Move right. + Right, + /// Move to start of line. + First, + /// Move to end of line. + Last, + /// Move to the first non-empty cell. + FirstOccupied, + /// Move to top of screen. + High, + /// Move to center of screen. + Middle, + /// Move to bottom of screen. + Low, + /// Move to start of semantically separated word. + SemanticLeft, + /// Move to start of next semantically separated word. + SemanticRight, + /// Move to end of previous semantically separated word. + SemanticLeftEnd, + /// Move to end of semantically separated word. + SemanticRightEnd, + /// Move to start of whitespace separated word. + WordLeft, + /// Move to start of next whitespace separated word. + WordRight, + /// Move to end of previous whitespace separated word. + WordLeftEnd, + /// Move to end of whitespace separated word. + WordRightEnd, + /// Move to opposing bracket. + Bracket, +} + +/// Cursor tracking vi mode position. +#[derive(Default, Copy, Clone)] +pub struct ViModeCursor { + pub point: Point, +} + +impl ViModeCursor { + pub fn new(point: Point) -> Self { + Self { point } + } + + /// Move vi mode cursor. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn motion(mut self, term: &mut Term, motion: ViMotion) -> Self { + let display_offset = term.grid().display_offset(); + let lines = term.grid().num_lines(); + let cols = term.grid().num_cols(); + + let mut buffer_point = term.visible_to_buffer(self.point); + + match motion { + ViMotion::Up => { + if buffer_point.line + 1 < term.grid().len() { + buffer_point.line += 1; + } + }, + ViMotion::Down => buffer_point.line = buffer_point.line.saturating_sub(1), + ViMotion::Left => { + buffer_point = expand_wide(term, buffer_point, true); + let wrap_point = Point::new(buffer_point.line + 1, cols - 1); + if buffer_point.col.0 == 0 + && buffer_point.line + 1 < term.grid().len() + && is_wrap(term, wrap_point) + { + buffer_point = wrap_point; + } else { + buffer_point.col = Column(buffer_point.col.saturating_sub(1)); + } + }, + ViMotion::Right => { + buffer_point = expand_wide(term, buffer_point, false); + if is_wrap(term, buffer_point) { + buffer_point = Point::new(buffer_point.line - 1, Column(0)); + } else { + buffer_point.col = min(buffer_point.col + 1, cols - 1); + } + }, + ViMotion::First => { + buffer_point = expand_wide(term, buffer_point, true); + while buffer_point.col.0 == 0 + && buffer_point.line + 1 < term.grid().len() + && is_wrap(term, Point::new(buffer_point.line + 1, cols - 1)) + { + buffer_point.line += 1; + } + buffer_point.col = Column(0); + }, + ViMotion::Last => buffer_point = last(term, buffer_point), + ViMotion::FirstOccupied => buffer_point = first_occupied(term, buffer_point), + ViMotion::High => { + let line = display_offset + lines.0 - 1; + let col = first_occupied_in_line(term, line).unwrap_or_default().col; + buffer_point = Point::new(line, col); + }, + ViMotion::Middle => { + let line = display_offset + lines.0 / 2; + let col = first_occupied_in_line(term, line).unwrap_or_default().col; + buffer_point = Point::new(line, col); + }, + ViMotion::Low => { + let line = display_offset; + let col = first_occupied_in_line(term, line).unwrap_or_default().col; + buffer_point = Point::new(line, col); + }, + ViMotion::SemanticLeft => buffer_point = semantic(term, buffer_point, true, true), + ViMotion::SemanticRight => buffer_point = semantic(term, buffer_point, false, true), + ViMotion::SemanticLeftEnd => buffer_point = semantic(term, buffer_point, true, false), + ViMotion::SemanticRightEnd => buffer_point = semantic(term, buffer_point, false, false), + ViMotion::WordLeft => buffer_point = word(term, buffer_point, true, true), + ViMotion::WordRight => buffer_point = word(term, buffer_point, false, true), + ViMotion::WordLeftEnd => buffer_point = word(term, buffer_point, true, false), + ViMotion::WordRightEnd => buffer_point = word(term, buffer_point, false, false), + ViMotion::Bracket => { + buffer_point = term.bracket_search(buffer_point).unwrap_or(buffer_point); + }, + } + + scroll_to_point(term, buffer_point); + self.point = term.grid().clamp_buffer_to_visible(buffer_point); + + self + } + + /// Get target cursor point for vim-like page movement. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn scroll(mut self, term: &Term, lines: isize) -> Self { + // Check number of lines the cursor needs to be moved + let overscroll = if lines > 0 { + let max_scroll = term.grid().history_size() - term.grid().display_offset(); + max(0, lines - max_scroll as isize) + } else { + let max_scroll = term.grid().display_offset(); + min(0, lines + max_scroll as isize) + }; + + // Clamp movement to within visible region + let mut line = self.point.line.0 as isize; + line -= overscroll; + line = max(0, min(term.grid().num_lines().0 as isize - 1, line)); + + // Find the first occupied cell after scrolling has been performed + let buffer_point = term.visible_to_buffer(self.point); + let mut target_line = buffer_point.line as isize + lines; + target_line = max(0, min(term.grid().len() as isize - 1, target_line)); + let col = first_occupied_in_line(term, target_line as usize).unwrap_or_default().col; + + // Move cursor + self.point = Point::new(Line(line as usize), col); + + self + } +} + +/// Scroll display if point is outside of viewport. +fn scroll_to_point(term: &mut Term, point: Point) { + let display_offset = term.grid().display_offset(); + let lines = term.grid().num_lines(); + + // Scroll once the top/bottom has been reached + if point.line >= display_offset + lines.0 { + let lines = point.line.saturating_sub(display_offset + lines.0 - 1); + term.scroll_display(Scroll::Lines(lines as isize)); + } else if point.line < display_offset { + let lines = display_offset.saturating_sub(point.line); + term.scroll_display(Scroll::Lines(-(lines as isize))); + }; +} + +/// Find next end of line to move to. +fn last(term: &Term, mut point: Point) -> Point { + let cols = term.grid().num_cols(); + + // Expand across wide cells + point = expand_wide(term, point, false); + + // Find last non-empty cell in the current line + let occupied = last_occupied_in_line(term, point.line).unwrap_or_default(); + + if point.col < occupied.col { + // Jump to last occupied cell when not already at or beyond it + occupied + } else if is_wrap(term, point) { + // Jump to last occupied cell across linewraps + while point.line > 0 && is_wrap(term, point) { + point.line -= 1; + } + + last_occupied_in_line(term, point.line).unwrap_or(point) + } else { + // Jump to last column when beyond the last occupied cell + Point::new(point.line, cols - 1) + } +} + +/// Find next non-empty cell to move to. +fn first_occupied(term: &Term, mut point: Point) -> Point { + let cols = term.grid().num_cols(); + + // Expand left across wide chars, since we're searching lines left to right + point = expand_wide(term, point, true); + + // Find first non-empty cell in current line + let occupied = first_occupied_in_line(term, point.line) + .unwrap_or_else(|| Point::new(point.line, cols - 1)); + + // Jump across wrapped lines if we're already at this line's first occupied cell + if point == occupied { + let mut occupied = None; + + // Search for non-empty cell in previous lines + for line in (point.line + 1)..term.grid().len() { + if !is_wrap(term, Point::new(line, cols - 1)) { + break; + } + + occupied = first_occupied_in_line(term, line).or(occupied); + } + + // Fallback to the next non-empty cell + let mut line = point.line; + occupied.unwrap_or_else(|| loop { + if let Some(occupied) = first_occupied_in_line(term, line) { + break occupied; + } + + let last_cell = Point::new(line, cols - 1); + if line == 0 || !is_wrap(term, last_cell) { + break last_cell; + } + + line -= 1; + }) + } else { + occupied + } +} + +/// Move by semantically separated word, like w/b/e/ge in vi. +fn semantic( + term: &mut Term, + mut point: Point, + left: bool, + start: bool, +) -> Point { + // Expand semantically based on movement direction + let expand_semantic = |point: Point| { + // Do not expand when currently on a semantic escape char + let cell = term.grid()[point.line][point.col]; + if term.semantic_escape_chars().contains(cell.c) + && !cell.flags.contains(Flags::WIDE_CHAR_SPACER) + { + point + } else if left { + term.semantic_search_left(point) + } else { + term.semantic_search_right(point) + } + }; + + // Make sure we jump above wide chars + point = expand_wide(term, point, left); + + // Move to word boundary + if left != start && !is_boundary(term, point, left) { + point = expand_semantic(point); + } + + // Skip whitespace + let mut next_point = advance(term, point, left); + while !is_boundary(term, point, left) && is_space(term, next_point) { + point = next_point; + next_point = advance(term, point, left); + } + + // Assure minimum movement of one cell + if !is_boundary(term, point, left) { + point = advance(term, point, left); + } + + // Move to word boundary + if left == start && !is_boundary(term, point, left) { + point = expand_semantic(point); + } + + point +} + +/// Move by whitespace separated word, like W/B/E/gE in vi. +fn word( + term: &mut Term, + mut point: Point, + left: bool, + start: bool, +) -> Point { + // Make sure we jump above wide chars + point = expand_wide(term, point, left); + + if left == start { + // Skip whitespace until right before a word + let mut next_point = advance(term, point, left); + while !is_boundary(term, point, left) && is_space(term, next_point) { + point = next_point; + next_point = advance(term, point, left); + } + + // Skip non-whitespace until right inside word boundary + let mut next_point = advance(term, point, left); + while !is_boundary(term, point, left) && !is_space(term, next_point) { + point = next_point; + next_point = advance(term, point, left); + } + } + + if left != start { + // Skip non-whitespace until just beyond word + while !is_boundary(term, point, left) && !is_space(term, point) { + point = advance(term, point, left); + } + + // Skip whitespace until right inside word boundary + while !is_boundary(term, point, left) && is_space(term, point) { + point = advance(term, point, left); + } + } + + point +} + +/// Jump to the end of a wide cell. +fn expand_wide(term: &Term, point: P, left: bool) -> Point +where + P: Into>, +{ + let mut point = point.into(); + let cell = term.grid()[point.line][point.col]; + + if cell.flags.contains(Flags::WIDE_CHAR) && !left { + point.col += 1; + } else if cell.flags.contains(Flags::WIDE_CHAR_SPACER) + && term.grid()[point.line][point.col - 1].flags.contains(Flags::WIDE_CHAR) + && left + { + point.col -= 1; + } + + point +} + +/// Find first non-empty cell in line. +fn first_occupied_in_line(term: &Term, line: usize) -> Option> { + (0..term.grid().num_cols().0) + .map(|col| Point::new(line, Column(col))) + .find(|&point| !is_space(term, point)) +} + +/// Find last non-empty cell in line. +fn last_occupied_in_line(term: &Term, line: usize) -> Option> { + (0..term.grid().num_cols().0) + .map(|col| Point::new(line, Column(col))) + .rfind(|&point| !is_space(term, point)) +} + +/// Advance point based on direction. +fn advance(term: &Term, point: Point, left: bool) -> Point { + let cols = term.grid().num_cols(); + if left { + point.sub_absolute(cols.0, 1) + } else { + point.add_absolute(cols.0, 1) + } +} + +/// Check if cell at point contains whitespace. +fn is_space(term: &Term, point: Point) -> bool { + let cell = term.grid()[point.line][point.col]; + cell.c == ' ' || cell.c == '\t' && !cell.flags().contains(Flags::WIDE_CHAR_SPACER) +} + +fn is_wrap(term: &Term, point: Point) -> bool { + term.grid()[point.line][point.col].flags.contains(Flags::WRAPLINE) +} + +/// Check if point is at screen boundary. +fn is_boundary(term: &Term, point: Point, left: bool) -> bool { + (point.line == 0 && point.col + 1 >= term.grid().num_cols() && !left) + || (point.line + 1 >= term.grid().len() && point.col.0 == 0 && left) +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::clipboard::Clipboard; + use crate::config::MockConfig; + use crate::event::Event; + use crate::index::{Column, Line}; + use crate::term::{SizeInfo, Term}; + + struct Mock; + impl EventListener for Mock { + fn send_event(&self, _event: Event) {} + } + + fn term() -> Term { + let size = SizeInfo { + width: 20., + height: 20., + cell_width: 1.0, + cell_height: 1.0, + padding_x: 0.0, + padding_y: 0.0, + dpr: 1.0, + }; + Term::new(&MockConfig::default(), &size, Clipboard::new_nop(), Mock) + } + + #[test] + fn motion_simple() { + let mut term = term(); + + let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0))); + + cursor = cursor.motion(&mut term, ViMotion::Right); + assert_eq!(cursor.point, Point::new(Line(0), Column(1))); + + cursor = cursor.motion(&mut term, ViMotion::Left); + assert_eq!(cursor.point, Point::new(Line(0), Column(0))); + + cursor = cursor.motion(&mut term, ViMotion::Down); + assert_eq!(cursor.point, Point::new(Line(1), Column(0))); + + cursor = cursor.motion(&mut term, ViMotion::Up); + assert_eq!(cursor.point, Point::new(Line(0), Column(0))); + } + + #[test] + fn simple_wide() { + let mut term = term(); + term.grid_mut()[Line(0)][Column(0)].c = 'a'; + term.grid_mut()[Line(0)][Column(1)].c = '汉'; + term.grid_mut()[Line(0)][Column(1)].flags.insert(Flags::WIDE_CHAR); + term.grid_mut()[Line(0)][Column(2)].c = ' '; + term.grid_mut()[Line(0)][Column(2)].flags.insert(Flags::WIDE_CHAR_SPACER); + term.grid_mut()[Line(0)][Column(3)].c = 'a'; + + let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(1))); + cursor = cursor.motion(&mut term, ViMotion::Right); + assert_eq!(cursor.point, Point::new(Line(0), Column(3))); + + let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(2))); + cursor = cursor.motion(&mut term, ViMotion::Left); + assert_eq!(cursor.point, Point::new(Line(0), Column(0))); + } + + #[test] + fn motion_start_end() { + let mut term = term(); + + let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0))); + + cursor = cursor.motion(&mut term, ViMotion::Last); + assert_eq!(cursor.point, Point::new(Line(0), Column(19))); + + cursor = cursor.motion(&mut term, ViMotion::First); + assert_eq!(cursor.point, Point::new(Line(0), Column(0))); + } + + #[test] + fn motion_first_occupied() { + let mut term = term(); + term.grid_mut()[Line(0)][Column(0)].c = ' '; + term.grid_mut()[Line(0)][Column(1)].c = 'x'; + term.grid_mut()[Line(0)][Column(2)].c = ' '; + term.grid_mut()[Line(0)][Column(3)].c = 'y'; + term.grid_mut()[Line(0)][Column(19)].flags.insert(Flags::WRAPLINE); + term.grid_mut()[Line(1)][Column(19)].flags.insert(Flags::WRAPLINE); + term.grid_mut()[Line(2)][Column(0)].c = 'z'; + term.grid_mut()[Line(2)][Column(1)].c = ' '; + + let mut cursor = ViModeCursor::new(Point::new(Line(2), Column(1))); + + cursor = cursor.motion(&mut term, ViMotion::FirstOccupied); + assert_eq!(cursor.point, Point::new(Line(2), Column(0))); + + cursor = cursor.motion(&mut term, ViMotion::FirstOccupied); + assert_eq!(cursor.point, Point::new(Line(0), Column(1))); + } + + #[test] + fn motion_high_middle_low() { + let mut term = term(); + + let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0))); + + cursor = cursor.motion(&mut term, ViMotion::High); + assert_eq!(cursor.point, Point::new(Line(0), Column(0))); + + cursor = cursor.motion(&mut term, ViMotion::Middle); + assert_eq!(cursor.point, Point::new(Line(9), Column(0))); + + cursor = cursor.motion(&mut term, ViMotion::Low); + assert_eq!(cursor.point, Point::new(Line(19), Column(0))); + } + + #[test] + fn motion_bracket() { + let mut term = term(); + term.grid_mut()[Line(0)][Column(0)].c = '('; + term.grid_mut()[Line(0)][Column(1)].c = 'x'; + term.grid_mut()[Line(0)][Column(2)].c = ')'; + + let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0))); + + cursor = cursor.motion(&mut term, ViMotion::Bracket); + assert_eq!(cursor.point, Point::new(Line(0), Column(2))); + + cursor = cursor.motion(&mut term, ViMotion::Bracket); + assert_eq!(cursor.point, Point::new(Line(0), Column(0))); + } + + fn motion_semantic_term() -> Term { + let mut term = term(); + + term.grid_mut()[Line(0)][Column(0)].c = 'x'; + term.grid_mut()[Line(0)][Column(1)].c = ' '; + term.grid_mut()[Line(0)][Column(2)].c = 'x'; + term.grid_mut()[Line(0)][Column(3)].c = 'x'; + term.grid_mut()[Line(0)][Column(4)].c = ' '; + term.grid_mut()[Line(0)][Column(5)].c = ' '; + term.grid_mut()[Line(0)][Column(6)].c = ':'; + term.grid_mut()[Line(0)][Column(7)].c = ' '; + term.grid_mut()[Line(0)][Column(8)].c = 'x'; + term.grid_mut()[Line(0)][Column(9)].c = ':'; + term.grid_mut()[Line(0)][Column(10)].c = 'x'; + term.grid_mut()[Line(0)][Column(11)].c = ' '; + term.grid_mut()[Line(0)][Column(12)].c = ' '; + term.grid_mut()[Line(0)][Column(13)].c = ':'; + term.grid_mut()[Line(0)][Column(14)].c = ' '; + term.grid_mut()[Line(0)][Column(15)].c = 'x'; + + term + } + + #[test] + fn motion_semantic_right_end() { + let mut term = motion_semantic_term(); + + let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticRightEnd); + assert_eq!(cursor.point, Point::new(Line(0), Column(3))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticRightEnd); + assert_eq!(cursor.point, Point::new(Line(0), Column(6))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticRightEnd); + assert_eq!(cursor.point, Point::new(Line(0), Column(8))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticRightEnd); + assert_eq!(cursor.point, Point::new(Line(0), Column(9))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticRightEnd); + assert_eq!(cursor.point, Point::new(Line(0), Column(10))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticRightEnd); + assert_eq!(cursor.point, Point::new(Line(0), Column(13))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticRightEnd); + assert_eq!(cursor.point, Point::new(Line(0), Column(15))); + } + + #[test] + fn motion_semantic_left_start() { + let mut term = motion_semantic_term(); + + let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(15))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticLeft); + assert_eq!(cursor.point, Point::new(Line(0), Column(13))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticLeft); + assert_eq!(cursor.point, Point::new(Line(0), Column(10))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticLeft); + assert_eq!(cursor.point, Point::new(Line(0), Column(9))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticLeft); + assert_eq!(cursor.point, Point::new(Line(0), Column(8))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticLeft); + assert_eq!(cursor.point, Point::new(Line(0), Column(6))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticLeft); + assert_eq!(cursor.point, Point::new(Line(0), Column(2))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticLeft); + assert_eq!(cursor.point, Point::new(Line(0), Column(0))); + } + + #[test] + fn motion_semantic_right_start() { + let mut term = motion_semantic_term(); + + let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticRight); + assert_eq!(cursor.point, Point::new(Line(0), Column(2))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticRight); + assert_eq!(cursor.point, Point::new(Line(0), Column(6))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticRight); + assert_eq!(cursor.point, Point::new(Line(0), Column(8))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticRight); + assert_eq!(cursor.point, Point::new(Line(0), Column(9))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticRight); + assert_eq!(cursor.point, Point::new(Line(0), Column(10))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticRight); + assert_eq!(cursor.point, Point::new(Line(0), Column(13))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticRight); + assert_eq!(cursor.point, Point::new(Line(0), Column(15))); + } + + #[test] + fn motion_semantic_left_end() { + let mut term = motion_semantic_term(); + + let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(15))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticLeftEnd); + assert_eq!(cursor.point, Point::new(Line(0), Column(13))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticLeftEnd); + assert_eq!(cursor.point, Point::new(Line(0), Column(10))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticLeftEnd); + assert_eq!(cursor.point, Point::new(Line(0), Column(9))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticLeftEnd); + assert_eq!(cursor.point, Point::new(Line(0), Column(8))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticLeftEnd); + assert_eq!(cursor.point, Point::new(Line(0), Column(6))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticLeftEnd); + assert_eq!(cursor.point, Point::new(Line(0), Column(3))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticLeftEnd); + assert_eq!(cursor.point, Point::new(Line(0), Column(0))); + } + + #[test] + fn scroll_semantic() { + let mut term = term(); + term.grid_mut().scroll_up(&(Line(0)..Line(20)), Line(5), &Default::default()); + + let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0))); + + cursor = cursor.motion(&mut term, ViMotion::SemanticLeft); + assert_eq!(cursor.point, Point::new(Line(0), Column(0))); + assert_eq!(term.grid().display_offset(), 5); + + cursor = cursor.motion(&mut term, ViMotion::SemanticRight); + assert_eq!(cursor.point, Point::new(Line(19), Column(19))); + assert_eq!(term.grid().display_offset(), 0); + + cursor = cursor.motion(&mut term, ViMotion::SemanticLeftEnd); + assert_eq!(cursor.point, Point::new(Line(0), Column(0))); + assert_eq!(term.grid().display_offset(), 5); + + cursor = cursor.motion(&mut term, ViMotion::SemanticRightEnd); + assert_eq!(cursor.point, Point::new(Line(19), Column(19))); + assert_eq!(term.grid().display_offset(), 0); + } + + #[test] + fn semantic_wide() { + let mut term = term(); + term.grid_mut()[Line(0)][Column(0)].c = 'a'; + term.grid_mut()[Line(0)][Column(1)].c = ' '; + term.grid_mut()[Line(0)][Column(2)].c = '汉'; + term.grid_mut()[Line(0)][Column(2)].flags.insert(Flags::WIDE_CHAR); + term.grid_mut()[Line(0)][Column(3)].c = ' '; + term.grid_mut()[Line(0)][Column(3)].flags.insert(Flags::WIDE_CHAR_SPACER); + term.grid_mut()[Line(0)][Column(4)].c = ' '; + term.grid_mut()[Line(0)][Column(5)].c = 'a'; + + let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(2))); + cursor = cursor.motion(&mut term, ViMotion::SemanticRight); + assert_eq!(cursor.point, Point::new(Line(0), Column(5))); + + let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(3))); + cursor = cursor.motion(&mut term, ViMotion::SemanticLeft); + assert_eq!(cursor.point, Point::new(Line(0), Column(0))); + } + + #[test] + fn motion_word() { + let mut term = term(); + term.grid_mut()[Line(0)][Column(0)].c = 'a'; + term.grid_mut()[Line(0)][Column(1)].c = ';'; + term.grid_mut()[Line(0)][Column(2)].c = ' '; + term.grid_mut()[Line(0)][Column(3)].c = ' '; + term.grid_mut()[Line(0)][Column(4)].c = 'a'; + term.grid_mut()[Line(0)][Column(5)].c = ';'; + + let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0))); + + cursor = cursor.motion(&mut term, ViMotion::WordRightEnd); + assert_eq!(cursor.point, Point::new(Line(0), Column(1))); + + cursor = cursor.motion(&mut term, ViMotion::WordRightEnd); + assert_eq!(cursor.point, Point::new(Line(0), Column(5))); + + cursor = cursor.motion(&mut term, ViMotion::WordLeft); + assert_eq!(cursor.point, Point::new(Line(0), Column(4))); + + cursor = cursor.motion(&mut term, ViMotion::WordLeft); + assert_eq!(cursor.point, Point::new(Line(0), Column(0))); + + cursor = cursor.motion(&mut term, ViMotion::WordRight); + assert_eq!(cursor.point, Point::new(Line(0), Column(4))); + + cursor = cursor.motion(&mut term, ViMotion::WordLeftEnd); + assert_eq!(cursor.point, Point::new(Line(0), Column(1))); + } + + #[test] + fn scroll_word() { + let mut term = term(); + term.grid_mut().scroll_up(&(Line(0)..Line(20)), Line(5), &Default::default()); + + let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0))); + + cursor = cursor.motion(&mut term, ViMotion::WordLeft); + assert_eq!(cursor.point, Point::new(Line(0), Column(0))); + assert_eq!(term.grid().display_offset(), 5); + + cursor = cursor.motion(&mut term, ViMotion::WordRight); + assert_eq!(cursor.point, Point::new(Line(19), Column(19))); + assert_eq!(term.grid().display_offset(), 0); + + cursor = cursor.motion(&mut term, ViMotion::WordLeftEnd); + assert_eq!(cursor.point, Point::new(Line(0), Column(0))); + assert_eq!(term.grid().display_offset(), 5); + + cursor = cursor.motion(&mut term, ViMotion::WordRightEnd); + assert_eq!(cursor.point, Point::new(Line(19), Column(19))); + assert_eq!(term.grid().display_offset(), 0); + } + + #[test] + fn word_wide() { + let mut term = term(); + term.grid_mut()[Line(0)][Column(0)].c = 'a'; + term.grid_mut()[Line(0)][Column(1)].c = ' '; + term.grid_mut()[Line(0)][Column(2)].c = '汉'; + term.grid_mut()[Line(0)][Column(2)].flags.insert(Flags::WIDE_CHAR); + term.grid_mut()[Line(0)][Column(3)].c = ' '; + term.grid_mut()[Line(0)][Column(3)].flags.insert(Flags::WIDE_CHAR_SPACER); + term.grid_mut()[Line(0)][Column(4)].c = ' '; + term.grid_mut()[Line(0)][Column(5)].c = 'a'; + + let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(2))); + cursor = cursor.motion(&mut term, ViMotion::WordRight); + assert_eq!(cursor.point, Point::new(Line(0), Column(5))); + + let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(3))); + cursor = cursor.motion(&mut term, ViMotion::WordLeft); + assert_eq!(cursor.point, Point::new(Line(0), Column(0))); + } +} diff --git a/extra/linux/redhat/alacritty.spec b/extra/linux/redhat/alacritty.spec index 9652732..07a0dff 100644 --- a/extra/linux/redhat/alacritty.spec +++ b/extra/linux/redhat/alacritty.spec @@ -7,7 +7,7 @@ URL: https://github.com/alacritty/alacritty VCS: https://github.com/alacritty/alacritty.git Source: alacritty-%{version}.tar -BuildRequires: rust >= 1.37.0 +BuildRequires: rust >= 1.39.0 BuildRequires: cargo BuildRequires: cmake BuildRequires: freetype-devel