summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Duerr <contact@christianduerr.com>2023-10-20 11:33:38 +0200
committerGitHub <noreply@github.com>2023-10-20 13:33:38 +0400
commit845a5d8a8d47c233c4ed8177ecbb20b05b22118b (patch)
tree057f4c5974f60defa0c6f79060ef26e59398b1f1
parent7ceb638ff80eca99ac63df5fd8cbb2f703d4637a (diff)
downloadalacritty-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.md1
-rw-r--r--alacritty/src/config/bindings.rs18
-rw-r--r--alacritty/src/event.rs83
-rw-r--r--alacritty/src/input.rs42
-rw-r--r--alacritty/src/window_context.rs9
-rw-r--r--alacritty_terminal/src/term/search.rs50
-rw-r--r--extra/man/alacritty-bindings.5.scd24
-rw-r--r--extra/man/alacritty.5.scd32
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:_