Add setrepeat() and getrepeat() functions for dot command control by Shougo · Pull Request #19413 · vim/vim · GitHub
Skip to content

Add setrepeat() and getrepeat() functions for dot command control#19413

Closed
Shougo wants to merge 43 commits into
vim:masterfrom
Shougo:repeat-functions
Closed

Add setrepeat() and getrepeat() functions for dot command control#19413
Shougo wants to merge 43 commits into
vim:masterfrom
Shougo:repeat-functions

Conversation

@Shougo

@Shougo Shougo commented Feb 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Add setrepeat() and getrepeat() functions to allow scripts to programmatically control the dot (.) repeat command.

This enables plugins to:

  • Save and restore the repeat command
  • Make custom commands repeatable with .
  • Build repeat history functionality

Motivation

This addresses several long-standing feature requests:

Current Limitations

  • No way to save/restore the repeat command from scripts
  • Plugins like vim-repeat require complex workarounds
  • Temporary edits (e.g., in prompt buffers) permanently overwrite user's repeat command
  • No way for plugins to integrate custom commands with . repeat
  • Does not support <Plug> mappings

Design

API

" 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)

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)

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

2. Plugin Integration (vim-repeat replacement)

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

3. Repeat History

let g:repeat_history = []

" Save current repeat
call add(g:repeat_history, getrepeat())

" Later, restore from history
call setrepeat(g:repeat_history[0])
normal! .

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:

  1. Backward compatibility: Changing getreg('.') behavior could break scripts
  2. Double duty: Register would serve two different purposes
  3. Limited extensibility: Hard to add features like visual mode support

The new approach:

  • ✅ Keeps getreg('.') unchanged (backward compatible)
  • ✅ Separate interfaces for separate purposes (cleaner)
  • ✅ Dictionary allows future extensions without breaking changes

Why dictionary interface?

  • Extensible: Can add new fields in Phase 2 without breaking existing code
  • Flexible: Supports both simple and complex operations
  • Clear: Self-documenting structure

Phase 1 Limitations

Documented in the help files:

  1. Insert mode overwrites setrepeat()
    When entering/leaving insert mode, Vim's automatic recording overwrites custom repeats.
    Workaround: Call setrepeat() after insert mode operations.

  2. setline() and similar not recorded
    Text modification functions don't update the repeat command.
    Workaround: Use feedkeys() to simulate typing.

  3. Visual mode not supported
    Will be added in Phase 2 with additional dictionary fields.

  4. 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).

Related Work

Future Work (Phase 2)

Potential enhancements:

  • Visual mode support (via type and mode fields)
  • Count support (via count field)
  • Register support (via register field)
  • Enhanced getrepeat() for user operations

All 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.

Comment thread runtime/doc/builtin.txt Outdated
Comment thread runtime/doc/builtin.txt Outdated
Comment thread runtime/doc/builtin.txt Outdated
Comment thread runtime/doc/builtin.txt Outdated
@arp242

arp242 commented Feb 15, 2026

Copy link
Copy Markdown
Contributor

@Shougo

Shougo commented Feb 16, 2026

Copy link
Copy Markdown
Contributor Author

@arp242

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:

1. Predictability and Clarity

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

2. Extensibility

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'
\ })

3. Complex Commands

String representation has limitations:

  • <Plug> mappings
  • Multi-byte characters
  • Commands with special characters
  • Escaping issues

Dictionary structure handles these naturally.

History feature

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.

Main Use Cases

The primary use cases that drove this design:

  • Save/restore ("telescope"): let saved = getrepeat() → call setrepeat(saved)
  • Plugin integration: call setrepeat({'cmd': ':call MyPlugin()'})
  • Future: "vim-repeat" replacement with <Plug> support

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.

Comment thread runtime/doc/builtin.txt Outdated
Comment thread runtime/doc/builtin.txt Outdated
Comment thread src/edit.c Outdated
Comment thread src/testdir/test_functions.vim Outdated
Comment on lines +4685 to +4687
" Test getrepeat when no setrepeat was called
" Should return a dictionary with empty or limited info
let result = getrepeat()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So getrepeat cannot be used to get the current dot-repeat register, i.e. the last "action" that a user performed (without setrepeat)?

