mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Add Vim buffer jump motions (#40958)
## What changed - Add `gg` and `G` motions to jump to the first and last buffer lines. - Support the motions with delete, change, and yank operators, including dot-repeat for edits. - Expose normal-mode and operator-motion bindings in the configurable keymap while preserving conflicting user bindings. ## Testing - Cover buffer jumps, operator combinations, repeat behavior, chord routing, and custom remapping. GitOrigin-RevId: 24a9178cf7964053aaf9a8cd268c5434dda0778e
This commit is contained in:
1 parent
6ac012a0d4
commit
d47e5cc0e2
17 files changed
+433
-13
No files matched your search
@@ -52,6 +52,9 @@ pub(super) enum VimEditTarget {
|
||||
motion: VimFindMotion,
|
||||
target: char,
|
||||
},
|
||||
BufferJump {
|
||||
last: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -262,6 +265,9 @@ impl TextArea {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
VimEditTarget::BufferJump { last } => {
|
||||
self.jump_to_vim_buffer_line(last, Some(operator));
|
||||
}
|
||||
}
|
||||
if operator == VimOperator::Change {
|
||||
return self.vim_mode == VimMode::Insert;
|
||||
@@ -330,6 +336,10 @@ impl TextArea {
|
||||
self.start_vim_find(VimFindMotion::TillForward, /*operator*/ None);
|
||||
} else if self.vim_normal_keymap.till_backward.is_pressed(event) {
|
||||
self.start_vim_find(VimFindMotion::TillBackward, /*operator*/ None);
|
||||
} else if self.vim_normal_keymap.jump_top.is_pressed(event) {
|
||||
self.jump_to_vim_buffer_line(/*last*/ false, /*operator*/ None);
|
||||
} else if self.vim_normal_keymap.jump_bottom.is_pressed(event) {
|
||||
self.jump_to_vim_buffer_line(/*last*/ true, /*operator*/ None);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -365,6 +375,25 @@ impl TextArea {
|
||||
.is_pressed(event)
|
||||
{
|
||||
self.start_vim_find(VimFindMotion::TillBackward, Some(operator));
|
||||
} else if self.vim_operator_keymap.motion_jump_top.is_pressed(event)
|
||||
|| self
|
||||
.vim_operator_keymap
|
||||
.motion_jump_bottom
|
||||
.is_pressed(event)
|
||||
{
|
||||
let last = self
|
||||
.vim_operator_keymap
|
||||
.motion_jump_bottom
|
||||
.is_pressed(event);
|
||||
match operator {
|
||||
VimOperator::Delete => {
|
||||
self.start_vim_edit(VimAction::Delete(VimEditTarget::BufferJump { last }));
|
||||
}
|
||||
VimOperator::Change => {
|
||||
self.start_vim_edit(VimAction::Change(VimEditTarget::BufferJump { last }));
|
||||
}
|
||||
VimOperator::Yank => self.jump_to_vim_buffer_line(last, Some(operator)),
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -475,6 +504,33 @@ impl TextArea {
|
||||
true
|
||||
}
|
||||
|
||||
fn jump_to_vim_buffer_line(&mut self, last: bool, operator: Option<VimOperator>) {
|
||||
if let Some(operator) = operator {
|
||||
let current = self.current_line_range_with_newline();
|
||||
let range = if last {
|
||||
current.start..self.text.len()
|
||||
} else {
|
||||
0..current.end
|
||||
};
|
||||
match operator {
|
||||
VimOperator::Delete => self.kill_line_range(range),
|
||||
VimOperator::Yank => self.yank_line_range(range),
|
||||
VimOperator::Change => {
|
||||
self.kill_line_range(range);
|
||||
self.vim_mode = VimMode::Insert;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
let start = if last {
|
||||
self.beginning_of_line(self.text.len())
|
||||
} else {
|
||||
0
|
||||
};
|
||||
self.set_cursor(start);
|
||||
self.set_cursor(self.first_non_blank_of_current_line());
|
||||
}
|
||||
|
||||
fn is_vim_command_target(&self, position: usize) -> bool {
|
||||
!self
|
||||
.elements
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
use super::super::TextArea;
|
||||
use super::VimAction;
|
||||
use crate::keymap::KeyChordMatch;
|
||||
use crate::keymap::KeyChordMatcher;
|
||||
use crate::keymap::KeymapContextSet;
|
||||
use crate::keymap::RuntimeKeymap;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tokio::time::Instant;
|
||||
|
||||
fn vim_textarea(text: &str, cursor: usize) -> TextArea {
|
||||
let mut textarea = TextArea::new();
|
||||
@@ -14,13 +19,25 @@ fn vim_textarea(text: &str, cursor: usize) -> TextArea {
|
||||
}
|
||||
|
||||
fn keys(textarea: &mut TextArea, keys: &str) {
|
||||
let keymap = RuntimeKeymap::defaults();
|
||||
let mut matcher = KeyChordMatcher::default();
|
||||
for key in keys.chars() {
|
||||
let code = if key == '\n' {
|
||||
KeyCode::Enter
|
||||
} else {
|
||||
KeyCode::Char(key)
|
||||
};
|
||||
textarea.input(KeyEvent::new(code, KeyModifiers::NONE));
|
||||
let event = KeyEvent::new(code, KeyModifiers::NONE);
|
||||
match matcher.advance(
|
||||
event,
|
||||
&keymap.chords,
|
||||
KeymapContextSet::new(textarea.keymap_context()),
|
||||
Instant::now(),
|
||||
) {
|
||||
KeyChordMatch::PassThrough => textarea.input(event),
|
||||
KeyChordMatch::Completed(event) => textarea.input(event),
|
||||
KeyChordMatch::Pending(_) | KeyChordMatch::Cancelled | KeyChordMatch::Ignored => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,7 +400,7 @@ fn find_and_till_handle_missing_cancelled_and_adjacent_targets() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dot_repeat_replays_character_find_operators() {
|
||||
fn dot_repeat_replays_character_find_and_buffer_jump_operators() {
|
||||
let mut textarea = vim_textarea("one:two:three", /*cursor*/ 0);
|
||||
keys(&mut textarea, "df:.");
|
||||
assert_eq!(textarea.text(), "three");
|
||||
@@ -403,6 +420,10 @@ fn dot_repeat_replays_character_find_operators() {
|
||||
(expected, Some("Normal"))
|
||||
);
|
||||
}
|
||||
|
||||
let mut textarea = vim_textarea("one\ntwo\nthree\nfour\nfive", "one\n".len());
|
||||
keys(&mut textarea, "dggj.");
|
||||
assert_eq!(textarea.text(), "five");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -470,6 +491,9 @@ fn character_find_and_operator_motion_use_configured_bindings() {
|
||||
#[test]
|
||||
fn uppercase_commands_accept_shift_only_terminal_events() {
|
||||
let mut textarea = vim_textarea("alpha\nbeta", /*cursor*/ 0);
|
||||
textarea.input(KeyEvent::new(KeyCode::Char('g'), KeyModifiers::SHIFT));
|
||||
assert_eq!(textarea.cursor(), "alpha\n".len());
|
||||
|
||||
textarea.set_cursor("alpha\nbe".len());
|
||||
textarea.input(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::SHIFT));
|
||||
keys(&mut textarea, "b");
|
||||
@@ -480,6 +504,35 @@ fn uppercase_commands_accept_shift_only_terminal_events() {
|
||||
assert_eq!(textarea.cursor(), "alpha\nb".len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_jumps_target_first_non_blank_and_support_operators() {
|
||||
let mut textarea = vim_textarea(" first\n second\n third", /*cursor*/ 2);
|
||||
keys(&mut textarea, "G");
|
||||
assert_eq!(textarea.cursor(), " first\n second\n ".len());
|
||||
keys(&mut textarea, "gg");
|
||||
assert_eq!(textarea.cursor(), 2);
|
||||
|
||||
let mut textarea = vim_textarea("first\nsecond\nthird", "first\n".len());
|
||||
keys(&mut textarea, "dG");
|
||||
assert_eq!(textarea.text(), "first\n");
|
||||
|
||||
let mut textarea = vim_textarea("first\nsecond\nthird", "first\n".len());
|
||||
keys(&mut textarea, "dgg");
|
||||
assert_eq!(textarea.text(), "third");
|
||||
|
||||
let mut textarea = vim_textarea("one\ntwo\nthree\nfour", "one\n".len());
|
||||
keys(&mut textarea, "dggp");
|
||||
assert_eq!(textarea.text(), "three\none\ntwo\nfour");
|
||||
|
||||
let mut textarea = vim_textarea("one\ntwo\nthree", "one\n".len());
|
||||
keys(&mut textarea, "yGp");
|
||||
assert_eq!(textarea.text(), "one\ntwo\ntwo\nthree\nthree");
|
||||
|
||||
let mut textarea = vim_textarea("one\ntwo\nthree", "one\n".len());
|
||||
keys(&mut textarea, "cG");
|
||||
assert_eq!(textarea.vim_mode_label(), Some("Insert"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pending_replacement_owns_escape_before_turn_interruption() {
|
||||
let mut textarea = vim_textarea("alpha", /*cursor*/ 0);
|
||||
@@ -539,7 +592,7 @@ fn dot_repeat_has_visual_snapshot_coverage() {
|
||||
fn find_and_navigation_have_visual_snapshot_coverage() {
|
||||
let mut textarea = vim_textarea("alpha beta\ngamma delta", /*cursor*/ 0);
|
||||
let mut states = Vec::new();
|
||||
for command in ["tb", "fb", "Ta", "Fa"] {
|
||||
for command in ["tb", "fb", "Ta", "Fa", "G", "gg"] {
|
||||
keys(&mut textarea, command);
|
||||
states.push(format!(
|
||||
"{command}: {}\n{}^",
|
||||
@@ -560,5 +613,10 @@ fn find_and_navigation_have_visual_snapshot_coverage() {
|
||||
Fa: alpha beta\ngamma delta
|
||||
^
|
||||
|
||||
G: alpha beta\ngamma delta
|
||||
^
|
||||
|
||||
gg: alpha beta\ngamma delta
|
||||
^
|
||||
"###);
|
||||
}
|
||||
Reference in new issue
Block a user