Add setrepeat() and getrepeat() functions to allow scripts to programmatically control the dot (.) repeat command.
This enables plugins to:
.This addresses several long-standing feature requests:
. command while making other changes, then restore it. repeat" Set repeat command call setrepeat({'cmd': 'dd'}) " Normal mode call setrepeat({'cmd': 'i', 'text': 'Hello'}) " Insert mode " Get repeat command echo getrepeat() " → {'cmd': 'dd', 'text': ''} " → {'cmd': 'i', 'text': 'Hello'} " Existing functionality unchanged echo getreg('.') " Still returns last inserted text (read-only)
Phase 1 (this PR):
{
'cmd': 'string' " Required - the command to repeat
'text': 'string' " Optional - text to insert (for insert mode)
}Future extensions (Phase 2):
{
'cmd': 'string'
'text': 'string'
'type': 'string' " 'normal', 'insert', 'visual'
'mode': 'string' " 'v', 'V', CTRL-V
'count': number " Repeat count
'register': 'string' " Target register
}function! PromptBufferOperation() " Save current repeat let saved = getrepeat() " Temporary edits in prompt buffer " (these would normally overwrite the repeat) " Restore original repeat call setrepeat(saved) endfunction
function! MyComplexCommand() " ... complex operation ... " Make it repeatable with . call setrepeat({'cmd': ':call MyComplexCommand()'}) endfunction command! MyCommand call MyComplexCommand() " User can now use . to repeat :MyCommand
let g:repeat_history = [] " Save current repeat call add(g:repeat_history, getrepeat()) " Later, restore from history call setrepeat(g:repeat_history[0]) normal! .
. register?This is a redesign of #19342 based on community feedback. The original approach had several issues:
getreg('.') behavior could break scriptsThe new approach:
getreg('.') unchanged (backward compatible)Documented in the help files:
Insert mode overwrites setrepeat()
When entering/leaving insert mode, Vim's automatic recording overwrites custom repeats.
Workaround: Call setrepeat() after insert mode operations.
setline() and similar not recorded
Text modification functions don't update the repeat command.
Workaround: Use feedkeys() to simulate typing.
Visual mode not supported
Will be added in Phase 2 with additional dictionary fields.
Limited info for user operations
getrepeat() provides full info only for setrepeat()-set values.
User operations return limited information.
These limitations don't affect the primary use cases (save/restore, plugin integration, history).
Potential enhancements:
type and mode fields)count field)register field)getrepeat() for user operationsAll can be added via dictionary fields without breaking changes.
Ready for review. This provides a solid foundation for script control of
the dot command while maintaining backward compatibility and allowing future
enhancements.
https://github.com/vim/vim/pull/19413
(8 files)
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
@Shougo pushed 2 commits.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@Shougo pushed 2 commits.
You are receiving this because you are subscribed to this thread.![]()
@64-bitman commented on this pull request.
> @@ -5195,6 +5197,37 @@ getregtype([{regname}]) *getregtype()*
<
Return type: |String|
+getrepeat() *getrepeat()*
+ Get the last repeat command as a |Dictionary|.
+
+ Returns a dictionary with the following keys:
+ 'cmd' The command that will be repeated (string)
+ 'text' Text to insert (string)
+
+ If the repeat was set via |setrepeat()|, returns the exact
+ dictionary that was passed. Otherwise, returns limited
+ information from Vim's internal repeat buffer.
+
+ This is useful for:
+ - Saving and restoring the repeat command (e.g., in plugins
+ like telescope that perform temporary operations)
I don't think we should refer to external plugins (telescope) in the help doc
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
> + endfunction +< + 2. Save/restore (telescope style): > + let saved = getrepeat() + " ... temporary operations ... + call setrepeat(saved) +< + 3. Repeat history: > + let g:history = [] + call add(g:history, getrepeat()) + " ... later ... + call setrepeat(g:history[0]) +< + Limitations: + + Phase 1 has the following limitations:
The phase 1 and 2 stuff should probably not be talked about in the help docs
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
> + Examples: >
+ " Normal mode command
+ call setrepeat({'cmd': 'dd'})
+ normal! . " deletes a line
+
+ " Insert mode operation
+ call setrepeat({'cmd': 'i', 'text': 'Hello'})
+ normal! . " inserts 'Hello'
+
+ " Change operation
+ call setrepeat({'cmd': 'cw', 'text': 'newword'})
+ normal! . " changes word to 'newword'
+<
+ Use cases:
+
+ 1. Plugin integration (vim-repeat style): >
ditto
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
> + call setrepeat({'cmd': 'i', 'text': 'Hello'})
+ normal! . " inserts 'Hello'
+
+ " Change operation
+ call setrepeat({'cmd': 'cw', 'text': 'newword'})
+ normal! . " changes word to 'newword'
+<
+ Use cases:
+
+ 1. Plugin integration (vim-repeat style): >
+ function! MyCommand()
+ " ... complex operation ...
+ call setrepeat({'cmd': ':call MyCommand()'})
+ endfunction
+<
+ 2. Save/restore (telescope style): >
ditto
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
When I wrote a patch for this a few years ago (which I never ended up finishing/submitting) I used the , register, which is basically the same as your other PR (#19342) but with a different register name.
I also added the ; register for the previous redo command. I added that so it's easy to "undo" the last redo with something like:
nnoremap <silent> g. :let [@,, @;] = [@;, '']<CR>
I find that's about 99% of what I want this for: "I want to repeat this change 6 times but oh, after the 2nd I find that I want to make one other tiny change, and now my . is messed up".
Personally I kind of liked that approach. I'm okay with functions too. Just some ideas.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
@Shougo commented on this pull request.
> @@ -5195,6 +5197,37 @@ getregtype([{regname}]) *getregtype()*
<
Return type: |String|
+getrepeat() *getrepeat()*
+ Get the last repeat command as a |Dictionary|.
+
+ Returns a dictionary with the following keys:
+ 'cmd' The command that will be repeated (string)
+ 'text' Text to insert (string)
+
+ If the repeat was set via |setrepeat()|, returns the exact
+ dictionary that was passed. Otherwise, returns limited
+ information from Vim's internal repeat buffer.
+
+ This is useful for:
+ - Saving and restoring the repeat command (e.g., in plugins
+ like telescope that perform temporary operations)
Fixed.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
> + endfunction +< + 2. Save/restore (telescope style): > + let saved = getrepeat() + " ... temporary operations ... + call setrepeat(saved) +< + 3. Repeat history: > + let g:history = [] + call add(g:history, getrepeat()) + " ... later ... + call setrepeat(g:history[0]) +< + Limitations: + + Phase 1 has the following limitations:
Fixed.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
> + Examples: >
+ " Normal mode command
+ call setrepeat({'cmd': 'dd'})
+ normal! . " deletes a line
+
+ " Insert mode operation
+ call setrepeat({'cmd': 'i', 'text': 'Hello'})
+ normal! . " inserts 'Hello'
+
+ " Change operation
+ call setrepeat({'cmd': 'cw', 'text': 'newword'})
+ normal! . " changes word to 'newword'
+<
+ Use cases:
+
+ 1. Plugin integration (vim-repeat style): >
Fixed.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
> + call setrepeat({'cmd': 'i', 'text': 'Hello'})
+ normal! . " inserts 'Hello'
+
+ " Change operation
+ call setrepeat({'cmd': 'cw', 'text': 'newword'})
+ normal! . " changes word to 'newword'
+<
+ Use cases:
+
+ 1. Plugin integration (vim-repeat style): >
+ function! MyCommand()
+ " ... complex operation ...
+ call setrepeat({'cmd': ':call MyCommand()'})
+ endfunction
+<
+ 2. Save/restore (telescope style): >
Fixed.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
Thanks for sharing your approach! The , and ; register idea is elegant and concise.
I considered a register-based approach in #19342, but ultimately chose functions for several reasons:
Registers have implicit behavior that can be surprising:
" What does this do? let @, = "dd" " Is it: " - Delete line? " - Set repeat to delete line? " - Something else?
Functions are more explicit:
call setrepeat({'cmd': 'dd'}) " Clear intent: set repeat command
Registers are string-based, limiting future enhancements:
" How do we add count, visual mode, or register info? let @, = "dd" " Just a string " vs " Phase 1 (current)
call setrepeat({'cmd': 'dd'})
" Phase 2 (future) - no breaking changes call setrepeat({ \ 'cmd': 'dd', \ 'count': 3, \ 'register': 'a', \ 'type': 'normal' \ })
String representation has limitations:
<Plug> mappingsDictionary structure handles these naturally.
I really like your ; register idea for "undo last repeat change"! We could add similar functionality:
" Future enhancement call setrepeat(getrepeat(-1)) " Restore previous repeat " Or a history function let history = getrepeathistory() call setrepeat(history[1])
This could be added in Phase 2 without breaking existing code.
The primary use cases that drove this design:
call setrepeat({'cmd': ':call MyPlugin()'})<Plug> supportFor your use case ("undo accidental repeat change"), would a history function work?
" Hypothetical Phase 2 feature nnoremap <silent> g. :call setrepeat(getrepeat(-1))<CR>
I'm open to feedback, but I believe the function approach provides better long-term maintainability and clarity.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@zeertzjq commented on this pull request.
> + 'cmd' The command that will be repeated (string) + 'text' Text to insert (string)⬇️ Suggested change
- 'cmd' The command that will be repeated (string) - 'text' Text to insert (string) + "cmd" The command that will be repeated (string) + "text" Text to insert (string)
> + 'cmd' (required) The command to repeat (string) + 'text' (optional) Text to insert (string) + + The 'cmd' field specifies the command. For insert mode + commands (i, a, o, O, I, A, c, s, C, S), the 'text' field + contains the text to insert. For normal mode commands (dd, + yy, x, etc.), 'text' can be omitted or empty.⬇️ Suggested change
- 'cmd' (required) The command to repeat (string) - 'text' (optional) Text to insert (string) - - The 'cmd' field specifies the command. For insert mode - commands (i, a, o, O, I, A, c, s, C, S), the 'text' field - contains the text to insert. For normal mode commands (dd, - yy, x, etc.), 'text' can be omitted or empty. + "cmd" (required) The command to repeat (string) + "text" (optional) Text to insert (string) + + The "cmd" field specifies the command. For Insert mode + commands (i, a, o, O, I, A, c, s, C, S), the "text" field + contains the text to insert. For Normal mode commands (dd, + yy, x, etc.), "text" can be omitted or empty.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@zeertzjq commented on this pull request.
In src/edit.c:
> + // Return empty dictionary if nothing was set
+ dict = dict_alloc();
+ if (dict != NULL)
+ {
+ ++dict->dv_refcount;
+ dict_add_string(dict, "cmd", (char_u *)"");
+ dict_add_string(dict, "text", (char_u *)"");
+ }
The documentation says "Otherwise, returns limited information from Vim's internal repeat buffer", but this doesn't include any of that information.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@justinmk commented on this pull request.
In src/testdir/test_functions.vim:
> + " Test getrepeat when no setrepeat was called + " Should return a dictionary with empty or limited info + let result = getrepeat()
So getrepeat cannot be used to get the current dot-repeat register, i.e. the last "action" that a user performed (without setrepeat)?
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
I feel that the specifications are still not well-regulated.
$ vim --clean +"call setrepeat(#{cmd:'i', text:'Hello'})" +"call feedkeys('.', 't')"
Hello is typed, but Vim remains in insert mode, which is different from the existing . convention and confuses users.
Write according to the rules.
:h help-writing /^STYLE
Especially the following:
Use two spaces between the final dot of a sentence of the first letter of the
next sentence.
The return value must be listed at the end of the function description.
Return type: |Number|
The function names (setrepeat(), getrepeat()) is too abstract. Give it a better name.
Exsample:
dotrepeat_get()
dotrepeat_set()
dotrepeat_clear()
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
So getrepeat cannot be used to get the current dot-repeat register, i.e. the last "action" that a user performed (without setrepeat)?
Currently, no. The implementation has two modes:
1. After setrepeat(): Full support ✅
getrepeat() returns the exact {'cmd': '...', 'text': '...'} that was set. command correctly repeats the operation2. After user operations (like iHello<Esc>): Limited/no support ❌
getrepeat() returns empty or incomplete informationi, a, o, etc.) from Vim's internal stateWhy this limitation?
Vim stores repeat information in several places:
last_insert buffer: contains the inserted text but not the command type. register: contains the text but not the commandredo_buffer: contains the full command but is opaque (can't easily read from Vimscript)Parsing these internal structures reliably is complex and error-prone.
The main use case still works:
Plugins can save/restore repeat state around temporary operations:
" Plugin performs user operation execute "normal! iHello\<Esc>" " Plugin saves its own repeat state let saved = getrepeat() " Plugin does temporary work
call setrepeat({'cmd': 'dd'})
normal! .
" Restore original repeat call setrepeat(saved)
" User's . command still works normal! . " repeats the plugin's 'iHello'
We could add user operation support by:
Would you like me to implement this, or is the current behavior acceptable for the initial version?
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
Would you like me to implement this, or is the current behavior acceptable for the initial version?
Not a blocker imo, if we agree the door is open for getrepeat() in the future to get "user operations". Could perhaps be opt-in via a flag.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
I have implemented simple user commands support.
@justinmk @zeertzjq What do you think?
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
@zeertzjq commented on this pull request.
In src/edit.c:
> + // If setrepeat() was used, return that + if (g_last_repeat_dict != NULL) + return dict_copy(g_last_repeat_dict, TRUE, TRUE, get_copyID());
It seems that after using setrepeat() once, future calls of getrepeat() will return the value of the last setrepeat() even if the redo buffer has changed by user actions. I don't think that's desired.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@Shougo pushed 3 commits.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
Thank you for the detailed feedback! I've addressed all the issues:
Bug fix: Fixed the insert mode issue. The problem was that ESC wasn't being appended to the redo buffer for insert mode commands. Now insert commands (i, a, o, etc.) properly exit to normal mode after repeat.
Documentation style: Updated to follow :help help-writing guidelines with two spaces between sentences and return type at the end.
Function naming: I believe getrepeat() and setrepeat() are appropriate for this feature, following Vim's existing patterns like getpos()/setpos(). Could you explain the use case for dotrepeat_clear()? Currently, setrepeat({}) can clear the repeat state, so I'm not sure when a separate clear function would be needed. If there are other related functions you think should be added, I'd be interested to hear about the requirements.
The latest changes are now pushed. Please let me know what you think.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
@Shougo pushed 2 commits.
You are receiving this because you are subscribed to this thread.![]()
@Shougo commented on this pull request.
In src/edit.c:
> + // If setrepeat() was used, return that + if (g_last_repeat_dict != NULL) + return dict_copy(g_last_repeat_dict, TRUE, TRUE, get_copyID());
Thanks. I have fixed the problem.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@Shougo pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
Function naming: I believe getrepeat() and setrepeat() are appropriate for this feature, following Vim's existing patterns like getpos()/setpos().
There's no persuasive power in listing APIs that were implemented early on.
Vim 8 and later actively use API names that follow the format function name prefix + _ + verb, etc.. There's no reason not to follow that.
assert_~(), autocmd_~(), balloon_~(), base64_~(), ch_~(), digraph_~(), job_~(),
js_~(), json_~(), listener_~(), popup_~(), prompt_~(), prop_~(), reg_~(),
sign_~(), sound_~(), test_~(), timer_~(), uri_~(), win_~()
Also, repeat is too "abstract".
Shouldn't you at least add dot?
Could you explain the use case for dotrepeat_clear()?
The specifications haven't been finalized yet, and it was just mentioned as an example, so there's no need to go into too much detail. It might even be a catalyst for something...
Regardless of the API name, I would not accept the current specification.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
Documentation style: Updated to follow :help help-writing guidelines with two spaces between sentences and return type at the end.
Doubt!
You can let the AI do it, but you have to do the final check yourself.
You are responsible for the AI's output!
If you work for a company, this is the first thing you'll be told in your AI usage training.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
Thank you for the feedback! I agree that following the Vim 8+ naming convention makes sense.
I'll rename the functions to dot_set() and dot_get() to follow the prefix_verb pattern and make it clear they operate on the dot command.
Regarding your comment "I would not accept the current specification" - could you clarify what concerns you have? Is it:
I want to make sure I address all the issues before proceeding.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
@Shougo pushed 24 commits.
You are receiving this because you are subscribed to this thread.![]()
@mattn commented on this pull request.
In src/evalfunc.c:
> @@ -11726,6 +11748,22 @@ f_setreg(typval_T *argvars, typval_T *rettv)
rettv->vval.v_number = 0;
}
+/*
+ * "setrepeat({dict})" function
+ */
+ static void
+f_setrepeat(typval_T *argvars, typval_T *rettv UNUSED)
+{
+ dict_T *dict;
+
+ if (check_for_dict_arg(argvars, 0) == FAIL)
+ return;
+
+ dict = argvars[0].vval.v_dict;
+ if (dict != NULL)
+ set_repeat_dict(dict);
Since there is a NULL check within set_repeat_dict, it is unnecessary here.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@Shougo pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@Shougo commented on this pull request.
In src/evalfunc.c:
> @@ -11726,6 +11748,22 @@ f_setreg(typval_T *argvars, typval_T *rettv)
rettv->vval.v_number = 0;
}
+/*
+ * "setrepeat({dict})" function
+ */
+ static void
+f_setrepeat(typval_T *argvars, typval_T *rettv UNUSED)
+{
+ dict_T *dict;
+
+ if (check_for_dict_arg(argvars, 0) == FAIL)
+ return;
+
+ dict = argvars[0].vval.v_dict;
+ if (dict != NULL)
+ set_repeat_dict(dict);
Fixed. Thanks.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
I agree that following the Vim 8+ naming convention makes sense.
I'll rename the functions to
dot_set()anddot_get()
Since when is it a vim convention to name functions by the punctuation name ("dot" in this case)? If "repeat" isn't wanted for some reason (why?) perhaps "redo" works.
Also,
repeatis too "abstract".
Shouldn't you at least adddot?
I assume this was suggesting dotrepeat, not dot.
But I don't get why we want to avoid "abstraction" to such an extreme; this feature may gain more capabilities in the future.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
Thank you for the feedback on naming!
I initially considered repeat_set() / repeat_get(), but avoided it because repeat() already exists and I was concerned about potential confusion.
I chose dot_ because the "dot command" seems to be the most commonly used term among users (rather than internal implementation terms like "redo").
I think that dotrepeat_set() / dotrepeat_get() would be too verbose.
However, you raise a good point about Vim's official terminology. Looking at :help ., "repeat" is indeed the documented term. If the concern about repeat() confusion isn't significant (since they're used in different contexts), I'm happy to go with repeat_set() / repeat_get().
What do you think would be clearest for users?
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
If we're bikeshedding names, then my 2c is lastchange_get() and lastchange_set(), or something along those lines. In my mind at least that's what . holds: "the last change".
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
@arp242 Thanks for the suggestion!
After thinking about this more, I believe repeat_set() / repeat_get() is the right choice:
:help repeat.txt documents this feature as "repeat". command, they think "repeat"Regarding potential confusion with the existing repeat() function: I think the risk is low because they're used in completely different contexts:
repeat(string, count) - string manipulationrepeat_set(dict) / repeat_get() - editor behavior controlThe different argument types and use cases make them clearly distinct, similar to how getpos() and pos() coexist without confusion.
What do you think?
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
@chrisbra commented on this pull request.
In src/edit.c:
> +
+ // Save a copy of the dictionary for getrepeat()
+ g_last_repeat_dict = dict_copy(dict, TRUE, FALSE, get_copyID());
+ if (g_last_repeat_dict != NULL)
+ ++g_last_repeat_dict->dv_refcount;
+
+ // Build the command string: cmd + text
+ cmd_len = STRLEN(cmd);
+ text_len = (text != NULL && *text != NUL) ? STRLEN(text) : 0;
+
+ if (text_len > 0)
+ {
+ // Combine cmd and text
+ combined = alloc(cmd_len + text_len + 1);
+ if (combined == NULL)
+ return;
You are leaking g_last_repeat_dict here.
In src/edit.c:
> +
+ if (text_len > 0)
+ {
+ // Combine cmd and text
+ combined = alloc(cmd_len + text_len + 1);
+ if (combined == NULL)
+ return;
+
+ STRCPY(combined, cmd);
+ STRCPY(combined + cmd_len, text);
+
+ set_last_insert_str(combined);
+ vim_free(combined);
+ }
+ else
+ {
you can drop the braces here.
In src/edit.c:
> + if (dict == NULL)
+ return NULL;
+
+ dict->dv_refcount = 1;
+
+ // Get command from tracked insert command
+ cmd_str[0] = NUL;
+ cmd_str[1] = NUL;
+ if (last_insert_cmdchar != NUL && last_insert_cmdchar != 'i')
+ {
+ // Only include insert mode commands: i, a, o, I, A, O, etc.
+ if (vim_strchr((char_u *)"iIaAoOcCsS", last_insert_cmdchar) != NULL)
+ cmd_str[0] = last_insert_cmdchar;
+ }
+ else if (last_insert_cmdchar == 'i')
+ cmd_str[0] = 'i';
This is a bit strange case for insert mode: We manually skip i in the first if statement, then check it in the vim_strchr() again, although it cannot be there and then in the else if case we assign it anyhow.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@Copilot commented on this pull request.
Adds a Vimscript API for programmatic control of dot-repeat (.) via new setrepeat()/getrepeat() functions, with supporting core changes, tests, and documentation.
Changes:
setrepeat({dict}) and getrepeat() to the builtin function table and implement them in core.:help and add the test target to the test runner makefile.Copilot reviewed 11 out of 11 changed files in this pull request and generated 7 comments.
Show a summary per file| File | Description |
|---|---|
| src/edit.c | Implements repeat dict storage, set_repeat_dict()/get_repeat_dict(), and redo buffer updates. |
| src/evalfunc.c | Registers and implements the Vimscript functions setrepeat() and getrepeat(). |
| src/getchar.c | Adds skipRestoreRedobuff() and modifies redo save/restore behavior for programmatic redo updates. |
| src/normal.c | Clears programmatic repeat state after certain user operations (operator path). |
| src/proto/edit.pro | Adds prototypes for repeat dict helpers. |
| src/proto/getchar.pro | Adds prototype for skipRestoreRedobuff(). |
| src/testdir/test_repeat.vim | New tests for setrepeat()/getrepeat() behavior and . integration. |
| src/testdir/Make_all.mak | Adds test_repeat to the NEW_TESTS / NEW_TESTS_RES lists. |
| runtime/doc/builtin.txt | Documents getrepeat() and setrepeat() in builtin function help. |
| runtime/doc/usr_41.txt | Adds getrepeat()/setrepeat() to the register-related function list. |
| runtime/doc/repeat.txt | Notes that scripts can control dot-repeat via the new functions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
In src/edit.c:
> + // Check if the first character is an insert mode command
+ if (str != NULL && *str != NUL)
+ {
+ int first_char = *p;
+ // Check if it's a valid insert command character
+ if (vim_strchr((char_u *)"iaoOIAcsSC", first_char) != NULL)
+ {
+ has_command = TRUE;
+ MB_PTR_ADV(p); // Skip the command character
+ text_start = p;
+ }
+ }
+
+ // Copy the text part to last_insert.string
+ if (text_start != NULL && *text_start != NUL)
+ {
+ // Copy the text part to last_insert.string
+ for (; *p != NUL; MB_PTR_ADV(p))
+ {
+ c = mb_ptr2char(p);
+ // Use the CTRL-V only when entering a special char
+ if (c < ' ' || c == DEL)
+ *s++ = Ctrl_V;
+ s = add_char2buf(c, s);
+ }
+ }
+
+ *s++ = ESC;
+ *s = NUL;
+ last_insert.length = (size_t)(s - last_insert.string);
+ last_insert_skip = 0;
set_last_insert_str() updates last_insert (used by register . via get_last_insert_save() in src/register.c) even when the repeat command is a non-insert change like dd/yy (i.e. has_command is FALSE). That means setrepeat({'cmd': 'dd'}) will change getreg('.') from “last inserted text” to "dd", which is a backward-incompatible behavior change and contradicts the help text that getreg('.') is unchanged. Additionally, for change commands like cw the current parsing treats the motion (w) as part of inserted text, so getreg('.') would become "w{newtext}" instead of just the inserted text. This needs a different split between redo command sequence vs. last_insert contents (e.g. only update last_insert from the dict's text field for insert/change commands, and avoid touching last_insert for pure Normal commands).
In src/evalfunc.c:
> @@ -6566,6 +6572,22 @@ f_getregtype(typval_T *argvars, typval_T *rettv)
rettv->vval.v_string = vim_strsave(buf);
}
+/*
+ * "getrepeat()" function
+ */
+ static void
+f_getrepeat(typval_T *argvars UNUSED, typval_T *rettv)
+{
+ dict_T *dict = get_repeat_dict();
+
+ if (dict != NULL)
+ {
+ rettv->v_type = VAR_DICT;
+ rettv->vval.v_dict = dict;
+ ++dict->dv_refcount;
get_repeat_dict() currently returns a dict with refcount 1 (either via dict_copy() which sets refcount to 1, or via dict_alloc() + dv_refcount = 1). f_getrepeat() then unconditionally increments dv_refcount again, which will leak the returned dict (it will only be unref'd once when the return value is cleared). Make the refcount/ownership consistent: either return a dict with refcount 0 and let rettv_dict_set()/the caller take the reference, or keep returning refcount 1 and do not increment in f_getrepeat().
- ++dict->dv_refcount;
In src/normal.c:
> + +#ifdef FEAT_EVAL + // Clear programmatically setrepeat() after user operations + clear_repeat_dict(); +#endif
Clearing g_last_repeat_dict only from nv_operator() means non-operator changes (e.g. p/P put, J join, some Ex commands that change text, etc.) can still overwrite the redo buffer while getrepeat() keeps returning the last setrepeat() dict. That reintroduces the “stale getrepeat() after user actions” problem for those commands. Consider clearing the programmatic repeat dict from a more central place that runs for any user-initiated change, or add additional clear sites for non-operator change commands.
- -#ifdef FEAT_EVAL - // Clear programmatically setrepeat() after user operations - clear_repeat_dict(); -#endif
> + If the repeat was set via |setrepeat()|, returns the exact + dictionary that was passed. Otherwise, returns information + from Vim's internal repeat buffer. + + For user operations (like typing 'iHello<Esc>'): + - "cmd" contains the command character (i, a, o, etc.) + - "text" contains the inserted text
The help text says that for non-setrepeat() cases getrepeat() “returns information from Vim's internal repeat buffer”, and the example implies repeat_info.cmd can be "dd". But the current implementation only reconstructs insert/change info (tracked insert cmdchar + register . contents) and returns an empty cmd for non-insert user changes (like dd). The docs should either document this limitation explicitly (e.g. cmd may be empty unless the repeat was set via setrepeat() or the last change was an insert/change), or the implementation should be extended to actually derive command info from the redo buffer.
In src/testdir/test_repeat.vim:
> +func Test_setrepeat_getrepeat_basic()
+ " Test basic dictionary set and get
+ call setrepeat({'cmd': 'dd'})
+ let result = getrepeat()
+ call assert_equal('dd', result.cmd)
+ call assert_equal(v:t_dict, type(result))
+endfunc
+
+func Test_setrepeat_with_text()
+ " Test insert mode command with text
+ call setrepeat({'cmd': 'i', 'text': 'Hello'})
+ let result = getrepeat()
+ call assert_equal('i', result.cmd)
+ call assert_equal('Hello', result.text)
+endfunc
+
+func Test_setrepeat_normal_command()
+ " Test various normal mode commands
+ call setrepeat({'cmd': '3x'})
+ call assert_equal('3x', getrepeat().cmd)
+
The tests validate that setrepeat() drives . and that getrepeat() roundtrips, but there is no test asserting the backward-compatibility guarantee from the PR description/docs that getreg('.') remains “last inserted text”. Given the current implementation touches last_insert, please add coverage that (1) setrepeat({'cmd':'dd'}) does not change getreg('.'), and (2) change commands like setrepeat({'cmd':'cw','text':'new'}) keep getreg('.') equal to the inserted text, not including the motion.
In src/edit.c:
> + if (g_last_repeat_dict != NULL) + ++g_last_repeat_dict->dv_refcount;
g_last_repeat_dict is a freshly allocated copy from dict_copy(), which already returns a dict with refcount 1. Incrementing dv_refcount here causes the dict to never be freed by clear_repeat_dict() (only one dict_unref() happens), leaking memory each time setrepeat() is called. Remove the extra refcount increment, or otherwise make the ownership/refcounting consistent with how the dict is later unref'd.
- if (g_last_repeat_dict != NULL) - ++g_last_repeat_dict->dv_refcount;
In src/edit.c:
> + if (text_len > 0)
+ {
+ // Combine cmd and text
+ combined = alloc(cmd_len + text_len + 1);
+ if (combined == NULL)
+ return;
On allocation failure for combined, set_repeat_dict() returns after having already cleared the previous repeat dict and storing a new g_last_repeat_dict, but without updating the redo/insert state. This leaves getrepeat() reporting the new value while . still repeats the previous change. Consider only committing g_last_repeat_dict after the redo buffer has been successfully updated, or rolling back on failure (and emitting an error).
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@Shougo pushed 26 commits.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@Shougo commented on this pull request.
In src/edit.c:
> + if (dict == NULL)
+ return NULL;
+
+ dict->dv_refcount = 1;
+
+ // Get command from tracked insert command
+ cmd_str[0] = NUL;
+ cmd_str[1] = NUL;
+ if (last_insert_cmdchar != NUL && last_insert_cmdchar != 'i')
+ {
+ // Only include insert mode commands: i, a, o, I, A, O, etc.
+ if (vim_strchr((char_u *)"iIaAoOcCsS", last_insert_cmdchar) != NULL)
+ cmd_str[0] = last_insert_cmdchar;
+ }
+ else if (last_insert_cmdchar == 'i')
+ cmd_str[0] = 'i';
Fixed.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
> +
+ if (text_len > 0)
+ {
+ // Combine cmd and text
+ combined = alloc(cmd_len + text_len + 1);
+ if (combined == NULL)
+ return;
+
+ STRCPY(combined, cmd);
+ STRCPY(combined + cmd_len, text);
+
+ set_last_insert_str(combined);
+ vim_free(combined);
+ }
+ else
+ {
Fixed.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
> +
+ // Save a copy of the dictionary for getrepeat()
+ g_last_repeat_dict = dict_copy(dict, TRUE, FALSE, get_copyID());
+ if (g_last_repeat_dict != NULL)
+ ++g_last_repeat_dict->dv_refcount;
+
+ // Build the command string: cmd + text
+ cmd_len = STRLEN(cmd);
+ text_len = (text != NULL && *text != NUL) ? STRLEN(text) : 0;
+
+ if (text_len > 0)
+ {
+ // Combine cmd and text
+ combined = alloc(cmd_len + text_len + 1);
+ if (combined == NULL)
+ return;
Fixed.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
I did pipe this through claude and he is not too happy about this:
This is a substantial new feature. Overall thoughts:
The fundamental approach of storing repeat state in a dictionary with
"cmd" and "text" fields is fragile. Vim's repeat mechanism is much
richer than this — it stores a full redo buffer sequence including counts,
registers, operator+motion combinations, special keys, etc. Reducing it to
just cmd+text means:
setrepeat({'cmd': '3dd'}) — does the count work? Unclear.dw, ci", etc.) — how are these handled? The codeiaoOIAcsSC as insert commands.setrepeat({'cmd': 'cw', 'text': 'newword'}) — this is documented ascw is not in the insert command list, soset_last_insert_str() would treat it as a non-insert command and justcw without the text.skipRestoreRedobuff mechanism is worryingHooking into saveRedobuff/restoreRedobuff with a counter and a skip
flag is fragile — these are called in many contexts and adding bypass logic
could cause subtle redo corruption in edge cases.
g_last_repeat_dict lifetimeg_last_repeat_dict = dict_copy(dict, TRUE, FALSE, get_copyID());
if (g_last_repeat_dict != NULL) ++g_last_repeat_dict->dv_refcount;
dict_copy typically sets dv_refcount = 1, so incrementing again to 2
means it would never be freed by normal GC — only by clear_repeat_dict().
That's intentional but worth a comment.
f_getrepeat refcount issuedict_T *dict = get_repeat_dict(); if (dict != NULL) { rettv->v_type = VAR_DICT; rettv->vval.v_dict = dict; ++dict->dv_refcount; }
get_repeat_dict() already sets dv_refcount = 1 for the new dict, then
this increments to 2. The rettv should own the only reference — this looks
like a leak.
clear_repeat_dict() called in nv_operator()This means any operator command clears the programmatic repeat, which seems
overly aggressive. Moving the cursor with w before . shouldn't clear
it, but any operator will.
The docs are verbose and list many limitations upfront, which makes the
feature feel half-baked. The "Limitations" section in setrepeat() with 5
numbered caveats is a red flag — if there are this many limitations, the
API design may need rethinking before exposing it publicly.
The existing repeat.vim plugin (by tpope) already solves this problem for
plugins using feedkeys() + a mapping approach. It would be worth
considering whether this feature is sufficiently more capable to justify the
added complexity.
Good coverage of the happy path, but no tests for error cases (missing
cmd key, wrong types, etc.) and no tests for the tricky interactions with
counts/registers.
This feels like it needs more design iteration before merging.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
The existing repeat.vim plugin (by tpope) already solves this problem for
plugins using feedkeys() + a mapping approach. It would be worth
considering whether this feature is sufficiently more capable to justify the
added complexity.
Thank you for the feedback. A brief clarification.
First, why hasn't anyone added this to core before? Simply put, safely handling the redo buffer / the . internal representation is hard. There are multiple complexities:
. encodes more than a key sequence: counts, registers, operator+motion, special keys (and their escaping), ESC handling, multibyte text, etc.Because of these difficulties, leaving the problem alone is understandable. That said, I believe adding this feature to core is worthwhile for several reasons.
Why this is valuable
feedkeys()-based solutions are useful, but they rely on input emulation and per-plugin adoption. Expecting every plugin author to adopt and correctly integrate an external plugin is not realistic.Enabling Vim script to directly read and set redo/repeat state is, in my view, a net benefit. It reduces reliance on fragile input emulation, improves reproducibility and testability, and provides a stable foundation that plugins can build on. For safety, we must proceed incrementally: fix critical bugs first, then harden the save/restore and operator interactions with tests and careful design.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@Shougo pushed 27 commits.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
What about repeating the <plug>(...) mappings? In other words, would repeat_get/repeat_set be able to save . with <plug>(...) and restore it back?
https://asciinema.org/a/QeVhfOP97aHzvp3H
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
I hate to be that guy, but after some ~20+ years of using Vim and barely using 10% of its features, never written a line of Vimscript in my life (because I've had zero need to and it's not really a thing I like to do to customize my editor in an obscure language), this proposed PR and proposed setrepeat() and getrepeat() don't feel right to me. I have enough experience to know that this is something that should be better placed in a fuller richer API rather then trying to abused the internals of Vim, which itself is based on Vi and then on Sed and Ed. This feels wrong.
Perhaps it's worth going back and rethinking the API design overall for plugins? 🧐
-- James Mills / prologic (no Copilot, Claude or Codex involved, just my own reading and thinking...)
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@Shougo pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
Seeing what looks like a conversation between two LLMs is very disappointing.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
this is something that should be better placed in a fuller richer API rather then trying to abused the internals of Vim, which itself is based on Vi and then on Sed and Ed. This feels wrong.
"Feels wrong" seems pretty vague @prologic; what kind of richer API would you want?
For me personally, I'd like something similar to the "0-"9 registers but for the . redo buffer. A simple (quickly written, untested) implementation might be something like:
# Keep track of last 9 changes
g:redo_list = []
# Amend to redo_list if it changes
au TextChanged,TextChangedI {
g:redo_list->add(getrepeat())
if g:redo_list->len() > 9
g:redo_list = g:redo_list[9 :]
endif
}
# <Leader>. to print list of redo
nnoremap <Leader>. :echo g:redo_list->copy()->reverse()->join("\n")
# <Leader>1. to set . redo buffer
for i in range(1, 9)
exe printf('nnoremap <Leader>%d. :call setrepeat(redo_list[%d]) | normal! .<CR>', i, i - 1)
endfor
Another common use case is plugins that want to have some control over what . does. This is what people use vim-repeat for.
Maybe other people want something significantly different?
Either way, it seems to me that offering basic primitives to allow doing this sort of thing is a good start – we need that regardless of any "richer API", which can always be added later, I think?
Also, I'm not sure if au TextChanged,TextChangedI is enough to reliably detect if . changed? It might be helpful if that would be documented.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
Seeing what looks like a conversation between two LLMs is very disappointing.
I understand your frustration, but I hope you can be understanding of @chrisbra's use of AI. He is responsible for reviewing and making calls on every single issue and PR. While other members do help, we are severely understaffed. Using AI to streamline this overwhelming workload is a practical necessity and, frankly, makes perfect sense to keep the project moving.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
something similar to the "0-"9 registers but for the . redo buffer
It needs to be an event, or perhaps a callback. Otherwise plugins would need to "poll" these registers...
Basically, you are discovering that this feature ends up exposing commands as "atoms", which is a key feature needed to implement multicursor.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
something similar to the "0-"9 registers but for the . redo buffer
It needs to be an event, or perhaps a callback. Otherwise plugins would need to "poll" these registers.
I had to read this a few times to understand what you mean, but I think what you mean is that after setrepeat() is called, there is no autocmd event(?)
So we also need to add a new RedoChanged autocmd, which gets fired not only on TextChanged, but also when setrepeat() is called.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
I understand your frustration, but I hope you can be understanding of @chrisbra's use of AI. He is responsible for reviewing and making calls on every single issue and PR. While other members do help, we are severely understaffed. Using AI to streamline this overwhelming workload is a practical necessity and, frankly, makes perfect sense to keep the project moving.
very curious, how did large popular open source projects survive with the overwhelming contributions, pull requests and issues raised before the existence of LLMs? 🧐
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
I don't know, perhaps being on the edge of becoming burnt out while trying to keep the project alive in their spare time and having barely enough time for their own family, while at the same time aggregating issues and pull requests and trying to manage the community, project goals and security incidents and still trying to be friendly, motivated and open to a random strangers on the internet and managing the communities expectations?
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
For what it's worth, your friendliness to everyone here (above all else you do) is exemplary, and more than I would have been able to manage. Your efforts are very much seen and appreciated.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
I think the conversation about Vim project management/AI somewhere is best done somewhere else. Maybe open a new issue, discussion, mailing list thread, or something. It's not really on-topic here.
That said, maybe we can change some things to be less less Christian-centric? Also see the discussion on how to do patch releases from a few days ago. Again: probably best discussed somewhere else.
But also: I don't think anyone needs to justify themselves to random internet strangers who never contributed to Vim or Vim-adjacent projects (as near as I can tell) and came here from a Mastodon thread to express their outrage. If I were to go around bollocking every project run in a way I dislike then I'd have a full-time job.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
@Shougo pushed 2 commits.
You are receiving this because you are subscribed to this thread.![]()
The docs are verbose and list many limitations upfront, which makes the
feature feel half-baked. The "Limitations" section in setrepeat() with 5
numbered caveats is a red flag — if there are this many limitations, the
API design may need rethinking before exposing it publicly.
Thank you for the careful review and for pointing out that the current implementation has many limitations.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@Shougo pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
I don't know, perhaps being on the edge of becoming burnt out while trying to keep the project alive in their spare time and having barely enough time for their own family, while at the same time aggregating issues and pull requests and trying to manage the community, project goals and security incidents and still trying to be friendly, motivated and open to a random strangers on the internet and managing the communities expectations?
If you're at the edge of a burnout, more and faster paced work is not going to help you. Take rest, work however much you feel comfortable with. We will be here, and we will appreciate carefully laid out and slowly paced development all the same. Don't feel pressured to "keep up" with whatever speed is advertised by snake oil sellers. Take care.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
I have been thinking about this for a bit more and I am wondering if we shouldn't approach it slightly differently, since this approach here is a bit fragile. First we would need to refactor the existing redo byte stream buffer into a bit more structured representation for those redo commands (command, count, motion, insertedtext, key, register, etc). That would make getrepeat()/setrepeat() straightforward to implement cleanly on top, and would benefit other areas that currently have to parse or reconstruct redo buffer contents. The raw byte stream would however likely needed for macros or mappings.
But note, that this would likely be a larger refactor, but it might save complexity in the long run.
What do you think?
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
I agree — treating the redo buffer as a raw byte stream is fragile.
But note, that this would likely be a larger refactor, but it might save complexity in the long run.
My approach also requires multiple pull requests to implement the feature correctly.
I think refactoring first is preferable.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
Thank you for the detailed feedback! I’ve addressed all the issues:
Thank you for the feedback! I agree that following the Vim 8+ naming convention makes sense.
Thank you for the feedback on naming!
Thanks for the suggestion! After thinking about this more, I believe repeat_set() / repeat_get() is the right choice:
Thank you for the feedback. A brief clarification.
Wow, this real person must be really thankfull
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
Wow, this real person must be really thankfull
Lol, Thank you for all your feedback.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()