mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +00:00
feat(tui): add vim visual modes
This commit is contained in:
@@ -247,6 +247,10 @@ pub struct TuiVimNormalKeymap {
|
||||
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`).
|
||||
@@ -341,6 +345,24 @@ pub struct TuiVimTextObjectKeymap {
|
||||
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)]
|
||||
@@ -447,6 +469,8 @@ pub struct TuiKeymap {
|
||||
#[serde(default)]
|
||||
pub vim_text_object: TuiVimTextObjectKeymap,
|
||||
#[serde(default)]
|
||||
pub vim_visual: TuiVimVisualKeymap,
|
||||
#[serde(default)]
|
||||
pub pager: TuiPagerKeymap,
|
||||
#[serde(default)]
|
||||
pub list: TuiListKeymap,
|
||||
|
||||
@@ -2828,6 +2828,8 @@
|
||||
"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,
|
||||
@@ -2891,6 +2893,12 @@
|
||||
"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."
|
||||
@@ -3528,6 +3536,8 @@
|
||||
"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,
|
||||
@@ -3606,6 +3616,19 @@
|
||||
"tag": null,
|
||||
"word": null
|
||||
}
|
||||
},
|
||||
"vim_visual": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TuiVimVisualKeymap"
|
||||
}
|
||||
],
|
||||
"default": {
|
||||
"cancel": null,
|
||||
"change": null,
|
||||
"delete": null,
|
||||
"yank": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -3862,6 +3885,22 @@
|
||||
],
|
||||
"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": [
|
||||
{
|
||||
@@ -4351,6 +4390,45 @@
|
||||
},
|
||||
"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;
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" "
|
||||
"› alpha beta "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" 100% context left | Vim: Visual "
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" "
|
||||
"› alpha "
|
||||
" beta "
|
||||
" gamma "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" 100% context left | Vim: Visual Line "
|
||||
@@ -17,6 +17,7 @@ use crate::keymap::RuntimeKeymap;
|
||||
use crate::keymap::VimNormalKeymap;
|
||||
use crate::keymap::VimOperatorKeymap;
|
||||
use crate::keymap::VimTextObjectKeymap;
|
||||
use crate::keymap::VimVisualKeymap;
|
||||
use codex_protocol::user_input::ByteRange;
|
||||
use codex_protocol::user_input::TextElement as UserTextElement;
|
||||
use crossterm::event::KeyCode;
|
||||
@@ -45,6 +46,7 @@ use self::vim::VimOperator;
|
||||
use self::vim::VimPending;
|
||||
use self::vim::VimRegisterSelection;
|
||||
use self::vim::VimTextObjectScope;
|
||||
use self::vim::VimVisualKind;
|
||||
|
||||
const WORD_SEPARATORS: &str = "`~!@#$%^&*()-=+[{]}\\|;:'\",.<>/?";
|
||||
|
||||
@@ -118,10 +120,12 @@ pub(crate) struct TextArea {
|
||||
vim_count: Option<usize>,
|
||||
vim_operator_count: Option<usize>,
|
||||
vim_last_find: Option<VimFind>,
|
||||
vim_visual_anchor: Option<usize>,
|
||||
editor_keymap: EditorKeymap,
|
||||
vim_normal_keymap: VimNormalKeymap,
|
||||
vim_operator_keymap: VimOperatorKeymap,
|
||||
vim_text_object_keymap: VimTextObjectKeymap,
|
||||
vim_visual_keymap: VimVisualKeymap,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -164,10 +168,12 @@ impl TextArea {
|
||||
vim_count: None,
|
||||
vim_operator_count: None,
|
||||
vim_last_find: None,
|
||||
vim_visual_anchor: None,
|
||||
editor_keymap: defaults.editor,
|
||||
vim_normal_keymap: defaults.vim_normal,
|
||||
vim_operator_keymap: defaults.vim_operator,
|
||||
vim_text_object_keymap: defaults.vim_text_object,
|
||||
vim_visual_keymap: defaults.vim_visual,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,6 +188,7 @@ impl TextArea {
|
||||
self.vim_normal_keymap = keymap.vim_normal.clone();
|
||||
self.vim_operator_keymap = keymap.vim_operator.clone();
|
||||
self.vim_text_object_keymap = keymap.vim_text_object.clone();
|
||||
self.vim_visual_keymap = keymap.vim_visual.clone();
|
||||
}
|
||||
|
||||
/// Replace the visible textarea text and clear any existing text elements.
|
||||
@@ -233,6 +240,10 @@ impl TextArea {
|
||||
self.cursor_pos = self.clamp_pos_to_nearest_boundary(self.cursor_pos);
|
||||
self.wrap_cache.replace(None);
|
||||
self.preferred_col = None;
|
||||
self.clear_vim_visual_selection();
|
||||
if matches!(self.vim_mode, VimMode::Visual(_)) {
|
||||
self.vim_mode = VimMode::Normal;
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable or disable modal Vim editing for the textarea.
|
||||
@@ -246,6 +257,7 @@ impl TextArea {
|
||||
self.vim_pending = VimPending::None;
|
||||
self.clear_vim_counts();
|
||||
self.clear_vim_register_selection();
|
||||
self.clear_vim_visual_selection();
|
||||
self.vim_mode = if enabled {
|
||||
VimMode::Normal
|
||||
} else {
|
||||
@@ -267,6 +279,11 @@ impl TextArea {
|
||||
self.vim_enabled && self.vim_mode == VimMode::Normal
|
||||
}
|
||||
|
||||
/// Return whether Vim mode is enabled with an active visual selection.
|
||||
pub(crate) fn is_vim_visual_mode(&self) -> bool {
|
||||
self.vim_enabled && matches!(self.vim_mode, VimMode::Visual(_))
|
||||
}
|
||||
|
||||
/// Return the cursor position that represents the last editable item in Vim normal mode.
|
||||
pub(crate) fn vim_normal_end_cursor(&self) -> usize {
|
||||
if self.text.is_empty() {
|
||||
@@ -295,6 +312,7 @@ impl TextArea {
|
||||
self.vim_pending = VimPending::None;
|
||||
self.clear_vim_counts();
|
||||
self.clear_vim_register_selection();
|
||||
self.clear_vim_visual_selection();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,6 +328,7 @@ impl TextArea {
|
||||
self.vim_pending = VimPending::None;
|
||||
self.clear_vim_counts();
|
||||
self.clear_vim_register_selection();
|
||||
self.clear_vim_visual_selection();
|
||||
self.preferred_col = None;
|
||||
}
|
||||
}
|
||||
@@ -353,6 +372,8 @@ impl TextArea {
|
||||
Some(match self.vim_mode {
|
||||
VimMode::Normal => "Normal",
|
||||
VimMode::Insert => "Insert",
|
||||
VimMode::Visual(VimVisualKind::Characterwise) => "Visual",
|
||||
VimMode::Visual(VimVisualKind::Linewise) => "Visual Line",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -644,6 +665,7 @@ impl TextArea {
|
||||
match self.vim_mode {
|
||||
VimMode::Insert => self.handle_vim_insert(event),
|
||||
VimMode::Normal => self.handle_vim_normal(event),
|
||||
VimMode::Visual(_) => self.handle_vim_visual(event),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -685,12 +707,21 @@ impl TextArea {
|
||||
self.handle_vim_find_target(operator, kind, event);
|
||||
return;
|
||||
}
|
||||
VimPending::VisualTextObject { .. } => 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) {
|
||||
self.enter_vim_visual_mode(VimVisualKind::Characterwise);
|
||||
return;
|
||||
}
|
||||
if self.vim_normal_keymap.enter_visual_line.is_pressed(event) {
|
||||
self.enter_vim_visual_mode(VimVisualKind::Linewise);
|
||||
return;
|
||||
}
|
||||
if self.vim_normal_keymap.enter_insert.is_pressed(event) {
|
||||
self.clear_vim_counts();
|
||||
self.clear_vim_register_selection();
|
||||
@@ -2858,6 +2889,108 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_visual_characterwise_delete_yank_and_change_preserve_elements() {
|
||||
let mut t = ta_with("abcdef");
|
||||
t.set_cursor(/*pos*/ 0);
|
||||
t.set_vim_enabled(/*enabled*/ true);
|
||||
t.input(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(t.text(), "cdef");
|
||||
assert_eq!(t.kill_buffer, "ab");
|
||||
assert_eq!(t.vim_mode_label(), Some("Normal"));
|
||||
|
||||
let mut t = ta_with("one two");
|
||||
t.set_cursor(/*pos*/ 0);
|
||||
t.set_vim_enabled(/*enabled*/ true);
|
||||
t.input(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(t.text(), "one two");
|
||||
assert_eq!(t.kill_buffer, "one");
|
||||
assert_eq!(t.vim_mode_label(), Some("Normal"));
|
||||
|
||||
let mut t = ta_with("@file tail");
|
||||
t.add_element_range(/*range*/ 0.."@file".len())
|
||||
.expect("valid element");
|
||||
t.set_cursor(/*pos*/ 0);
|
||||
t.set_vim_enabled(/*enabled*/ true);
|
||||
t.input(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(t.text(), " tail");
|
||||
assert_eq!(t.kill_buffer, "@file");
|
||||
assert_eq!(t.vim_mode_label(), Some("Insert"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_visual_line_yank_and_change_use_linewise_ranges() {
|
||||
let mut t = ta_with("one\ntwo\nthree");
|
||||
t.set_cursor(/*pos*/ "one\n".len());
|
||||
t.set_vim_enabled(/*enabled*/ true);
|
||||
t.input(KeyEvent::new(KeyCode::Char('V'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('k'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(t.kill_buffer, "one\ntwo\n");
|
||||
assert_eq!(t.kill_buffer_kind, KillBufferKind::Linewise);
|
||||
assert_eq!(t.vim_mode_label(), Some("Normal"));
|
||||
|
||||
let mut t = ta_with("one\ntwo\nthree");
|
||||
t.set_cursor(/*pos*/ "one\n".len());
|
||||
t.set_vim_enabled(/*enabled*/ true);
|
||||
t.input(KeyEvent::new(KeyCode::Char('V'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(t.text(), "one\n\nthree");
|
||||
assert_eq!(t.kill_buffer, "two");
|
||||
assert_eq!(t.vim_mode_label(), Some("Insert"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_visual_find_text_objects_registers_and_cancel() {
|
||||
let mut t = ta_with("one, two");
|
||||
t.set_cursor(/*pos*/ 0);
|
||||
t.set_vim_enabled(/*enabled*/ true);
|
||||
t.input(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char(','), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(
|
||||
t.vim_named_registers.get(&'a'),
|
||||
Some(&("one,".to_string(), KillBufferKind::Characterwise))
|
||||
);
|
||||
|
||||
let mut t = ta_with("(hello) rest");
|
||||
t.set_cursor(/*pos*/ 2);
|
||||
t.set_vim_enabled(/*enabled*/ true);
|
||||
t.input(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE));
|
||||
t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(t.text(), "() rest");
|
||||
assert_eq!(t.kill_buffer, "hello");
|
||||
assert_eq!(t.vim_mode_label(), Some("Insert"));
|
||||
|
||||
let mut t = ta_with("keep");
|
||||
t.set_cursor(/*pos*/ 0);
|
||||
t.set_vim_enabled(/*enabled*/ true);
|
||||
t.input(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::NONE));
|
||||
assert_eq!(t.vim_visual_selection_range(), Some(/*range*/ 0..1));
|
||||
t.input(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(t.text(), "keep");
|
||||
assert_eq!(t.vim_visual_selection_range(), None);
|
||||
assert_eq!(t.vim_mode_label(), Some("Normal"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_find_till_motions_repeat_and_stay_on_current_line() {
|
||||
let mut t = ta_with("ab,cd,ef\naz");
|
||||
|
||||
@@ -9,6 +9,7 @@ mod find;
|
||||
mod prose;
|
||||
mod register;
|
||||
mod tag;
|
||||
mod visual;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum VimMode {
|
||||
@@ -16,6 +17,8 @@ pub(super) enum VimMode {
|
||||
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)]
|
||||
@@ -38,6 +41,9 @@ pub(super) enum VimPending {
|
||||
operator: Option<VimOperator>,
|
||||
kind: VimFindKind,
|
||||
},
|
||||
VisualTextObject {
|
||||
scope: VimTextObjectScope,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -46,6 +52,12 @@ pub(super) struct VimRegisterSelection {
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
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 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;
|
||||
} else {
|
||||
self.vim_mode = VimMode::Normal;
|
||||
self.set_cursor(start.min(self.vim_normal_end_cursor()));
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,7 @@ pub(crate) struct RuntimeKeymap {
|
||||
pub(crate) vim_normal: VimNormalKeymap,
|
||||
pub(crate) vim_operator: VimOperatorKeymap,
|
||||
pub(crate) vim_text_object: VimTextObjectKeymap,
|
||||
pub(crate) vim_visual: VimVisualKeymap,
|
||||
pub(crate) pager: PagerKeymap,
|
||||
pub(crate) list: ListKeymap,
|
||||
pub(crate) approval: ApprovalKeymap,
|
||||
@@ -169,6 +170,8 @@ pub(crate) struct VimNormalKeymap {
|
||||
pub(crate) yank_line: Vec<KeyBinding>,
|
||||
pub(crate) paste_after: Vec<KeyBinding>,
|
||||
pub(crate) select_register: Vec<KeyBinding>,
|
||||
pub(crate) enter_visual: Vec<KeyBinding>,
|
||||
pub(crate) enter_visual_line: Vec<KeyBinding>,
|
||||
pub(crate) start_delete_operator: Vec<KeyBinding>,
|
||||
pub(crate) start_yank_operator: Vec<KeyBinding>,
|
||||
pub(crate) start_change_operator: Vec<KeyBinding>,
|
||||
@@ -223,6 +226,15 @@ pub(crate) struct VimTextObjectKeymap {
|
||||
pub(crate) cancel: Vec<KeyBinding>,
|
||||
}
|
||||
|
||||
/// Vim visual-mode operations evaluated while a selection is active.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct VimVisualKeymap {
|
||||
pub(crate) delete: Vec<KeyBinding>,
|
||||
pub(crate) yank: Vec<KeyBinding>,
|
||||
pub(crate) change: Vec<KeyBinding>,
|
||||
pub(crate) cancel: Vec<KeyBinding>,
|
||||
}
|
||||
|
||||
/// Pager/overlay keybindings for transcript and static help views.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct PagerKeymap {
|
||||
@@ -519,6 +531,8 @@ impl RuntimeKeymap {
|
||||
yank_line: resolve_local!(keymap, defaults, vim_normal, yank_line),
|
||||
paste_after: resolve_local!(keymap, defaults, vim_normal, paste_after),
|
||||
select_register: resolve_local!(keymap, defaults, vim_normal, select_register),
|
||||
enter_visual: resolve_local!(keymap, defaults, vim_normal, enter_visual),
|
||||
enter_visual_line: resolve_local!(keymap, defaults, vim_normal, enter_visual_line),
|
||||
start_delete_operator: resolve_local!(
|
||||
keymap,
|
||||
defaults,
|
||||
@@ -712,6 +726,16 @@ impl RuntimeKeymap {
|
||||
.select_register
|
||||
.retain(|binding| !configured_vim_normal_bindings_to_preserve.contains(binding));
|
||||
}
|
||||
if keymap.vim_normal.enter_visual.is_none() {
|
||||
vim_normal
|
||||
.enter_visual
|
||||
.retain(|binding| !configured_vim_normal_bindings_to_preserve.contains(binding));
|
||||
}
|
||||
if keymap.vim_normal.enter_visual_line.is_none() {
|
||||
vim_normal
|
||||
.enter_visual_line
|
||||
.retain(|binding| !configured_vim_normal_bindings_to_preserve.contains(binding));
|
||||
}
|
||||
|
||||
let mut vim_operator = VimOperatorKeymap {
|
||||
delete_line: resolve_local!(keymap, defaults, vim_operator, delete_line),
|
||||
@@ -965,6 +989,13 @@ impl RuntimeKeymap {
|
||||
});
|
||||
}
|
||||
|
||||
let vim_visual = VimVisualKeymap {
|
||||
delete: resolve_local!(keymap, defaults, vim_visual, delete),
|
||||
yank: resolve_local!(keymap, defaults, vim_visual, yank),
|
||||
change: resolve_local!(keymap, defaults, vim_visual, change),
|
||||
cancel: resolve_local!(keymap, defaults, vim_visual, cancel),
|
||||
};
|
||||
|
||||
let pager = PagerKeymap {
|
||||
scroll_up: resolve_local!(keymap, defaults, pager, scroll_up),
|
||||
scroll_down: resolve_local!(keymap, defaults, pager, scroll_down),
|
||||
@@ -1102,6 +1133,7 @@ impl RuntimeKeymap {
|
||||
vim_normal,
|
||||
vim_operator,
|
||||
vim_text_object,
|
||||
vim_visual,
|
||||
pager,
|
||||
list,
|
||||
approval,
|
||||
@@ -1260,6 +1292,11 @@ impl RuntimeKeymap {
|
||||
yank_line: default_bindings![shift(KeyCode::Char('y')), plain(KeyCode::Char('Y'))],
|
||||
paste_after: default_bindings![plain(KeyCode::Char('p'))],
|
||||
select_register: default_bindings![plain(KeyCode::Char('"'))],
|
||||
enter_visual: default_bindings![plain(KeyCode::Char('v'))],
|
||||
enter_visual_line: default_bindings![
|
||||
shift(KeyCode::Char('v')),
|
||||
plain(KeyCode::Char('V'))
|
||||
],
|
||||
start_delete_operator: default_bindings![plain(KeyCode::Char('d'))],
|
||||
start_yank_operator: default_bindings![plain(KeyCode::Char('y'))],
|
||||
start_change_operator: default_bindings![plain(KeyCode::Char('c'))],
|
||||
@@ -1327,6 +1364,12 @@ impl RuntimeKeymap {
|
||||
tag: default_bindings![plain(KeyCode::Char('t'))],
|
||||
cancel: default_bindings![plain(KeyCode::Esc)],
|
||||
},
|
||||
vim_visual: VimVisualKeymap {
|
||||
delete: default_bindings![plain(KeyCode::Char('d'))],
|
||||
yank: default_bindings![plain(KeyCode::Char('y'))],
|
||||
change: default_bindings![plain(KeyCode::Char('c'))],
|
||||
cancel: default_bindings![plain(KeyCode::Esc)],
|
||||
},
|
||||
pager: PagerKeymap {
|
||||
scroll_up: default_bindings![plain(KeyCode::Up), plain(KeyCode::Char('k'))],
|
||||
scroll_down: default_bindings![plain(KeyCode::Down), plain(KeyCode::Char('j'))],
|
||||
@@ -1721,6 +1764,11 @@ impl RuntimeKeymap {
|
||||
"select_register",
|
||||
self.vim_normal.select_register.as_slice(),
|
||||
),
|
||||
("enter_visual", self.vim_normal.enter_visual.as_slice()),
|
||||
(
|
||||
"enter_visual_line",
|
||||
self.vim_normal.enter_visual_line.as_slice(),
|
||||
),
|
||||
(
|
||||
"start_delete_operator",
|
||||
self.vim_normal.start_delete_operator.as_slice(),
|
||||
@@ -1809,6 +1857,16 @@ impl RuntimeKeymap {
|
||||
],
|
||||
)?;
|
||||
|
||||
validate_unique(
|
||||
"vim_visual",
|
||||
[
|
||||
("delete", self.vim_visual.delete.as_slice()),
|
||||
("yank", self.vim_visual.yank.as_slice()),
|
||||
("change", self.vim_visual.change.as_slice()),
|
||||
("cancel", self.vim_visual.cancel.as_slice()),
|
||||
],
|
||||
)?;
|
||||
|
||||
validate_unique(
|
||||
"pager",
|
||||
[
|
||||
@@ -2625,6 +2683,21 @@ mod tests {
|
||||
assert_eq!(runtime.vim_normal.select_register, Vec::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_legacy_vim_bindings_prune_new_visual_mode_defaults() {
|
||||
let mut keymap = TuiKeymap::default();
|
||||
keymap.vim_normal.move_left = Some(one("v"));
|
||||
keymap.vim_normal.move_right = Some(KeybindingsSpec::Many(vec![
|
||||
KeybindingSpec("shift-v".to_string()),
|
||||
KeybindingSpec("V".to_string()),
|
||||
]));
|
||||
|
||||
let runtime = RuntimeKeymap::from_config(&keymap).expect("config should parse");
|
||||
|
||||
assert_eq!(runtime.vim_normal.enter_visual, Vec::new());
|
||||
assert_eq!(runtime.vim_normal.enter_visual_line, Vec::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_legacy_vim_operator_bindings_prune_new_text_object_defaults() {
|
||||
let mut keymap = TuiKeymap::default();
|
||||
|
||||
@@ -147,6 +147,8 @@ pub(super) const KEYMAP_ACTIONS: &[KeymapActionDescriptor] = &[
|
||||
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."),
|
||||
@@ -184,6 +186,10 @@ pub(super) const KEYMAP_ACTIONS: &[KeymapActionDescriptor] = &[
|
||||
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."),
|
||||
@@ -307,6 +313,8 @@ pub(super) fn binding_slot<'a>(
|
||||
("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),
|
||||
@@ -344,6 +352,10 @@ pub(super) fn binding_slot<'a>(
|
||||
("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),
|
||||
@@ -449,6 +461,8 @@ pub(super) fn bindings_for_action<'a>(
|
||||
("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()),
|
||||
@@ -486,6 +500,10 @@ pub(super) fn bindings_for_action<'a>(
|
||||
("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", "vim_text_object"],
|
||||
description: "Vim normal, operator, text-object, and visual shortcuts.",
|
||||
contexts: &[
|
||||
"vim_normal",
|
||||
"vim_operator",
|
||||
"vim_text_object",
|
||||
"vim_visual",
|
||||
],
|
||||
},
|
||||
KeymapContextTab {
|
||||
id: "navigation-shortcuts",
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ expression: "render_picker(params, 120)"
|
||||
|
||||
Keymap
|
||||
All configurable shortcuts.
|
||||
125 actions, 1 customized, 2 unbound.
|
||||
131 actions, 1 customized, 2 unbound.
|
||||
|
||||
[All] Common Customized (1) Unbound (2) App Composer Editor Vim Navigation Approval Debug
|
||||
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ expression: "render_picker(params, 120)"
|
||||
|
||||
Keymap
|
||||
All configurable shortcuts.
|
||||
126 actions, 0 customized, 3 unbound.
|
||||
132 actions, 0 customized, 3 unbound.
|
||||
|
||||
[All] Common Customized (0) Unbound (3) App Composer Editor Vim Navigation Approval Debug
|
||||
|
||||
|
||||
+2
-2
@@ -2,14 +2,14 @@
|
||||
source: tui/src/keymap_setup.rs
|
||||
expression: snapshot
|
||||
---
|
||||
tab: All (125 selectable)
|
||||
tab: All (131 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 (66 selectable)
|
||||
tab: Vim (72 selectable)
|
||||
tab: Navigation (20 selectable)
|
||||
tab: Approval (8 selectable)
|
||||
tab: Debug (1 selectable)
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ expression: "render_picker(params, 78)"
|
||||
|
||||
Keymap
|
||||
All configurable shortcuts.
|
||||
125 actions, 0 customized, 2 unbound.
|
||||
131 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.
|
||||
125 actions, 0 customized, 2 unbound.
|
||||
131 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