diff options
| author | Christian Duerr <contact@christianduerr.com> | 2025-05-20 23:53:03 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-05-20 23:53:03 +0000 |
| commit | 71feeeeccc422d8092bda56b0d38693290f7585f (patch) | |
| tree | 22fc0210063e9222e3c899faa81e1aaa2be00356 | |
| parent | 2bc1bb49e9c9a4861a2bc3eafcbaa652cda26ded (diff) | |
| download | r-alacritty-71feeeeccc422d8092bda56b0d38693290f7585f.tar.gz r-alacritty-71feeeeccc422d8092bda56b0d38693290f7585f.tar.bz2 r-alacritty-71feeeeccc422d8092bda56b0d38693290f7585f.zip | |
Add * # { } vi motions
This patch adds Vi's semantic search and paragraph motion.
The semantic search uses either the selection or the semantic word under
the cursor and jumps to the next match in the desired direction.
Paragraph motion jumps to just above or below the current paragraph.
Closes #7961.
Co-authored-by: Fletcher Gornick <fletcher@gornick.dev>
| -rw-r--r-- | alacritty/src/config/bindings.rs | 8 | ||||
| -rw-r--r-- | alacritty/src/event.rs | 96 | ||||
| -rw-r--r-- | alacritty/src/input/mod.rs | 21 | ||||
| -rw-r--r-- | alacritty_terminal/src/vi_mode.rs | 25 | ||||
| -rw-r--r-- | extra/man/alacritty-bindings.5.scd | 16 | ||||
| -rw-r--r-- | extra/man/alacritty.5.scd | 8 |
6 files changed, 174 insertions, 0 deletions
diff --git a/alacritty/src/config/bindings.rs b/alacritty/src/config/bindings.rs index d7ce9a9e..a755cf70 100644 --- a/alacritty/src/config/bindings.rs +++ b/alacritty/src/config/bindings.rs @@ -328,6 +328,10 @@ pub enum ViAction { InlineSearchNext, /// Jump to the previous inline search match. InlineSearchPrevious, + /// Search forward for selection or word under the cursor. + SemanticSearchForward, + /// Search backward for selection or word under the cursor. + SemanticSearchBackward, } /// Search mode specific actions. @@ -488,6 +492,8 @@ pub fn default_key_bindings() -> Vec<KeyBinding> { "t", ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; ViAction::InlineSearchBackwardShort; ";", +BindingMode::VI, ~BindingMode::SEARCH; ViAction::InlineSearchNext; ",", +BindingMode::VI, ~BindingMode::SEARCH; ViAction::InlineSearchPrevious; + "*", ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; ViAction::SemanticSearchForward; + "#", ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; ViAction::SemanticSearchBackward; "k", +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::Up; "j", +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::Down; "h", +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::Left; @@ -511,6 +517,8 @@ pub fn default_key_bindings() -> Vec<KeyBinding> { "w", ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::WordRight; "e", ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::WordRightEnd; "%", ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::Bracket; + "{", ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::ParagraphUp; + "}", ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::ParagraphDown; Enter, +BindingMode::VI, +BindingMode::SEARCH; SearchAction::SearchConfirm; // Plain search. Escape, +BindingMode::SEARCH; SearchAction::SearchCancel; diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs index c761f5ae..d2fd5c0d 100644 --- a/alacritty/src/event.rs +++ b/alacritty/src/event.rs @@ -35,6 +35,7 @@ use alacritty_terminal::event_loop::Notifier; 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::cell::Flags; use alacritty_terminal::term::search::{Match, RegexSearch}; use alacritty_terminal::term::{self, ClipboardType, Term, TermMode}; use alacritty_terminal::vte::ansi::NamedColor; @@ -941,6 +942,52 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon } #[inline] + fn start_seeded_search(&mut self, direction: Direction, text: String) { + let origin = self.terminal.vi_mode_cursor.point; + + // Start new search. + self.clear_selection(); + self.start_search(direction); + + // Enter initial selection text. + for c in text.chars() { + if let '$' | '('..='+' | '?' | '['..='^' | '{'..='}' = c { + self.search_input('\\'); + } + self.search_input(c); + } + + // Leave search mode. + self.confirm_search(); + + if !self.terminal.mode().contains(TermMode::VI) { + return; + } + + // Find the target vi cursor point by going to the next match to the right of the origin, + // then jump to the next search match in the target direction. + let target = self.search_next(origin, Direction::Right, Side::Right).and_then(|rm| { + let regex_match = match direction { + Direction::Right => { + let origin = rm.end().add(self.terminal, Boundary::None, 1); + self.search_next(origin, Direction::Right, Side::Left)? + }, + Direction::Left => { + let origin = rm.start().sub(self.terminal, Boundary::None, 1); + self.search_next(origin, Direction::Left, Side::Left)? + }, + }; + Some(*regex_match.start()) + }); + + // Move the vi cursor to the target position. + if let Some(target) = target { + self.terminal_mut().vi_goto_point(target); + self.mark_dirty(); + } + } + + #[inline] fn confirm_search(&mut self) { // Just cancel search when not in vi mode. if !self.terminal.mode().contains(TermMode::VI) { @@ -1217,6 +1264,55 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon } } + /// Get the semantic word at the specified point. + fn semantic_word(&self, point: Point) -> String { + let terminal = self.terminal(); + let grid = terminal.grid(); + + // Find the next semantic word boundary to the right. + let mut end = terminal.semantic_search_right(point); + + // Get point at which skipping over semantic characters has led us back to the + // original character. + let start_cell = &grid[point]; + let search_end = if start_cell.flags.intersects(Flags::LEADING_WIDE_CHAR_SPACER) { + point.add(terminal, Boundary::None, 2) + } else if start_cell.flags.intersects(Flags::WIDE_CHAR) { + point.add(terminal, Boundary::None, 1) + } else { + point + }; + + // Keep moving until we're not on top of a semantic escape character. + let semantic_chars = terminal.semantic_escape_chars(); + loop { + let cell = &grid[end]; + + // Get cell's character, taking wide characters into account. + let c = if cell.flags.contains(Flags::WIDE_CHAR_SPACER) { + grid[end.sub(terminal, Boundary::None, 1)].c + } else { + cell.c + }; + + if !semantic_chars.contains(c) { + break; + } + + end = terminal.semantic_search_right(end.add(terminal, Boundary::None, 1)); + + // Stop if the entire grid is only semantic escape characters. + if end == search_end { + return String::new(); + } + } + + // Find the beginning of the semantic word. + let start = terminal.semantic_search_left(end); + + terminal.bounds_to_string(start, end) + } + /// Handle beginning of terminal text input. fn on_terminal_input_start(&mut self) { self.on_typing_start(); diff --git a/alacritty/src/input/mod.rs b/alacritty/src/input/mod.rs index 3f85512f..a69d2989 100644 --- a/alacritty/src/input/mod.rs +++ b/alacritty/src/input/mod.rs @@ -112,6 +112,7 @@ pub trait ActionContext<T: EventListener> { fn clipboard_mut(&mut self) -> &mut Clipboard; fn scheduler_mut(&mut self) -> &mut Scheduler; fn start_search(&mut self, _direction: Direction) {} + fn start_seeded_search(&mut self, _direction: Direction, _text: String) {} fn confirm_search(&mut self) {} fn cancel_search(&mut self) {} fn search_input(&mut self, _c: char) {} @@ -132,6 +133,7 @@ pub trait ActionContext<T: EventListener> { fn hint_input(&mut self, _character: char) {} fn trigger_hint(&mut self, _hint: &HintMatch) {} fn expand_selection(&mut self) {} + fn semantic_word(&self, point: Point) -> String; fn on_terminal_input_start(&mut self) {} fn paste(&mut self, _text: &str, _bracketed: bool) {} fn spawn_daemon<I, S>(&self, _program: &str, _args: I) @@ -278,6 +280,21 @@ impl<T: EventListener> Execute<T> for Action { }, Action::Vi(ViAction::InlineSearchNext) => ctx.inline_search_next(), Action::Vi(ViAction::InlineSearchPrevious) => ctx.inline_search_previous(), + Action::Vi(ViAction::SemanticSearchForward | ViAction::SemanticSearchBackward) => { + let seed_text = match ctx.terminal().selection_to_string() { + Some(selection) if !selection.is_empty() => selection, + // Get semantic word at the vi cursor position. + _ => ctx.semantic_word(ctx.terminal().vi_mode_cursor.point), + }; + + if !seed_text.is_empty() { + let direction = match self { + Action::Vi(ViAction::SemanticSearchForward) => Direction::Right, + _ => Direction::Left, + }; + ctx.start_seeded_search(direction, seed_text); + } + }, action @ Action::Search(_) if !ctx.search_active() => { debug!("Ignoring {action:?}: Search mode inactive"); }, @@ -1237,6 +1254,10 @@ mod tests { fn scheduler_mut(&mut self) -> &mut Scheduler { unimplemented!(); } + + fn semantic_word(&self, _point: Point) -> String { + unimplemented!(); + } } macro_rules! test_clickstate { diff --git a/alacritty_terminal/src/vi_mode.rs b/alacritty_terminal/src/vi_mode.rs index e23e9b80..af2e56e0 100644 --- a/alacritty_terminal/src/vi_mode.rs +++ b/alacritty_terminal/src/vi_mode.rs @@ -52,6 +52,10 @@ pub enum ViMotion { WordRightEnd, /// Move to opposing bracket. Bracket, + /// Move above the current paragraph. + ParagraphUp, + /// Move below the current paragraph. + ParagraphDown, } /// Cursor tracking vi mode position. @@ -153,6 +157,27 @@ impl ViModeCursor { self.point = word(term, self.point, Direction::Right, Side::Right); }, ViMotion::Bracket => self.point = term.bracket_search(self.point).unwrap_or(self.point), + ViMotion::ParagraphUp => { + // Skip empty lines until we find the next paragraph, + // then skip over the paragraph until we reach the next empty line. + let topmost_line = term.topmost_line(); + self.point.line = (*topmost_line..*self.point.line) + .rev() + .skip_while(|line| term.grid()[Line(*line)].is_clear()) + .find(|line| term.grid()[Line(*line)].is_clear()) + .map_or(topmost_line, Line); + self.point.column = Column(0); + }, + ViMotion::ParagraphDown => { + // Skip empty lines until we find the next paragraph, + // then skip over the paragraph until we reach the next empty line. + let bottommost_line = term.bottommost_line(); + self.point.line = (*self.point.line..*bottommost_line) + .skip_while(|line| term.grid()[Line(*line)].is_clear()) + .find(|line| term.grid()[Line(*line)].is_clear()) + .map_or(bottommost_line, Line); + self.point.column = Column(0); + }, } term.scroll_to_point(self.point); diff --git a/extra/man/alacritty-bindings.5.scd b/extra/man/alacritty-bindings.5.scd index 7f0bdf34..ee426255 100644 --- a/extra/man/alacritty-bindings.5.scd +++ b/extra/man/alacritty-bindings.5.scd @@ -189,6 +189,14 @@ configuration. See *alacritty*(5) for full configuration format documentation. :[ : _"Vi|~Search"_ : _"InlineSearchPrevious"_ +| _"\*"_ +: _"Shift"_ +: _"Vi|~Search"_ +: _"SemanticSearchForward"_ +| _"#"_ +: _"Shift"_ +: _"Vi|~Search"_ +: _"SemanticSearchBackward"_ | _"K"_ :[ : _"Vi|~Search"_ @@ -281,6 +289,14 @@ configuration. See *alacritty*(5) for full configuration format documentation. : _"Shift"_ : _"Vi|~Search"_ : _"Bracket"_ +| _"{"_ +: _"Shift"_ +: _"Vi|~Search"_ +: _"ParagraphUp"_ +| _"}"_ +: _"Shift"_ +: _"Vi|~Search"_ +: _"ParagraphDown"_ | _"/"_ :[ : _"Vi|~Search"_ diff --git a/extra/man/alacritty.5.scd b/extra/man/alacritty.5.scd index e2f5b252..ef6e4f19 100644 --- a/extra/man/alacritty.5.scd +++ b/extra/man/alacritty.5.scd @@ -894,6 +894,10 @@ _https://docs.rs/winit/latest/winit/keyboard/enum.Key.html#variant.Dead_ Move to end of whitespace separated word. *Bracket* Move to opposing bracket. + *ParagraphUp* + Move above the current paragraph. + *ParagraphDown* + Move below the current paragraph. *ToggleNormalSelection* Toggle normal vi selection. *ToggleLineSelection* @@ -926,6 +930,10 @@ _https://docs.rs/winit/latest/winit/keyboard/enum.Key.html#variant.Dead_ Jump to the next inline search match. *InlineSearchPrevious* Jump to the previous inline search match. + *SemanticSearchForward* + Search forward for selection or word under the cursor. + *SemanticSearchBackward* + Search backward for selection or word under the cursor. _Search actions:_ |