mirror of
https://github.com/openai/codex.git
synced 2026-11-06 08:31:30 +00:00
Add redo support to Vim composer history (#42140)
## What changed - Add a bounded redo stack for complete composer drafts, including paste payloads and image attachments. `Ctrl+R` now reapplies the last undone edit in Vim normal mode, while new edits clear stale redo history. - Add the configurable `vim_normal.redo` action to the keymap schema and picker. Preserve reverse prompt-history search in Vim insert mode and non-Vim mode, and honor explicitly configured bindings. - Keep pending edit snapshots outside the shared undo/redo byte budget so canceled commands cannot evict committed history. ## Testing - Cover grouped edits, Vim searches, direct draft changes, history limits, empty redo stacks, and custom or unbound shortcuts. GitOrigin-RevId: 0cf5bb376ff865ddc0a77705dcb322ef6b57b3a3
This commit is contained in:
1 parent
612e6491d5
commit
b192442fc7
18 files changed
+318
-59
No files matched your search
+67
-34
@@ -211,6 +211,7 @@ pub(crate) struct VimNormalKeymap {
|
||||
pub(crate) start_yank_operator: Vec<KeyBinding>,
|
||||
pub(crate) start_change_operator: Vec<KeyBinding>,
|
||||
pub(crate) undo: Vec<KeyBinding>,
|
||||
pub(crate) redo: Vec<KeyBinding>,
|
||||
pub(crate) cancel_operator: Vec<KeyBinding>,
|
||||
}
|
||||
|
||||
@@ -759,6 +760,7 @@ impl RuntimeKeymap {
|
||||
start_change_operator
|
||||
),
|
||||
undo: resolve_local!(keymap, defaults, vim_normal, undo),
|
||||
redo: resolve_local!(keymap, defaults, vim_normal, redo),
|
||||
cancel_operator: resolve_local!(keymap, defaults, vim_normal, cancel_operator),
|
||||
};
|
||||
|
||||
@@ -892,6 +894,7 @@ impl RuntimeKeymap {
|
||||
vim_normal.start_change_operator.as_slice(),
|
||||
),
|
||||
(keymap.vim_normal.undo.as_ref(), vim_normal.undo.as_slice()),
|
||||
(keymap.vim_normal.redo.as_ref(), vim_normal.redo.as_slice()),
|
||||
(
|
||||
keymap.vim_normal.cancel_operator.as_ref(),
|
||||
vim_normal.cancel_operator.as_slice(),
|
||||
@@ -1365,22 +1368,33 @@ impl RuntimeKeymap {
|
||||
approval,
|
||||
};
|
||||
|
||||
if keymap.vim_normal.undo.is_none() {
|
||||
let configured: Vec<_> = runtime_action_bindings(&resolved)
|
||||
.filter(|action| {
|
||||
action.id.context.overlaps(KeymapContext::VimNormal)
|
||||
&& bindings::configured_binding_for_action(keymap, action.id)
|
||||
.is_some_and(Option::is_some)
|
||||
})
|
||||
.flat_map(|action| action.bindings.iter().copied())
|
||||
.collect();
|
||||
resolved.vim_normal.undo.retain(|binding| {
|
||||
!configured.contains(binding)
|
||||
&& !resolved.chords.bindings.iter().any(|chord| {
|
||||
chord.action.context.overlaps(KeymapContext::VimNormal)
|
||||
&& chord.chord.prefix.parts() == binding.parts()
|
||||
})
|
||||
});
|
||||
let configured: Vec<_> = runtime_action_bindings(&resolved)
|
||||
.filter(|action| {
|
||||
action.id.context.overlaps(KeymapContext::VimNormal)
|
||||
&& bindings::configured_binding_for_action(keymap, action.id)
|
||||
.is_some_and(Option::is_some)
|
||||
})
|
||||
.flat_map(|action| action.bindings.iter().copied())
|
||||
.collect();
|
||||
for (setting, bindings) in [
|
||||
(
|
||||
keymap.vim_normal.undo.as_ref(),
|
||||
&mut resolved.vim_normal.undo,
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.redo.as_ref(),
|
||||
&mut resolved.vim_normal.redo,
|
||||
),
|
||||
] {
|
||||
if setting.is_none() {
|
||||
bindings.retain(|binding| {
|
||||
!configured.contains(binding)
|
||||
&& !resolved.chords.bindings.iter().any(|chord| {
|
||||
chord.action.context.overlaps(KeymapContext::VimNormal)
|
||||
&& chord.chord.prefix.parts() == binding.parts()
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
resolved.configure_vim_search(keymap)?;
|
||||
resolved.validate_conflicts()?;
|
||||
@@ -1559,6 +1573,7 @@ impl RuntimeKeymap {
|
||||
start_yank_operator: default_bindings![plain(KeyCode::Char('y'))],
|
||||
start_change_operator: default_bindings![plain(KeyCode::Char('c'))],
|
||||
undo: default_bindings![plain(KeyCode::Char('u'))],
|
||||
redo: default_bindings![ctrl(KeyCode::Char('r'))],
|
||||
cancel_operator: default_bindings![plain(KeyCode::Esc)],
|
||||
},
|
||||
vim_search: VimSearchKeymap::default(),
|
||||
@@ -2982,24 +2997,35 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn configured_legacy_bindings_prune_new_history_defaults() {
|
||||
for (context, action, binding) in [
|
||||
("vim_normal", "move_left", "u"),
|
||||
("vim_normal", "move_left", "u g"),
|
||||
("vim_search", "forward", "u"),
|
||||
("vim_search", "forward", "u g"),
|
||||
("composer", "submit", "u"),
|
||||
("global", "submit", "u"),
|
||||
("global", "queue", "u"),
|
||||
("global", "toggle_shortcuts", "u"),
|
||||
] {
|
||||
let keymap = serde_json::from_value(serde_json::json!({
|
||||
(context): { (action): binding }
|
||||
}))
|
||||
.expect("config should deserialize");
|
||||
|
||||
let runtime = RuntimeKeymap::from_config(&keymap).expect("config should parse");
|
||||
|
||||
assert_eq!(runtime.vim_normal.undo, Vec::new());
|
||||
for key in ["u", "ctrl-r"] {
|
||||
for (context, action, suffix) in [
|
||||
("vim_normal", "move_left", ""),
|
||||
("vim_normal", "move_left", " g"),
|
||||
("vim_search", "forward", ""),
|
||||
("vim_search", "forward", " g"),
|
||||
("composer", "submit", ""),
|
||||
("global", "submit", ""),
|
||||
("global", "queue", ""),
|
||||
("global", "toggle_shortcuts", ""),
|
||||
] {
|
||||
let mut keymap: TuiKeymap = serde_json::from_value(serde_json::json!({
|
||||
(context): { (action): format!("{key}{suffix}") }
|
||||
}))
|
||||
.expect("config should deserialize");
|
||||
if key == "ctrl-r"
|
||||
&& (!suffix.is_empty() || matches!(context, "composer" | "global"))
|
||||
{
|
||||
// Ctrl+R chords and main-surface actions must unbind the older history key.
|
||||
keymap.composer.history_search_previous = Some(KeybindingsSpec::Many(vec![]));
|
||||
}
|
||||
let runtime = RuntimeKeymap::from_config(&keymap).expect("config should parse");
|
||||
let bindings = if key == "u" {
|
||||
runtime.vim_normal.undo
|
||||
} else {
|
||||
runtime.vim_normal.redo
|
||||
};
|
||||
assert_eq!(bindings, Vec::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3009,16 +3035,23 @@ mod tests {
|
||||
keymap.vim_normal.move_left = Some(one("u"));
|
||||
keymap.vim_normal.undo = Some(one("u"));
|
||||
expect_conflict(&keymap, "move_left", "undo");
|
||||
|
||||
keymap.vim_normal.move_left = Some(one("ctrl-r"));
|
||||
keymap.vim_normal.undo = None;
|
||||
keymap.vim_normal.redo = Some(one("ctrl-r"));
|
||||
expect_conflict(&keymap, "move_left", "redo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_empty_arrays_unbind_vim_history_actions() {
|
||||
let mut keymap = TuiKeymap::default();
|
||||
keymap.vim_normal.undo = Some(KeybindingsSpec::Many(vec![]));
|
||||
keymap.vim_normal.redo = Some(KeybindingsSpec::Many(vec![]));
|
||||
|
||||
let runtime = RuntimeKeymap::from_config(&keymap).expect("config should parse");
|
||||
|
||||
assert!(runtime.vim_normal.undo.is_empty());
|
||||
assert!(runtime.vim_normal.redo.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user