mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat(tui): add vim text object bindings (#24382)
## Why Vim mode currently supports some normal-mode operators and motions, but common text-object combinations like `ciw`, `daw`, `di(`, and quote/bracket variants are still missing. That makes the composer feel incomplete for users who expect operator + text object editing to work inside prompts. Closes #21383. ## What Changed - Add Vim pending-state support for operator/text-object sequences. - Add `c` as a normal-mode operator for text objects, so combinations like `ciw` delete the object and enter insert mode. - Support word, WORD, delimiter, and quote text objects: - `iw`, `aw`, `iW`, `aW` - `i(`, `a(`, `i)`, `a)`, `ib`, `ab` - `i[`, `a[`, `i]`, `a]` - `i{`, `a{`, `i}`, `a}`, `iB`, `aB` - `i"`, `a"`, `i'`, `a'`, `i\``, `a\`` - Add configurable keymap entries and keymap picker coverage for the new Vim text-object context. - Regenerate the config schema and update keymap picker snapshots. ## How to Test Manual smoke test: 1. Start Codex with Vim composer mode enabled. 2. Type a draft such as: ```text alpha beta gamma call(foo[bar], {"x": "hello world"}) say "one \"two\" three" now ``` 3. Put the cursor on `beta`, press `ciw`, and confirm `beta` is removed and the composer enters insert mode. 4. Escape back to normal mode, put the cursor on `gamma`, press `daw`, and confirm `gamma` plus surrounding whitespace is removed. 5. Put the cursor inside `foo[bar]`, press `di[`, and confirm only `bar` is removed. 6. Put the cursor inside `call(...)`, press `da(`, and confirm the whole parenthesized section is removed. 7. Put the cursor inside the quoted text, press `ci"`, and confirm the quote contents are removed and insert mode starts. 8. Verify cancellation does not edit text: press `d` then `Esc`, and press `d` then `i` then `Esc`. Targeted tests: - `cargo test -p codex-tui --lib vim_` - `cargo nextest run -p codex-tui keymap_setup::tests` Additional local checks: - `just write-config-schema` - `just fmt` - `just fix -p codex-tui` - `git diff --check` - `cargo insta pending-snapshots --manifest-path tui/Cargo.toml` Local full-suite note: `just test -p codex-tui` ran to completion. The keymap snapshot failures were expected and accepted. Two unrelated guardian feature-flag tests still fail locally: - `app::tests::update_feature_flags_disabling_guardian_clears_review_policy_and_restores_default` - `app::tests::update_feature_flags_disabling_guardian_clears_manual_review_policy_without_history` `just argument-comment-lint` is currently blocked locally by Bazel analysis before the lint runs because `compiler-rt` has an empty `include/sanitizer/*.h` glob in the local Bazel cache. The touched Rust diff was manually inspected for opaque positional literals.
This commit is contained in:
committed by
GitHub
Unverified
parent
b1cbf622ad
commit
8d398d3c52
@@ -233,6 +233,8 @@ pub struct TuiVimNormalKeymap {
|
||||
pub start_delete_operator: Option<KeybindingsSpec>,
|
||||
/// Begin yank operator; next key selects motion (`y`).
|
||||
pub start_yank_operator: Option<KeybindingsSpec>,
|
||||
/// Begin change operator; next keys select a text object.
|
||||
pub start_change_operator: Option<KeybindingsSpec>,
|
||||
/// Cancel a pending operator and return to normal mode.
|
||||
pub cancel_operator: Option<KeybindingsSpec>,
|
||||
}
|
||||
@@ -268,10 +270,39 @@ pub struct TuiVimOperatorKeymap {
|
||||
pub motion_line_start: Option<KeybindingsSpec>,
|
||||
/// Motion: to end of line (`$`).
|
||||
pub motion_line_end: Option<KeybindingsSpec>,
|
||||
/// Select an inner text object after an operator.
|
||||
pub select_inner_text_object: Option<KeybindingsSpec>,
|
||||
/// Select an around text object after an operator.
|
||||
pub select_around_text_object: Option<KeybindingsSpec>,
|
||||
/// Cancel the pending operator and return to normal mode.
|
||||
pub cancel: Option<KeybindingsSpec>,
|
||||
}
|
||||
|
||||
/// Vim text-object keybindings for modal editing inside text areas.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct TuiVimTextObjectKeymap {
|
||||
/// Text object: word.
|
||||
pub word: Option<KeybindingsSpec>,
|
||||
/// Text object: whitespace-delimited WORD.
|
||||
pub big_word: Option<KeybindingsSpec>,
|
||||
/// Text object: parentheses.
|
||||
pub parentheses: Option<KeybindingsSpec>,
|
||||
/// Text object: brackets.
|
||||
pub brackets: Option<KeybindingsSpec>,
|
||||
/// Text object: braces.
|
||||
pub braces: Option<KeybindingsSpec>,
|
||||
/// Text object: double quotes.
|
||||
pub double_quote: Option<KeybindingsSpec>,
|
||||
/// Text object: single quotes.
|
||||
pub single_quote: Option<KeybindingsSpec>,
|
||||
/// Text object: backticks.
|
||||
pub backtick: Option<KeybindingsSpec>,
|
||||
/// Cancel the pending text-object command.
|
||||
pub cancel: Option<KeybindingsSpec>,
|
||||
}
|
||||
|
||||
/// Pager context keybindings for transcript and static overlays.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -376,6 +407,8 @@ pub struct TuiKeymap {
|
||||
#[serde(default)]
|
||||
pub vim_operator: TuiVimOperatorKeymap,
|
||||
#[serde(default)]
|
||||
pub vim_text_object: TuiVimTextObjectKeymap,
|
||||
#[serde(default)]
|
||||
pub pager: TuiPagerKeymap,
|
||||
#[serde(default)]
|
||||
pub list: TuiListKeymap,
|
||||
@@ -562,6 +595,20 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn misspelled_vim_text_object_action_is_rejected() {
|
||||
let toml_input = r#"
|
||||
[vim_text_object]
|
||||
double_quotes = "shift-quote"
|
||||
"#;
|
||||
let err = toml::from_str::<TuiKeymap>(toml_input)
|
||||
.expect_err("expected unknown vim text object action");
|
||||
assert!(
|
||||
err.to_string().contains("double_quotes"),
|
||||
"expected error to mention misspelled field, got: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removed_backtrack_actions_are_rejected() {
|
||||
for (context, action) in [
|
||||
|
||||
@@ -2851,6 +2851,7 @@
|
||||
"open_line_above": null,
|
||||
"open_line_below": null,
|
||||
"paste_after": null,
|
||||
"start_change_operator": null,
|
||||
"start_delete_operator": null,
|
||||
"start_yank_operator": null,
|
||||
"yank_line": null
|
||||
@@ -2867,7 +2868,20 @@
|
||||
"motion_word_backward": null,
|
||||
"motion_word_end": null,
|
||||
"motion_word_forward": null,
|
||||
"select_around_text_object": null,
|
||||
"select_inner_text_object": null,
|
||||
"yank_line": null
|
||||
},
|
||||
"vim_text_object": {
|
||||
"backtick": null,
|
||||
"big_word": null,
|
||||
"braces": null,
|
||||
"brackets": null,
|
||||
"cancel": null,
|
||||
"double_quote": null,
|
||||
"parentheses": null,
|
||||
"single_quote": null,
|
||||
"word": null
|
||||
}
|
||||
},
|
||||
"description": "Keybinding overrides for the TUI.\n\nThis supports rebinding selected actions globally and by context. Context bindings take precedence over `global` bindings."
|
||||
@@ -3518,6 +3532,7 @@
|
||||
"open_line_above": null,
|
||||
"open_line_below": null,
|
||||
"paste_after": null,
|
||||
"start_change_operator": null,
|
||||
"start_delete_operator": null,
|
||||
"start_yank_operator": null,
|
||||
"yank_line": null
|
||||
@@ -3541,8 +3556,28 @@
|
||||
"motion_word_backward": null,
|
||||
"motion_word_end": null,
|
||||
"motion_word_forward": null,
|
||||
"select_around_text_object": null,
|
||||
"select_inner_text_object": null,
|
||||
"yank_line": null
|
||||
}
|
||||
},
|
||||
"vim_text_object": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TuiVimTextObjectKeymap"
|
||||
}
|
||||
],
|
||||
"default": {
|
||||
"backtick": null,
|
||||
"big_word": null,
|
||||
"braces": null,
|
||||
"brackets": null,
|
||||
"cancel": null,
|
||||
"double_quote": null,
|
||||
"parentheses": null,
|
||||
"single_quote": null,
|
||||
"word": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -3903,6 +3938,14 @@
|
||||
],
|
||||
"description": "Paste after cursor (`p`)."
|
||||
},
|
||||
"start_change_operator": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Begin change operator; next keys select a text object."
|
||||
},
|
||||
"start_delete_operator": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -4022,6 +4065,22 @@
|
||||
],
|
||||
"description": "Motion: to start of next word (`w`)."
|
||||
},
|
||||
"select_around_text_object": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Select an around text object after an operator."
|
||||
},
|
||||
"select_inner_text_object": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Select an inner text object after an operator."
|
||||
},
|
||||
"yank_line": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -4033,6 +4092,85 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"TuiVimTextObjectKeymap": {
|
||||
"additionalProperties": false,
|
||||
"description": "Vim text-object keybindings for modal editing inside text areas.",
|
||||
"properties": {
|
||||
"backtick": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Text object: backticks."
|
||||
},
|
||||
"big_word": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Text object: whitespace-delimited WORD."
|
||||
},
|
||||
"braces": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Text object: braces."
|
||||
},
|
||||
"brackets": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Text object: brackets."
|
||||
},
|
||||
"cancel": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Cancel the pending text-object command."
|
||||
},
|
||||
"double_quote": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Text object: double quotes."
|
||||
},
|
||||
"parentheses": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Text object: parentheses."
|
||||
},
|
||||
"single_quote": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Text object: single quotes."
|
||||
},
|
||||
"word": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Text object: word."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"UriBasedFileOpener": {
|
||||
"oneOf": [
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@ use crate::keymap::EditorKeymap;
|
||||
use crate::keymap::RuntimeKeymap;
|
||||
use crate::keymap::VimNormalKeymap;
|
||||
use crate::keymap::VimOperatorKeymap;
|
||||
use crate::keymap::VimTextObjectKeymap;
|
||||
use codex_protocol::user_input::ByteRange;
|
||||
use codex_protocol::user_input::TextElement as UserTextElement;
|
||||
use crossterm::event::KeyCode;
|
||||
@@ -35,6 +36,13 @@ use textwrap::Options;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
mod vim;
|
||||
use self::vim::VimMode;
|
||||
use self::vim::VimMotion;
|
||||
use self::vim::VimOperator;
|
||||
use self::vim::VimPending;
|
||||
use self::vim::VimTextObjectScope;
|
||||
|
||||
const WORD_SEPARATORS: &str = "`~!@#$%^&*()-=+[{]}\\|;:'\",.<>/?";
|
||||
|
||||
fn is_word_separator(ch: char) -> bool {
|
||||
@@ -101,10 +109,11 @@ pub(crate) struct TextArea {
|
||||
kill_buffer_kind: KillBufferKind,
|
||||
vim_enabled: bool,
|
||||
vim_mode: VimMode,
|
||||
vim_operator: Option<VimOperator>,
|
||||
vim_pending: VimPending,
|
||||
editor_keymap: EditorKeymap,
|
||||
vim_normal_keymap: VimNormalKeymap,
|
||||
vim_operator_keymap: VimOperatorKeymap,
|
||||
vim_text_object_keymap: VimTextObjectKeymap,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -119,14 +128,6 @@ pub(crate) struct TextAreaState {
|
||||
scroll: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum VimMode {
|
||||
/// Normal mode routes printable keys to movement, operators, and mode transitions.
|
||||
Normal,
|
||||
/// Insert mode routes input through the regular editor keymap until Escape is pressed.
|
||||
Insert,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum KillBufferKind {
|
||||
/// Characterwise kills and yanks paste at the cursor.
|
||||
@@ -135,36 +136,6 @@ enum KillBufferKind {
|
||||
Linewise,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum VimOperator {
|
||||
/// Delete the range selected by the next motion or repeated operator key.
|
||||
Delete,
|
||||
/// Copy the range selected by the next motion or repeated operator key.
|
||||
Yank,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum VimMotion {
|
||||
/// Move one atomic boundary to the left.
|
||||
Left,
|
||||
/// Move one atomic boundary to the right.
|
||||
Right,
|
||||
/// Move one visual row up, preserving preferred display column.
|
||||
Up,
|
||||
/// Move one visual row down, preserving preferred display column.
|
||||
Down,
|
||||
/// Move to the start of the next word-like run.
|
||||
WordForward,
|
||||
/// Move to the start of the previous word-like run.
|
||||
WordBackward,
|
||||
/// Move to the end of the current or next word-like run.
|
||||
WordEnd,
|
||||
/// Move to the start of the current line.
|
||||
LineStart,
|
||||
/// Move to the end of the current line.
|
||||
LineEnd,
|
||||
}
|
||||
|
||||
impl TextArea {
|
||||
pub fn new() -> Self {
|
||||
let defaults = RuntimeKeymap::defaults();
|
||||
@@ -179,10 +150,11 @@ impl TextArea {
|
||||
kill_buffer_kind: KillBufferKind::Characterwise,
|
||||
vim_enabled: false,
|
||||
vim_mode: VimMode::Insert,
|
||||
vim_operator: None,
|
||||
vim_pending: VimPending::None,
|
||||
editor_keymap: defaults.editor,
|
||||
vim_normal_keymap: defaults.vim_normal,
|
||||
vim_operator_keymap: defaults.vim_operator,
|
||||
vim_text_object_keymap: defaults.vim_text_object,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,6 +168,7 @@ impl TextArea {
|
||||
self.editor_keymap = keymap.editor.clone();
|
||||
self.vim_normal_keymap = keymap.vim_normal.clone();
|
||||
self.vim_operator_keymap = keymap.vim_operator.clone();
|
||||
self.vim_text_object_keymap = keymap.vim_text_object.clone();
|
||||
}
|
||||
|
||||
/// Replace the visible textarea text and clear any existing text elements.
|
||||
@@ -257,7 +230,7 @@ impl TextArea {
|
||||
/// an old `d` or `y` command.
|
||||
pub(crate) fn set_vim_enabled(&mut self, enabled: bool) {
|
||||
self.vim_enabled = enabled;
|
||||
self.vim_operator = None;
|
||||
self.vim_pending = VimPending::None;
|
||||
self.vim_mode = if enabled {
|
||||
VimMode::Normal
|
||||
} else {
|
||||
@@ -293,7 +266,7 @@ impl TextArea {
|
||||
/// This is observable so the composer can avoid stealing the second key of
|
||||
/// `d{motion}` or `y{motion}` for higher-level shortcuts.
|
||||
pub(crate) fn is_vim_operator_pending(&self) -> bool {
|
||||
self.vim_operator.is_some()
|
||||
!matches!(self.vim_pending, VimPending::None)
|
||||
}
|
||||
|
||||
/// Enter Vim insert mode if modal editing is enabled.
|
||||
@@ -304,7 +277,7 @@ impl TextArea {
|
||||
pub(crate) fn enter_vim_insert_mode(&mut self) {
|
||||
if self.vim_enabled {
|
||||
self.vim_mode = VimMode::Insert;
|
||||
self.vim_operator = None;
|
||||
self.vim_pending = VimPending::None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,7 +290,7 @@ impl TextArea {
|
||||
pub(crate) fn enter_vim_normal_mode(&mut self) {
|
||||
if self.vim_enabled {
|
||||
self.vim_mode = VimMode::Normal;
|
||||
self.vim_operator = None;
|
||||
self.vim_pending = VimPending::None;
|
||||
self.preferred_col = None;
|
||||
}
|
||||
}
|
||||
@@ -669,9 +642,17 @@ impl TextArea {
|
||||
}
|
||||
|
||||
fn handle_vim_normal(&mut self, event: KeyEvent) {
|
||||
if let Some(op) = self.vim_operator.take() {
|
||||
self.handle_vim_operator(op, event);
|
||||
return;
|
||||
let pending = std::mem::replace(&mut self.vim_pending, VimPending::None);
|
||||
match pending {
|
||||
VimPending::None => {}
|
||||
VimPending::Operator(op) => {
|
||||
self.handle_vim_operator(op, event);
|
||||
return;
|
||||
}
|
||||
VimPending::TextObject { operator, scope } => {
|
||||
self.handle_vim_text_object(operator, scope, event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if self.vim_normal_keymap.enter_insert.is_pressed(event) {
|
||||
@@ -776,15 +757,23 @@ impl TextArea {
|
||||
.start_delete_operator
|
||||
.is_pressed(event)
|
||||
{
|
||||
self.vim_operator = Some(VimOperator::Delete);
|
||||
self.vim_pending = VimPending::Operator(VimOperator::Delete);
|
||||
return;
|
||||
}
|
||||
if self.vim_normal_keymap.start_yank_operator.is_pressed(event) {
|
||||
self.vim_operator = Some(VimOperator::Yank);
|
||||
self.vim_pending = VimPending::Operator(VimOperator::Yank);
|
||||
return;
|
||||
}
|
||||
if self
|
||||
.vim_normal_keymap
|
||||
.start_change_operator
|
||||
.is_pressed(event)
|
||||
{
|
||||
self.vim_pending = VimPending::Operator(VimOperator::Change);
|
||||
return;
|
||||
}
|
||||
if self.vim_normal_keymap.cancel_operator.is_pressed(event) {
|
||||
self.vim_operator = None;
|
||||
self.vim_pending = VimPending::None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -800,14 +789,41 @@ impl TextArea {
|
||||
if self.vim_operator_keymap.cancel.is_pressed(event) {
|
||||
return true;
|
||||
}
|
||||
if let Some(scope) = self.vim_text_object_scope_for_event(event) {
|
||||
self.vim_pending = VimPending::TextObject {
|
||||
operator: op,
|
||||
scope,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(motion) = self.vim_motion_for_event(event) {
|
||||
if op != VimOperator::Change
|
||||
&& let Some(motion) = self.vim_motion_for_event(event)
|
||||
{
|
||||
self.apply_vim_operator(op, motion);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn handle_vim_text_object(
|
||||
&mut self,
|
||||
op: VimOperator,
|
||||
scope: VimTextObjectScope,
|
||||
event: KeyEvent,
|
||||
) -> bool {
|
||||
if self.vim_text_object_keymap.cancel.is_pressed(event) {
|
||||
return true;
|
||||
}
|
||||
let Some(object) = self.vim_text_object_for_event(event) else {
|
||||
return false;
|
||||
};
|
||||
if let Some(range) = self.text_object_range(object, scope) {
|
||||
self.apply_vim_operator_to_range(op, range);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn vim_motion_for_event(&self, event: KeyEvent) -> Option<VimMotion> {
|
||||
if self.vim_operator_keymap.motion_left.is_pressed(event) {
|
||||
return Some(VimMotion::Left);
|
||||
@@ -854,6 +870,18 @@ impl TextArea {
|
||||
match op {
|
||||
VimOperator::Delete => self.kill_range(range),
|
||||
VimOperator::Yank => self.yank_range(range),
|
||||
VimOperator::Change => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_vim_operator_to_range(&mut self, op: VimOperator, range: Range<usize>) {
|
||||
match op {
|
||||
VimOperator::Delete => self.kill_range(range),
|
||||
VimOperator::Yank => self.yank_range(range),
|
||||
VimOperator::Change => {
|
||||
self.kill_range(range);
|
||||
self.vim_mode = VimMode::Insert;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2232,7 +2260,7 @@ mod tests {
|
||||
let mut t = TextArea::new();
|
||||
t.set_vim_enabled(/*enabled*/ true);
|
||||
|
||||
t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('z'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(t.vim_mode_label(), Some("Normal"));
|
||||
@@ -2399,6 +2427,180 @@ mod tests {
|
||||
assert_eq!(t.kill_buffer, "hello ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_change_inner_word_deletes_word_and_enters_insert() {
|
||||
let mut t = ta_with("hello world");
|
||||
t.set_cursor(/*pos*/ "hello ".len());
|
||||
t.set_vim_enabled(/*enabled*/ true);
|
||||
|
||||
t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(t.text(), "hello ");
|
||||
assert_eq!(t.kill_buffer, "world");
|
||||
assert_eq!(t.cursor(), "hello ".len());
|
||||
assert_eq!(t.vim_mode_label(), Some("Insert"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_word_text_objects_cover_delete_yank_and_big_word() {
|
||||
let mut t = ta_with("hello world");
|
||||
t.set_cursor(/*pos*/ 1);
|
||||
t.set_vim_enabled(/*enabled*/ true);
|
||||
|
||||
t.input(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(t.text(), "hello world");
|
||||
assert_eq!(t.kill_buffer, "hello ");
|
||||
assert_eq!(t.vim_mode_label(), Some("Normal"));
|
||||
|
||||
let mut t = ta_with("foo.bar/baz qux");
|
||||
t.set_cursor(/*pos*/ "foo.".len());
|
||||
t.set_vim_enabled(/*enabled*/ true);
|
||||
|
||||
t.input(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('W'), KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(t.text(), " qux");
|
||||
assert_eq!(t.kill_buffer, "foo.bar/baz");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_word_text_objects_accept_cursor_at_word_end() {
|
||||
let mut t = ta_with("hello world");
|
||||
t.set_cursor(/*pos*/ "hello".len());
|
||||
t.set_vim_enabled(/*enabled*/ true);
|
||||
|
||||
t.input(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(t.text(), "world");
|
||||
assert_eq!(t.kill_buffer, "hello ");
|
||||
|
||||
let mut t = ta_with("foo bar");
|
||||
t.set_cursor(t.text().len());
|
||||
t.set_vim_enabled(/*enabled*/ true);
|
||||
|
||||
t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('W'), KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(t.text(), "foo ");
|
||||
assert_eq!(t.kill_buffer, "bar");
|
||||
assert_eq!(t.cursor(), "foo ".len());
|
||||
assert_eq!(t.vim_mode_label(), Some("Insert"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_delimiter_text_objects_select_innermost_pair_and_aliases() {
|
||||
let mut t = ta_with("a(b(c)d)e");
|
||||
t.set_cursor(/*pos*/ "a(b(".len());
|
||||
t.set_vim_enabled(/*enabled*/ true);
|
||||
|
||||
t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(t.text(), "a(b()d)e");
|
||||
assert_eq!(t.kill_buffer, "c");
|
||||
assert_eq!(t.vim_mode_label(), Some("Insert"));
|
||||
|
||||
let mut t = ta_with("a [b] c");
|
||||
t.set_cursor(/*pos*/ "a [".len());
|
||||
t.set_vim_enabled(/*enabled*/ true);
|
||||
|
||||
t.input(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char(']'), KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(t.text(), "a c");
|
||||
assert_eq!(t.kill_buffer, "[b]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_empty_inner_text_objects_are_valid_targets() {
|
||||
let mut t = ta_with("call()");
|
||||
t.set_cursor(/*pos*/ "call(".len());
|
||||
t.set_vim_enabled(/*enabled*/ true);
|
||||
|
||||
t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('('), KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(t.text(), "call()");
|
||||
assert_eq!(t.kill_buffer, "");
|
||||
assert_eq!(t.cursor(), "call(".len());
|
||||
assert_eq!(t.vim_mode_label(), Some("Insert"));
|
||||
|
||||
let mut t = ta_with(r#"say "" now"#);
|
||||
t.set_cursor(/*pos*/ r#"say ""#.len());
|
||||
t.set_vim_enabled(/*enabled*/ true);
|
||||
|
||||
t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(t.text(), r#"say "" now"#);
|
||||
assert_eq!(t.kill_buffer, "");
|
||||
assert_eq!(t.cursor(), r#"say ""#.len());
|
||||
assert_eq!(t.vim_mode_label(), Some("Insert"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_quote_text_objects_are_line_local_and_handle_escapes() {
|
||||
let mut t = ta_with(r#"say "a \"b\" c" now"#);
|
||||
t.set_cursor(/*pos*/ r#"say "a \"#.len());
|
||||
t.set_vim_enabled(/*enabled*/ true);
|
||||
|
||||
t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::SHIFT));
|
||||
|
||||
assert_eq!(t.text(), r#"say "" now"#);
|
||||
assert_eq!(t.kill_buffer, r#"a \"b\" c"#);
|
||||
assert_eq!(t.vim_mode_label(), Some("Insert"));
|
||||
|
||||
let mut t = ta_with("one \"two\nthree\" four");
|
||||
t.set_cursor(/*pos*/ "one \"two\n".len());
|
||||
t.set_vim_enabled(/*enabled*/ true);
|
||||
|
||||
t.input(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(t.text(), "one \"two\nthree\" four");
|
||||
assert_eq!(t.kill_buffer, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_text_object_cancellation_and_unsupported_change_motions_do_not_edit() {
|
||||
let mut t = ta_with("hello world");
|
||||
t.set_cursor(/*pos*/ 1);
|
||||
t.set_vim_enabled(/*enabled*/ true);
|
||||
|
||||
t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('$'), KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(t.text(), "hello world");
|
||||
assert_eq!(t.kill_buffer, "");
|
||||
assert_eq!(t.vim_mode_label(), Some("Normal"));
|
||||
assert!(!t.is_vim_operator_pending());
|
||||
|
||||
t.input(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));
|
||||
assert!(t.is_vim_operator_pending());
|
||||
t.input(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(t.text(), "hello world");
|
||||
assert_eq!(t.kill_buffer, "");
|
||||
assert!(!t.is_vim_operator_pending());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_operator_invalid_motion_is_consumed() {
|
||||
let mut t = ta_with("hello");
|
||||
@@ -2408,7 +2610,7 @@ mod tests {
|
||||
t.input(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));
|
||||
assert!(t.is_vim_operator_pending());
|
||||
|
||||
t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('z'), KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(t.text(), "hello");
|
||||
assert_eq!(t.vim_mode_label(), Some("Normal"));
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
use super::TextArea;
|
||||
use super::split_word_pieces;
|
||||
use crate::key_hint::KeyBindingListExt;
|
||||
use crossterm::event::KeyEvent;
|
||||
use std::ops::Range;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum VimMode {
|
||||
/// Normal mode routes printable keys to movement, operators, and mode transitions.
|
||||
Normal,
|
||||
/// Insert mode routes input through the regular editor keymap until Escape is pressed.
|
||||
Insert,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum VimOperator {
|
||||
Delete,
|
||||
Yank,
|
||||
Change,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum VimPending {
|
||||
None,
|
||||
Operator(VimOperator),
|
||||
TextObject {
|
||||
operator: VimOperator,
|
||||
scope: VimTextObjectScope,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum VimMotion {
|
||||
Left,
|
||||
Right,
|
||||
Up,
|
||||
Down,
|
||||
WordForward,
|
||||
WordBackward,
|
||||
WordEnd,
|
||||
LineStart,
|
||||
LineEnd,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum VimTextObjectScope {
|
||||
Inner,
|
||||
Around,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum VimTextObject {
|
||||
Word,
|
||||
BigWord,
|
||||
Parentheses,
|
||||
Brackets,
|
||||
Braces,
|
||||
DoubleQuote,
|
||||
SingleQuote,
|
||||
Backtick,
|
||||
}
|
||||
|
||||
impl TextArea {
|
||||
pub(super) fn vim_text_object_scope_for_event(
|
||||
&self,
|
||||
event: KeyEvent,
|
||||
) -> Option<VimTextObjectScope> {
|
||||
if self
|
||||
.vim_operator_keymap
|
||||
.select_inner_text_object
|
||||
.is_pressed(event)
|
||||
{
|
||||
return Some(VimTextObjectScope::Inner);
|
||||
}
|
||||
if self
|
||||
.vim_operator_keymap
|
||||
.select_around_text_object
|
||||
.is_pressed(event)
|
||||
{
|
||||
return Some(VimTextObjectScope::Around);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn vim_text_object_for_event(&self, event: KeyEvent) -> Option<VimTextObject> {
|
||||
if self.vim_text_object_keymap.word.is_pressed(event) {
|
||||
return Some(VimTextObject::Word);
|
||||
}
|
||||
if self.vim_text_object_keymap.big_word.is_pressed(event) {
|
||||
return Some(VimTextObject::BigWord);
|
||||
}
|
||||
if self.vim_text_object_keymap.parentheses.is_pressed(event) {
|
||||
return Some(VimTextObject::Parentheses);
|
||||
}
|
||||
if self.vim_text_object_keymap.brackets.is_pressed(event) {
|
||||
return Some(VimTextObject::Brackets);
|
||||
}
|
||||
if self.vim_text_object_keymap.braces.is_pressed(event) {
|
||||
return Some(VimTextObject::Braces);
|
||||
}
|
||||
if self.vim_text_object_keymap.double_quote.is_pressed(event) {
|
||||
return Some(VimTextObject::DoubleQuote);
|
||||
}
|
||||
if self.vim_text_object_keymap.single_quote.is_pressed(event) {
|
||||
return Some(VimTextObject::SingleQuote);
|
||||
}
|
||||
if self.vim_text_object_keymap.backtick.is_pressed(event) {
|
||||
return Some(VimTextObject::Backtick);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn text_object_range(
|
||||
&self,
|
||||
object: VimTextObject,
|
||||
scope: VimTextObjectScope,
|
||||
) -> Option<Range<usize>> {
|
||||
match object {
|
||||
VimTextObject::Word => self.word_text_object_range(scope, /*big_word*/ false),
|
||||
VimTextObject::BigWord => self.word_text_object_range(scope, /*big_word*/ true),
|
||||
VimTextObject::Parentheses => self.paired_text_object_range(scope, '(', ')'),
|
||||
VimTextObject::Brackets => self.paired_text_object_range(scope, '[', ']'),
|
||||
VimTextObject::Braces => self.paired_text_object_range(scope, '{', '}'),
|
||||
VimTextObject::DoubleQuote => self.quoted_text_object_range(scope, '"'),
|
||||
VimTextObject::SingleQuote => self.quoted_text_object_range(scope, '\''),
|
||||
VimTextObject::Backtick => self.quoted_text_object_range(scope, '`'),
|
||||
}
|
||||
}
|
||||
|
||||
fn word_text_object_range(
|
||||
&self,
|
||||
scope: VimTextObjectScope,
|
||||
big_word: bool,
|
||||
) -> Option<Range<usize>> {
|
||||
let inner = if big_word {
|
||||
self.big_word_range_at_cursor()?
|
||||
} else {
|
||||
self.small_word_range_at_cursor()?
|
||||
};
|
||||
Some(match scope {
|
||||
VimTextObjectScope::Inner => inner,
|
||||
VimTextObjectScope::Around => self.expand_word_around(inner),
|
||||
})
|
||||
}
|
||||
|
||||
fn big_word_range_at_cursor(&self) -> Option<Range<usize>> {
|
||||
self.non_ws_runs()
|
||||
.into_iter()
|
||||
.find(|range| self.cursor_overlaps_range(range) || self.cursor_is_at_range_end(range))
|
||||
}
|
||||
|
||||
fn small_word_range_at_cursor(&self) -> Option<Range<usize>> {
|
||||
for run in self.non_ws_runs() {
|
||||
if !self.cursor_overlaps_range(&run) && !self.cursor_is_at_range_end(&run) {
|
||||
continue;
|
||||
}
|
||||
let mut last_piece = None;
|
||||
for (piece_start, piece) in split_word_pieces(&self.text[run.clone()]) {
|
||||
let piece = run.start + piece_start..run.start + piece_start + piece.len();
|
||||
if self.cursor_overlaps_range(&piece) {
|
||||
return Some(piece);
|
||||
}
|
||||
last_piece = Some(piece);
|
||||
}
|
||||
if self.cursor_is_at_range_end(&run) {
|
||||
return last_piece.or(Some(run));
|
||||
}
|
||||
return Some(run);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn non_ws_runs(&self) -> Vec<Range<usize>> {
|
||||
let mut runs = Vec::new();
|
||||
let mut start = None;
|
||||
for (idx, ch) in self.text.char_indices() {
|
||||
if ch.is_whitespace() {
|
||||
if let Some(run_start) = start.take() {
|
||||
runs.push(run_start..idx);
|
||||
}
|
||||
} else if start.is_none() {
|
||||
start = Some(idx);
|
||||
}
|
||||
}
|
||||
if let Some(run_start) = start {
|
||||
runs.push(run_start..self.text.len());
|
||||
}
|
||||
runs
|
||||
}
|
||||
|
||||
fn cursor_overlaps_range(&self, range: &Range<usize>) -> bool {
|
||||
range.start <= self.cursor_pos && self.cursor_pos < range.end
|
||||
}
|
||||
|
||||
fn cursor_is_at_range_end(&self, range: &Range<usize>) -> bool {
|
||||
range.start < range.end && self.cursor_pos == range.end
|
||||
}
|
||||
|
||||
fn expand_word_around(&self, inner: Range<usize>) -> Range<usize> {
|
||||
let following = self.following_whitespace_end(inner.end);
|
||||
if following > inner.end {
|
||||
return inner.start..following;
|
||||
}
|
||||
self.preceding_whitespace_start(inner.start)..inner.end
|
||||
}
|
||||
|
||||
fn following_whitespace_end(&self, start: usize) -> usize {
|
||||
let mut end = start;
|
||||
for (offset, ch) in self.text[start..].char_indices() {
|
||||
if !ch.is_whitespace() {
|
||||
break;
|
||||
}
|
||||
end = start + offset + ch.len_utf8();
|
||||
}
|
||||
end
|
||||
}
|
||||
|
||||
fn preceding_whitespace_start(&self, end: usize) -> usize {
|
||||
let mut start = end;
|
||||
for (idx, ch) in self.text[..end].char_indices().rev() {
|
||||
if !ch.is_whitespace() {
|
||||
break;
|
||||
}
|
||||
start = idx;
|
||||
}
|
||||
start
|
||||
}
|
||||
|
||||
fn paired_text_object_range(
|
||||
&self,
|
||||
scope: VimTextObjectScope,
|
||||
open: char,
|
||||
close: char,
|
||||
) -> Option<Range<usize>> {
|
||||
let mut stack: Vec<usize> = Vec::new();
|
||||
let mut best: Option<Range<usize>> = None;
|
||||
for (idx, ch) in self.text.char_indices() {
|
||||
if self.is_inside_element(idx) {
|
||||
continue;
|
||||
}
|
||||
if ch == open {
|
||||
stack.push(idx);
|
||||
} else if ch == close {
|
||||
let Some(open_idx) = stack.pop() else {
|
||||
continue;
|
||||
};
|
||||
let close_end = idx + ch.len_utf8();
|
||||
if open_idx <= self.cursor_pos && self.cursor_pos <= idx {
|
||||
let candidate = match scope {
|
||||
VimTextObjectScope::Inner => open_idx + open.len_utf8()..idx,
|
||||
VimTextObjectScope::Around => open_idx..close_end,
|
||||
};
|
||||
if candidate.start <= candidate.end
|
||||
&& best
|
||||
.as_ref()
|
||||
.is_none_or(|current| candidate.len() < current.len())
|
||||
{
|
||||
best = Some(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
fn quoted_text_object_range(
|
||||
&self,
|
||||
scope: VimTextObjectScope,
|
||||
quote: char,
|
||||
) -> Option<Range<usize>> {
|
||||
let line = self.beginning_of_current_line()..self.end_of_current_line();
|
||||
let mut open = None;
|
||||
let mut best: Option<Range<usize>> = None;
|
||||
for (offset, ch) in self.text[line.clone()].char_indices() {
|
||||
let idx = line.start + offset;
|
||||
if self.is_inside_element(idx) || ch != quote || self.is_escaped(idx) {
|
||||
continue;
|
||||
}
|
||||
if let Some(open_idx) = open.take() {
|
||||
if open_idx <= self.cursor_pos && self.cursor_pos <= idx {
|
||||
let candidate = match scope {
|
||||
VimTextObjectScope::Inner => open_idx + quote.len_utf8()..idx,
|
||||
VimTextObjectScope::Around => idx_range(open_idx, idx, quote),
|
||||
};
|
||||
if candidate.start <= candidate.end
|
||||
&& best
|
||||
.as_ref()
|
||||
.is_none_or(|current| candidate.len() < current.len())
|
||||
{
|
||||
best = Some(candidate);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
open = Some(idx);
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
fn is_inside_element(&self, pos: usize) -> bool {
|
||||
self.elements
|
||||
.iter()
|
||||
.any(|element| pos >= element.range.start && pos < element.range.end)
|
||||
}
|
||||
|
||||
fn is_escaped(&self, pos: usize) -> bool {
|
||||
let mut backslashes = 0;
|
||||
for ch in self.text[..pos].chars().rev() {
|
||||
if ch != '\\' {
|
||||
break;
|
||||
}
|
||||
backslashes += 1;
|
||||
}
|
||||
backslashes % 2 == 1
|
||||
}
|
||||
}
|
||||
|
||||
fn idx_range(open_idx: usize, close_idx: usize, quote: char) -> Range<usize> {
|
||||
open_idx..close_idx + quote.len_utf8()
|
||||
}
|
||||
+345
-21
@@ -46,6 +46,7 @@ pub(crate) struct RuntimeKeymap {
|
||||
pub(crate) editor: EditorKeymap,
|
||||
pub(crate) vim_normal: VimNormalKeymap,
|
||||
pub(crate) vim_operator: VimOperatorKeymap,
|
||||
pub(crate) vim_text_object: VimTextObjectKeymap,
|
||||
pub(crate) pager: PagerKeymap,
|
||||
pub(crate) list: ListKeymap,
|
||||
pub(crate) approval: ApprovalKeymap,
|
||||
@@ -161,6 +162,7 @@ pub(crate) struct VimNormalKeymap {
|
||||
pub(crate) paste_after: Vec<KeyBinding>,
|
||||
pub(crate) start_delete_operator: Vec<KeyBinding>,
|
||||
pub(crate) start_yank_operator: Vec<KeyBinding>,
|
||||
pub(crate) start_change_operator: Vec<KeyBinding>,
|
||||
pub(crate) cancel_operator: Vec<KeyBinding>,
|
||||
}
|
||||
|
||||
@@ -183,6 +185,22 @@ pub(crate) struct VimOperatorKeymap {
|
||||
pub(crate) motion_word_end: Vec<KeyBinding>,
|
||||
pub(crate) motion_line_start: Vec<KeyBinding>,
|
||||
pub(crate) motion_line_end: Vec<KeyBinding>,
|
||||
pub(crate) select_inner_text_object: Vec<KeyBinding>,
|
||||
pub(crate) select_around_text_object: Vec<KeyBinding>,
|
||||
pub(crate) cancel: Vec<KeyBinding>,
|
||||
}
|
||||
|
||||
/// Vim text-object keybindings active after an operator plus inner/around prefix.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct VimTextObjectKeymap {
|
||||
pub(crate) word: Vec<KeyBinding>,
|
||||
pub(crate) big_word: Vec<KeyBinding>,
|
||||
pub(crate) parentheses: Vec<KeyBinding>,
|
||||
pub(crate) brackets: Vec<KeyBinding>,
|
||||
pub(crate) braces: Vec<KeyBinding>,
|
||||
pub(crate) double_quote: Vec<KeyBinding>,
|
||||
pub(crate) single_quote: Vec<KeyBinding>,
|
||||
pub(crate) backtick: Vec<KeyBinding>,
|
||||
pub(crate) cancel: Vec<KeyBinding>,
|
||||
}
|
||||
|
||||
@@ -452,7 +470,7 @@ impl RuntimeKeymap {
|
||||
yank: resolve_local!(keymap, defaults, editor, yank),
|
||||
};
|
||||
|
||||
let vim_normal = VimNormalKeymap {
|
||||
let mut vim_normal = VimNormalKeymap {
|
||||
enter_insert: resolve_local!(keymap, defaults, vim_normal, enter_insert),
|
||||
append_after_cursor: resolve_local!(keymap, defaults, vim_normal, append_after_cursor),
|
||||
append_line_end: resolve_local!(keymap, defaults, vim_normal, append_line_end),
|
||||
@@ -480,10 +498,113 @@ impl RuntimeKeymap {
|
||||
start_delete_operator
|
||||
),
|
||||
start_yank_operator: resolve_local!(keymap, defaults, vim_normal, start_yank_operator),
|
||||
start_change_operator: resolve_local!(
|
||||
keymap,
|
||||
defaults,
|
||||
vim_normal,
|
||||
start_change_operator
|
||||
),
|
||||
cancel_operator: resolve_local!(keymap, defaults, vim_normal, cancel_operator),
|
||||
};
|
||||
|
||||
let vim_operator = VimOperatorKeymap {
|
||||
let configured_vim_normal_bindings_to_preserve = configured_bindings_to_preserve([
|
||||
(
|
||||
keymap.vim_normal.enter_insert.as_ref(),
|
||||
vim_normal.enter_insert.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.append_after_cursor.as_ref(),
|
||||
vim_normal.append_after_cursor.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.append_line_end.as_ref(),
|
||||
vim_normal.append_line_end.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.insert_line_start.as_ref(),
|
||||
vim_normal.insert_line_start.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.open_line_below.as_ref(),
|
||||
vim_normal.open_line_below.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.open_line_above.as_ref(),
|
||||
vim_normal.open_line_above.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.move_left.as_ref(),
|
||||
vim_normal.move_left.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.move_right.as_ref(),
|
||||
vim_normal.move_right.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.move_up.as_ref(),
|
||||
vim_normal.move_up.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.move_down.as_ref(),
|
||||
vim_normal.move_down.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.move_word_forward.as_ref(),
|
||||
vim_normal.move_word_forward.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.move_word_backward.as_ref(),
|
||||
vim_normal.move_word_backward.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.move_word_end.as_ref(),
|
||||
vim_normal.move_word_end.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.move_line_start.as_ref(),
|
||||
vim_normal.move_line_start.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.move_line_end.as_ref(),
|
||||
vim_normal.move_line_end.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.delete_char.as_ref(),
|
||||
vim_normal.delete_char.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.delete_to_line_end.as_ref(),
|
||||
vim_normal.delete_to_line_end.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.yank_line.as_ref(),
|
||||
vim_normal.yank_line.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.paste_after.as_ref(),
|
||||
vim_normal.paste_after.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.start_delete_operator.as_ref(),
|
||||
vim_normal.start_delete_operator.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.start_yank_operator.as_ref(),
|
||||
vim_normal.start_yank_operator.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.cancel_operator.as_ref(),
|
||||
vim_normal.cancel_operator.as_slice(),
|
||||
),
|
||||
]);
|
||||
|
||||
if keymap.vim_normal.start_change_operator.is_none() {
|
||||
vim_normal
|
||||
.start_change_operator
|
||||
.retain(|binding| !configured_vim_normal_bindings_to_preserve.contains(binding));
|
||||
}
|
||||
|
||||
let mut vim_operator = VimOperatorKeymap {
|
||||
delete_line: resolve_local!(keymap, defaults, vim_operator, delete_line),
|
||||
yank_line: resolve_local!(keymap, defaults, vim_operator, yank_line),
|
||||
motion_left: resolve_local!(keymap, defaults, vim_operator, motion_left),
|
||||
@@ -505,9 +626,95 @@ impl RuntimeKeymap {
|
||||
motion_word_end: resolve_local!(keymap, defaults, vim_operator, motion_word_end),
|
||||
motion_line_start: resolve_local!(keymap, defaults, vim_operator, motion_line_start),
|
||||
motion_line_end: resolve_local!(keymap, defaults, vim_operator, motion_line_end),
|
||||
select_inner_text_object: resolve_local!(
|
||||
keymap,
|
||||
defaults,
|
||||
vim_operator,
|
||||
select_inner_text_object
|
||||
),
|
||||
select_around_text_object: resolve_local!(
|
||||
keymap,
|
||||
defaults,
|
||||
vim_operator,
|
||||
select_around_text_object
|
||||
),
|
||||
cancel: resolve_local!(keymap, defaults, vim_operator, cancel),
|
||||
};
|
||||
|
||||
let configured_vim_operator_bindings_to_preserve = configured_bindings_to_preserve([
|
||||
(
|
||||
keymap.vim_operator.delete_line.as_ref(),
|
||||
vim_operator.delete_line.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_operator.yank_line.as_ref(),
|
||||
vim_operator.yank_line.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_operator.motion_left.as_ref(),
|
||||
vim_operator.motion_left.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_operator.motion_right.as_ref(),
|
||||
vim_operator.motion_right.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_operator.motion_up.as_ref(),
|
||||
vim_operator.motion_up.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_operator.motion_down.as_ref(),
|
||||
vim_operator.motion_down.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_operator.motion_word_forward.as_ref(),
|
||||
vim_operator.motion_word_forward.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_operator.motion_word_backward.as_ref(),
|
||||
vim_operator.motion_word_backward.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_operator.motion_word_end.as_ref(),
|
||||
vim_operator.motion_word_end.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_operator.motion_line_start.as_ref(),
|
||||
vim_operator.motion_line_start.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_operator.motion_line_end.as_ref(),
|
||||
vim_operator.motion_line_end.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_operator.cancel.as_ref(),
|
||||
vim_operator.cancel.as_slice(),
|
||||
),
|
||||
]);
|
||||
|
||||
if keymap.vim_operator.select_inner_text_object.is_none() {
|
||||
vim_operator
|
||||
.select_inner_text_object
|
||||
.retain(|binding| !configured_vim_operator_bindings_to_preserve.contains(binding));
|
||||
}
|
||||
if keymap.vim_operator.select_around_text_object.is_none() {
|
||||
vim_operator
|
||||
.select_around_text_object
|
||||
.retain(|binding| !configured_vim_operator_bindings_to_preserve.contains(binding));
|
||||
}
|
||||
|
||||
let vim_text_object = VimTextObjectKeymap {
|
||||
word: resolve_local!(keymap, defaults, vim_text_object, word),
|
||||
big_word: resolve_local!(keymap, defaults, vim_text_object, big_word),
|
||||
parentheses: resolve_local!(keymap, defaults, vim_text_object, parentheses),
|
||||
brackets: resolve_local!(keymap, defaults, vim_text_object, brackets),
|
||||
braces: resolve_local!(keymap, defaults, vim_text_object, braces),
|
||||
double_quote: resolve_local!(keymap, defaults, vim_text_object, double_quote),
|
||||
single_quote: resolve_local!(keymap, defaults, vim_text_object, single_quote),
|
||||
backtick: resolve_local!(keymap, defaults, vim_text_object, backtick),
|
||||
cancel: resolve_local!(keymap, defaults, vim_text_object, cancel),
|
||||
};
|
||||
|
||||
let pager = PagerKeymap {
|
||||
scroll_up: resolve_local!(keymap, defaults, pager, scroll_up),
|
||||
scroll_down: resolve_local!(keymap, defaults, pager, scroll_down),
|
||||
@@ -536,8 +743,7 @@ impl RuntimeKeymap {
|
||||
let list_move_down = resolve_local!(keymap, defaults, list, move_down);
|
||||
let list_accept = resolve_local!(keymap, defaults, list, accept);
|
||||
let list_cancel = resolve_local!(keymap, defaults, list, cancel);
|
||||
let mut configured_bindings_to_preserve = Vec::new();
|
||||
for (configured, resolved) in [
|
||||
let configured_bindings_to_preserve = configured_bindings_to_preserve([
|
||||
(
|
||||
keymap.global.open_transcript.as_ref(),
|
||||
app.open_transcript.as_slice(),
|
||||
@@ -593,51 +799,42 @@ impl RuntimeKeymap {
|
||||
approval.decline.as_slice(),
|
||||
),
|
||||
(keymap.approval.cancel.as_ref(), approval.cancel.as_slice()),
|
||||
] {
|
||||
if configured.is_none() {
|
||||
continue;
|
||||
}
|
||||
for binding in resolved {
|
||||
if !configured_bindings_to_preserve.contains(binding) {
|
||||
configured_bindings_to_preserve.push(*binding);
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
let list = ListKeymap {
|
||||
move_up: list_move_up,
|
||||
move_down: list_move_down,
|
||||
move_left: resolve_new_list_bindings(
|
||||
move_left: resolve_new_default_bindings(
|
||||
keymap.list.move_left.as_ref(),
|
||||
&defaults.list.move_left,
|
||||
&configured_bindings_to_preserve,
|
||||
"tui.keymap.list.move_left",
|
||||
)?,
|
||||
move_right: resolve_new_list_bindings(
|
||||
move_right: resolve_new_default_bindings(
|
||||
keymap.list.move_right.as_ref(),
|
||||
&defaults.list.move_right,
|
||||
&configured_bindings_to_preserve,
|
||||
"tui.keymap.list.move_right",
|
||||
)?,
|
||||
page_up: resolve_new_list_bindings(
|
||||
page_up: resolve_new_default_bindings(
|
||||
keymap.list.page_up.as_ref(),
|
||||
&defaults.list.page_up,
|
||||
&configured_bindings_to_preserve,
|
||||
"tui.keymap.list.page_up",
|
||||
)?,
|
||||
page_down: resolve_new_list_bindings(
|
||||
page_down: resolve_new_default_bindings(
|
||||
keymap.list.page_down.as_ref(),
|
||||
&defaults.list.page_down,
|
||||
&configured_bindings_to_preserve,
|
||||
"tui.keymap.list.page_down",
|
||||
)?,
|
||||
jump_top: resolve_new_list_bindings(
|
||||
jump_top: resolve_new_default_bindings(
|
||||
keymap.list.jump_top.as_ref(),
|
||||
&defaults.list.jump_top,
|
||||
&configured_bindings_to_preserve,
|
||||
"tui.keymap.list.jump_top",
|
||||
)?,
|
||||
jump_bottom: resolve_new_list_bindings(
|
||||
jump_bottom: resolve_new_default_bindings(
|
||||
keymap.list.jump_bottom.as_ref(),
|
||||
&defaults.list.jump_bottom,
|
||||
&configured_bindings_to_preserve,
|
||||
@@ -654,6 +851,7 @@ impl RuntimeKeymap {
|
||||
editor,
|
||||
vim_normal,
|
||||
vim_operator,
|
||||
vim_text_object,
|
||||
pager,
|
||||
list,
|
||||
approval,
|
||||
@@ -796,6 +994,7 @@ impl RuntimeKeymap {
|
||||
paste_after: default_bindings![plain(KeyCode::Char('p'))],
|
||||
start_delete_operator: default_bindings![plain(KeyCode::Char('d'))],
|
||||
start_yank_operator: default_bindings![plain(KeyCode::Char('y'))],
|
||||
start_change_operator: default_bindings![plain(KeyCode::Char('c'))],
|
||||
cancel_operator: default_bindings![plain(KeyCode::Esc)],
|
||||
},
|
||||
vim_operator: VimOperatorKeymap {
|
||||
@@ -813,6 +1012,35 @@ impl RuntimeKeymap {
|
||||
plain(KeyCode::Char('$')),
|
||||
shift(KeyCode::Char('$'))
|
||||
],
|
||||
select_inner_text_object: default_bindings![plain(KeyCode::Char('i'))],
|
||||
select_around_text_object: default_bindings![plain(KeyCode::Char('a'))],
|
||||
cancel: default_bindings![plain(KeyCode::Esc)],
|
||||
},
|
||||
vim_text_object: VimTextObjectKeymap {
|
||||
word: default_bindings![plain(KeyCode::Char('w'))],
|
||||
big_word: default_bindings![shift(KeyCode::Char('w')), plain(KeyCode::Char('W'))],
|
||||
parentheses: default_bindings![
|
||||
plain(KeyCode::Char('(')),
|
||||
shift(KeyCode::Char('(')),
|
||||
plain(KeyCode::Char(')')),
|
||||
shift(KeyCode::Char(')')),
|
||||
plain(KeyCode::Char('b'))
|
||||
],
|
||||
brackets: default_bindings![plain(KeyCode::Char('[')), plain(KeyCode::Char(']'))],
|
||||
braces: default_bindings![
|
||||
plain(KeyCode::Char('{')),
|
||||
shift(KeyCode::Char('{')),
|
||||
plain(KeyCode::Char('}')),
|
||||
shift(KeyCode::Char('}')),
|
||||
shift(KeyCode::Char('b')),
|
||||
plain(KeyCode::Char('B'))
|
||||
],
|
||||
double_quote: default_bindings![
|
||||
plain(KeyCode::Char('"')),
|
||||
shift(KeyCode::Char('"'))
|
||||
],
|
||||
single_quote: default_bindings![plain(KeyCode::Char('\''))],
|
||||
backtick: default_bindings![plain(KeyCode::Char('`'))],
|
||||
cancel: default_bindings![plain(KeyCode::Esc)],
|
||||
},
|
||||
pager: PagerKeymap {
|
||||
@@ -1196,6 +1424,10 @@ impl RuntimeKeymap {
|
||||
"start_yank_operator",
|
||||
self.vim_normal.start_yank_operator.as_slice(),
|
||||
),
|
||||
(
|
||||
"start_change_operator",
|
||||
self.vim_normal.start_change_operator.as_slice(),
|
||||
),
|
||||
(
|
||||
"cancel_operator",
|
||||
self.vim_normal.cancel_operator.as_slice(),
|
||||
@@ -1232,10 +1464,33 @@ impl RuntimeKeymap {
|
||||
"motion_line_end",
|
||||
self.vim_operator.motion_line_end.as_slice(),
|
||||
),
|
||||
(
|
||||
"select_inner_text_object",
|
||||
self.vim_operator.select_inner_text_object.as_slice(),
|
||||
),
|
||||
(
|
||||
"select_around_text_object",
|
||||
self.vim_operator.select_around_text_object.as_slice(),
|
||||
),
|
||||
("cancel", self.vim_operator.cancel.as_slice()),
|
||||
],
|
||||
)?;
|
||||
|
||||
validate_unique(
|
||||
"vim_text_object",
|
||||
[
|
||||
("word", self.vim_text_object.word.as_slice()),
|
||||
("big_word", self.vim_text_object.big_word.as_slice()),
|
||||
("parentheses", self.vim_text_object.parentheses.as_slice()),
|
||||
("brackets", self.vim_text_object.brackets.as_slice()),
|
||||
("braces", self.vim_text_object.braces.as_slice()),
|
||||
("double_quote", self.vim_text_object.double_quote.as_slice()),
|
||||
("single_quote", self.vim_text_object.single_quote.as_slice()),
|
||||
("backtick", self.vim_text_object.backtick.as_slice()),
|
||||
("cancel", self.vim_text_object.cancel.as_slice()),
|
||||
],
|
||||
)?;
|
||||
|
||||
validate_unique(
|
||||
"pager",
|
||||
[
|
||||
@@ -1524,7 +1779,24 @@ fn resolve_bindings(
|
||||
parse_bindings(spec, path)
|
||||
}
|
||||
|
||||
fn resolve_new_list_bindings(
|
||||
fn configured_bindings_to_preserve<const N: usize>(
|
||||
pairs: [(Option<&KeybindingsSpec>, &[KeyBinding]); N],
|
||||
) -> Vec<KeyBinding> {
|
||||
let mut configured_bindings = Vec::new();
|
||||
for (configured, resolved) in pairs {
|
||||
if configured.is_none() {
|
||||
continue;
|
||||
}
|
||||
for binding in resolved {
|
||||
if !configured_bindings.contains(binding) {
|
||||
configured_bindings.push(*binding);
|
||||
}
|
||||
}
|
||||
}
|
||||
configured_bindings
|
||||
}
|
||||
|
||||
fn resolve_new_default_bindings(
|
||||
configured: Option<&KeybindingsSpec>,
|
||||
fallback: &[KeyBinding],
|
||||
configured_bindings_to_preserve: &[KeyBinding],
|
||||
@@ -1973,6 +2245,58 @@ mod tests {
|
||||
expect_conflict(&keymap, "list.jump_top", "approval.approve");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_legacy_vim_normal_bindings_prune_new_change_operator_default() {
|
||||
let mut keymap = TuiKeymap::default();
|
||||
keymap.vim_normal.move_left = Some(one("c"));
|
||||
|
||||
let runtime = RuntimeKeymap::from_config(&keymap).expect("config should parse");
|
||||
|
||||
assert_eq!(
|
||||
runtime.vim_normal.move_left,
|
||||
vec![key_hint::plain(KeyCode::Char('c'))]
|
||||
);
|
||||
assert_eq!(runtime.vim_normal.start_change_operator, Vec::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_new_vim_normal_binding_still_conflicts_with_legacy_binding() {
|
||||
let mut keymap = TuiKeymap::default();
|
||||
keymap.vim_normal.move_left = Some(one("c"));
|
||||
keymap.vim_normal.start_change_operator = Some(one("c"));
|
||||
|
||||
expect_conflict(&keymap, "move_left", "start_change_operator");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_legacy_vim_operator_bindings_prune_new_text_object_defaults() {
|
||||
let mut keymap = TuiKeymap::default();
|
||||
keymap.vim_operator.motion_left = Some(one("i"));
|
||||
keymap.vim_operator.motion_right = Some(one("a"));
|
||||
|
||||
let runtime = RuntimeKeymap::from_config(&keymap).expect("config should parse");
|
||||
|
||||
assert_eq!(
|
||||
runtime.vim_operator.motion_left,
|
||||
vec![key_hint::plain(KeyCode::Char('i'))]
|
||||
);
|
||||
assert_eq!(
|
||||
runtime.vim_operator.motion_right,
|
||||
vec![key_hint::plain(KeyCode::Char('a'))]
|
||||
);
|
||||
assert_eq!(runtime.vim_operator.select_inner_text_object, Vec::new());
|
||||
assert_eq!(runtime.vim_operator.select_around_text_object, Vec::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_new_vim_operator_binding_still_conflicts_with_legacy_binding() {
|
||||
let mut keymap = TuiKeymap::default();
|
||||
keymap.vim_operator.motion_left = Some(one("i"));
|
||||
keymap.vim_operator.select_inner_text_object = Some(one("i"));
|
||||
|
||||
expect_conflict(&keymap, "motion_left", "select_inner_text_object");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_normal_defaults_include_insert_and_arrow_aliases() {
|
||||
let runtime = RuntimeKeymap::defaults();
|
||||
|
||||
@@ -140,6 +140,7 @@ pub(super) const KEYMAP_ACTIONS: &[KeymapActionDescriptor] = &[
|
||||
action("vim_normal", "Vim normal", "paste_after", "Paste after the cursor."),
|
||||
action("vim_normal", "Vim normal", "start_delete_operator", "Begin a delete operator and wait for a motion."),
|
||||
action("vim_normal", "Vim normal", "start_yank_operator", "Begin a yank operator and wait for a motion."),
|
||||
action("vim_normal", "Vim normal", "start_change_operator", "Begin a change operator and wait for a text object."),
|
||||
action("vim_normal", "Vim normal", "cancel_operator", "Cancel a pending Vim operator."),
|
||||
action("vim_operator", "Vim operator", "delete_line", "Repeat delete operator to delete the whole line."),
|
||||
action("vim_operator", "Vim operator", "yank_line", "Repeat yank operator to yank the whole line."),
|
||||
@@ -152,7 +153,18 @@ pub(super) const KEYMAP_ACTIONS: &[KeymapActionDescriptor] = &[
|
||||
action("vim_operator", "Vim operator", "motion_word_end", "Operator motion to end of word."),
|
||||
action("vim_operator", "Vim operator", "motion_line_start", "Operator motion to line start."),
|
||||
action("vim_operator", "Vim operator", "motion_line_end", "Operator motion to line end."),
|
||||
action("vim_operator", "Vim operator", "select_inner_text_object", "Select an inner text object."),
|
||||
action("vim_operator", "Vim operator", "select_around_text_object", "Select an around text object."),
|
||||
action("vim_operator", "Vim operator", "cancel", "Cancel the pending operator."),
|
||||
action("vim_text_object", "Vim text object", "word", "Target the current word."),
|
||||
action("vim_text_object", "Vim text object", "big_word", "Target the current WORD."),
|
||||
action("vim_text_object", "Vim text object", "parentheses", "Target enclosing parentheses."),
|
||||
action("vim_text_object", "Vim text object", "brackets", "Target enclosing brackets."),
|
||||
action("vim_text_object", "Vim text object", "braces", "Target enclosing braces."),
|
||||
action("vim_text_object", "Vim text object", "double_quote", "Target enclosing double quotes."),
|
||||
action("vim_text_object", "Vim text object", "single_quote", "Target enclosing single quotes."),
|
||||
action("vim_text_object", "Vim text object", "backtick", "Target enclosing backticks."),
|
||||
action("vim_text_object", "Vim text object", "cancel", "Cancel the pending text object."),
|
||||
action("pager", "Pager", "scroll_up", "Scroll up by one row."),
|
||||
action("pager", "Pager", "scroll_down", "Scroll down by one row."),
|
||||
action("pager", "Pager", "page_up", "Scroll up by one page."),
|
||||
@@ -269,6 +281,7 @@ pub(super) fn binding_slot<'a>(
|
||||
("vim_normal", "paste_after") => Some(&mut keymap.vim_normal.paste_after),
|
||||
("vim_normal", "start_delete_operator") => Some(&mut keymap.vim_normal.start_delete_operator),
|
||||
("vim_normal", "start_yank_operator") => Some(&mut keymap.vim_normal.start_yank_operator),
|
||||
("vim_normal", "start_change_operator") => Some(&mut keymap.vim_normal.start_change_operator),
|
||||
("vim_normal", "cancel_operator") => Some(&mut keymap.vim_normal.cancel_operator),
|
||||
("vim_operator", "delete_line") => Some(&mut keymap.vim_operator.delete_line),
|
||||
("vim_operator", "yank_line") => Some(&mut keymap.vim_operator.yank_line),
|
||||
@@ -281,7 +294,18 @@ pub(super) fn binding_slot<'a>(
|
||||
("vim_operator", "motion_word_end") => Some(&mut keymap.vim_operator.motion_word_end),
|
||||
("vim_operator", "motion_line_start") => Some(&mut keymap.vim_operator.motion_line_start),
|
||||
("vim_operator", "motion_line_end") => Some(&mut keymap.vim_operator.motion_line_end),
|
||||
("vim_operator", "select_inner_text_object") => Some(&mut keymap.vim_operator.select_inner_text_object),
|
||||
("vim_operator", "select_around_text_object") => Some(&mut keymap.vim_operator.select_around_text_object),
|
||||
("vim_operator", "cancel") => Some(&mut keymap.vim_operator.cancel),
|
||||
("vim_text_object", "word") => Some(&mut keymap.vim_text_object.word),
|
||||
("vim_text_object", "big_word") => Some(&mut keymap.vim_text_object.big_word),
|
||||
("vim_text_object", "parentheses") => Some(&mut keymap.vim_text_object.parentheses),
|
||||
("vim_text_object", "brackets") => Some(&mut keymap.vim_text_object.brackets),
|
||||
("vim_text_object", "braces") => Some(&mut keymap.vim_text_object.braces),
|
||||
("vim_text_object", "double_quote") => Some(&mut keymap.vim_text_object.double_quote),
|
||||
("vim_text_object", "single_quote") => Some(&mut keymap.vim_text_object.single_quote),
|
||||
("vim_text_object", "backtick") => Some(&mut keymap.vim_text_object.backtick),
|
||||
("vim_text_object", "cancel") => Some(&mut keymap.vim_text_object.cancel),
|
||||
("pager", "scroll_up") => Some(&mut keymap.pager.scroll_up),
|
||||
("pager", "scroll_down") => Some(&mut keymap.pager.scroll_down),
|
||||
("pager", "page_up") => Some(&mut keymap.pager.page_up),
|
||||
@@ -380,6 +404,7 @@ pub(super) fn bindings_for_action<'a>(
|
||||
("vim_normal", "paste_after") => Some(runtime_keymap.vim_normal.paste_after.as_slice()),
|
||||
("vim_normal", "start_delete_operator") => Some(runtime_keymap.vim_normal.start_delete_operator.as_slice()),
|
||||
("vim_normal", "start_yank_operator") => Some(runtime_keymap.vim_normal.start_yank_operator.as_slice()),
|
||||
("vim_normal", "start_change_operator") => Some(runtime_keymap.vim_normal.start_change_operator.as_slice()),
|
||||
("vim_normal", "cancel_operator") => Some(runtime_keymap.vim_normal.cancel_operator.as_slice()),
|
||||
("vim_operator", "delete_line") => Some(runtime_keymap.vim_operator.delete_line.as_slice()),
|
||||
("vim_operator", "yank_line") => Some(runtime_keymap.vim_operator.yank_line.as_slice()),
|
||||
@@ -392,7 +417,18 @@ pub(super) fn bindings_for_action<'a>(
|
||||
("vim_operator", "motion_word_end") => Some(runtime_keymap.vim_operator.motion_word_end.as_slice()),
|
||||
("vim_operator", "motion_line_start") => Some(runtime_keymap.vim_operator.motion_line_start.as_slice()),
|
||||
("vim_operator", "motion_line_end") => Some(runtime_keymap.vim_operator.motion_line_end.as_slice()),
|
||||
("vim_operator", "select_inner_text_object") => Some(runtime_keymap.vim_operator.select_inner_text_object.as_slice()),
|
||||
("vim_operator", "select_around_text_object") => Some(runtime_keymap.vim_operator.select_around_text_object.as_slice()),
|
||||
("vim_operator", "cancel") => Some(runtime_keymap.vim_operator.cancel.as_slice()),
|
||||
("vim_text_object", "word") => Some(runtime_keymap.vim_text_object.word.as_slice()),
|
||||
("vim_text_object", "big_word") => Some(runtime_keymap.vim_text_object.big_word.as_slice()),
|
||||
("vim_text_object", "parentheses") => Some(runtime_keymap.vim_text_object.parentheses.as_slice()),
|
||||
("vim_text_object", "brackets") => Some(runtime_keymap.vim_text_object.brackets.as_slice()),
|
||||
("vim_text_object", "braces") => Some(runtime_keymap.vim_text_object.braces.as_slice()),
|
||||
("vim_text_object", "double_quote") => Some(runtime_keymap.vim_text_object.double_quote.as_slice()),
|
||||
("vim_text_object", "single_quote") => Some(runtime_keymap.vim_text_object.single_quote.as_slice()),
|
||||
("vim_text_object", "backtick") => Some(runtime_keymap.vim_text_object.backtick.as_slice()),
|
||||
("vim_text_object", "cancel") => Some(runtime_keymap.vim_text_object.cancel.as_slice()),
|
||||
("pager", "scroll_up") => Some(runtime_keymap.pager.scroll_up.as_slice()),
|
||||
("pager", "scroll_down") => Some(runtime_keymap.pager.scroll_down.as_slice()),
|
||||
("pager", "page_up") => Some(runtime_keymap.pager.page_up.as_slice()),
|
||||
|
||||
@@ -104,7 +104,7 @@ const KEYMAP_CONTEXT_TABS: &[KeymapContextTab] = &[
|
||||
id: "vim-shortcuts",
|
||||
label: "Vim",
|
||||
description: "Vim normal-mode and operator shortcuts.",
|
||||
contexts: &["vim_normal", "vim_operator"],
|
||||
contexts: &["vim_normal", "vim_operator", "vim_text_object"],
|
||||
},
|
||||
KeymapContextTab {
|
||||
id: "navigation-shortcuts",
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ expression: "render_picker(params, 120)"
|
||||
|
||||
Keymap
|
||||
All configurable shortcuts.
|
||||
94 actions, 1 customized, 2 unbound.
|
||||
106 actions, 1 customized, 2 unbound.
|
||||
|
||||
[All] Common Customized (1) Unbound (2) App Composer Editor Vim Navigation Approval Debug
|
||||
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ expression: "render_picker(params, 120)"
|
||||
|
||||
Keymap
|
||||
All configurable shortcuts.
|
||||
95 actions, 0 customized, 3 unbound.
|
||||
107 actions, 0 customized, 3 unbound.
|
||||
|
||||
[All] Common Customized (0) Unbound (3) App Composer Editor Vim Navigation Approval Debug
|
||||
|
||||
|
||||
+2
-2
@@ -2,14 +2,14 @@
|
||||
source: tui/src/keymap_setup.rs
|
||||
expression: snapshot
|
||||
---
|
||||
tab: All (94 selectable)
|
||||
tab: All (106 selectable)
|
||||
tab: Common (19 selectable)
|
||||
tab: Customized (0) (0 selectable)
|
||||
tab: Unbound (2) (2 selectable)
|
||||
tab: App (9 selectable)
|
||||
tab: Composer (5 selectable)
|
||||
tab: Editor (17 selectable)
|
||||
tab: Vim (35 selectable)
|
||||
tab: Vim (47 selectable)
|
||||
tab: Navigation (20 selectable)
|
||||
tab: Approval (8 selectable)
|
||||
tab: Debug (1 selectable)
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ expression: "render_picker(params, 78)"
|
||||
|
||||
Keymap
|
||||
All configurable shortcuts.
|
||||
94 actions, 0 customized, 2 unbound.
|
||||
106 actions, 0 customized, 2 unbound.
|
||||
|
||||
[All] Common Customized (0) Unbound (2) App Composer Editor Vim
|
||||
Navigation Approval Debug
|
||||
|
||||
@@ -5,7 +5,7 @@ expression: "render_picker(params, 120)"
|
||||
|
||||
Keymap
|
||||
All configurable shortcuts.
|
||||
94 actions, 0 customized, 2 unbound.
|
||||
106 actions, 0 customized, 2 unbound.
|
||||
|
||||
[All] Common Customized (0) Unbound (2) App Composer Editor Vim Navigation Approval Debug
|
||||
|
||||
|
||||
Reference in New Issue
Block a user