Detailed changes
@@ -949,17 +949,16 @@ impl Vim {
let current_line = point.row;
let percentage = current_line as f32 / lines as f32;
let modified = if buffer.is_dirty() { " [modified]" } else { "" };
- vim.status_label = Some(
+ vim.set_status_label(
format!(
"{}{} {} lines --{:.0}%--",
filename,
modified,
lines,
percentage * 100.0,
- )
- .into(),
+ ),
+ cx,
);
- cx.notify();
});
}
@@ -50,6 +50,10 @@ impl Vim {
})
.filter(|reg| !reg.text.is_empty())
else {
+ vim.set_status_label(
+ format!("Nothing in register {}", selected_register.unwrap_or('"')),
+ cx,
+ );
return;
};
let clipboard_selections = clipboard_selections
@@ -249,7 +253,7 @@ impl Vim {
) {
self.stop_recording(cx);
let selected_register = self.selected_register.take();
- self.update_editor(cx, |_, editor, cx| {
+ self.update_editor(cx, |vim, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
@@ -262,6 +266,10 @@ impl Vim {
globals.read_register(selected_register, Some(editor), cx)
})
.filter(|reg| !reg.text.is_empty()) else {
+ vim.set_status_label(
+ format!("Nothing in register {}", selected_register.unwrap_or('"')),
+ cx,
+ );
return;
};
editor.insert(&text, window, cx);
@@ -286,7 +294,7 @@ impl Vim {
) {
self.stop_recording(cx);
let selected_register = self.selected_register.take();
- self.update_editor(cx, |_, editor, cx| {
+ self.update_editor(cx, |vim, editor, cx| {
let text_layout_details = editor.text_layout_details(window, cx);
editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);
@@ -306,6 +314,10 @@ impl Vim {
globals.read_register(selected_register, Some(editor), cx)
})
.filter(|reg| !reg.text.is_empty()) else {
+ vim.set_status_label(
+ format!("Nothing in register {}", selected_register.unwrap_or('"')),
+ cx,
+ );
return;
};
editor.insert(&text, window, cx);
@@ -291,6 +291,24 @@ impl Vim {
}) else {
return;
};
+
+ // Dot repeat always uses the recorded register, ignoring any "X
+ // override, as the register is an inherent part of the recorded action.
+ // For numbered registers, Neovim increments on each dot repeat so after
+ // using `"1p`, using `.` will equate to `"2p", the next `.` to `"3p`,
+ // etc..
+ let recorded_register = cx.global::<VimGlobals>().recorded_register_for_dot;
+ let next_register = recorded_register
+ .filter(|c| matches!(c, '1'..='9'))
+ .map(|c| ((c as u8 + 1).min(b'9')) as char);
+
+ self.selected_register = next_register.or(recorded_register);
+ if let Some(next_register) = next_register {
+ Vim::update_globals(cx, |globals, _| {
+ globals.recorded_register_for_dot = Some(next_register)
+ })
+ };
+
if mode != Some(self.mode) {
if let Some(mode) = mode {
self.switch_mode(mode, false, window, cx)
@@ -441,6 +459,207 @@ mod test {
cx.shared_state().await.assert_eq("THE QUICK ˇbrown fox");
}
+ #[gpui::test]
+ async fn test_dot_repeat_registers_paste(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ // basic paste repeat uses the unnamed register
+ cx.set_shared_state("ˇhello\n").await;
+ cx.simulate_shared_keystrokes("y y p").await;
+ cx.shared_state().await.assert_eq("hello\nˇhello\n");
+ cx.simulate_shared_keystrokes(".").await;
+ cx.shared_state().await.assert_eq("hello\nhello\nˇhello\n");
+
+ // "_ (blackhole) is recorded and replayed, so the pasted text is still
+ // the original yanked line.
+ cx.set_shared_state(indoc! {"
+ ˇone
+ two
+ three
+ four
+ "})
+ .await;
+ cx.simulate_shared_keystrokes("y y j \" _ d d . p").await;
+ cx.shared_state().await.assert_eq(indoc! {"
+ one
+ four
+ ˇone
+ "});
+
+ // the recorded register is replayed, not whatever is in the unnamed register
+ cx.set_shared_state(indoc! {"
+ ˇone
+ two
+ "})
+ .await;
+ cx.simulate_shared_keystrokes("y y j \" a y y \" a p .")
+ .await;
+ cx.shared_state().await.assert_eq(indoc! {"
+ one
+ two
+ two
+ ˇtwo
+ "});
+
+ // `"X.` ignores the override and always uses the recorded register.
+ // Both `dd` calls go into register `a`, so register `b` is empty and
+ // `"bp` pastes nothing.
+ cx.set_shared_state(indoc! {"
+ ˇone
+ two
+ three
+ "})
+ .await;
+ cx.simulate_shared_keystrokes("\" a d d \" b .").await;
+ cx.shared_state().await.assert_eq(indoc! {"
+ ˇthree
+ "});
+ cx.simulate_shared_keystrokes("\" a p \" b p").await;
+ cx.shared_state().await.assert_eq(indoc! {"
+ three
+ ˇtwo
+ "});
+
+ // numbered registers cycle on each dot repeat: "1p . . uses registers 2, 3, …
+ // Since the cycling behavior caps at register 9, the first line to be
+ // deleted `1`, is no longer in any of the registers.
+ cx.set_shared_state(indoc! {"
+ ˇone
+ two
+ three
+ four
+ five
+ six
+ seven
+ eight
+ nine
+ ten
+ "})
+ .await;
+ cx.simulate_shared_keystrokes("d d . . . . . . . . .").await;
+ cx.shared_state().await.assert_eq(indoc! {"ˇ"});
+ cx.simulate_shared_keystrokes("\" 1 p . . . . . . . . .")
+ .await;
+ cx.shared_state().await.assert_eq(indoc! {"
+
+ ten
+ nine
+ eight
+ seven
+ six
+ five
+ four
+ three
+ two
+ ˇtwo"});
+
+ // unnamed register repeat: dd records None, so . pastes the same
+ // deleted text
+ cx.set_shared_state(indoc! {"
+ ˇone
+ two
+ three
+ "})
+ .await;
+ cx.simulate_shared_keystrokes("d d p .").await;
+ cx.shared_state().await.assert_eq(indoc! {"
+ two
+ one
+ ˇone
+ three
+ "});
+
+ // After `"1p` cycles to `2`, using `"ap` resets recorded_register to `a`,
+ // so the next `.` uses `a` and not 3.
+ cx.set_shared_state(indoc! {"
+ one
+ two
+ ˇthree
+ "})
+ .await;
+ cx.simulate_shared_keystrokes("\" 2 y y k k \" a y y j \" 1 y y k \" 1 p . \" a p .")
+ .await;
+ cx.shared_state().await.assert_eq(indoc! {"
+ one
+ two
+ three
+ one
+ ˇone
+ two
+ three
+ "});
+ }
+
+ // This needs to be a separate test from `test_dot_repeat_registers_paste`
+ // as Neovim doesn't have support for using registers in replace operations
+ // by default.
+ #[gpui::test]
+ async fn test_dot_repeat_registers_replace(cx: &mut gpui::TestAppContext) {
+ let mut cx = VimTestContext::new(cx, true).await;
+
+ cx.set_state(
+ indoc! {"
+ line ˇone
+ line two
+ line three
+ "},
+ Mode::Normal,
+ );
+
+ // 1. Yank `one` into register `a`
+ // 2. Move down and yank `two` into the default register
+ // 3. Replace `two` with the contents of register `a`
+ cx.simulate_keystrokes("\" a y w j y w \" a g R w");
+ cx.assert_state(
+ indoc! {"
+ line one
+ line onˇe
+ line three
+ "},
+ Mode::Normal,
+ );
+
+ // 1. Move down to `three`
+ // 2. Repeat the replace operation
+ cx.simulate_keystrokes("j .");
+ cx.assert_state(
+ indoc! {"
+ line one
+ line one
+ line onˇe
+ "},
+ Mode::Normal,
+ );
+
+ // Similar test, but this time using numbered registers, as those should
+ // automatically increase on successive uses of `.` .
+ cx.set_state(
+ indoc! {"
+ line ˇone
+ line two
+ line three
+ line four
+ "},
+ Mode::Normal,
+ );
+
+ // 1. Yank `one` into register `1`
+ // 2. Yank `two` into register `2`
+ // 3. Move down and yank `three` into the default register
+ // 4. Replace `three` with the contents of register `1`
+ // 5. Move down and repeat
+ cx.simulate_keystrokes("\" 1 y w j \" 2 y w j y w \" 1 g R w j .");
+ cx.assert_state(
+ indoc! {"
+ line one
+ line two
+ line one
+ line twˇo
+ "},
+ Mode::Normal,
+ );
+ }
+
#[gpui::test]
async fn test_repeat_ime(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
@@ -232,7 +232,15 @@ pub struct VimGlobals {
pub recorded_actions: Vec<ReplayableAction>,
pub recorded_selection: RecordedSelection,
+ /// The register being written to by the active `q{register}` macro
+ /// recording.
pub recording_register: Option<char>,
+ /// The register that was selected at the start of the current
+ /// dot-recording, for example, `"ap`.
+ pub recording_register_for_dot: Option<char>,
+ /// The register from the last completed dot-recording. Used when replaying
+ /// with `.`.
+ pub recorded_register_for_dot: Option<char>,
pub last_recorded_register: Option<char>,
pub last_replayed_register: Option<char>,
pub replayer: Option<Replayer>,
@@ -919,6 +927,7 @@ impl VimGlobals {
self.dot_recording = false;
self.recorded_actions = std::mem::take(&mut self.recording_actions);
self.recorded_count = self.recording_count.take();
+ self.recorded_register_for_dot = self.recording_register_for_dot.take();
self.stop_recording_after_next_action = false;
}
}
@@ -946,6 +955,7 @@ impl VimGlobals {
self.dot_recording = false;
self.recorded_actions = std::mem::take(&mut self.recording_actions);
self.recorded_count = self.recording_count.take();
+ self.recorded_register_for_dot = self.recording_register_for_dot.take();
self.stop_recording_after_next_action = false;
}
}
@@ -996,7 +996,14 @@ impl Vim {
cx: &mut Context<Vim>,
f: impl Fn(&mut Vim, &A, &mut Window, &mut Context<Vim>) + 'static,
) {
- let subscription = editor.register_action(cx.listener(f));
+ let subscription = editor.register_action(cx.listener(move |vim, action, window, cx| {
+ if !Vim::globals(cx).dot_replaying {
+ if vim.status_label.take().is_some() {
+ cx.notify();
+ }
+ }
+ f(vim, action, window, cx);
+ }));
cx.on_release(|_, _| drop(subscription)).detach();
}
@@ -1155,7 +1162,6 @@ impl Vim {
let last_mode = self.mode;
let prior_mode = self.last_mode;
let prior_tx = self.current_tx;
- self.status_label.take();
self.last_mode = last_mode;
self.mode = mode;
self.operator_stack.clear();
@@ -1586,6 +1592,7 @@ impl Vim {
globals.dot_recording = true;
globals.recording_actions = Default::default();
globals.recording_count = None;
+ globals.recording_register_for_dot = self.selected_register;
let selections = self.editor().map(|editor| {
editor.update(cx, |editor, cx| {
@@ -2092,6 +2099,11 @@ impl Vim {
editor.selections.set_line_mode(state.line_mode);
editor.set_edit_predictions_hidden_for_vim_mode(state.hide_edit_predictions, window, cx);
}
+
+ fn set_status_label(&mut self, label: impl Into<SharedString>, cx: &mut Context<Editor>) {
+ self.status_label = Some(label.into());
+ cx.notify();
+ }
}
struct VimEditorSettingsState {
@@ -0,0 +1,125 @@
+{"Put":{"state":"ˇhello\n"}}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"p"}
+{"Get":{"state":"hello\nˇhello\n","mode":"Normal"}}
+{"Key":"."}
+{"Get":{"state":"hello\nhello\nˇhello\n","mode":"Normal"}}
+{"Put":{"state":"ˇtocopytext\n1\n2\n3\n"}}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"j"}
+{"Key":"\""}
+{"Key":"_"}
+{"Key":"d"}
+{"Key":"d"}
+{"Key":"."}
+{"Key":"p"}
+{"Get":{"state":"tocopytext\n3\nˇtocopytext\n","mode":"Normal"}}
+{"Put":{"state":"ˇtocopytext\n1\n2\n3\n"}}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"j"}
+{"Key":"\""}
+{"Key":"1"}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"j"}
+{"Key":"j"}
+{"Key":"\""}
+{"Key":"1"}
+{"Key":"p"}
+{"Key":"."}
+{"Get":{"state":"tocopytext\n1\n2\n3\nˇ1\n","mode":"Normal"}}
+{"Put":{"state":"ˇone\ntwo\nthree\n"}}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"d"}
+{"Key":"d"}
+{"Key":"\""}
+{"Key":"b"}
+{"Key":"."}
+{"Get":{"state":"ˇthree\n","mode":"Normal"}}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"p"}
+{"Key":"\""}
+{"Key":"b"}
+{"Key":"p"}
+{"Get":{"state":"three\nˇtwo\n","mode":"Normal"}}
+{"Put":{"state":"ˇline one\nline two\n"}}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"j"}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"p"}
+{"Key":"."}
+{"Key":"\""}
+{"Key":"b"}
+{"Key":"."}
+{"Get":{"state":"line one\nline two\nline one\nline one\nˇline one\n","mode":"Normal"}}
+{"Put":{"state":"ˇ1\n2\n3\n4\n5\n6\n7\n8\n9\n"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Get":{"state":"ˇ","mode":"Normal"}}
+{"Key":"\""}
+{"Key":"1"}
+{"Key":"p"}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Get":{"state":"\n9\n8\n7\n6\n5\n4\n3\n2\n1\nˇ1","mode":"Normal"}}
+{"Put":{"state":"ˇa\nb\nc\n"}}
+{"Key":"\""}
+{"Key":"9"}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"\""}
+{"Key":"9"}
+{"Key":"p"}
+{"Key":"."}
+{"Key":"."}
+{"Get":{"state":"a\na\na\nˇa\nb\nc\n","mode":"Normal"}}
+{"Put":{"state":"ˇone\ntwo\nthree\n"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Key":"p"}
+{"Key":"."}
+{"Get":{"state":"two\none\nˇone\nthree\n","mode":"Normal"}}
+{"Put":{"state":"ˇone\ntwo\nthree\n"}}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"j"}
+{"Key":"\""}
+{"Key":"1"}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"k"}
+{"Key":"\""}
+{"Key":"1"}
+{"Key":"p"}
+{"Key":"."}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"p"}
+{"Key":"."}
+{"Get":{"state":"one\ntwo\n9\none\nˇone\ntwo\nthree\n","mode":"Normal"}}
@@ -0,0 +1,105 @@
+{"Put":{"state":"ˇhello\n"}}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"p"}
+{"Get":{"state":"hello\nˇhello\n","mode":"Normal"}}
+{"Key":"."}
+{"Get":{"state":"hello\nhello\nˇhello\n","mode":"Normal"}}
+{"Put":{"state":"ˇone\ntwo\nthree\nfour\n"}}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"j"}
+{"Key":"\""}
+{"Key":"_"}
+{"Key":"d"}
+{"Key":"d"}
+{"Key":"."}
+{"Key":"p"}
+{"Get":{"state":"one\nfour\nˇone\n","mode":"Normal"}}
+{"Put":{"state":"ˇone\ntwo\n"}}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"j"}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"p"}
+{"Key":"."}
+{"Get":{"state":"one\ntwo\ntwo\nˇtwo\n","mode":"Normal"}}
+{"Put":{"state":"ˇone\ntwo\nthree\n"}}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"d"}
+{"Key":"d"}
+{"Key":"\""}
+{"Key":"b"}
+{"Key":"."}
+{"Get":{"state":"ˇthree\n","mode":"Normal"}}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"p"}
+{"Key":"\""}
+{"Key":"b"}
+{"Key":"p"}
+{"Get":{"state":"three\nˇtwo\n","mode":"Normal"}}
+{"Put":{"state":"ˇone\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\n"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Get":{"state":"ˇ","mode":"Normal"}}
+{"Key":"\""}
+{"Key":"1"}
+{"Key":"p"}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Get":{"state":"\nten\nnine\neight\nseven\nsix\nfive\nfour\nthree\ntwo\nˇtwo","mode":"Normal"}}
+{"Put":{"state":"ˇone\ntwo\nthree\n"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Key":"p"}
+{"Key":"."}
+{"Get":{"state":"two\none\nˇone\nthree\n","mode":"Normal"}}
+{"Put":{"state":"one\ntwo\nˇthree\n"}}
+{"Key":"\""}
+{"Key":"2"}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"k"}
+{"Key":"k"}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"j"}
+{"Key":"\""}
+{"Key":"1"}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"k"}
+{"Key":"\""}
+{"Key":"1"}
+{"Key":"p"}
+{"Key":"."}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"p"}
+{"Key":"."}
+{"Get":{"state":"one\ntwo\nthree\none\nˇone\ntwo\nthree\n","mode":"Normal"}}