diff options
author | Christian Duerr <contact@christianduerr.com> | 2023-10-20 11:33:38 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-10-20 13:33:38 +0400 |
commit | 845a5d8a8d47c233c4ed8177ecbb20b05b22118b (patch) | |
tree | 057f4c5974f60defa0c6f79060ef26e59398b1f1 | |
parent | 7ceb638ff80eca99ac63df5fd8cbb2f703d4637a (diff) | |
download | alacritty-845a5d8a8d47c233c4ed8177ecbb20b05b22118b.tar.gz alacritty-845a5d8a8d47c233c4ed8177ecbb20b05b22118b.zip |
Add inline vi mode search
This patch adds inline search to vi mode using `f`/`F` and `t`/`T` as
default bindings. The behavior matches that of vim.
Fixes #7203.
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | alacritty/src/config/bindings.rs | 18 | ||||
-rw-r--r-- | alacritty/src/event.rs | 83 | ||||
-rw-r--r-- | alacritty/src/input.rs | 42 | ||||
-rw-r--r-- | alacritty/src/window_context.rs | 9 | ||||
-rw-r--r-- | alacritty_terminal/src/term/search.rs | 50 | ||||
-rw-r--r-- | extra/man/alacritty-bindings.5.scd | 24 | ||||
-rw-r--r-- | extra/man/alacritty.5.scd | 32 |
8 files changed, 239 insertions, 20 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index e4aac6d4..d15626fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Bindings to create and navigate tabs on macOS - Support startup notify protocol to raise initial window on Wayland/X11 - Debug option `prefer_egl` to prioritize EGL over other display APIs +- Inline vi-mode search using `f`/`F`/`t`/`T` ### Changed diff --git a/alacritty/src/config/bindings.rs b/alacritty/src/config/bindings.rs index 6e25ac9d..71278fd7 100644 --- a/alacritty/src/config/bindings.rs +++ b/alacritty/src/config/bindings.rs @@ -331,6 +331,18 @@ pub enum ViAction { Open, /// Centers the screen around the vi mode cursor. CenterAroundViCursor, + /// Search forward within the current line. + InlineSearchForward, + /// Search backward within the current line. + InlineSearchBackward, + /// Search forward within the current line, stopping just short of the character. + InlineSearchForwardShort, + /// Search backward within the current line, stopping just short of the character. + InlineSearchBackwardShort, + /// Jump to the next inline search match. + InlineSearchNext, + /// Jump to the previous inline search match. + InlineSearchPrevious, } /// Search mode specific actions. @@ -506,6 +518,12 @@ pub fn default_key_bindings() -> Vec<KeyBinding> { "n", ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; ViAction::SearchPrevious; Enter, +BindingMode::VI, ~BindingMode::SEARCH; ViAction::Open; "z", +BindingMode::VI, ~BindingMode::SEARCH; ViAction::CenterAroundViCursor; + "f", +BindingMode::VI, ~BindingMode::SEARCH; ViAction::InlineSearchForward; + "f", ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; ViAction::InlineSearchBackward; + "t", +BindingMode::VI, ~BindingMode::SEARCH; ViAction::InlineSearchForwardShort; + "t", ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; ViAction::InlineSearchBackwardShort; + ";", +BindingMode::VI, ~BindingMode::SEARCH; ViAction::InlineSearchNext; + ",", +BindingMode::VI, ~BindingMode::SEARCH; ViAction::InlineSearchPrevious; "k", +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::Up; "j", +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::Down; "h", +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::Left; diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs index 1b7e280c..7bb3a83e 100644 --- a/alacritty/src/event.rs +++ b/alacritty/src/event.rs @@ -29,7 +29,7 @@ use winit::window::WindowId; use alacritty_terminal::config::LOG_TARGET_CONFIG; use alacritty_terminal::event::{Event as TerminalEvent, EventListener, Notify}; use alacritty_terminal::event_loop::Notifier; -use alacritty_terminal::grid::{Dimensions, Scroll}; +use alacritty_terminal::grid::{BidirectionalIterator, Dimensions, Scroll}; use alacritty_terminal::index::{Boundary, Column, Direction, Line, Point, Side}; use alacritty_terminal::selection::{Selection, SelectionType}; use alacritty_terminal::term::search::{Match, RegexSearch}; @@ -178,6 +178,27 @@ impl Default for SearchState { } } +/// Vi inline search state. +pub struct InlineSearchState { + /// Whether inline search is currently waiting for search character input. + pub char_pending: bool, + pub character: Option<char>, + + direction: Direction, + stop_short: bool, +} + +impl Default for InlineSearchState { + fn default() -> Self { + Self { + direction: Direction::Right, + char_pending: Default::default(), + stop_short: Default::default(), + character: Default::default(), + } + } +} + pub struct ActionContext<'a, N, T> { pub notifier: &'a mut N, pub terminal: &'a mut Term<T>, @@ -193,6 +214,7 @@ pub struct ActionContext<'a, N, T> { pub event_proxy: &'a EventLoopProxy<Event>, pub scheduler: &'a mut Scheduler, pub search_state: &'a mut SearchState, + pub inline_search_state: &'a mut InlineSearchState, pub font_size: &'a mut Size, pub dirty: &'a mut bool, pub occluded: &'a mut bool, @@ -839,6 +861,30 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon *self.dirty = true; } + /// Get vi inline search state. + fn inline_search_state(&mut self) -> &mut InlineSearchState { + self.inline_search_state + } + + /// Start vi mode inline search. + fn start_inline_search(&mut self, direction: Direction, stop_short: bool) { + self.inline_search_state.stop_short = stop_short; + self.inline_search_state.direction = direction; + self.inline_search_state.char_pending = true; + } + + /// Jump to the next matching character in the line. + fn inline_search_next(&mut self) { + let direction = self.inline_search_state.direction; + self.inline_search(direction); + } + + /// Jump to the next matching character in the line. + fn inline_search_previous(&mut self) { + let direction = self.inline_search_state.direction.opposite(); + self.inline_search(direction); + } + fn message(&self) -> Option<&Message> { self.message_buffer.message() } @@ -1032,6 +1078,41 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { self.scheduler.schedule(event, blinking_timeout_interval, false, timer_id); } + + /// Perferm vi mode inline search in the specified direction. + fn inline_search(&mut self, direction: Direction) { + let c = match self.inline_search_state.character { + Some(c) => c, + None => return, + }; + let mut buf = [0; 4]; + let search_character = c.encode_utf8(&mut buf); + + // Find next match in this line. + let vi_point = self.terminal.vi_mode_cursor.point; + let point = match direction { + Direction::Right => self.terminal.inline_search_right(vi_point, search_character), + Direction::Left => self.terminal.inline_search_left(vi_point, search_character), + }; + + // Jump to point if there's a match. + if let Ok(mut point) = point { + if self.inline_search_state.stop_short { + let grid = self.terminal.grid(); + point = match direction { + Direction::Right => { + grid.iter_from(point).prev().map_or(point, |cell| cell.point) + }, + Direction::Left => { + grid.iter_from(point).next().map_or(point, |cell| cell.point) + }, + }; + } + + self.terminal.vi_goto_point(point); + self.mark_dirty(); + } + } } /// Identified purpose of the touch input. diff --git a/alacritty/src/input.rs b/alacritty/src/input.rs index 14413f75..428cda0d 100644 --- a/alacritty/src/input.rs +++ b/alacritty/src/input.rs @@ -46,7 +46,8 @@ use crate::display::hint::HintMatch; use crate::display::window::Window; use crate::display::{Display, SizeInfo}; use crate::event::{ - ClickState, Event, EventType, Mouse, TouchPurpose, TouchZoom, TYPING_SEARCH_DELAY, + ClickState, Event, EventType, InlineSearchState, Mouse, TouchPurpose, TouchZoom, + TYPING_SEARCH_DELAY, }; use crate::message_bar::{self, Message}; use crate::scheduler::{Scheduler, TimerId, Topic}; @@ -124,6 +125,10 @@ pub trait ActionContext<T: EventListener> { fn search_active(&self) -> bool; fn on_typing_start(&mut self) {} fn toggle_vi_mode(&mut self) {} + fn inline_search_state(&mut self) -> &mut InlineSearchState; + fn start_inline_search(&mut self, _direction: Direction, _stop_short: bool) {} + fn inline_search_next(&mut self) {} + fn inline_search_previous(&mut self) {} fn hint_input(&mut self, _character: char) {} fn trigger_hint(&mut self, _hint: &HintMatch) {} fn expand_selection(&mut self) {} @@ -259,6 +264,20 @@ impl<T: EventListener> Execute<T> for Action { ctx.scroll(Scroll::Delta(scroll_lines)); }, + Action::Vi(ViAction::InlineSearchForward) => { + ctx.start_inline_search(Direction::Right, false) + }, + Action::Vi(ViAction::InlineSearchBackward) => { + ctx.start_inline_search(Direction::Left, false) + }, + Action::Vi(ViAction::InlineSearchForwardShort) => { + ctx.start_inline_search(Direction::Right, true) + }, + Action::Vi(ViAction::InlineSearchBackwardShort) => { + ctx.start_inline_search(Direction::Left, true) + }, + Action::Vi(ViAction::InlineSearchNext) => ctx.inline_search_next(), + Action::Vi(ViAction::InlineSearchPrevious) => ctx.inline_search_previous(), action @ Action::Search(_) if !ctx.search_active() => { debug!("Ignoring {action:?}: Search mode inactive"); }, @@ -1016,6 +1035,20 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { return; } + // First key after inline search is captured. + let inline_state = self.ctx.inline_search_state(); + if mem::take(&mut inline_state.char_pending) { + if let Some(c) = text.chars().next() { + inline_state.character = Some(c); + + // Immediately move to the captured character. + self.ctx.inline_search_next(); + } + + // Ignore all other characters in `text`. + return; + } + // Reset search delay when the user is still typing. if self.ctx.search_active() { let timer_id = TimerId::new(Topic::DelayedSearch, self.ctx.window().id()); @@ -1218,6 +1251,7 @@ mod tests { pub message_buffer: &'a mut MessageBuffer, pub modifiers: Modifiers, config: &'a UiConfig, + inline_search_state: &'a mut InlineSearchState, } impl<'a, T: EventListener> super::ActionContext<T> for ActionContext<'a, T> { @@ -1234,6 +1268,10 @@ mod tests { Direction::Right } + fn inline_search_state(&mut self) -> &mut InlineSearchState { + self.inline_search_state + } + fn search_active(&self) -> bool { false } @@ -1346,6 +1384,7 @@ mod tests { ..Mouse::default() }; + let mut inline_search_state = InlineSearchState::default(); let mut message_buffer = MessageBuffer::default(); let context = ActionContext { @@ -1355,6 +1394,7 @@ mod tests { clipboard: &mut clipboard, modifiers: Default::default(), message_buffer: &mut message_buffer, + inline_search_state: &mut inline_search_state, config: &cfg, }; diff --git a/alacritty/src/window_context.rs b/alacritty/src/window_context.rs index ed384e4c..301d30ad 100644 --- a/alacritty/src/window_context.rs +++ b/alacritty/src/window_context.rs @@ -39,7 +39,9 @@ use crate::clipboard::Clipboard; use crate::config::UiConfig; use crate::display::window::Window; use crate::display::Display; -use crate::event::{ActionContext, Event, EventProxy, Mouse, SearchState, TouchPurpose}; +use crate::event::{ + ActionContext, Event, EventProxy, InlineSearchState, Mouse, SearchState, TouchPurpose, +}; use crate::logging::LOG_TARGET_IPC_CONFIG; use crate::message_bar::MessageBuffer; use crate::scheduler::Scheduler; @@ -54,6 +56,7 @@ pub struct WindowContext { terminal: Arc<FairMutex<Term<EventProxy>>>, cursor_blink_timed_out: bool, modifiers: Modifiers, + inline_search_state: InlineSearchState, search_state: SearchState, notifier: Notifier, font_size: Size, @@ -242,15 +245,16 @@ impl WindowContext { config, notifier: Notifier(loop_tx), cursor_blink_timed_out: Default::default(), + inline_search_state: Default::default(), message_buffer: Default::default(), search_state: Default::default(), event_queue: Default::default(), ipc_config: Default::default(), modifiers: Default::default(), + occluded: Default::default(), mouse: Default::default(), touch: Default::default(), dirty: Default::default(), - occluded: Default::default(), }) } @@ -436,6 +440,7 @@ impl WindowContext { let context = ActionContext { cursor_blink_timed_out: &mut self.cursor_blink_timed_out, message_buffer: &mut self.message_buffer, + inline_search_state: &mut self.inline_search_state, search_state: &mut self.search_state, modifiers: &mut self.modifiers, font_size: &mut self.font_size, diff --git a/alacritty_terminal/src/term/search.rs b/alacritty_terminal/src/term/search.rs index c10ef40d..8e329255 100644 --- a/alacritty_terminal/src/term/search.rs +++ b/alacritty_terminal/src/term/search.rs @@ -513,7 +513,25 @@ impl<T> Term<T> { } /// Find left end of semantic block. - pub fn semantic_search_left(&self, mut point: Point) -> Point { + #[must_use] + pub fn semantic_search_left(&self, point: Point) -> Point { + match self.inline_search_left(point, &self.semantic_escape_chars) { + Ok(point) => self.grid.iter_from(point).next().map_or(point, |cell| cell.point), + Err(point) => point, + } + } + + /// Find right end of semantic block. + #[must_use] + pub fn semantic_search_right(&self, point: Point) -> Point { + match self.inline_search_right(point, &self.semantic_escape_chars) { + Ok(point) => self.grid.iter_from(point).prev().map_or(point, |cell| cell.point), + Err(point) => point, + } + } + + /// Searching to the left, find the next character contained in `needles`. + pub fn inline_search_left(&self, mut point: Point, needles: &str) -> Result<Point, Point> { // Limit the starting point to the last line in the history point.line = max(point.line, self.topmost_line()); @@ -522,22 +540,22 @@ impl<T> Term<T> { let wide = Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER; while let Some(cell) = iter.prev() { - if !cell.flags.intersects(wide) && self.semantic_escape_chars.contains(cell.c) { - break; - } + point = cell.point; - if cell.point.column == last_column && !cell.flags.contains(Flags::WRAPLINE) { - break; // cut off if on new line or hit escape char + if !cell.flags.intersects(wide) && needles.contains(cell.c) { + return Ok(point); } - point = cell.point; + if point.column == last_column && !cell.flags.contains(Flags::WRAPLINE) { + break; + } } - point + Err(point) } - /// Find right end of semantic block. - pub fn semantic_search_right(&self, mut point: Point) -> Point { + /// Searching to the right, find the next character contained in `needles`. + pub fn inline_search_right(&self, mut point: Point, needles: &str) -> Result<Point, Point> { // Limit the starting point to the last line in the history point.line = max(point.line, self.topmost_line()); @@ -545,18 +563,18 @@ impl<T> Term<T> { let last_column = self.columns() - 1; for cell in self.grid.iter_from(point) { - if !cell.flags.intersects(wide) && self.semantic_escape_chars.contains(cell.c) { - break; - } - point = cell.point; + if !cell.flags.intersects(wide) && needles.contains(cell.c) { + return Ok(point); + } + if point.column == last_column && !cell.flags.contains(Flags::WRAPLINE) { - break; // cut off if on new line or hit escape char + break; } } - point + Err(point) } /// Find the beginning of the current line across linewraps. diff --git a/extra/man/alacritty-bindings.5.scd b/extra/man/alacritty-bindings.5.scd index a8f8dfe0..ecdf06d3 100644 --- a/extra/man/alacritty-bindings.5.scd +++ b/extra/man/alacritty-bindings.5.scd @@ -161,6 +161,30 @@ configuration. See *alacritty*(5) for full configuration format documentation. :[ : _"Vi|~Search"_ : _"CenterAroundViCursor"_ +| _"F"_ +:[ +: _"Vi|~Search"_ +: _"InlineSearchForward"_ +| _"F"_ +: _"Shift"_ +: _"Vi|~Search"_ +: _"InlineSearchBackward"_ +| _"T"_ +:[ +: _"Vi|~Search"_ +: _"InlineSearchForwardShort"_ +| _"T"_ +: _"Shift"_ +: _"Vi|~Search"_ +: _"InlineSearchBackwardShort"_ +| _";"_ +:[ +: _"Vi|~Search"_ +: _"InlineSearchNext"_ +| _","_ +:[ +: _"Vi|~Search"_ +: _"InlineSearchPrevious"_ | _"K"_ :[ : _"Vi|~Search"_ diff --git a/extra/man/alacritty.5.scd b/extra/man/alacritty.5.scd index b79a89d3..685f5e97 100644 --- a/extra/man/alacritty.5.scd +++ b/extra/man/alacritty.5.scd @@ -827,6 +827,38 @@ https://docs.rs/winit/\*/winit/keyboard/enum.Key.html Move to end of whitespace separated word. *Bracket* Move to opposing bracket. + *ToggleNormalSelection* + Toggle normal vi selection. + *ToggleLineSelection* + Toggle line vi selection. + *ToggleBlockSelection* + Toggle block vi selection. + *ToggleSemanticSelection* + Toggle semantic vi selection. + *SearchNext* + Jump to the beginning of the next match. + *SearchPrevious* + Jump to the beginning of the previous match. + *SearchStart* + Jump to the next start of a match to the left of the origin. + *SearchEnd* + Jump to the next end of a match to the right of the origin. + *Open* + Launch the URL below the vi mode cursor. + *CenterAroundViCursor* + Centers the screen around the vi mode cursor. + *InlineSearchForward* + Search forward within the current line. + *InlineSearchBcakward* + Search backard within the current line. + *InlineSearchForwardShort* + Search forward within the current line, stopping just short of the character. + *InlineSearchBackwardShort* + Search backward within the current line, stopping just short of the character. + *InlineSearchNext* + Jump to the next inline search match. + *InlineSearchPrevious* + Jump to the previous inline search match. _Search actions:_ |