@h-east

h-east commented Feb 16, 2026

Copy link
Copy Markdown
Member

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 . convention and confuses users.


About the document.

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|

API name

The function names (setrepeat(), getrepeat()) is too abstract. Give it a better name.
Exsample:

dotrepeat_get()
dotrepeat_set()
dotrepeat_clear()

customdot_~()

@Shougo

Shougo commented Feb 17, 2026

Copy link
Copy Markdown
Contributor Author

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 operation

2. After user operations (like iHello<Esc>): Limited/no support ❌

  • getrepeat() returns empty or incomplete information
  • The main challenge is extracting the command type (i, a, o, etc.) from Vim's internal state

Why 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 command
  • redo_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'

Future enhancement:

We could add user operation support by:

  • Tracking the last insert command type in a new global variable
  • Hooking into insert mode entry/exit to capture this information

Would you like me to implement this, or is the current behavior acceptable for the initial version?

@justinmk

Copy link
Copy Markdown
Contributor

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.

@Shougo

Shougo commented Feb 18, 2026

Copy link
Copy Markdown
Contributor Author

I have implemented simple user commands support.

@justinmk @zeertzjq What do you think?

Comment thread src/edit.c
@Shougo

Shougo commented Feb 18, 2026

Copy link
Copy Markdown
Contributor Author

@h-east

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.

@h-east

h-east commented Feb 18, 2026

Copy link
Copy Markdown
Member

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.

@Shougo

Shougo commented Feb 22, 2026

Copy link
Copy Markdown
Contributor Author

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:

  • The function naming?
  • The API design (dict structure, return values)?
  • The behavior (how it interacts with user operations)?
  • Something else?

I want to make sure I address all the issues before proceeding.

Comment thread src/evalfunc.c Outdated

dict = argvars[0].vval.v_dict;
if (dict != NULL)
set_repeat_dict(dict);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since there is a NULL check within set_repeat_dict, it is unnecessary here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Thanks.

@justinmk

Copy link
Copy Markdown
Contributor

I agree that following the Vim 8+ naming convention makes sense.

I'll rename the functions to dot_set() and dot_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, repeat is too "abstract".
Shouldn't you at least add dot?

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.

@Shougo

Shougo commented Feb 23, 2026

Copy link
Copy Markdown
Contributor Author

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?

@arp242

arp242 commented Feb 23, 2026

Copy link
Copy Markdown
Contributor

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".

Just an idea; I'm okay with both repeat_get() or dot_get(), or even dotrepeat_get() as well.

@Shougo

Shougo commented Feb 24, 2026

Copy link
Copy Markdown
Contributor Author

@arp242 Thanks for the suggestion!

After thinking about this more, I believe repeat_set() / repeat_get() is the right choice:

  1. Official Vim terminology: :help repeat.txt documents this feature as "repeat"
  2. Community understanding: The popular vim-repeat plugin has established "repeat" as the intuitive term for this functionality
  3. User expectations: When users think about the . 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 manipulation
  • repeat_set(dict) / repeat_get() - editor behavior control

The different argument types and use cases make them clearly distinct, similar to how getpos() and pos() coexist without confusion.

What do you think?

Comment thread src/edit.c
Comment thread src/edit.c
Comment thread src/edit.c Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}) and getrepeat() 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 :help and 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.

Comment thread src/edit.c
Comment on lines +2726 to +2756
// 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;

Copilot AI Mar 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment thread src/evalfunc.c Outdated
Comment thread src/normal.c Outdated
Comment on lines +6486 to +6490

#ifdef FEAT_EVAL
// Clear programmatically setrepeat() after user operations
clear_repeat_dict();
#endif

Copilot AI Mar 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
#ifdef FEAT_EVAL
// Clear programmatically setrepeat() after user operations
clear_repeat_dict();
#endif

