mirror of
https://github.com/openai/codex.git
synced 2026-06-02 11:22:01 +00:00
Compare commits
16 Commits
fix/window
...
fcoury/vim
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3d0c003ea | ||
|
|
50fbb55995 | ||
|
|
4494bfe368 | ||
|
|
43472a6f02 | ||
|
|
9d9e89ca29 | ||
|
|
cbdae45c3f | ||
|
|
ff077a6a07 | ||
|
|
e98e93e277 | ||
|
|
ac5067f921 | ||
|
|
fd0e287d96 | ||
|
|
5f86c5259b | ||
|
|
11bf378594 | ||
|
|
727b57e73e | ||
|
|
26b9335406 | ||
|
|
5f69d89d94 | ||
|
|
4e3f68330a |
@@ -219,26 +219,54 @@ pub struct TuiVimNormalKeymap {
|
||||
pub move_line_start: Option<KeybindingsSpec>,
|
||||
/// Move cursor to end of line (`$`).
|
||||
pub move_line_end: Option<KeybindingsSpec>,
|
||||
/// Find the next target character on the current line (`f{char}`).
|
||||
pub find_forward: Option<KeybindingsSpec>,
|
||||
/// Find the previous target character on the current line (`F{char}`).
|
||||
pub find_backward: Option<KeybindingsSpec>,
|
||||
/// Move until before the next target character on the current line (`t{char}`).
|
||||
pub till_forward: Option<KeybindingsSpec>,
|
||||
/// Move until after the previous target character on the current line (`T{char}`).
|
||||
pub till_backward: Option<KeybindingsSpec>,
|
||||
/// Repeat the most recent find/till motion (`;`).
|
||||
pub repeat_find: Option<KeybindingsSpec>,
|
||||
/// Repeat the most recent find/till motion in the opposite direction (`,`).
|
||||
pub repeat_find_reverse: Option<KeybindingsSpec>,
|
||||
/// Repeat the most recent text-changing Vim command (`.`).
|
||||
pub repeat_change: Option<KeybindingsSpec>,
|
||||
/// Delete character under cursor (`x`).
|
||||
pub delete_char: Option<KeybindingsSpec>,
|
||||
/// Delete from cursor to end of line (`D`).
|
||||
pub delete_to_line_end: Option<KeybindingsSpec>,
|
||||
/// Change from cursor to end of line and enter insert mode (`C`).
|
||||
pub change_to_line_end: Option<KeybindingsSpec>,
|
||||
/// Substitute the character under the cursor and enter insert mode (`s`).
|
||||
pub substitute_char: Option<KeybindingsSpec>,
|
||||
/// Substitute the current line and enter insert mode (`S`).
|
||||
pub substitute_line: Option<KeybindingsSpec>,
|
||||
/// Yank the entire line (`Y`).
|
||||
pub yank_line: Option<KeybindingsSpec>,
|
||||
/// Paste after cursor (`p`).
|
||||
pub paste_after: Option<KeybindingsSpec>,
|
||||
/// Select a named register for the next Vim command (`"{register}`).
|
||||
pub select_register: Option<KeybindingsSpec>,
|
||||
/// Enter characterwise visual mode (`v`).
|
||||
pub enter_visual: Option<KeybindingsSpec>,
|
||||
/// Enter linewise visual mode (`V`).
|
||||
pub enter_visual_line: Option<KeybindingsSpec>,
|
||||
/// Begin delete operator; next key selects motion (`d`).
|
||||
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 motion or text object.
|
||||
pub start_change_operator: Option<KeybindingsSpec>,
|
||||
/// Cancel a pending operator and return to normal mode.
|
||||
pub cancel_operator: Option<KeybindingsSpec>,
|
||||
}
|
||||
|
||||
/// Vim operator-pending keybindings for modal editing inside text areas.
|
||||
///
|
||||
/// This context is active only while waiting for a motion after `d` or `y`.
|
||||
/// Repeating the operator key (`dd`, `yy`) targets the entire line. Pressing
|
||||
/// This context is active only while waiting for a motion after `d`, `y`, or `c`.
|
||||
/// Repeating the operator key (`dd`, `yy`, `cc`) targets the entire line. Pressing
|
||||
/// `Esc` cancels the pending operator and returns to normal mode without
|
||||
/// modifying text.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
|
||||
@@ -248,6 +276,8 @@ pub struct TuiVimOperatorKeymap {
|
||||
pub delete_line: Option<KeybindingsSpec>,
|
||||
/// Repeat yank operator to yank the whole line (`yy`).
|
||||
pub yank_line: Option<KeybindingsSpec>,
|
||||
/// Repeat change operator to change the whole line (`cc`).
|
||||
pub change_line: Option<KeybindingsSpec>,
|
||||
/// Motion: left (`h`).
|
||||
pub motion_left: Option<KeybindingsSpec>,
|
||||
/// Motion: right (`l`).
|
||||
@@ -266,10 +296,75 @@ pub struct TuiVimOperatorKeymap {
|
||||
pub motion_line_start: Option<KeybindingsSpec>,
|
||||
/// Motion: to end of line (`$`).
|
||||
pub motion_line_end: Option<KeybindingsSpec>,
|
||||
/// Enter a forward find motion and await its literal target (`f{char}`).
|
||||
pub find_forward: Option<KeybindingsSpec>,
|
||||
/// Enter a backward find motion and await its literal target (`F{char}`).
|
||||
pub find_backward: Option<KeybindingsSpec>,
|
||||
/// Enter a forward till motion and await its literal target (`t{char}`).
|
||||
pub till_forward: Option<KeybindingsSpec>,
|
||||
/// Enter a backward till motion and await its literal target (`T{char}`).
|
||||
pub till_backward: Option<KeybindingsSpec>,
|
||||
/// Repeat the most recent find/till motion (`;`).
|
||||
pub repeat_find: Option<KeybindingsSpec>,
|
||||
/// Repeat the most recent find/till motion in the opposite direction (`,`).
|
||||
pub repeat_find_reverse: 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>,
|
||||
/// Text object: sentence.
|
||||
pub sentence: Option<KeybindingsSpec>,
|
||||
/// Text object: paragraph.
|
||||
pub paragraph: Option<KeybindingsSpec>,
|
||||
/// Text object: enclosing paired tag.
|
||||
pub tag: Option<KeybindingsSpec>,
|
||||
/// Cancel the pending text-object command.
|
||||
pub cancel: Option<KeybindingsSpec>,
|
||||
}
|
||||
|
||||
/// Vim visual-mode keybindings for selected-text operations.
|
||||
///
|
||||
/// Normal-mode movement, find, and text-object bindings continue to extend
|
||||
/// selections while this context supplies actions specific to an active
|
||||
/// selection.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct TuiVimVisualKeymap {
|
||||
/// Delete the visual selection (`d`).
|
||||
pub delete: Option<KeybindingsSpec>,
|
||||
/// Yank the visual selection (`y`).
|
||||
pub yank: Option<KeybindingsSpec>,
|
||||
/// Change the visual selection and enter insert mode (`c`).
|
||||
pub change: Option<KeybindingsSpec>,
|
||||
/// Cancel visual mode and return to normal mode (`Esc`).
|
||||
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)]
|
||||
@@ -374,6 +469,10 @@ pub struct TuiKeymap {
|
||||
#[serde(default)]
|
||||
pub vim_operator: TuiVimOperatorKeymap,
|
||||
#[serde(default)]
|
||||
pub vim_text_object: TuiVimTextObjectKeymap,
|
||||
#[serde(default)]
|
||||
pub vim_visual: TuiVimVisualKeymap,
|
||||
#[serde(default)]
|
||||
pub pager: TuiPagerKeymap,
|
||||
#[serde(default)]
|
||||
pub list: TuiListKeymap,
|
||||
@@ -560,6 +659,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 [
|
||||
|
||||
@@ -2824,9 +2824,14 @@
|
||||
"append_after_cursor": null,
|
||||
"append_line_end": null,
|
||||
"cancel_operator": null,
|
||||
"change_to_line_end": null,
|
||||
"delete_char": null,
|
||||
"delete_to_line_end": null,
|
||||
"enter_insert": null,
|
||||
"enter_visual": null,
|
||||
"enter_visual_line": null,
|
||||
"find_backward": null,
|
||||
"find_forward": null,
|
||||
"insert_line_start": null,
|
||||
"move_down": null,
|
||||
"move_left": null,
|
||||
@@ -2840,13 +2845,25 @@
|
||||
"open_line_above": null,
|
||||
"open_line_below": null,
|
||||
"paste_after": null,
|
||||
"repeat_change": null,
|
||||
"repeat_find": null,
|
||||
"repeat_find_reverse": null,
|
||||
"select_register": null,
|
||||
"start_change_operator": null,
|
||||
"start_delete_operator": null,
|
||||
"start_yank_operator": null,
|
||||
"substitute_char": null,
|
||||
"substitute_line": null,
|
||||
"till_backward": null,
|
||||
"till_forward": null,
|
||||
"yank_line": null
|
||||
},
|
||||
"vim_operator": {
|
||||
"cancel": null,
|
||||
"change_line": null,
|
||||
"delete_line": null,
|
||||
"find_backward": null,
|
||||
"find_forward": null,
|
||||
"motion_down": null,
|
||||
"motion_left": null,
|
||||
"motion_line_end": null,
|
||||
@@ -2856,7 +2873,33 @@
|
||||
"motion_word_backward": null,
|
||||
"motion_word_end": null,
|
||||
"motion_word_forward": null,
|
||||
"repeat_find": null,
|
||||
"repeat_find_reverse": null,
|
||||
"select_around_text_object": null,
|
||||
"select_inner_text_object": null,
|
||||
"till_backward": null,
|
||||
"till_forward": null,
|
||||
"yank_line": null
|
||||
},
|
||||
"vim_text_object": {
|
||||
"backtick": null,
|
||||
"big_word": null,
|
||||
"braces": null,
|
||||
"brackets": null,
|
||||
"cancel": null,
|
||||
"double_quote": null,
|
||||
"paragraph": null,
|
||||
"parentheses": null,
|
||||
"sentence": null,
|
||||
"single_quote": null,
|
||||
"tag": null,
|
||||
"word": null
|
||||
},
|
||||
"vim_visual": {
|
||||
"cancel": null,
|
||||
"change": null,
|
||||
"delete": null,
|
||||
"yank": null
|
||||
}
|
||||
},
|
||||
"description": "Keybinding overrides for the TUI.\n\nThis supports rebinding selected actions globally and by context. Context bindings take precedence over `global` bindings."
|
||||
@@ -3490,9 +3533,14 @@
|
||||
"append_after_cursor": null,
|
||||
"append_line_end": null,
|
||||
"cancel_operator": null,
|
||||
"change_to_line_end": null,
|
||||
"delete_char": null,
|
||||
"delete_to_line_end": null,
|
||||
"enter_insert": null,
|
||||
"enter_visual": null,
|
||||
"enter_visual_line": null,
|
||||
"find_backward": null,
|
||||
"find_forward": null,
|
||||
"insert_line_start": null,
|
||||
"move_down": null,
|
||||
"move_left": null,
|
||||
@@ -3506,8 +3554,17 @@
|
||||
"open_line_above": null,
|
||||
"open_line_below": null,
|
||||
"paste_after": null,
|
||||
"repeat_change": null,
|
||||
"repeat_find": null,
|
||||
"repeat_find_reverse": null,
|
||||
"select_register": null,
|
||||
"start_change_operator": null,
|
||||
"start_delete_operator": null,
|
||||
"start_yank_operator": null,
|
||||
"substitute_char": null,
|
||||
"substitute_line": null,
|
||||
"till_backward": null,
|
||||
"till_forward": null,
|
||||
"yank_line": null
|
||||
}
|
||||
},
|
||||
@@ -3519,7 +3576,10 @@
|
||||
],
|
||||
"default": {
|
||||
"cancel": null,
|
||||
"change_line": null,
|
||||
"delete_line": null,
|
||||
"find_backward": null,
|
||||
"find_forward": null,
|
||||
"motion_down": null,
|
||||
"motion_left": null,
|
||||
"motion_line_end": null,
|
||||
@@ -3529,8 +3589,48 @@
|
||||
"motion_word_backward": null,
|
||||
"motion_word_end": null,
|
||||
"motion_word_forward": null,
|
||||
"repeat_find": null,
|
||||
"repeat_find_reverse": null,
|
||||
"select_around_text_object": null,
|
||||
"select_inner_text_object": null,
|
||||
"till_backward": null,
|
||||
"till_forward": 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,
|
||||
"paragraph": null,
|
||||
"parentheses": null,
|
||||
"sentence": null,
|
||||
"single_quote": null,
|
||||
"tag": null,
|
||||
"word": null
|
||||
}
|
||||
},
|
||||
"vim_visual": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TuiVimVisualKeymap"
|
||||
}
|
||||
],
|
||||
"default": {
|
||||
"cancel": null,
|
||||
"change": null,
|
||||
"delete": null,
|
||||
"yank": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -3755,6 +3855,14 @@
|
||||
],
|
||||
"description": "Cancel a pending operator and return to normal mode."
|
||||
},
|
||||
"change_to_line_end": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Change from cursor to end of line and enter insert mode (`C`)."
|
||||
},
|
||||
"delete_char": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -3779,6 +3887,38 @@
|
||||
],
|
||||
"description": "Enter insert mode at cursor (`i`)."
|
||||
},
|
||||
"enter_visual": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Enter characterwise visual mode (`v`)."
|
||||
},
|
||||
"enter_visual_line": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Enter linewise visual mode (`V`)."
|
||||
},
|
||||
"find_backward": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Find the previous target character on the current line (`F{char}`)."
|
||||
},
|
||||
"find_forward": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Find the next target character on the current line (`f{char}`)."
|
||||
},
|
||||
"insert_line_start": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -3883,6 +4023,46 @@
|
||||
],
|
||||
"description": "Paste after cursor (`p`)."
|
||||
},
|
||||
"repeat_change": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Repeat the most recent text-changing Vim command (`.`)."
|
||||
},
|
||||
"repeat_find": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Repeat the most recent find/till motion (`;`)."
|
||||
},
|
||||
"repeat_find_reverse": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Repeat the most recent find/till motion in the opposite direction (`,`)."
|
||||
},
|
||||
"select_register": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Select a named register for the next Vim command (`\"{register}`)."
|
||||
},
|
||||
"start_change_operator": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Begin change operator; next keys select a motion or text object."
|
||||
},
|
||||
"start_delete_operator": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -3899,6 +4079,38 @@
|
||||
],
|
||||
"description": "Begin yank operator; next key selects motion (`y`)."
|
||||
},
|
||||
"substitute_char": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Substitute the character under the cursor and enter insert mode (`s`)."
|
||||
},
|
||||
"substitute_line": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Substitute the current line and enter insert mode (`S`)."
|
||||
},
|
||||
"till_backward": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Move until after the previous target character on the current line (`T{char}`)."
|
||||
},
|
||||
"till_forward": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Move until before the next target character on the current line (`t{char}`)."
|
||||
},
|
||||
"yank_line": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -3912,7 +4124,7 @@
|
||||
},
|
||||
"TuiVimOperatorKeymap": {
|
||||
"additionalProperties": false,
|
||||
"description": "Vim operator-pending keybindings for modal editing inside text areas.\n\nThis context is active only while waiting for a motion after `d` or `y`. Repeating the operator key (`dd`, `yy`) targets the entire line. Pressing `Esc` cancels the pending operator and returns to normal mode without modifying text.",
|
||||
"description": "Vim operator-pending keybindings for modal editing inside text areas.\n\nThis context is active only while waiting for a motion after `d`, `y`, or `c`. Repeating the operator key (`dd`, `yy`, `cc`) targets the entire line. Pressing `Esc` cancels the pending operator and returns to normal mode without modifying text.",
|
||||
"properties": {
|
||||
"cancel": {
|
||||
"allOf": [
|
||||
@@ -3922,6 +4134,14 @@
|
||||
],
|
||||
"description": "Cancel the pending operator and return to normal mode."
|
||||
},
|
||||
"change_line": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Repeat change operator to change the whole line (`cc`)."
|
||||
},
|
||||
"delete_line": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -3930,6 +4150,22 @@
|
||||
],
|
||||
"description": "Repeat delete operator to delete the whole line (`dd`)."
|
||||
},
|
||||
"find_backward": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Enter a backward find motion and await its literal target (`F{char}`)."
|
||||
},
|
||||
"find_forward": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Enter a forward find motion and await its literal target (`f{char}`)."
|
||||
},
|
||||
"motion_down": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -4002,6 +4238,54 @@
|
||||
],
|
||||
"description": "Motion: to start of next word (`w`)."
|
||||
},
|
||||
"repeat_find": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Repeat the most recent find/till motion (`;`)."
|
||||
},
|
||||
"repeat_find_reverse": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Repeat the most recent find/till motion in the opposite direction (`,`)."
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"till_backward": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Enter a backward till motion and await its literal target (`T{char}`)."
|
||||
},
|
||||
"till_forward": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Enter a forward till motion and await its literal target (`t{char}`)."
|
||||
},
|
||||
"yank_line": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -4013,6 +4297,148 @@
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"paragraph": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Text object: paragraph."
|
||||
},
|
||||
"parentheses": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Text object: parentheses."
|
||||
},
|
||||
"sentence": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Text object: sentence."
|
||||
},
|
||||
"single_quote": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Text object: single quotes."
|
||||
},
|
||||
"tag": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Text object: enclosing paired tag."
|
||||
},
|
||||
"word": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Text object: word."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"TuiVimVisualKeymap": {
|
||||
"additionalProperties": false,
|
||||
"description": "Vim visual-mode keybindings for selected-text operations.\n\nNormal-mode movement, find, and text-object bindings continue to extend selections while this context supplies actions specific to an active selection.",
|
||||
"properties": {
|
||||
"cancel": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Cancel visual mode and return to normal mode (`Esc`)."
|
||||
},
|
||||
"change": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Change the visual selection and enter insert mode (`c`)."
|
||||
},
|
||||
"delete": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Delete the visual selection (`d`)."
|
||||
},
|
||||
"yank": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Yank the visual selection (`y`)."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"UriBasedFileOpener": {
|
||||
"oneOf": [
|
||||
{
|
||||
|
||||
@@ -1074,6 +1074,8 @@ impl ChatComposer {
|
||||
.map(|label| match label {
|
||||
"Normal" => "Vim: Normal".magenta(),
|
||||
"Insert" => "Vim: Insert".green(),
|
||||
"Visual" => "Vim: Visual".cyan(),
|
||||
"Visual Line" => "Vim: Visual Line".cyan(),
|
||||
_ => unreachable!(),
|
||||
})
|
||||
}
|
||||
@@ -3179,6 +3181,9 @@ impl ChatComposer {
|
||||
if self.should_handle_vim_insert_escape(key_event) {
|
||||
return self.handle_input_basic(key_event);
|
||||
}
|
||||
if self.draft.textarea.is_vim_visual_mode() {
|
||||
return self.handle_input_basic(key_event);
|
||||
}
|
||||
if self.draft.textarea.is_vim_normal_mode() && self.draft.textarea.is_vim_operator_pending()
|
||||
{
|
||||
return self.handle_input_basic(key_event);
|
||||
@@ -4651,8 +4656,9 @@ impl ChatComposer {
|
||||
.textarea
|
||||
.render_ref_masked(textarea_rect, buf, &mut state, mask_char);
|
||||
} else {
|
||||
let highlight_ranges = self.history_search_highlight_ranges();
|
||||
if highlight_ranges.is_empty() {
|
||||
let history_highlight_ranges = self.history_search_highlight_ranges();
|
||||
let visual_highlight_range = self.draft.textarea.vim_visual_selection_range();
|
||||
if history_highlight_ranges.is_empty() && visual_highlight_range.is_none() {
|
||||
StatefulWidgetRef::render_ref(
|
||||
&(&self.draft.textarea),
|
||||
textarea_rect,
|
||||
@@ -4662,10 +4668,13 @@ impl ChatComposer {
|
||||
} else {
|
||||
let highlight_style =
|
||||
Style::default().add_modifier(Modifier::REVERSED | Modifier::BOLD);
|
||||
let highlights = highlight_ranges
|
||||
let mut highlights = history_highlight_ranges
|
||||
.into_iter()
|
||||
.map(|range| (range, highlight_style))
|
||||
.collect::<Vec<_>>();
|
||||
if let Some(range) = visual_highlight_range {
|
||||
highlights.push((range, Style::default().add_modifier(Modifier::REVERSED)));
|
||||
}
|
||||
self.draft.textarea.render_ref_styled_with_highlights(
|
||||
textarea_rect,
|
||||
buf,
|
||||
@@ -5499,6 +5508,68 @@ mod tests {
|
||||
assert!(!composer.footer.esc_backtrack_hint);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_visual_mode_selection_snapshots() {
|
||||
snapshot_composer_state(
|
||||
"vim_visual_characterwise_selection",
|
||||
/*enhanced_keys_supported*/ true,
|
||||
|composer| {
|
||||
composer.set_text_content("alpha beta".to_string(), Vec::new(), Vec::new());
|
||||
composer.set_vim_enabled(/*enabled*/ true);
|
||||
let _ = composer
|
||||
.handle_key_event(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::NONE));
|
||||
let _ = composer
|
||||
.handle_key_event(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::NONE));
|
||||
},
|
||||
);
|
||||
snapshot_composer_state(
|
||||
"vim_visual_line_selection",
|
||||
/*enhanced_keys_supported*/ true,
|
||||
|composer| {
|
||||
composer.set_text_content("alpha\nbeta\ngamma".to_string(), Vec::new(), Vec::new());
|
||||
composer.set_vim_enabled(/*enabled*/ true);
|
||||
let _ = composer
|
||||
.handle_key_event(KeyEvent::new(KeyCode::Char('V'), KeyModifiers::NONE));
|
||||
let _ = composer
|
||||
.handle_key_event(KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_visual_selection_renders_reversed_cells() {
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
/*has_input_focus*/ true,
|
||||
sender,
|
||||
/*enhanced_keys_supported*/ true,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
/*disable_paste_burst*/ false,
|
||||
);
|
||||
composer.set_text_content("alpha beta".to_string(), Vec::new(), Vec::new());
|
||||
composer.set_vim_enabled(/*enabled*/ true);
|
||||
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::NONE));
|
||||
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::NONE));
|
||||
|
||||
let area = Rect::new(0, 0, 100, 9);
|
||||
let mut buf = Buffer::empty(area);
|
||||
composer.render(area, &mut buf);
|
||||
|
||||
assert!(
|
||||
buf[(2, 1)]
|
||||
.style()
|
||||
.add_modifier
|
||||
.contains(Modifier::REVERSED)
|
||||
);
|
||||
assert!(
|
||||
!buf[(8, 1)]
|
||||
.style()
|
||||
.add_modifier
|
||||
.contains(Modifier::REVERSED)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_opens_command_popup_in_vim_normal_mode() {
|
||||
use crossterm::event::KeyCode;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" "
|
||||
"› alpha beta "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" 100% context left | Vim: Visual "
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" "
|
||||
"› alpha "
|
||||
" beta "
|
||||
" gamma "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" 100% context left | Vim: Visual Line "
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/textarea.rs
|
||||
expression: "states.join(\"\\n\\n\")"
|
||||
---
|
||||
alpha beta gamma
|
||||
^
|
||||
|
||||
alpha beta gamma
|
||||
^
|
||||
|
||||
alpha beta gamma
|
||||
^
|
||||
File diff suppressed because it is too large
Load Diff
442
codex-rs/tui/src/bottom_pane/textarea/vim.rs
Normal file
442
codex-rs/tui/src/bottom_pane/textarea/vim.rs
Normal file
@@ -0,0 +1,442 @@
|
||||
use super::TextArea;
|
||||
use super::split_word_pieces;
|
||||
use crate::key_hint::KeyBindingListExt;
|
||||
use crossterm::event::KeyEvent;
|
||||
use std::ops::Range;
|
||||
|
||||
mod count;
|
||||
mod find;
|
||||
mod prose;
|
||||
mod register;
|
||||
mod repeat;
|
||||
mod tag;
|
||||
mod visual;
|
||||
|
||||
#[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,
|
||||
/// Visual mode extends a characterwise or linewise selection from an anchor.
|
||||
Visual(VimVisualKind),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum VimOperator {
|
||||
Delete,
|
||||
Yank,
|
||||
Change,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum VimPending {
|
||||
None,
|
||||
Register,
|
||||
Operator(VimOperator),
|
||||
TextObject {
|
||||
operator: VimOperator,
|
||||
scope: VimTextObjectScope,
|
||||
},
|
||||
Find {
|
||||
operator: Option<VimOperator>,
|
||||
kind: VimFindKind,
|
||||
},
|
||||
VisualTextObject {
|
||||
scope: VimTextObjectScope,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) struct VimRegisterSelection {
|
||||
pub(super) name: char,
|
||||
pub(super) append: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum VimVisualKind {
|
||||
Characterwise,
|
||||
Linewise,
|
||||
}
|
||||
|
||||
#[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 VimFindKind {
|
||||
FindForward,
|
||||
FindBackward,
|
||||
TillForward,
|
||||
TillBackward,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) struct VimFind {
|
||||
pub(super) kind: VimFindKind,
|
||||
pub(super) target: char,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) enum VimRepeatTarget {
|
||||
Characters(usize),
|
||||
Lines(usize),
|
||||
Motion {
|
||||
motion: VimMotion,
|
||||
count: usize,
|
||||
},
|
||||
TextObject {
|
||||
object: VimTextObject,
|
||||
scope: VimTextObjectScope,
|
||||
count: usize,
|
||||
},
|
||||
Find {
|
||||
find: VimFind,
|
||||
count: usize,
|
||||
},
|
||||
Visual {
|
||||
kind: VimVisualKind,
|
||||
atomic_units: usize,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) enum VimInsertEdit {
|
||||
Insert(String),
|
||||
DeleteBackward,
|
||||
DeleteForward,
|
||||
DeleteBackwardWord,
|
||||
DeleteForwardWord,
|
||||
KillLineStart,
|
||||
KillWholeLine,
|
||||
KillLineEnd,
|
||||
MoveLeft,
|
||||
MoveRight,
|
||||
MoveUp,
|
||||
MoveDown,
|
||||
MoveWordLeft,
|
||||
MoveWordRight,
|
||||
MoveLineStart { move_up_at_bol: bool },
|
||||
MoveLineEnd { move_down_at_eol: bool },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) struct VimInsertRecording {
|
||||
pub(super) target: VimRepeatTarget,
|
||||
pub(super) edits: Vec<VimInsertEdit>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) enum VimRepeatChange {
|
||||
Delete(VimRepeatTarget),
|
||||
Change {
|
||||
target: VimRepeatTarget,
|
||||
edits: Vec<VimInsertEdit>,
|
||||
},
|
||||
Paste {
|
||||
text: String,
|
||||
kind: super::KillBufferKind,
|
||||
count: usize,
|
||||
},
|
||||
}
|
||||
|
||||
#[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,
|
||||
Sentence,
|
||||
Paragraph,
|
||||
Tag,
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
if self.vim_text_object_keymap.sentence.is_pressed(event) {
|
||||
return Some(VimTextObject::Sentence);
|
||||
}
|
||||
if self.vim_text_object_keymap.paragraph.is_pressed(event) {
|
||||
return Some(VimTextObject::Paragraph);
|
||||
}
|
||||
if self.vim_text_object_keymap.tag.is_pressed(event) {
|
||||
return Some(VimTextObject::Tag);
|
||||
}
|
||||
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, '`'),
|
||||
VimTextObject::Sentence => self.sentence_text_object_range(scope),
|
||||
VimTextObject::Paragraph => self.paragraph_text_object_range(scope),
|
||||
VimTextObject::Tag => self.tag_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()
|
||||
}
|
||||
160
codex-rs/tui/src/bottom_pane/textarea/vim/count.rs
Normal file
160
codex-rs/tui/src/bottom_pane/textarea/vim/count.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use super::super::TextArea;
|
||||
use super::VimMotion;
|
||||
use super::VimOperator;
|
||||
use super::VimTextObject;
|
||||
use super::VimTextObjectScope;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
use std::ops::Range;
|
||||
|
||||
impl TextArea {
|
||||
pub(in super::super) fn capture_vim_count_digit(&mut self, event: KeyEvent) -> bool {
|
||||
let KeyEvent {
|
||||
code: KeyCode::Char(digit @ '0'..='9'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
} = event
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if digit == '0' && self.vim_count.is_none() {
|
||||
return false;
|
||||
}
|
||||
let digit = digit.to_digit(10).unwrap_or_default() as usize;
|
||||
self.vim_count = Some(
|
||||
self.vim_count
|
||||
.unwrap_or_default()
|
||||
.saturating_mul(10)
|
||||
.saturating_add(digit),
|
||||
);
|
||||
true
|
||||
}
|
||||
|
||||
pub(in super::super) fn begin_vim_operator(&mut self, operator: VimOperator) {
|
||||
self.vim_operator_count = self.vim_count.take();
|
||||
self.vim_pending = super::VimPending::Operator(operator);
|
||||
}
|
||||
|
||||
pub(in super::super) fn take_vim_count(&mut self) -> usize {
|
||||
self.vim_count.take().unwrap_or(1).max(1)
|
||||
}
|
||||
|
||||
pub(in super::super) fn take_vim_operator_count(&mut self) -> usize {
|
||||
let operator = self.vim_operator_count.take().unwrap_or(1);
|
||||
operator.saturating_mul(self.take_vim_count()).max(1)
|
||||
}
|
||||
|
||||
pub(in super::super) fn clear_vim_counts(&mut self) {
|
||||
self.vim_count = None;
|
||||
self.vim_operator_count = None;
|
||||
}
|
||||
|
||||
pub(in super::super) fn apply_counted_vim_normal_motion(
|
||||
&mut self,
|
||||
motion: VimMotion,
|
||||
count: usize,
|
||||
) {
|
||||
self.clear_vim_register_selection();
|
||||
if motion == VimMotion::LineEnd {
|
||||
for _ in 1..count {
|
||||
self.move_cursor_down();
|
||||
}
|
||||
self.set_cursor(self.vim_line_end_cursor());
|
||||
return;
|
||||
}
|
||||
for _ in 0..count {
|
||||
match motion {
|
||||
VimMotion::Left => self.move_cursor_left(),
|
||||
VimMotion::Right => self.move_cursor_right(),
|
||||
VimMotion::Up => self.move_cursor_up(),
|
||||
VimMotion::Down => self.move_cursor_down(),
|
||||
VimMotion::WordForward => self.set_cursor(self.beginning_of_next_word()),
|
||||
VimMotion::WordBackward => self.set_cursor(self.beginning_of_previous_word()),
|
||||
VimMotion::WordEnd => self.set_cursor(self.vim_word_end_cursor()),
|
||||
VimMotion::LineStart => self.set_cursor(self.beginning_of_current_line()),
|
||||
VimMotion::LineEnd => unreachable!("handled before count loop"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(in super::super) fn counted_text_object_range(
|
||||
&mut self,
|
||||
object: VimTextObject,
|
||||
scope: VimTextObjectScope,
|
||||
count: usize,
|
||||
) -> Option<Range<usize>> {
|
||||
let original_cursor = self.cursor_pos;
|
||||
let mut aggregate = self.text_object_range(object, scope)?;
|
||||
for _ in 1..count {
|
||||
let previous_end = aggregate.end;
|
||||
let mut probe = previous_end;
|
||||
let mut next_range = None;
|
||||
while probe <= self.text.len() {
|
||||
self.cursor_pos = probe;
|
||||
if let Some(candidate) = self.text_object_range(object, scope)
|
||||
&& candidate.end > previous_end
|
||||
{
|
||||
next_range = Some(candidate);
|
||||
break;
|
||||
}
|
||||
if probe == self.text.len() {
|
||||
break;
|
||||
}
|
||||
let next = self.next_atomic_boundary(probe);
|
||||
if next <= probe {
|
||||
break;
|
||||
}
|
||||
probe = next;
|
||||
}
|
||||
let Some(next_range) = next_range else {
|
||||
break;
|
||||
};
|
||||
aggregate.start = aggregate.start.min(next_range.start);
|
||||
aggregate.end = aggregate.end.max(next_range.end);
|
||||
}
|
||||
self.cursor_pos = original_cursor;
|
||||
Some(aggregate)
|
||||
}
|
||||
|
||||
pub(in super::super) fn vim_yank_current_lines(&mut self, count: usize) {
|
||||
let range = self.current_line_range_with_count(count);
|
||||
self.yank_line_range(range);
|
||||
}
|
||||
|
||||
pub(in super::super) fn vim_kill_current_lines(&mut self, count: usize) {
|
||||
let range = self.current_line_range_with_count(count);
|
||||
self.kill_line_range(range);
|
||||
}
|
||||
|
||||
pub(in super::super) fn vim_change_current_lines(&mut self, count: usize) {
|
||||
let range = self.current_lines_change_range(count);
|
||||
self.kill_line_range(range);
|
||||
self.vim_mode = super::VimMode::Insert;
|
||||
}
|
||||
|
||||
pub(in super::super) fn current_line_range_with_count(&self, count: usize) -> Range<usize> {
|
||||
let start = self.beginning_of_current_line();
|
||||
let mut end = self.current_line_range_with_newline().end;
|
||||
for _ in 1..count {
|
||||
if end >= self.text.len() {
|
||||
break;
|
||||
}
|
||||
let eol = self.end_of_line(end);
|
||||
end = if eol < self.text.len() { eol + 1 } else { eol };
|
||||
}
|
||||
start..end
|
||||
}
|
||||
|
||||
fn current_lines_change_range(&self, count: usize) -> Range<usize> {
|
||||
let start = self.beginning_of_current_line();
|
||||
let mut end = self.end_of_current_line();
|
||||
for _ in 1..count {
|
||||
if end >= self.text.len() {
|
||||
break;
|
||||
}
|
||||
end = self.end_of_line(end + 1);
|
||||
}
|
||||
start..end
|
||||
}
|
||||
}
|
||||
169
codex-rs/tui/src/bottom_pane/textarea/vim/find.rs
Normal file
169
codex-rs/tui/src/bottom_pane/textarea/vim/find.rs
Normal file
@@ -0,0 +1,169 @@
|
||||
use super::super::TextArea;
|
||||
use super::VimFind;
|
||||
use super::VimFindKind;
|
||||
use super::VimOperator;
|
||||
use super::VimRepeatTarget;
|
||||
use crate::key_hint::KeyBindingListExt;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use std::ops::Range;
|
||||
|
||||
impl VimFindKind {
|
||||
pub(in super::super) fn reverse(self) -> Self {
|
||||
match self {
|
||||
Self::FindForward => Self::FindBackward,
|
||||
Self::FindBackward => Self::FindForward,
|
||||
Self::TillForward => Self::TillBackward,
|
||||
Self::TillBackward => Self::TillForward,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextArea {
|
||||
pub(in super::super) fn handle_vim_find_target(
|
||||
&mut self,
|
||||
operator: Option<VimOperator>,
|
||||
kind: VimFindKind,
|
||||
event: KeyEvent,
|
||||
) -> bool {
|
||||
if self.vim_normal_keymap.cancel_operator.is_pressed(event)
|
||||
|| self.vim_operator_keymap.cancel.is_pressed(event)
|
||||
{
|
||||
self.clear_vim_counts();
|
||||
return true;
|
||||
}
|
||||
let KeyCode::Char(target) = event.code else {
|
||||
self.clear_vim_counts();
|
||||
return true;
|
||||
};
|
||||
let find = VimFind { kind, target };
|
||||
self.vim_last_find = Some(find);
|
||||
let count = if operator.is_some() {
|
||||
self.take_vim_operator_count()
|
||||
} else {
|
||||
self.take_vim_count()
|
||||
};
|
||||
self.execute_vim_find(operator, find, count);
|
||||
true
|
||||
}
|
||||
|
||||
pub(in super::super) fn vim_normal_find_kind_for_event(
|
||||
&self,
|
||||
event: KeyEvent,
|
||||
) -> Option<VimFindKind> {
|
||||
if self.vim_normal_keymap.find_forward.is_pressed(event) {
|
||||
return Some(VimFindKind::FindForward);
|
||||
}
|
||||
if self.vim_normal_keymap.find_backward.is_pressed(event) {
|
||||
return Some(VimFindKind::FindBackward);
|
||||
}
|
||||
if self.vim_normal_keymap.till_forward.is_pressed(event) {
|
||||
return Some(VimFindKind::TillForward);
|
||||
}
|
||||
if self.vim_normal_keymap.till_backward.is_pressed(event) {
|
||||
return Some(VimFindKind::TillBackward);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(in super::super) fn vim_operator_find_kind_for_event(
|
||||
&self,
|
||||
event: KeyEvent,
|
||||
) -> Option<VimFindKind> {
|
||||
if self.vim_operator_keymap.find_forward.is_pressed(event) {
|
||||
return Some(VimFindKind::FindForward);
|
||||
}
|
||||
if self.vim_operator_keymap.find_backward.is_pressed(event) {
|
||||
return Some(VimFindKind::FindBackward);
|
||||
}
|
||||
if self.vim_operator_keymap.till_forward.is_pressed(event) {
|
||||
return Some(VimFindKind::TillForward);
|
||||
}
|
||||
if self.vim_operator_keymap.till_backward.is_pressed(event) {
|
||||
return Some(VimFindKind::TillBackward);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(in super::super) fn repeat_vim_find(
|
||||
&mut self,
|
||||
operator: Option<VimOperator>,
|
||||
reverse: bool,
|
||||
count: usize,
|
||||
) {
|
||||
let Some(mut find) = self.vim_last_find else {
|
||||
return;
|
||||
};
|
||||
if reverse {
|
||||
find.kind = find.kind.reverse();
|
||||
}
|
||||
self.execute_vim_find(operator, find, count);
|
||||
}
|
||||
|
||||
fn execute_vim_find(&mut self, operator: Option<VimOperator>, find: VimFind, count: usize) {
|
||||
if let Some(operator) = operator {
|
||||
if let Some(range) = self.range_for_find(find, count) {
|
||||
self.apply_vim_operator_to_range(
|
||||
operator,
|
||||
range,
|
||||
Some(VimRepeatTarget::Find { find, count }),
|
||||
);
|
||||
}
|
||||
} else if let Some(target) = self.target_for_find(find, count) {
|
||||
self.set_cursor(target);
|
||||
}
|
||||
self.clear_vim_register_selection();
|
||||
}
|
||||
|
||||
pub(in super::super) fn target_for_find(&self, find: VimFind, count: usize) -> Option<usize> {
|
||||
let matched = self.find_match(find, count)?;
|
||||
Some(match find.kind {
|
||||
VimFindKind::FindForward | VimFindKind::FindBackward => matched,
|
||||
VimFindKind::TillForward => self.prev_atomic_boundary(matched),
|
||||
VimFindKind::TillBackward => self.next_atomic_boundary(matched),
|
||||
})
|
||||
}
|
||||
|
||||
pub(in super::super) fn range_for_find(
|
||||
&self,
|
||||
find: VimFind,
|
||||
count: usize,
|
||||
) -> Option<Range<usize>> {
|
||||
let matched = self.find_match(find, count)?;
|
||||
let range = match find.kind {
|
||||
VimFindKind::FindForward => self.cursor_pos..self.next_atomic_boundary(matched),
|
||||
VimFindKind::TillForward => self.cursor_pos..matched,
|
||||
VimFindKind::FindBackward => matched..self.cursor_pos,
|
||||
VimFindKind::TillBackward => self.next_atomic_boundary(matched)..self.cursor_pos,
|
||||
};
|
||||
(range.start < range.end).then_some(range)
|
||||
}
|
||||
|
||||
fn find_match(&self, find: VimFind, count: usize) -> Option<usize> {
|
||||
let line_start = self.beginning_of_current_line();
|
||||
let line_end = self.end_of_current_line();
|
||||
match find.kind {
|
||||
VimFindKind::FindForward | VimFindKind::TillForward => {
|
||||
let start = self.next_atomic_boundary(self.cursor_pos).min(line_end);
|
||||
self.text[start..line_end]
|
||||
.char_indices()
|
||||
.map(|(offset, _)| start + offset)
|
||||
.filter(|&idx| self.matches_find_target(idx, find.target))
|
||||
.nth(count.saturating_sub(1))
|
||||
}
|
||||
VimFindKind::FindBackward | VimFindKind::TillBackward => self.text
|
||||
[line_start..self.cursor_pos]
|
||||
.char_indices()
|
||||
.map(|(offset, _)| line_start + offset)
|
||||
.rev()
|
||||
.filter(|&idx| self.matches_find_target(idx, find.target))
|
||||
.nth(count.saturating_sub(1)),
|
||||
}
|
||||
}
|
||||
|
||||
fn matches_find_target(&self, idx: usize, target: char) -> bool {
|
||||
!self.is_inside_element(idx)
|
||||
&& self.clamp_pos_to_nearest_boundary(idx) == idx
|
||||
&& self.text[idx..].starts_with(target)
|
||||
}
|
||||
}
|
||||
138
codex-rs/tui/src/bottom_pane/textarea/vim/prose.rs
Normal file
138
codex-rs/tui/src/bottom_pane/textarea/vim/prose.rs
Normal file
@@ -0,0 +1,138 @@
|
||||
use super::super::TextArea;
|
||||
use super::VimTextObjectScope;
|
||||
use std::ops::Range;
|
||||
|
||||
impl TextArea {
|
||||
pub(in super::super) fn sentence_text_object_range(
|
||||
&self,
|
||||
scope: VimTextObjectScope,
|
||||
) -> Option<Range<usize>> {
|
||||
let inner = self.prose_range_at_cursor(self.sentence_ranges())?;
|
||||
Some(self.expand_prose_range(scope, inner))
|
||||
}
|
||||
|
||||
pub(in super::super) fn paragraph_text_object_range(
|
||||
&self,
|
||||
scope: VimTextObjectScope,
|
||||
) -> Option<Range<usize>> {
|
||||
let inner = self.prose_range_at_cursor(self.paragraph_ranges())?;
|
||||
Some(self.expand_prose_range(scope, inner))
|
||||
}
|
||||
|
||||
fn sentence_ranges(&self) -> Vec<Range<usize>> {
|
||||
let mut ranges = Vec::new();
|
||||
let mut start = self.skip_prose_separator(/*pos*/ 0);
|
||||
let mut pos = start;
|
||||
while pos < self.text.len() {
|
||||
let Some(ch) = self.text[pos..].chars().next() else {
|
||||
break;
|
||||
};
|
||||
let next = pos + ch.len_utf8();
|
||||
if !self.is_inside_element(pos) && matches!(ch, '.' | '!' | '?') {
|
||||
let end = self.sentence_closing_punctuation_end(next);
|
||||
if end == self.text.len()
|
||||
|| self.text[end..]
|
||||
.chars()
|
||||
.next()
|
||||
.is_some_and(char::is_whitespace)
|
||||
{
|
||||
ranges.push(start..end);
|
||||
start = self.skip_prose_separator(end);
|
||||
pos = start;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
pos = next;
|
||||
}
|
||||
if start < self.text.len() {
|
||||
ranges.push(start..self.text.len());
|
||||
}
|
||||
ranges
|
||||
}
|
||||
|
||||
fn sentence_closing_punctuation_end(&self, mut pos: usize) -> usize {
|
||||
while pos < self.text.len() && !self.is_inside_element(pos) {
|
||||
let Some(ch) = self.text[pos..].chars().next() else {
|
||||
break;
|
||||
};
|
||||
if !matches!(ch, ')' | ']' | '}' | '"' | '\'' | '`') {
|
||||
break;
|
||||
}
|
||||
pos += ch.len_utf8();
|
||||
}
|
||||
pos
|
||||
}
|
||||
|
||||
fn paragraph_ranges(&self) -> Vec<Range<usize>> {
|
||||
let mut ranges = Vec::new();
|
||||
let mut block_start = None;
|
||||
let mut block_end = 0;
|
||||
let mut line_start = 0;
|
||||
loop {
|
||||
let line_end = self.text[line_start..]
|
||||
.find('\n')
|
||||
.map_or(self.text.len(), |offset| line_start + offset);
|
||||
if self.line_has_prose_content(line_start..line_end) {
|
||||
block_start.get_or_insert(line_start);
|
||||
block_end = line_end;
|
||||
} else if let Some(start) = block_start.take() {
|
||||
ranges.push(start..block_end);
|
||||
}
|
||||
if line_end == self.text.len() {
|
||||
break;
|
||||
}
|
||||
line_start = line_end + '\n'.len_utf8();
|
||||
}
|
||||
if let Some(start) = block_start {
|
||||
ranges.push(start..block_end);
|
||||
}
|
||||
ranges
|
||||
}
|
||||
|
||||
fn line_has_prose_content(&self, range: Range<usize>) -> bool {
|
||||
self.text[range.clone()]
|
||||
.char_indices()
|
||||
.any(|(offset, ch)| !ch.is_whitespace() || self.is_inside_element(range.start + offset))
|
||||
}
|
||||
|
||||
fn prose_range_at_cursor(&self, ranges: Vec<Range<usize>>) -> Option<Range<usize>> {
|
||||
ranges
|
||||
.iter()
|
||||
.find(|range| range.start <= self.cursor_pos && self.cursor_pos <= range.end)
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
ranges
|
||||
.iter()
|
||||
.find(|range| self.cursor_pos < range.start)
|
||||
.cloned()
|
||||
})
|
||||
.or_else(|| ranges.last().cloned())
|
||||
}
|
||||
|
||||
fn expand_prose_range(&self, scope: VimTextObjectScope, inner: Range<usize>) -> Range<usize> {
|
||||
match scope {
|
||||
VimTextObjectScope::Inner => inner,
|
||||
VimTextObjectScope::Around => {
|
||||
let following = self.following_whitespace_end(inner.end);
|
||||
if following > inner.end {
|
||||
inner.start..following
|
||||
} else {
|
||||
self.preceding_whitespace_start(inner.start)..inner.end
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn skip_prose_separator(&self, mut pos: usize) -> usize {
|
||||
while pos < self.text.len() && !self.is_inside_element(pos) {
|
||||
let Some(ch) = self.text[pos..].chars().next() else {
|
||||
break;
|
||||
};
|
||||
if !ch.is_whitespace() {
|
||||
break;
|
||||
}
|
||||
pos += ch.len_utf8();
|
||||
}
|
||||
pos
|
||||
}
|
||||
}
|
||||
75
codex-rs/tui/src/bottom_pane/textarea/vim/register.rs
Normal file
75
codex-rs/tui/src/bottom_pane/textarea/vim/register.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use super::super::KillBufferKind;
|
||||
use super::super::TextArea;
|
||||
use super::VimRegisterSelection;
|
||||
use crate::key_hint::KeyBindingListExt;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
|
||||
impl TextArea {
|
||||
pub(in super::super) fn handle_vim_register_name(&mut self, event: KeyEvent) -> bool {
|
||||
if self.vim_normal_keymap.cancel_operator.is_pressed(event) {
|
||||
self.clear_vim_register_selection();
|
||||
self.clear_vim_counts();
|
||||
return true;
|
||||
}
|
||||
let KeyCode::Char(name) = event.code else {
|
||||
self.clear_vim_register_selection();
|
||||
self.clear_vim_counts();
|
||||
return true;
|
||||
};
|
||||
if !name.is_ascii_alphabetic() {
|
||||
self.clear_vim_register_selection();
|
||||
self.clear_vim_counts();
|
||||
return true;
|
||||
}
|
||||
self.vim_selected_register = Some(VimRegisterSelection {
|
||||
name: name.to_ascii_lowercase(),
|
||||
append: name.is_ascii_uppercase(),
|
||||
});
|
||||
true
|
||||
}
|
||||
|
||||
pub(in super::super) fn clear_vim_register_selection(&mut self) {
|
||||
self.vim_selected_register = None;
|
||||
}
|
||||
|
||||
pub(in super::super) fn write_selected_vim_register(
|
||||
&mut self,
|
||||
text: &str,
|
||||
kind: KillBufferKind,
|
||||
) {
|
||||
let Some(selection) = self.vim_selected_register.take() else {
|
||||
return;
|
||||
};
|
||||
if selection.append {
|
||||
let entry = self
|
||||
.vim_named_registers
|
||||
.entry(selection.name)
|
||||
.or_insert_with(|| (String::new(), kind));
|
||||
entry.0.push_str(text);
|
||||
entry.1 = kind;
|
||||
} else {
|
||||
self.vim_named_registers
|
||||
.insert(selection.name, (text.to_string(), kind));
|
||||
}
|
||||
}
|
||||
|
||||
pub(in super::super) fn vim_paste_after_cursor_counted(&mut self, count: usize) {
|
||||
let source = if let Some(selection) = self.vim_selected_register.take() {
|
||||
self.vim_named_registers
|
||||
.get(&selection.name)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| (String::new(), KillBufferKind::Characterwise))
|
||||
} else {
|
||||
(self.kill_buffer.clone(), self.kill_buffer_kind)
|
||||
};
|
||||
if source.0.is_empty() {
|
||||
return;
|
||||
}
|
||||
let previous_len = self.text.len();
|
||||
for _ in 0..count {
|
||||
self.paste_text_after_cursor(&source.0, source.1);
|
||||
}
|
||||
self.record_vim_paste_if_changed(source.0, source.1, count, previous_len);
|
||||
}
|
||||
}
|
||||
337
codex-rs/tui/src/bottom_pane/textarea/vim/repeat.rs
Normal file
337
codex-rs/tui/src/bottom_pane/textarea/vim/repeat.rs
Normal file
@@ -0,0 +1,337 @@
|
||||
use super::super::KillBufferKind;
|
||||
use super::super::TextArea;
|
||||
use super::VimInsertEdit;
|
||||
use super::VimInsertRecording;
|
||||
use super::VimMode;
|
||||
use super::VimOperator;
|
||||
use super::VimRepeatChange;
|
||||
use super::VimRepeatTarget;
|
||||
use super::VimVisualKind;
|
||||
use crate::key_hint::KeyBindingListExt;
|
||||
use crate::key_hint::is_altgr;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
use std::ops::Range;
|
||||
|
||||
impl TextArea {
|
||||
pub(in super::super) fn invalidate_vim_repeat_change(&mut self) {
|
||||
if !self.vim_replaying_change {
|
||||
self.vim_last_change = None;
|
||||
self.vim_insert_recording = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub(in super::super) fn record_vim_delete_if_changed(
|
||||
&mut self,
|
||||
target: VimRepeatTarget,
|
||||
previous_len: usize,
|
||||
) {
|
||||
if !self.vim_replaying_change && self.text.len() < previous_len {
|
||||
self.vim_last_change = Some(VimRepeatChange::Delete(target));
|
||||
self.vim_insert_recording = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub(in super::super) fn begin_vim_change_recording(&mut self, target: VimRepeatTarget) {
|
||||
if !self.vim_replaying_change {
|
||||
self.vim_insert_recording = Some(VimInsertRecording {
|
||||
target,
|
||||
edits: Vec::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(in super::super) fn complete_vim_change_recording(&mut self) {
|
||||
if self.vim_replaying_change {
|
||||
return;
|
||||
}
|
||||
if let Some(recording) = self.vim_insert_recording.take() {
|
||||
self.vim_last_change = Some(VimRepeatChange::Change {
|
||||
target: recording.target,
|
||||
edits: recording.edits,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(in super::super) fn record_vim_paste_if_changed(
|
||||
&mut self,
|
||||
text: String,
|
||||
kind: KillBufferKind,
|
||||
count: usize,
|
||||
previous_len: usize,
|
||||
) {
|
||||
if !self.vim_replaying_change && self.text.len() > previous_len {
|
||||
self.vim_last_change = Some(VimRepeatChange::Paste { text, kind, count });
|
||||
self.vim_insert_recording = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub(in super::super) fn record_vim_insert_edit(&mut self, edit: Option<VimInsertEdit>) {
|
||||
if self.vim_replaying_change {
|
||||
return;
|
||||
}
|
||||
if let Some(recording) = self.vim_insert_recording.as_mut()
|
||||
&& let Some(edit) = edit
|
||||
{
|
||||
recording.edits.push(edit);
|
||||
}
|
||||
}
|
||||
|
||||
pub(in super::super) fn vim_insert_edit_for_event(
|
||||
&self,
|
||||
event: KeyEvent,
|
||||
) -> Option<VimInsertEdit> {
|
||||
let keymap = &self.editor_keymap;
|
||||
if keymap.insert_newline.is_pressed(event) {
|
||||
return Some(VimInsertEdit::Insert("\n".to_string()));
|
||||
}
|
||||
if keymap.delete_backward_word.is_pressed(event) {
|
||||
return Some(VimInsertEdit::DeleteBackwardWord);
|
||||
}
|
||||
if let KeyEvent {
|
||||
code: KeyCode::Char(c),
|
||||
modifiers,
|
||||
..
|
||||
} = event
|
||||
&& is_altgr(modifiers)
|
||||
{
|
||||
return Some(VimInsertEdit::Insert(c.to_string()));
|
||||
}
|
||||
if keymap.delete_backward.is_pressed(event) {
|
||||
return Some(VimInsertEdit::DeleteBackward);
|
||||
}
|
||||
if keymap.delete_forward_word.is_pressed(event) {
|
||||
return Some(VimInsertEdit::DeleteForwardWord);
|
||||
}
|
||||
if keymap.delete_forward.is_pressed(event) {
|
||||
return Some(VimInsertEdit::DeleteForward);
|
||||
}
|
||||
if keymap.kill_line_start.is_pressed(event) {
|
||||
return Some(VimInsertEdit::KillLineStart);
|
||||
}
|
||||
if keymap.kill_whole_line.is_pressed(event) {
|
||||
return Some(VimInsertEdit::KillWholeLine);
|
||||
}
|
||||
if keymap.kill_line_end.is_pressed(event) {
|
||||
return Some(VimInsertEdit::KillLineEnd);
|
||||
}
|
||||
if keymap.yank.is_pressed(event) {
|
||||
return (!self.kill_buffer.is_empty())
|
||||
.then(|| VimInsertEdit::Insert(self.kill_buffer.clone()));
|
||||
}
|
||||
if keymap.move_word_left.is_pressed(event) {
|
||||
return Some(VimInsertEdit::MoveWordLeft);
|
||||
}
|
||||
if keymap.move_word_right.is_pressed(event) {
|
||||
return Some(VimInsertEdit::MoveWordRight);
|
||||
}
|
||||
if keymap.move_left.is_pressed(event) {
|
||||
return Some(VimInsertEdit::MoveLeft);
|
||||
}
|
||||
if keymap.move_right.is_pressed(event) {
|
||||
return Some(VimInsertEdit::MoveRight);
|
||||
}
|
||||
if keymap.move_up.is_pressed(event) {
|
||||
return Some(VimInsertEdit::MoveUp);
|
||||
}
|
||||
if keymap.move_down.is_pressed(event) {
|
||||
return Some(VimInsertEdit::MoveDown);
|
||||
}
|
||||
if keymap.move_line_start.is_pressed(event) {
|
||||
let move_up_at_bol = matches!(
|
||||
event,
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('a'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}
|
||||
);
|
||||
return Some(VimInsertEdit::MoveLineStart { move_up_at_bol });
|
||||
}
|
||||
if keymap.move_line_end.is_pressed(event) {
|
||||
let move_down_at_eol = matches!(
|
||||
event,
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('e'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}
|
||||
);
|
||||
return Some(VimInsertEdit::MoveLineEnd { move_down_at_eol });
|
||||
}
|
||||
if let KeyEvent {
|
||||
code: KeyCode::Char(c),
|
||||
modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT,
|
||||
..
|
||||
} = event
|
||||
&& !c.is_ascii_control()
|
||||
{
|
||||
return Some(VimInsertEdit::Insert(c.to_string()));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(in super::super) fn repeat_vim_change(&mut self) {
|
||||
let Some(change) = self.vim_last_change.clone() else {
|
||||
return;
|
||||
};
|
||||
self.vim_replaying_change = true;
|
||||
match change {
|
||||
VimRepeatChange::Delete(target) => {
|
||||
self.execute_vim_repeat_target(VimOperator::Delete, target);
|
||||
}
|
||||
VimRepeatChange::Change { target, edits } => {
|
||||
if self.execute_vim_repeat_target(VimOperator::Change, target) {
|
||||
for edit in edits {
|
||||
self.apply_vim_insert_edit(edit);
|
||||
}
|
||||
self.exit_vim_insert_after_change();
|
||||
}
|
||||
}
|
||||
VimRepeatChange::Paste { text, kind, count } => {
|
||||
for _ in 0..count {
|
||||
self.paste_text_after_cursor(&text, kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.vim_replaying_change = false;
|
||||
self.clear_vim_counts();
|
||||
self.clear_vim_register_selection();
|
||||
}
|
||||
|
||||
fn execute_vim_repeat_target(
|
||||
&mut self,
|
||||
operator: VimOperator,
|
||||
target: VimRepeatTarget,
|
||||
) -> bool {
|
||||
let previous_len = self.text.len();
|
||||
match target {
|
||||
VimRepeatTarget::Characters(count) => {
|
||||
self.delete_forward_kill(count);
|
||||
if operator == VimOperator::Change {
|
||||
self.vim_mode = VimMode::Insert;
|
||||
}
|
||||
}
|
||||
VimRepeatTarget::Lines(count) => match operator {
|
||||
VimOperator::Delete => self.vim_kill_current_lines(count),
|
||||
VimOperator::Change => self.vim_change_current_lines(count),
|
||||
VimOperator::Yank => unreachable!("repeat changes never yank"),
|
||||
},
|
||||
VimRepeatTarget::Motion { motion, count } => {
|
||||
self.apply_vim_operator(operator, motion, count);
|
||||
}
|
||||
VimRepeatTarget::TextObject {
|
||||
object,
|
||||
scope,
|
||||
count,
|
||||
} => {
|
||||
if let Some(range) = self.counted_text_object_range(object, scope, count) {
|
||||
self.apply_vim_operator_to_range(operator, range, /*repeat_target*/ None);
|
||||
}
|
||||
}
|
||||
VimRepeatTarget::Find { find, count } => {
|
||||
if let Some(range) = self.range_for_find(find, count) {
|
||||
self.apply_vim_operator_to_range(operator, range, /*repeat_target*/ None);
|
||||
}
|
||||
}
|
||||
VimRepeatTarget::Visual { kind, atomic_units } => {
|
||||
if let Some(mut range) = self.visual_repeat_range(kind, atomic_units) {
|
||||
if operator == VimOperator::Change
|
||||
&& kind == VimVisualKind::Linewise
|
||||
&& range.end > range.start
|
||||
&& self.text[..range.end].ends_with('\n')
|
||||
{
|
||||
range.end -= 1;
|
||||
}
|
||||
let buffer_kind = if kind == VimVisualKind::Linewise {
|
||||
KillBufferKind::Linewise
|
||||
} else {
|
||||
KillBufferKind::Characterwise
|
||||
};
|
||||
self.kill_range_with_kind(range, buffer_kind);
|
||||
if operator == VimOperator::Change {
|
||||
self.vim_mode = VimMode::Insert;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.text.len() < previous_len
|
||||
|| operator == VimOperator::Change && self.vim_mode == VimMode::Insert
|
||||
}
|
||||
|
||||
pub(in super::super) fn visual_repeat_target(
|
||||
&self,
|
||||
kind: VimVisualKind,
|
||||
range: Range<usize>,
|
||||
) -> VimRepeatTarget {
|
||||
let atomic_units = match kind {
|
||||
VimVisualKind::Characterwise => {
|
||||
let mut pos = range.start;
|
||||
let mut units = 0;
|
||||
while pos < range.end {
|
||||
let next = self.next_atomic_boundary(pos);
|
||||
if next <= pos {
|
||||
break;
|
||||
}
|
||||
units += 1;
|
||||
pos = next;
|
||||
}
|
||||
units.max(1)
|
||||
}
|
||||
VimVisualKind::Linewise => self.text[range].lines().count().max(1),
|
||||
};
|
||||
VimRepeatTarget::Visual { kind, atomic_units }
|
||||
}
|
||||
|
||||
fn visual_repeat_range(
|
||||
&self,
|
||||
kind: VimVisualKind,
|
||||
atomic_units: usize,
|
||||
) -> Option<Range<usize>> {
|
||||
let range = match kind {
|
||||
VimVisualKind::Characterwise => {
|
||||
let mut end = self.cursor_pos;
|
||||
for _ in 0..atomic_units {
|
||||
end = self.next_atomic_boundary(end);
|
||||
}
|
||||
self.cursor_pos..end
|
||||
}
|
||||
VimVisualKind::Linewise => self.current_line_range_with_count(atomic_units),
|
||||
};
|
||||
(range.start < range.end).then(|| self.expand_range_to_element_boundaries(range))
|
||||
}
|
||||
|
||||
fn apply_vim_insert_edit(&mut self, edit: VimInsertEdit) {
|
||||
match edit {
|
||||
VimInsertEdit::Insert(text) => self.insert_str(&text),
|
||||
VimInsertEdit::DeleteBackward => self.delete_backward(/*n*/ 1),
|
||||
VimInsertEdit::DeleteForward => self.delete_forward(/*n*/ 1),
|
||||
VimInsertEdit::DeleteBackwardWord => self.delete_backward_word(),
|
||||
VimInsertEdit::DeleteForwardWord => self.delete_forward_word(),
|
||||
VimInsertEdit::KillLineStart => self.kill_to_beginning_of_line(),
|
||||
VimInsertEdit::KillWholeLine => self.kill_current_line(),
|
||||
VimInsertEdit::KillLineEnd => self.kill_to_end_of_line(),
|
||||
VimInsertEdit::MoveLeft => self.move_cursor_left(),
|
||||
VimInsertEdit::MoveRight => self.move_cursor_right(),
|
||||
VimInsertEdit::MoveUp => self.move_cursor_up(),
|
||||
VimInsertEdit::MoveDown => self.move_cursor_down(),
|
||||
VimInsertEdit::MoveWordLeft => self.set_cursor(self.beginning_of_previous_word()),
|
||||
VimInsertEdit::MoveWordRight => self.set_cursor(self.end_of_next_word()),
|
||||
VimInsertEdit::MoveLineStart { move_up_at_bol } => {
|
||||
self.move_cursor_to_beginning_of_line(move_up_at_bol);
|
||||
}
|
||||
VimInsertEdit::MoveLineEnd { move_down_at_eol } => {
|
||||
self.move_cursor_to_end_of_line(move_down_at_eol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(in super::super) fn exit_vim_insert_after_change(&mut self) {
|
||||
let bol = self.beginning_of_current_line();
|
||||
if self.cursor_pos > bol {
|
||||
self.cursor_pos = self.prev_atomic_boundary(self.cursor_pos).max(bol);
|
||||
}
|
||||
self.enter_vim_normal_mode();
|
||||
}
|
||||
}
|
||||
148
codex-rs/tui/src/bottom_pane/textarea/vim/tag.rs
Normal file
148
codex-rs/tui/src/bottom_pane/textarea/vim/tag.rs
Normal file
@@ -0,0 +1,148 @@
|
||||
use super::super::TextArea;
|
||||
use super::VimTextObjectScope;
|
||||
use std::ops::Range;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ParsedTag {
|
||||
Open { name: String, range: Range<usize> },
|
||||
Close { name: String, range: Range<usize> },
|
||||
Ignored { end: usize },
|
||||
}
|
||||
|
||||
impl ParsedTag {
|
||||
fn end(&self) -> usize {
|
||||
match self {
|
||||
Self::Open { range, .. } | Self::Close { range, .. } => range.end,
|
||||
Self::Ignored { end } => *end,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextArea {
|
||||
pub(in super::super) fn tag_text_object_range(
|
||||
&self,
|
||||
scope: VimTextObjectScope,
|
||||
) -> Option<Range<usize>> {
|
||||
let mut open_tags: Vec<(String, Range<usize>)> = Vec::new();
|
||||
let mut best = None;
|
||||
let mut pos = 0;
|
||||
while let Some(offset) = self.text[pos..].find('<') {
|
||||
let start = pos + offset;
|
||||
if self.is_inside_element(start) {
|
||||
pos = start + '<'.len_utf8();
|
||||
continue;
|
||||
}
|
||||
let Some(tag) = self.parse_tag(start) else {
|
||||
pos = start + '<'.len_utf8();
|
||||
continue;
|
||||
};
|
||||
pos = tag.end();
|
||||
match tag {
|
||||
ParsedTag::Open { name, range } => open_tags.push((name, range)),
|
||||
ParsedTag::Close { name, range } => {
|
||||
let Some((open_name, open_range)) = open_tags.last() else {
|
||||
continue;
|
||||
};
|
||||
if *open_name != name {
|
||||
continue;
|
||||
}
|
||||
let open_range = open_range.clone();
|
||||
open_tags.pop();
|
||||
if open_range.start <= self.cursor_pos && self.cursor_pos <= range.end {
|
||||
let candidate = match scope {
|
||||
VimTextObjectScope::Inner => open_range.end..range.start,
|
||||
VimTextObjectScope::Around => open_range.start..range.end,
|
||||
};
|
||||
if best
|
||||
.as_ref()
|
||||
.is_none_or(|current: &Range<usize>| candidate.len() < current.len())
|
||||
{
|
||||
best = Some(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
ParsedTag::Ignored { .. } => {}
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
fn parse_tag(&self, start: usize) -> Option<ParsedTag> {
|
||||
let rest = &self.text[start..];
|
||||
if rest.starts_with("<!--") {
|
||||
return rest.find("-->").map(|offset| ParsedTag::Ignored {
|
||||
end: start + offset + "-->".len(),
|
||||
});
|
||||
}
|
||||
if rest.starts_with("<!") || rest.starts_with("<?") {
|
||||
return self
|
||||
.tag_end(start + '<'.len_utf8())
|
||||
.map(|end| ParsedTag::Ignored { end });
|
||||
}
|
||||
|
||||
let mut pos = start + '<'.len_utf8();
|
||||
let closing = self.text[pos..].starts_with('/');
|
||||
if closing {
|
||||
pos += '/'.len_utf8();
|
||||
}
|
||||
let name_start = pos;
|
||||
let first = self.text[pos..].chars().next()?;
|
||||
if !is_tag_name_start(first) {
|
||||
return None;
|
||||
}
|
||||
pos += first.len_utf8();
|
||||
while let Some(ch) = self.text[pos..].chars().next() {
|
||||
if !is_tag_name_char(ch) {
|
||||
break;
|
||||
}
|
||||
pos += ch.len_utf8();
|
||||
}
|
||||
let name = self.text[name_start..pos].to_string();
|
||||
let end = self.tag_end(pos)?;
|
||||
if closing {
|
||||
if !self.text[pos..end - '>'.len_utf8()].trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
return Some(ParsedTag::Close {
|
||||
name,
|
||||
range: start..end,
|
||||
});
|
||||
}
|
||||
let body = self.text[pos..end - '>'.len_utf8()].trim_end();
|
||||
if body.ends_with('/') {
|
||||
return Some(ParsedTag::Ignored { end });
|
||||
}
|
||||
Some(ParsedTag::Open {
|
||||
name,
|
||||
range: start..end,
|
||||
})
|
||||
}
|
||||
|
||||
fn tag_end(&self, mut pos: usize) -> Option<usize> {
|
||||
let mut quote = None;
|
||||
while let Some(ch) = self.text[pos..].chars().next() {
|
||||
if self.is_inside_element(pos) {
|
||||
return None;
|
||||
}
|
||||
if let Some(open_quote) = quote {
|
||||
if ch == open_quote {
|
||||
quote = None;
|
||||
}
|
||||
} else if matches!(ch, '"' | '\'') {
|
||||
quote = Some(ch);
|
||||
} else if ch == '>' {
|
||||
return Some(pos + ch.len_utf8());
|
||||
}
|
||||
pos += ch.len_utf8();
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn is_tag_name_start(ch: char) -> bool {
|
||||
ch.is_ascii_alphabetic() || ch == '_'
|
||||
}
|
||||
|
||||
fn is_tag_name_char(ch: char) -> bool {
|
||||
ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | ':')
|
||||
}
|
||||
265
codex-rs/tui/src/bottom_pane/textarea/vim/visual.rs
Normal file
265
codex-rs/tui/src/bottom_pane/textarea/vim/visual.rs
Normal file
@@ -0,0 +1,265 @@
|
||||
use super::super::KillBufferKind;
|
||||
use super::super::TextArea;
|
||||
use super::VimMode;
|
||||
use super::VimMotion;
|
||||
use super::VimOperator;
|
||||
use super::VimPending;
|
||||
use super::VimTextObjectScope;
|
||||
use super::VimVisualKind;
|
||||
use crate::key_hint::KeyBindingListExt;
|
||||
use crossterm::event::KeyEvent;
|
||||
use std::ops::Range;
|
||||
|
||||
impl TextArea {
|
||||
pub(in super::super) fn enter_vim_visual_mode(&mut self, kind: VimVisualKind) {
|
||||
self.vim_visual_anchor = Some(self.cursor_pos);
|
||||
self.vim_mode = VimMode::Visual(kind);
|
||||
self.vim_pending = VimPending::None;
|
||||
self.clear_vim_counts();
|
||||
self.clear_vim_register_selection();
|
||||
}
|
||||
|
||||
pub(in super::super) fn clear_vim_visual_selection(&mut self) {
|
||||
self.vim_visual_anchor = None;
|
||||
}
|
||||
|
||||
pub(crate) fn vim_visual_selection_range(&self) -> Option<Range<usize>> {
|
||||
let anchor = self.vim_visual_anchor?;
|
||||
let VimMode::Visual(kind) = self.vim_mode else {
|
||||
return None;
|
||||
};
|
||||
if self.text.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let range = match kind {
|
||||
VimVisualKind::Characterwise => {
|
||||
let start = anchor.min(self.cursor_pos);
|
||||
let end = self.next_atomic_boundary(anchor.max(self.cursor_pos));
|
||||
start..end
|
||||
}
|
||||
VimVisualKind::Linewise => {
|
||||
let start = self.beginning_of_line(anchor.min(self.cursor_pos));
|
||||
let line_end = self.end_of_line(anchor.max(self.cursor_pos));
|
||||
let end = if line_end < self.text.len() {
|
||||
line_end + 1
|
||||
} else {
|
||||
line_end
|
||||
};
|
||||
start..end
|
||||
}
|
||||
};
|
||||
(range.start < range.end).then(|| self.expand_range_to_element_boundaries(range))
|
||||
}
|
||||
|
||||
pub(in super::super) fn handle_vim_visual(&mut self, event: KeyEvent) {
|
||||
if matches!(self.vim_pending, VimPending::None) && self.capture_vim_count_digit(event) {
|
||||
return;
|
||||
}
|
||||
let pending = std::mem::replace(&mut self.vim_pending, VimPending::None);
|
||||
match pending {
|
||||
VimPending::None => {}
|
||||
VimPending::Register => {
|
||||
self.handle_vim_register_name(event);
|
||||
return;
|
||||
}
|
||||
VimPending::Find {
|
||||
operator: None,
|
||||
kind,
|
||||
} => {
|
||||
self.handle_vim_find_target(/*operator*/ None, kind, event);
|
||||
return;
|
||||
}
|
||||
VimPending::VisualTextObject { scope } => {
|
||||
self.handle_vim_visual_text_object(scope, event);
|
||||
return;
|
||||
}
|
||||
VimPending::Operator(_)
|
||||
| VimPending::TextObject { .. }
|
||||
| VimPending::Find {
|
||||
operator: Some(_), ..
|
||||
} => {
|
||||
self.exit_vim_visual_mode();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if self.vim_visual_keymap.cancel.is_pressed(event) {
|
||||
self.exit_vim_visual_mode();
|
||||
return;
|
||||
}
|
||||
if self.vim_normal_keymap.select_register.is_pressed(event) {
|
||||
self.vim_pending = VimPending::Register;
|
||||
return;
|
||||
}
|
||||
if self.vim_normal_keymap.enter_visual.is_pressed(event) {
|
||||
if self.vim_mode == VimMode::Visual(VimVisualKind::Characterwise) {
|
||||
self.exit_vim_visual_mode();
|
||||
} else {
|
||||
self.enter_vim_visual_mode(VimVisualKind::Characterwise);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if self.vim_normal_keymap.enter_visual_line.is_pressed(event) {
|
||||
if self.vim_mode == VimMode::Visual(VimVisualKind::Linewise) {
|
||||
self.exit_vim_visual_mode();
|
||||
} else {
|
||||
self.enter_vim_visual_mode(VimVisualKind::Linewise);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if self.vim_visual_keymap.delete.is_pressed(event) {
|
||||
self.apply_vim_visual_operator(VimOperator::Delete);
|
||||
return;
|
||||
}
|
||||
if self.vim_visual_keymap.yank.is_pressed(event) {
|
||||
self.apply_vim_visual_operator(VimOperator::Yank);
|
||||
return;
|
||||
}
|
||||
if self.vim_visual_keymap.change.is_pressed(event) {
|
||||
self.apply_vim_visual_operator(VimOperator::Change);
|
||||
return;
|
||||
}
|
||||
if let Some(scope) = self.vim_text_object_scope_for_event(event) {
|
||||
self.vim_pending = VimPending::VisualTextObject { scope };
|
||||
return;
|
||||
}
|
||||
if let Some(kind) = self.vim_normal_find_kind_for_event(event) {
|
||||
self.vim_pending = VimPending::Find {
|
||||
operator: None,
|
||||
kind,
|
||||
};
|
||||
return;
|
||||
}
|
||||
if self.vim_normal_keymap.repeat_find.is_pressed(event) {
|
||||
let count = self.take_vim_count();
|
||||
self.repeat_vim_find(/*operator*/ None, /*reverse*/ false, count);
|
||||
return;
|
||||
}
|
||||
if self.vim_normal_keymap.repeat_find_reverse.is_pressed(event) {
|
||||
let count = self.take_vim_count();
|
||||
self.repeat_vim_find(/*operator*/ None, /*reverse*/ true, count);
|
||||
return;
|
||||
}
|
||||
if let Some(motion) = self.vim_visual_motion_for_event(event) {
|
||||
let count = self.take_vim_count();
|
||||
self.apply_counted_vim_normal_motion(motion, count);
|
||||
return;
|
||||
}
|
||||
self.clear_vim_counts();
|
||||
self.clear_vim_register_selection();
|
||||
}
|
||||
|
||||
fn vim_visual_motion_for_event(&self, event: KeyEvent) -> Option<VimMotion> {
|
||||
[
|
||||
(self.vim_normal_keymap.move_left.as_slice(), VimMotion::Left),
|
||||
(
|
||||
self.vim_normal_keymap.move_right.as_slice(),
|
||||
VimMotion::Right,
|
||||
),
|
||||
(self.vim_normal_keymap.move_up.as_slice(), VimMotion::Up),
|
||||
(self.vim_normal_keymap.move_down.as_slice(), VimMotion::Down),
|
||||
(
|
||||
self.vim_normal_keymap.move_word_forward.as_slice(),
|
||||
VimMotion::WordForward,
|
||||
),
|
||||
(
|
||||
self.vim_normal_keymap.move_word_backward.as_slice(),
|
||||
VimMotion::WordBackward,
|
||||
),
|
||||
(
|
||||
self.vim_normal_keymap.move_word_end.as_slice(),
|
||||
VimMotion::WordEnd,
|
||||
),
|
||||
(
|
||||
self.vim_normal_keymap.move_line_start.as_slice(),
|
||||
VimMotion::LineStart,
|
||||
),
|
||||
(
|
||||
self.vim_normal_keymap.move_line_end.as_slice(),
|
||||
VimMotion::LineEnd,
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.find_map(|(bindings, motion)| bindings.is_pressed(event).then_some(motion))
|
||||
}
|
||||
|
||||
fn handle_vim_visual_text_object(&mut self, scope: VimTextObjectScope, event: KeyEvent) {
|
||||
if self.vim_text_object_keymap.cancel.is_pressed(event) {
|
||||
self.clear_vim_counts();
|
||||
return;
|
||||
}
|
||||
let Some(object) = self.vim_text_object_for_event(event) else {
|
||||
self.clear_vim_counts();
|
||||
return;
|
||||
};
|
||||
let count = self.take_vim_count();
|
||||
if let Some(range) = self.counted_text_object_range(object, scope, count) {
|
||||
self.select_vim_visual_range(range);
|
||||
}
|
||||
}
|
||||
|
||||
fn select_vim_visual_range(&mut self, range: Range<usize>) {
|
||||
let range = self.expand_range_to_element_boundaries(range);
|
||||
if range.start >= range.end {
|
||||
return;
|
||||
}
|
||||
self.vim_visual_anchor = Some(range.start);
|
||||
self.set_cursor(self.prev_atomic_boundary(range.end));
|
||||
}
|
||||
|
||||
fn apply_vim_visual_operator(&mut self, operator: VimOperator) {
|
||||
let Some(mut range) = self.vim_visual_selection_range() else {
|
||||
self.exit_vim_visual_mode();
|
||||
return;
|
||||
};
|
||||
let linewise = self.vim_mode == VimMode::Visual(VimVisualKind::Linewise);
|
||||
if operator == VimOperator::Change
|
||||
&& linewise
|
||||
&& range.end > range.start
|
||||
&& self.text[..range.end].ends_with('\n')
|
||||
{
|
||||
range.end -= 1;
|
||||
}
|
||||
let kind = if linewise {
|
||||
KillBufferKind::Linewise
|
||||
} else {
|
||||
KillBufferKind::Characterwise
|
||||
};
|
||||
let repeat_target = self.visual_repeat_target(
|
||||
if linewise {
|
||||
VimVisualKind::Linewise
|
||||
} else {
|
||||
VimVisualKind::Characterwise
|
||||
},
|
||||
range.clone(),
|
||||
);
|
||||
let previous_len = self.text.len();
|
||||
let start = range.start;
|
||||
match operator {
|
||||
VimOperator::Delete => self.kill_range_with_kind(range, kind),
|
||||
VimOperator::Yank => self.yank_range_with_kind(range, kind),
|
||||
VimOperator::Change => self.kill_range_with_kind(range, kind),
|
||||
}
|
||||
self.clear_vim_visual_selection();
|
||||
self.clear_vim_counts();
|
||||
self.clear_vim_register_selection();
|
||||
if operator == VimOperator::Change {
|
||||
self.vim_mode = VimMode::Insert;
|
||||
self.begin_vim_change_recording(repeat_target);
|
||||
} else {
|
||||
self.vim_mode = VimMode::Normal;
|
||||
self.set_cursor(start.min(self.vim_normal_end_cursor()));
|
||||
if operator == VimOperator::Delete {
|
||||
self.record_vim_delete_if_changed(repeat_target, previous_len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn exit_vim_visual_mode(&mut self) {
|
||||
self.vim_mode = VimMode::Normal;
|
||||
self.vim_pending = VimPending::None;
|
||||
self.clear_vim_visual_selection();
|
||||
self.clear_vim_counts();
|
||||
self.clear_vim_register_selection();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -133,15 +133,30 @@ pub(super) const KEYMAP_ACTIONS: &[KeymapActionDescriptor] = &[
|
||||
action("vim_normal", "Vim normal", "move_word_end", "Move to the end of the current or next word."),
|
||||
action("vim_normal", "Vim normal", "move_line_start", "Move to the start of the line."),
|
||||
action("vim_normal", "Vim normal", "move_line_end", "Move to the end of the line."),
|
||||
action("vim_normal", "Vim normal", "find_forward", "Find the next target character on the current line."),
|
||||
action("vim_normal", "Vim normal", "find_backward", "Find the previous target character on the current line."),
|
||||
action("vim_normal", "Vim normal", "till_forward", "Move until before the next target character on the current line."),
|
||||
action("vim_normal", "Vim normal", "till_backward", "Move until after the previous target character on the current line."),
|
||||
action("vim_normal", "Vim normal", "repeat_find", "Repeat the latest find or till motion."),
|
||||
action("vim_normal", "Vim normal", "repeat_find_reverse", "Repeat the latest find or till motion in reverse."),
|
||||
action("vim_normal", "Vim normal", "repeat_change", "Repeat the most recent text-changing Vim command."),
|
||||
action("vim_normal", "Vim normal", "delete_char", "Delete the character under the cursor."),
|
||||
action("vim_normal", "Vim normal", "delete_to_line_end", "Delete from cursor to end of line."),
|
||||
action("vim_normal", "Vim normal", "change_to_line_end", "Change from cursor to end of line and enter insert mode."),
|
||||
action("vim_normal", "Vim normal", "substitute_char", "Substitute the character under the cursor and enter insert mode."),
|
||||
action("vim_normal", "Vim normal", "substitute_line", "Substitute the current line and enter insert mode."),
|
||||
action("vim_normal", "Vim normal", "yank_line", "Yank the entire line."),
|
||||
action("vim_normal", "Vim normal", "paste_after", "Paste after the cursor."),
|
||||
action("vim_normal", "Vim normal", "select_register", "Select a named register for the next command."),
|
||||
action("vim_normal", "Vim normal", "enter_visual", "Enter characterwise visual mode."),
|
||||
action("vim_normal", "Vim normal", "enter_visual_line", "Enter linewise visual mode."),
|
||||
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 motion or 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."),
|
||||
action("vim_operator", "Vim operator", "change_line", "Repeat change operator to change the whole line."),
|
||||
action("vim_operator", "Vim operator", "motion_left", "Operator motion left."),
|
||||
action("vim_operator", "Vim operator", "motion_right", "Operator motion right."),
|
||||
action("vim_operator", "Vim operator", "motion_up", "Operator motion up."),
|
||||
@@ -151,7 +166,31 @@ 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", "find_forward", "Operator motion to the next target character."),
|
||||
action("vim_operator", "Vim operator", "find_backward", "Operator motion to the previous target character."),
|
||||
action("vim_operator", "Vim operator", "till_forward", "Operator motion until before the next target character."),
|
||||
action("vim_operator", "Vim operator", "till_backward", "Operator motion until after the previous target character."),
|
||||
action("vim_operator", "Vim operator", "repeat_find", "Repeat the latest find or till motion for this operator."),
|
||||
action("vim_operator", "Vim operator", "repeat_find_reverse", "Repeat the latest find or till motion in reverse for this operator."),
|
||||
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", "sentence", "Target the current sentence."),
|
||||
action("vim_text_object", "Vim text object", "paragraph", "Target the current paragraph."),
|
||||
action("vim_text_object", "Vim text object", "tag", "Target the enclosing paired tag."),
|
||||
action("vim_text_object", "Vim text object", "cancel", "Cancel the pending text object."),
|
||||
action("vim_visual", "Vim visual", "delete", "Delete the active selection."),
|
||||
action("vim_visual", "Vim visual", "yank", "Yank the active selection."),
|
||||
action("vim_visual", "Vim visual", "change", "Change the active selection and enter insert mode."),
|
||||
action("vim_visual", "Vim visual", "cancel", "Cancel the active selection."),
|
||||
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."),
|
||||
@@ -261,15 +300,30 @@ pub(super) fn binding_slot<'a>(
|
||||
("vim_normal", "move_word_end") => Some(&mut keymap.vim_normal.move_word_end),
|
||||
("vim_normal", "move_line_start") => Some(&mut keymap.vim_normal.move_line_start),
|
||||
("vim_normal", "move_line_end") => Some(&mut keymap.vim_normal.move_line_end),
|
||||
("vim_normal", "find_forward") => Some(&mut keymap.vim_normal.find_forward),
|
||||
("vim_normal", "find_backward") => Some(&mut keymap.vim_normal.find_backward),
|
||||
("vim_normal", "till_forward") => Some(&mut keymap.vim_normal.till_forward),
|
||||
("vim_normal", "till_backward") => Some(&mut keymap.vim_normal.till_backward),
|
||||
("vim_normal", "repeat_find") => Some(&mut keymap.vim_normal.repeat_find),
|
||||
("vim_normal", "repeat_find_reverse") => Some(&mut keymap.vim_normal.repeat_find_reverse),
|
||||
("vim_normal", "repeat_change") => Some(&mut keymap.vim_normal.repeat_change),
|
||||
("vim_normal", "delete_char") => Some(&mut keymap.vim_normal.delete_char),
|
||||
("vim_normal", "delete_to_line_end") => Some(&mut keymap.vim_normal.delete_to_line_end),
|
||||
("vim_normal", "change_to_line_end") => Some(&mut keymap.vim_normal.change_to_line_end),
|
||||
("vim_normal", "substitute_char") => Some(&mut keymap.vim_normal.substitute_char),
|
||||
("vim_normal", "substitute_line") => Some(&mut keymap.vim_normal.substitute_line),
|
||||
("vim_normal", "yank_line") => Some(&mut keymap.vim_normal.yank_line),
|
||||
("vim_normal", "paste_after") => Some(&mut keymap.vim_normal.paste_after),
|
||||
("vim_normal", "select_register") => Some(&mut keymap.vim_normal.select_register),
|
||||
("vim_normal", "enter_visual") => Some(&mut keymap.vim_normal.enter_visual),
|
||||
("vim_normal", "enter_visual_line") => Some(&mut keymap.vim_normal.enter_visual_line),
|
||||
("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),
|
||||
("vim_operator", "change_line") => Some(&mut keymap.vim_operator.change_line),
|
||||
("vim_operator", "motion_left") => Some(&mut keymap.vim_operator.motion_left),
|
||||
("vim_operator", "motion_right") => Some(&mut keymap.vim_operator.motion_right),
|
||||
("vim_operator", "motion_up") => Some(&mut keymap.vim_operator.motion_up),
|
||||
@@ -279,7 +333,31 @@ 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", "find_forward") => Some(&mut keymap.vim_operator.find_forward),
|
||||
("vim_operator", "find_backward") => Some(&mut keymap.vim_operator.find_backward),
|
||||
("vim_operator", "till_forward") => Some(&mut keymap.vim_operator.till_forward),
|
||||
("vim_operator", "till_backward") => Some(&mut keymap.vim_operator.till_backward),
|
||||
("vim_operator", "repeat_find") => Some(&mut keymap.vim_operator.repeat_find),
|
||||
("vim_operator", "repeat_find_reverse") => Some(&mut keymap.vim_operator.repeat_find_reverse),
|
||||
("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", "sentence") => Some(&mut keymap.vim_text_object.sentence),
|
||||
("vim_text_object", "paragraph") => Some(&mut keymap.vim_text_object.paragraph),
|
||||
("vim_text_object", "tag") => Some(&mut keymap.vim_text_object.tag),
|
||||
("vim_text_object", "cancel") => Some(&mut keymap.vim_text_object.cancel),
|
||||
("vim_visual", "delete") => Some(&mut keymap.vim_visual.delete),
|
||||
("vim_visual", "yank") => Some(&mut keymap.vim_visual.yank),
|
||||
("vim_visual", "change") => Some(&mut keymap.vim_visual.change),
|
||||
("vim_visual", "cancel") => Some(&mut keymap.vim_visual.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),
|
||||
@@ -371,15 +449,30 @@ pub(super) fn bindings_for_action<'a>(
|
||||
("vim_normal", "move_word_end") => Some(runtime_keymap.vim_normal.move_word_end.as_slice()),
|
||||
("vim_normal", "move_line_start") => Some(runtime_keymap.vim_normal.move_line_start.as_slice()),
|
||||
("vim_normal", "move_line_end") => Some(runtime_keymap.vim_normal.move_line_end.as_slice()),
|
||||
("vim_normal", "find_forward") => Some(runtime_keymap.vim_normal.find_forward.as_slice()),
|
||||
("vim_normal", "find_backward") => Some(runtime_keymap.vim_normal.find_backward.as_slice()),
|
||||
("vim_normal", "till_forward") => Some(runtime_keymap.vim_normal.till_forward.as_slice()),
|
||||
("vim_normal", "till_backward") => Some(runtime_keymap.vim_normal.till_backward.as_slice()),
|
||||
("vim_normal", "repeat_find") => Some(runtime_keymap.vim_normal.repeat_find.as_slice()),
|
||||
("vim_normal", "repeat_find_reverse") => Some(runtime_keymap.vim_normal.repeat_find_reverse.as_slice()),
|
||||
("vim_normal", "repeat_change") => Some(runtime_keymap.vim_normal.repeat_change.as_slice()),
|
||||
("vim_normal", "delete_char") => Some(runtime_keymap.vim_normal.delete_char.as_slice()),
|
||||
("vim_normal", "delete_to_line_end") => Some(runtime_keymap.vim_normal.delete_to_line_end.as_slice()),
|
||||
("vim_normal", "change_to_line_end") => Some(runtime_keymap.vim_normal.change_to_line_end.as_slice()),
|
||||
("vim_normal", "substitute_char") => Some(runtime_keymap.vim_normal.substitute_char.as_slice()),
|
||||
("vim_normal", "substitute_line") => Some(runtime_keymap.vim_normal.substitute_line.as_slice()),
|
||||
("vim_normal", "yank_line") => Some(runtime_keymap.vim_normal.yank_line.as_slice()),
|
||||
("vim_normal", "paste_after") => Some(runtime_keymap.vim_normal.paste_after.as_slice()),
|
||||
("vim_normal", "select_register") => Some(runtime_keymap.vim_normal.select_register.as_slice()),
|
||||
("vim_normal", "enter_visual") => Some(runtime_keymap.vim_normal.enter_visual.as_slice()),
|
||||
("vim_normal", "enter_visual_line") => Some(runtime_keymap.vim_normal.enter_visual_line.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()),
|
||||
("vim_operator", "change_line") => Some(runtime_keymap.vim_operator.change_line.as_slice()),
|
||||
("vim_operator", "motion_left") => Some(runtime_keymap.vim_operator.motion_left.as_slice()),
|
||||
("vim_operator", "motion_right") => Some(runtime_keymap.vim_operator.motion_right.as_slice()),
|
||||
("vim_operator", "motion_up") => Some(runtime_keymap.vim_operator.motion_up.as_slice()),
|
||||
@@ -389,7 +482,31 @@ 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", "find_forward") => Some(runtime_keymap.vim_operator.find_forward.as_slice()),
|
||||
("vim_operator", "find_backward") => Some(runtime_keymap.vim_operator.find_backward.as_slice()),
|
||||
("vim_operator", "till_forward") => Some(runtime_keymap.vim_operator.till_forward.as_slice()),
|
||||
("vim_operator", "till_backward") => Some(runtime_keymap.vim_operator.till_backward.as_slice()),
|
||||
("vim_operator", "repeat_find") => Some(runtime_keymap.vim_operator.repeat_find.as_slice()),
|
||||
("vim_operator", "repeat_find_reverse") => Some(runtime_keymap.vim_operator.repeat_find_reverse.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", "sentence") => Some(runtime_keymap.vim_text_object.sentence.as_slice()),
|
||||
("vim_text_object", "paragraph") => Some(runtime_keymap.vim_text_object.paragraph.as_slice()),
|
||||
("vim_text_object", "tag") => Some(runtime_keymap.vim_text_object.tag.as_slice()),
|
||||
("vim_text_object", "cancel") => Some(runtime_keymap.vim_text_object.cancel.as_slice()),
|
||||
("vim_visual", "delete") => Some(runtime_keymap.vim_visual.delete.as_slice()),
|
||||
("vim_visual", "yank") => Some(runtime_keymap.vim_visual.yank.as_slice()),
|
||||
("vim_visual", "change") => Some(runtime_keymap.vim_visual.change.as_slice()),
|
||||
("vim_visual", "cancel") => Some(runtime_keymap.vim_visual.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()),
|
||||
|
||||
@@ -103,8 +103,13 @@ const KEYMAP_CONTEXT_TABS: &[KeymapContextTab] = &[
|
||||
KeymapContextTab {
|
||||
id: "vim-shortcuts",
|
||||
label: "Vim",
|
||||
description: "Vim normal-mode and operator shortcuts.",
|
||||
contexts: &["vim_normal", "vim_operator"],
|
||||
description: "Vim normal, operator, text-object, and visual shortcuts.",
|
||||
contexts: &[
|
||||
"vim_normal",
|
||||
"vim_operator",
|
||||
"vim_text_object",
|
||||
"vim_visual",
|
||||
],
|
||||
},
|
||||
KeymapContextTab {
|
||||
id: "navigation-shortcuts",
|
||||
|
||||
@@ -5,7 +5,7 @@ expression: "render_picker(params, 120)"
|
||||
|
||||
Keymap
|
||||
All configurable shortcuts.
|
||||
93 actions, 1 customized, 2 unbound.
|
||||
132 actions, 1 customized, 2 unbound.
|
||||
|
||||
[All] Common Customized (1) 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, 3 unbound.
|
||||
133 actions, 0 customized, 3 unbound.
|
||||
|
||||
[All] Common Customized (0) Unbound (3) App Composer Editor Vim Navigation Approval Debug
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
source: tui/src/keymap_setup.rs
|
||||
expression: snapshot
|
||||
---
|
||||
tab: All (93 selectable)
|
||||
tab: All (132 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 (34 selectable)
|
||||
tab: Vim (73 selectable)
|
||||
tab: Navigation (20 selectable)
|
||||
tab: Approval (8 selectable)
|
||||
tab: Debug (1 selectable)
|
||||
|
||||
@@ -5,7 +5,7 @@ expression: "render_picker(params, 78)"
|
||||
|
||||
Keymap
|
||||
All configurable shortcuts.
|
||||
93 actions, 0 customized, 2 unbound.
|
||||
132 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.
|
||||
93 actions, 0 customized, 2 unbound.
|
||||
132 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