aboutsummaryrefslogtreecommitdiff
path: root/alacritty/src
diff options
context:
space:
mode:
authorChristian Duerr <contact@christianduerr.com>2025-05-20 23:53:03 +0000
committerGitHub <noreply@github.com>2025-05-20 23:53:03 +0000
commit71feeeeccc422d8092bda56b0d38693290f7585f (patch)
tree22fc0210063e9222e3c899faa81e1aaa2be00356 /alacritty/src
parent2bc1bb49e9c9a4861a2bc3eafcbaa652cda26ded (diff)
downloadr-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>
Diffstat (limited to 'alacritty/src')
-rw-r--r--alacritty/src/config/bindings.rs8
-rw-r--r--alacritty/src/event.rs96
-rw-r--r--alacritty/src/input/mod.rs21
3 files changed, 125 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 {