Copilot uses AI. Check for mistakes.
Comment thread runtime/doc/builtin.txt Outdated
Comment on lines +5207 to +5213
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

Copilot AI Mar 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +26
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)

Copilot AI Mar 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/edit.c Outdated
Comment thread src/edit.c
Comment on lines +5788 to +5793
if (text_len > 0)
{
// Combine cmd and text
combined = alloc(cmd_len + text_len + 1);
if (combined == NULL)
return;

Copilot AI Mar 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
@Shougo Shougo force-pushed the repeat-functions branch from f485d34 to c1bb98b Compare March 3, 2026 03:51
@chrisbra

chrisbra commented Mar 4, 2026

Copy link
Copy Markdown
Member

I did pipe this through claude and he is not too happy about this:

Review

This is a substantial new feature. Overall thoughts:

Design concerns

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.
  • Operator+motion (dw, ci", etc.) — how are these handled? The code
    only explicitly handles iaoOIAcsSC as insert commands.
  • setrepeat({'cmd': 'cw', 'text': 'newword'}) — this is documented as
    working but cw is not in the insert command list, so
    set_last_insert_str() would treat it as a non-insert command and just
    replay cw without the text.

The skipRestoreRedobuff mechanism is worrying

Hooking 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 lifetime

g_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 issue

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() 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.

Documentation

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.

Prior art

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.

Tests

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.

@Shougo

Shougo commented Mar 5, 2026

Copy link
Copy Markdown
Contributor Author

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.
  • The redo buffer is saved/restored at many points in the code, so save/restore nesting and interactions with other logic must be handled carefully.
  • Mistakes in internal state management (e.g. refcount handling) can cause leaks or destructive side effects — and indeed some refcount issues have already been flagged in this PR.

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

  • Limits of plugin dependencies: repeat.vim and similar 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.
  • Improved stability and compatibility: a built-in API lets all plugins use a consistent, safe mechanism to handle repeat state, reducing duplication and incompatibilities across the ecosystem.
  • Testability: exposing repeat state explicitly makes unit and regression testing easier, which increases long-term reliability.
  • Extensibility: we can start with a minimal dictionary (cmd/text) and later extend it (count/register/type) in a backwards-compatible way.

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.

@arp242

arp242 commented Mar 6, 2026

Copy link
Copy Markdown
Contributor

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.

@prologic

prologic commented Mar 6, 2026

Copy link
Copy Markdown

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? 🧐

@chrisbra

chrisbra commented Mar 6, 2026

Copy link
Copy Markdown
Member

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?

@clason

clason commented Mar 6, 2026

Copy link
Copy Markdown
Contributor

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.

@arp242

arp242 commented Mar 6, 2026

Copy link
Copy Markdown
Contributor

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.

@Shougo

Shougo commented Mar 8, 2026

Copy link
Copy Markdown
Contributor Author

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.

  • I agree that the cmd/text form is intentionally minimal and does not capture the full richness of Vim's redo buffer.
  • The current choice was pragmatic: fully reproducing the redo buffer (counts, registers, operator+motion, mappings, etc.) is complex and risky to implement in one pass.
  • To address this:
    1. Documented the supported cases and clearly listed the current limitations.
    2. Added tests covering the supported behaviors.
    3. "raw redo-buffer" feature can be added later(planned).

@Folling

Folling commented Mar 9, 2026

Copy link
Copy Markdown

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.

@chrisbra

chrisbra commented Mar 9, 2026

Copy link
Copy Markdown
Member

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?

@Shougo

Shougo commented Mar 10, 2026

Copy link
Copy Markdown
Contributor Author

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.

@timkicker

Copy link
Copy Markdown

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

@Shougo

Shougo commented Mar 18, 2026

Copy link
Copy Markdown
Contributor Author

Wow, this real person must be really thankfull

Lol, Thank you for all your feedback.

@chrisbra

Copy link
Copy Markdown
Member

@chrisbra chrisbra closed this Mar 29, 2026
@Shougo Shougo deleted the repeat-functions branch March 30, 2026 10:29
@chrisbra chrisbra mentioned this pull request May 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.