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.
This commit is contained in:
parent
7ceb638ff8
commit
845a5d8a8d
|
@ -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
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
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;
|
||||
}
|
||||
|
||||
if cell.point.column == last_column && !cell.flags.contains(Flags::WRAPLINE) {
|
||||
break; // cut off if on new line or hit escape char
|
||||
}
|
||||
|
||||
point = cell.point;
|
||||
}
|
||||
|
||||
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.
|
||||
|
|
|
@ -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"_
|
||||
|
|
|
@ -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:_
|
||||
|
||||
|
|
Loading…
Reference in New Issue