Add setrepeat() and getrepeat() functions for dot command control#19413
Add setrepeat() and getrepeat() functions for dot command control#19413Shougo wants to merge 43 commits into
Conversation
|
Thanks for sharing your approach! The I considered a register-based approach in #19342, but ultimately chose functions for several reasons: 1. Predictability and ClarityRegisters 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 command2. ExtensibilityRegisters 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'
\ })3. Complex CommandsString representation has limitations:
Dictionary structure handles these naturally. History featureI 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. Main Use CasesThe primary use cases that drove this design:
For 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. |
| " Test getrepeat when no setrepeat was called | ||
| " Should return a dictionary with empty or limited info | ||
| let result = getrepeat() |
There was a problem hiding this comment.
So getrepeat cannot be used to get the current dot-repeat register, i.e. the last "action" that a user performed (without setrepeat)?
|
I feel that the specifications are still not well-regulated. Bug$ 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 About the document.Write according to the rules. :h help-writing
/^STYLEEspecially the following: The return value must be listed at the end of the function description. API nameThe function names ( |
Currently, no. The implementation has two modes: 1. After
2. After user operations (like
Why this limitation? Vim stores repeat information in several places:
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'Future enhancement:We could add user operation support by:
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 |
|
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. |
There's no persuasive power in listing APIs that were implemented early on. Also,
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. |
|
Thank you for the feedback! I agree that following the Vim 8+ naming convention makes sense. I'll rename the functions to 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. |
9bb60fa to
cdb457d
Compare
|
|
||
| dict = argvars[0].vval.v_dict; | ||
| if (dict != NULL) | ||
| set_repeat_dict(dict); |
There was a problem hiding this comment.
Since there is a NULL check within set_repeat_dict, it is unnecessary here.
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.
I assume this was suggesting But I don't get why we want to avoid "abstraction" to such an extreme; this feature may gain more capabilities in the future. |
|
Thank you for the feedback on naming! I initially considered I chose I think that However, you raise a good point about Vim's official terminology. Looking at What do you think would be clearest for users? |
|
If we're bikeshedding names, then my 2c is Just an idea; I'm okay with both repeat_get() or dot_get(), or even dotrepeat_get() as well. |
|
@arp242 Thanks for the suggestion! After thinking about this more, I believe
Regarding potential confusion with the existing
The different argument types and use cases make them clearly distinct, similar to how What do you think? |
There was a problem hiding this comment.
Pull request overview
Adds a Vimscript API for programmatic control of dot-repeat (.) via new setrepeat()/getrepeat() functions, with supporting core changes, tests, and documentation.
Changes:
- Add
setrepeat({dict})andgetrepeat()to the builtin function table and implement them in core. - Update redo/insert tracking to support programmatic repeat and add a new repeat-focused test suite.
- Document the new functions in
:helpand add the test target to the test runner makefile.
Reviewed changes
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.
| // 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; |
There was a problem hiding this comment.
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).
|
|
||
| #ifdef FEAT_EVAL | ||
| // Clear programmatically setrepeat() after user operations | ||
| clear_repeat_dict(); | ||
| #endif |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
| 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) | ||
|
|
There was a problem hiding this comment.
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.
| if (text_len > 0) | ||
| { | ||
| // Combine cmd and text | ||
| combined = alloc(cmd_len + text_len + 1); | ||
| if (combined == NULL) | ||
| return; |
There was a problem hiding this comment.
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).
|
I did pipe this through claude and he is not too happy about this: ReviewThis is a substantial new feature. Overall thoughts: Design concernsThe fundamental approach of storing repeat state in a dictionary with
The
|
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
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
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. |
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. |
very curious, how did large popular open source projects survive with the overwhelming contributions, pull requests and issues raised before the existence of LLMs? 🧐 |
|
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? |
|
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. |
|
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. |
Thank you for the careful review and for pointing out that the current implementation has many limitations.
|
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. |
|
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 ( What do you think? |
|
I agree — treating the redo buffer as a raw byte stream is fragile.
My approach also requires multiple pull requests to implement the feature correctly. |
Wow, this real person must be really thankfull |
Lol, Thank you for all your feedback. |

Summary
Add
setrepeat()andgetrepeat()functions to allow scripts to programmatically control the dot (.) repeat command.This enables plugins to:
.Motivation
This addresses several long-standing feature requests:
.#6299: Users want to save the.command while making other changes, then restore itCurrent Limitations
.repeat<Plug>mappingsDesign
API
Dictionary Structure
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 }Use Cases
1. Save/Restore (Telescope)
2. Plugin Integration (vim-repeat replacement)
3. Repeat History
Design Decisions
Why dedicated functions instead of writable
.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)Why dictionary interface?
Phase 1 Limitations
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 recordedText 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 forsetrepeat()-set values.User operations return limited information.
These limitations don't affect the primary use cases (save/restore, plugin integration, history).
Related Work
.#6299Future Work (Phase 2)
Potential enhancements:
typeandmodefields)countfield)registerfield)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.
Note: This work was produced with assistance from AI tools(ChatGPT/Copilot). The AI contributed
diagnostic scripts, patch suggestions (including save/restore handling and
updateSavedRedobuffs()), and test-case templates. All final design decisions,edits, builds, and tests were performed and verified by the author, who takes
responsibility for the submitted changes.