summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml12
-rw-r--r--CHANGELOG.md5
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--alacritty.yml115
-rw-r--r--alacritty/src/config/bindings.rs417
-rw-r--r--alacritty/src/config/mod.rs2
-rw-r--r--alacritty/src/config/ui_config.rs16
-rw-r--r--alacritty/src/display.rs13
-rw-r--r--alacritty/src/event.rs88
-rw-r--r--alacritty/src/input.rs279
-rw-r--r--alacritty/src/url.rs9
-rw-r--r--alacritty_terminal/src/config/colors.rs2
-rw-r--r--alacritty_terminal/src/config/mod.rs32
-rw-r--r--alacritty_terminal/src/grid/mod.rs12
-rw-r--r--alacritty_terminal/src/index.rs41
-rw-r--r--alacritty_terminal/src/lib.rs1
-rw-r--r--alacritty_terminal/src/selection.rs106
-rw-r--r--alacritty_terminal/src/term/mod.rs456
-rw-r--r--alacritty_terminal/src/vi_mode.rs799
-rw-r--r--extra/linux/redhat/alacritty.spec2
20 files changed, 1893 insertions, 516 deletions
diff --git a/.travis.yml b/.travis.yml
index f6454507..71100b10 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 009faadf..be8b62f3 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 7b977432..1ffb8802 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 1fcf46b8..744fd111 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 48a1b9fb..7485cd54 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<Key>;
/// Bindings that are triggered by a mouse button
pub type MouseBinding = Binding<MouseButton>;
-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<T: Eq> Binding<T> {
#[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<String>),
+
+ /// 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<String>),
-
/// 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<ViAction> for Action {
+ fn from(action: ViAction) -> Self {
+ Self::ViAction(action)
+ }
+}
+
+impl From<ViMotion> 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<MouseBinding> {
bindings!(
MouseBinding;
- MouseButton::Middle; Action::PasteSelection;
+ MouseButton::Middle, ~TermMode::VI; Action::PasteSelection;
)
}
pub fn default_key_bindings() -> Vec<KeyBinding> {
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<KeyBinding> {
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<KeyBinding> {
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<KeyBinding> {
fn common_keybindings() -> Vec<KeyBinding> {
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<KeyBinding> {
pub fn platform_key_bindings() -> Vec<KeyBinding> {
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<MouseButton> = None;
let mut command: Option<CommandWrapper> = None;
- use ::serde::de::Error;
+ use de::Error;
while let Some(struct_key) = map.next_key::<Field>()? {
match struct_key {
@@ -690,10 +763,10 @@ impl<'a> Deserialize<'a> for RawBinding {
return Err(<V::Error as Error>::duplicate_field("key"));
}
- let val = map.next_value::<serde_yaml::Value>()?;
+ let val = map.next_value::<SerdeValue>()?;
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(<V::Error as Error>::custom(format!(
"Invalid key binding, scancode too big: {}",
scancode
@@ -726,7 +799,36 @@ impl<'a> Deserialize<'a> for RawBinding {
return Err(<V::Error as Error>::duplicate_field("action"));
}
- action = Some(map.next_value::<Action>()?);
+ let value = map.next_value::<SerdeValue>()?;
+
+ 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<E>(self, value: &str) -> Result<ModsWrapper, E>
@@ -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 2a598714..bb05d980 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 d7a477a0..13a3b04e 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<T>: Deserialize<'a>,
{
- let mut bindings: Vec<Binding<T>> = failure_default(deserializer)?;
+ let values = Vec::<serde_yaml::Value>::deserialize(deserializer)?;
+
+ // Skip all invalid values
+ let mut bindings = Vec::with_capacity(values.len());
+ for value in values {
+ match Binding::<T>::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 317c8758..6d5d810d 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 e635283b..9757893d 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<Event>,
+ pub urls: &'a Urls,
font_size: &'a mut Size,
}
@@ -83,7 +85,12 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> 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<T> 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);
- }
-
- 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;
- }
+ if vi_mode {
+ selection.include_all();
+ }
- 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;
+ self.terminal.dirty = true;
+ }
}
- fn semantic_selection(&mut self, point: Point) {
+ 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::semantic(point));
+ *self.terminal.selection_mut() = Some(Selection::new(ty, point, side));
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<Point> {
@@ -156,6 +163,12 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> 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<T> for ActionCon
fn event_loop(&self) -> &EventLoopWindowTarget<Event> {
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<N: Notify + OnResize> Processor<N> {
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 844710d7..937457c4 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<T>> {
pub ctx: A,
- pub urls: &'a Urls,
pub highlighted_url: &'a Option<Url>,
_phantom: PhantomData<T>,
}
pub trait ActionContext<T: EventListener> {
- fn write_to_pty<B: Into<Cow<'static, [u8]>>>(&mut self, _: B);
+ fn write_to_pty<B: Into<Cow<'static, [u8]>>>(&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<T: EventListener> {
fn message(&self) -> Option<&Message>;
fn config(&self) -> &Config;
fn event_loop(&self) -> &EventLoopWindowTarget<Event>;
+ fn urls(&self) -> &Urls;
+ fn launch_url(&self, url: Url);
+ fn mouse_mode(&self) -> bool;
}
trait Execute<T: EventListener> {
@@ -107,6 +107,22 @@ impl<T, U: EventListener> Execute<U> for Binding<T> {
}
}
+impl Action {
+ fn toggle_selection<T, A>(ctx: &mut A, ty: SelectionType)
+ where
+ T: EventListener,
+ A: ActionContext<T>,
+ {
+ 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<T: EventListener> Execute<T> for Action {
#[inline]
fn execute<A: ActionContext<T>>(&self, ctx: &mut A) {
@@ -118,6 +134,11 @@ impl<T: EventListener> Execute<T> 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<T: EventListener> Execute<T> 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<T: EventListener> Execute<T> 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<MouseState> for CursorIcon {
}
impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
- pub fn new(ctx: A, urls: &'a Urls, highlighted_url: &'a Option<Url>) -> Self {
- Self { ctx, urls, highlighted_url, _phantom: Default::default() }
+ pub fn new(ctx: A, highlighted_url: &'a Option<Url>) -> Self {
+ Self { ctx, highlighted_url, _phantom: Default::default() }
}
#[inline]
@@ -238,12 +342,16 @@ impl<'a, T: EventListener, A: ActionContext<T>> 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<T>> 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<T>> 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<T>> 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<T>> 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<T>> 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<T>> 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<T>> 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<T>> 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<T>> 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<T>> 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<T>> 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<TerminalEvent> {
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 e538331d..fcdd477f 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<Url> {
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 35c03684..5c057619 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 da95391c..df8d37bd 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<T> Config<T> {
self.dynamic_title.0
}
- /// Cursor foreground color
+ /// Cursor foreground color.
#[inline]
pub fn cursor_text_color(&self) -> Option<Rgb> {
self.colors.cursor.text
}
- /// Cursor background color
+ /// Cursor background color.
#[inline]
- pub fn cursor_cursor_color(&self) -> Option<Color> {
- self.colors.cursor.cursor.map(|_| Color::Named(NamedColor::Cursor))
+ pub fn cursor_cursor_color(&self) -> Option<NamedColor> {
+ self.colors.cursor.cursor.map(|_| NamedColor::Cursor)
+ }
+
+ /// Vi mode cursor foreground color.
+ #[inline]
+ pub fn vi_mode_cursor_text_color(&self) -> Option<Rgb> {
+ self.colors.vi_mode_cursor.text
+ }
+
+ /// Vi mode cursor background color.
+ #[inline]
+ pub fn vi_mode_cursor_cursor_color(&self) -> Option<Rgb> {
+ 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<CursorStyle>,
#[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 34d989db..37cf0eb6 100644
--- a/alacritty_terminal/src/grid/mod.rs
+++ b/alacritty_terminal/src/grid/mod.rs
@@ -264,11 +264,9 @@ impl<T: GridCell + PartialEq + Copy> Grid<T> {
let mut new_empty_lines = 0;
let mut reversed: Vec<Row<T>> = 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<T: GridCell + PartialEq + Copy> Grid<T> {
}
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 56d32003..1334a74e 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<L = Line> {
@@ -49,7 +58,7 @@ impl<L> Point<L> {
L: Copy + Default + Into<Line> + Add<usize, Output = L> + Sub<usize, Output = L>,
{
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<L> Point<L> {
#[must_use = "this returns the result of the operation, without modifying the original"]
pub fn add(mut self, num_cols: usize, rhs: usize) -> Point<L>
where
- L: Add<usize, Output = L> + Sub<usize, Output = L>,
+ L: Copy + Default + Into<Line> + Add<usize, Output = L> + Sub<usize, Output = L>,
{
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<L>
+ where
+ L: Copy + Default + Into<Line> + Add<usize, Output = L> + Sub<usize, Output = L>,
+ {
+ 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<L>
+ where
+ L: Copy + Default + Into<Line> + Add<usize, Output = L> + Sub<usize, Output = L>,
+ {
+ 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 039f2b81..6991ffdc 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 f663417f..369846cf 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<usize>,
side: Side,
}
@@ -67,7 +67,7 @@ impl<L> SelectionRange<L> {
/// 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<Anchor>,
- ty: SelectionType,
}
impl Selection {
- pub fn simple(location: Point<usize>, side: Side) -> Selection {
+ pub fn new(ty: SelectionType, location: Point<usize>, 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<usize>, side: Side) -> Selection {
- Self {
- region: Range { start: Anchor::new(location, side), end: Anchor::new(location, side) },
- ty: SelectionType::Block,
- }
- }
-
- pub fn semantic(location: Point<usize>) -> Selection {
- Self {
- region: Range {
- start: Anchor::new(location, Side::Left),
- end: Anchor::new(location, Side::Right),
- },
- ty: SelectionType::Semantic,
- }
- }
-
- pub fn lines(location: Point<usize>) -> 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<usize>, side: Side) {
- self.region.end.point = location;
- self.region.end.side = side;
+ pub fn update(&mut self, point: Point<usize>, 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<T>(&self, term: &Term<T>) -> Option<SelectionRange> {
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 ac5e56b5..89c3723f 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<T> Search for Term<T> {
}
}
-/// 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<Rgb>,
+ cursor_color: Option<Rgb>,
+ 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<Cell>,
- cursor: &'a Point,
- cursor_offset: usize,
- cursor_key: Option<CursorKey>,
- cursor_style: CursorStyle,
+ cursor: RenderableCursor,
config: &'a Config<C>,
colors: &'a color::List,
selection: Option<SelectionRange<Line>>,
@@ -216,12 +224,10 @@ impl<'a, C> RenderableCellsIter<'a, C> {
term: &'b Term<T>,
config: &'b Config<C>,
selection: Option<SelectionRange>,
- 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<Self::Item> {
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 cell below cursor
+ if self.cursor.rendered {
+ let mut cell =
+ RenderableCell::new(self.config, self.colors, self.inner.next()?, selected);
- // Handle cursor
- if let Some(cursor_key) = self.cursor_key.take() {
+ if self.cursor.key.style == CursorStyle::Block {
+ mem::swap(&mut cell.bg, &mut cell.fg);
+
+ 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[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(),
+ 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(cursor_key);
+ renderable_cell.inner = RenderableCellContent::Cursor(self.cursor.key);
- if let Some(color) = self.config.cursor_cursor_color() {
- renderable_cell.fg = RenderableCell::compute_bg_rgb(self.colors, color);
+ if let Some(color) = self.cursor.cursor_color {
+ renderable_cell.fg = color;
}
return Some(renderable_cell);
- } else {
- 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 let Some(color) = self.config.cursor_text_color() {
- cell.fg = color;
- }
- }
-
- return Some(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,11 +736,69 @@ impl VisualBell {
}
}
+/// Terminal size info.
+#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq)]
+pub struct SizeInfo {
+ /// Terminal window width.
+ pub width: f32,
+
+ /// Terminal window height.
+ pub height: f32,
+
+ /// Width of individual cell.
+ pub cell_width: f32,
+
+ /// Height of individual cell.
+ pub cell_height: f32,
+
+ /// Horizontal window padding.
+ pub padding_x: f32,
+
+ /// Horizontal window padding.
+ pub padding_y: f32,
+
+ /// DPI factor of the current window.
+ #[serde(default)]
+ pub dpr: f64,
+}
+
+impl SizeInfo {
+ #[inline]
+ pub fn lines(&self) -> Line {
+ Line(((self.height - 2. * self.padding_y) / self.cell_height) as usize)
+ }
+
+ #[inline]
+ pub fn cols(&self) -> Column {
+ Column(((self.width - 2. * self.padding_x) / self.cell_width) as usize)
+ }
+
+ /// Check if coordinates are inside the terminal grid.
+ ///
+ /// The padding is not counted as part of the grid.
+ pub fn contains_point(&self, x: usize, y: usize) -> bool {
+ x < (self.width - self.padding_x) as usize
+ && x >= self.padding_x as usize
+ && y < (self.height - self.padding_y) as usize
+ && y >= self.padding_y as usize
+ }
+
+ pub fn pixels_to_coords(&self, x: usize, y: usize) -> Point {
+ let col = Column(x.saturating_sub(self.padding_x as usize) / (self.cell_width as usize));
+ let line = Line(y.saturating_sub(self.padding_y as usize) / (self.cell_height as usize));
+
+ Point {
+ line: min(line, Line(self.lines().saturating_sub(1))),
+ col: min(col, Column(self.cols().saturating_sub(1))),
+ }
+ }
+}
+
pub struct Term<T> {
- /// Terminal focus
+ /// Terminal focus.
pub is_focused: bool,
- /// The grid
+ /// The grid.
grid: Grid<Cell>,
/// Tracks if the next call to input will need to first handle wrapping.
@@ -744,23 +808,25 @@ pub struct Term<T> {
/// arrays. Without it we would have to sanitize cursor.col every time we used it.
input_needs_wrap: bool,
- /// Alternate grid
+ /// Alternate grid.
alt_grid: Grid<Cell>,
- /// Alt is active
+ /// Alt is active.
alt: bool,
- /// The cursor
+ /// The cursor.
cursor: Cursor,
- /// The graphic character set, out of `charsets`, which ASCII is currently
- /// being mapped to
+ /// 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
+ /// Tabstops.
tabs: TabStops,
- /// Mode flags
+ /// Mode flags.
mode: TermMode,
/// Scroll region.
@@ -772,33 +838,36 @@ pub struct Term<T> {
pub visual_bell: VisualBell,
- /// Saved cursor from main grid
+ /// Saved cursor from main grid.
cursor_save: Cursor,
- /// Saved cursor from alt grid
+ /// Saved cursor from alt grid.
cursor_save_alt: Cursor,
semantic_escape_chars: String,
- /// Colors used for rendering
+ /// Colors used for rendering.
colors: color::List,
- /// Is color in `colors` modified or not
+ /// Is color in `colors` modified or not.
color_modified: [bool; color::COUNT],
- /// Original colors from config
+ /// Original colors from config.
original_colors: color::List,
- /// Current style of the cursor
+ /// Current style of the cursor.
cursor_style: Option<CursorStyle>,
- /// Default style for resetting the cursor
+ /// Default style for resetting the cursor.
default_cursor_style: CursorStyle,
+ /// Style of the vi mode cursor.
+ vi_mode_cursor_style: Option<CursorStyle>,
+
/// Clipboard access coupled to the active window
clipboard: Clipboard,
- /// Proxy for sending events to the event loop
+ /// Proxy for sending events to the event loop.
event_proxy: T,
/// Current title of the window.
@@ -815,64 +884,6 @@ pub struct Term<T> {
title_stack: Vec<Option<String>>,
}
-/// Terminal size info
-#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq)]
-pub struct SizeInfo {
- /// Terminal window width
- pub width: f32,
-
- /// Terminal window height
- pub height: f32,
-
- /// Width of individual cell
- pub cell_width: f32,
-
- /// Height of individual cell
- pub cell_height: f32,
-
- /// Horizontal window padding
- pub padding_x: f32,
-
- /// Horizontal window padding
- pub padding_y: f32,
-
- /// DPI factor of the current window
- #[serde(default)]
- pub dpr: f64,
-}
-
-impl SizeInfo {
- #[inline]
- pub fn lines(&self) -> Line {
- Line(((self.height - 2. * self.padding_y) / self.cell_height) as usize)
- }
-
- #[inline]
- pub fn cols(&self) -> Column {
- Column(((self.width - 2. * self.padding_x) / self.cell_width) as usize)
- }
-
- /// Check if coordinates are inside the terminal grid.
- ///
- /// The padding is not counted as part of the grid.
- pub fn contains_point(&self, x: usize, y: usize) -> bool {
- x < (self.width - self.padding_x) as usize
- && x >= self.padding_x as usize
- && y < (self.height - self.padding_y) as usize
- && y >= self.padding_y as usize
- }
-
- pub fn pixels_to_coords(&self, x: usize, y: usize) -> Point {
- let col = Column(x.saturating_sub(self.padding_x as usize) / (self.cell_width as usize));
- let line = Line(y.saturating_sub(self.padding_y as usize) / (self.cell_height as usize));
-
- Point {
- line: min(line, Line(self.lines().saturating_sub(1))),
- col: min(col, Column(self.cols().saturating_sub(1))),
- }
- }
-}
-
impl<T> Term<T> {
pub fn selection(&self) -> &Option<Selection> {
&self.grid.selection
@@ -920,6 +931,7 @@ impl<T> Term<T> {
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<T> Term<T> {
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<T> Term<T> {
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<T> Term<T> {
pub fn renderable_cells<'b, C>(&'b self, config: &'b Config<C>) -> 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<T> Term<T> {
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<T> Term<T> {
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<T> Term<T> {
}
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<T> Term<T> {
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<T> Term<T> {
cell.c = self.cursor.charsets[self.active_charset].map(c);
cell
}
+
+ /// Get rendering information about the active cursor.
+ fn renderable_cursor<C>(&self, config: &Config<C>) -> 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<T> TermInfo for Term<T> {
@@ -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 00000000..196193e8
--- /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<T: EventListener>(mut self, term: &mut Term<T>, 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<T: EventListener>(mut self, term: &Term<T>, 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<T: EventListener>(term: &mut Term<T>, point: Point<usize>) {
+ 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<T>(term: &Term<T>, mut point: Point<usize>) -> Point<usize> {
+ 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<T>(term: &Term<T>, mut point: Point<usize>) -> Point<usize> {
+ 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<T: EventListener>(
+ term: &mut Term<T>,
+ mut point: Point<usize>,
+ left: bool,
+ start: bool,
+) -> Point<usize> {
+ // Expand semantically based on movement direction
+ let expand_semantic = |point: Point<usize>| {
+ // 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<T: EventListener>(
+ term: &mut Term<T>,
+ mut point: Point<usize>,
+ left: bool,
+ start: bool,
+) -> Point<usize> {
+ // 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<T, P>(term: &Term<T>, point: P, left: bool) -> Point<usize>
+where
+ P: Into<Point<usize>>,
+{
+ 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<T>(term: &Term<T>, line: usize) -> Option<Point<usize>> {
+ (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<T>(term: &Term<T>, line: usize) -> Option<Point<usize>> {
+ (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<T>(term: &Term<T>, point: Point<usize>, left: bool) -> Point<usize> {
+ 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<T>(term: &Term<T>, point: Point<usize>) -> bool {
+ let cell = term.grid()[point.line][point.col];
+ cell.c == ' ' || cell.c == '\t' && !cell.flags().contains(Flags::WIDE_CHAR_SPACER)
+}
+
+fn is_wrap<T>(term: &Term<T>, point: Point<usize>) -> bool {
+ term.grid()[point.line][point.col].flags.contains(Flags::WRAPLINE)
+}
+
+/// Check if point is at screen boundary.
+fn is_boundary<T>(term: &Term<T>, point: Point<usize>, 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<Mock> {
+ 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<Mock> {
+ 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 96527325..07a0dff5 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