[vim/vim] Add setrepeat() and getrepeat() functions for dot command control (PR #19413)

81 views
Skip to first unread message

Shougo

unread,
Feb 15, 2026, 2:46:23 AMFeb 15
to vim/vim, Subscribed

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:

  • neovim/neovim#33030: Telescope needs to save/restore repeat because prompt-buffer edits overwrite it
  • #6299: Users want to save the . command while making other changes, then restore it
  • #6346: Previous attempt to add repeat history (closed due to complexity)

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

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.


You can view, comment on, or merge this pull request online at:

  https://github.com/vim/vim/pull/19413

Commit Summary

File Changes

(8 files)

Patch Links:


Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413@github.com>

Shougo

unread,
Feb 15, 2026, 2:57:33 AMFeb 15
to vim/vim, Push

@Shougo pushed 2 commits.


View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/before/c6715aec1d11fd0788da3fce5448934ca5861be7/after/ea73edeeca2671ae14a0a7e31e0370bf31b8d1e7@github.com>

Shougo

unread,
Feb 15, 2026, 3:08:46 AMFeb 15
to vim/vim, Push

@Shougo pushed 2 commits.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/before/ea73edeeca2671ae14a0a7e31e0370bf31b8d1e7/after/42ca3c3918682e4a86ea7e03e83c826f6d5dc33c@github.com>

Foxe Chen

unread,
Feb 15, 2026, 11:21:52 AMFeb 15
to vim/vim, Subscribed

@64-bitman commented on this pull request.


In runtime/doc/builtin.txt:

> @@ -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.Message ID: <vim/vim/pull/19413/review/3805163410@github.com>

Foxe Chen

unread,
Feb 15, 2026, 11:23:38 AMFeb 15
to vim/vim, Subscribed

@64-bitman commented on this pull request.


In runtime/doc/builtin.txt:

> +		    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.Message ID: <vim/vim/pull/19413/review/3805165225@github.com>

Foxe Chen

unread,
Feb 15, 2026, 11:24:15 AMFeb 15
to vim/vim, Subscribed

@64-bitman commented on this pull request.


In runtime/doc/builtin.txt:

> +		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.Message ID: <vim/vim/pull/19413/review/3805166579@github.com>

Foxe Chen

unread,
Feb 15, 2026, 11:24:21 AMFeb 15
to vim/vim, Subscribed

@64-bitman commented on this pull request.


In runtime/doc/builtin.txt:

> +		    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.Message ID: <vim/vim/pull/19413/review/3805166840@github.com>

Martin Tournoij

unread,
Feb 15, 2026, 12:35:44 PMFeb 15
to vim/vim, Subscribed
arp242 left a comment (vim/vim#19413)

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.Message ID: <vim/vim/pull/19413/c3904884181@github.com>

Shougo

unread,
Feb 15, 2026, 7:47:29 PMFeb 15
to vim/vim, Push

@Shougo pushed 3 commits.

  • 7134af4 Add "vim-repeat" replacement limitations
  • ef402b5 Fix restoreRedobuff() memory leak
  • 6f548b7 Fix the documentation for reviews

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/before/0ff77c9e28f82597f07aa4eeddaf5d76e9ed37c0/after/6f548b722a25b5829974a40bb7798798461effea@github.com>

Shougo

unread,
Feb 15, 2026, 7:48:07 PMFeb 15
to vim/vim, Subscribed

@Shougo commented on this pull request.


In runtime/doc/builtin.txt:

> @@ -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.Message ID: <vim/vim/pull/19413/review/3806040158@github.com>

Shougo

unread,
Feb 15, 2026, 7:48:17 PMFeb 15
to vim/vim, Subscribed

@Shougo commented on this pull request.


In runtime/doc/builtin.txt:

> +		    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.Message ID: <vim/vim/pull/19413/review/3806040441@github.com>

Shougo

unread,
Feb 15, 2026, 7:48:20 PMFeb 15
to vim/vim, Subscribed

@Shougo commented on this pull request.


In runtime/doc/builtin.txt:

> +		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.Message ID: <vim/vim/pull/19413/review/3806040686@github.com>

Shougo

unread,
Feb 15, 2026, 7:48:26 PMFeb 15
to vim/vim, Subscribed

@Shougo commented on this pull request.


In runtime/doc/builtin.txt:

> +		    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.Message ID: <vim/vim/pull/19413/review/3806041002@github.com>

Shougo

unread,
Feb 15, 2026, 7:53:42 PMFeb 15
to vim/vim, Subscribed
Shougo left a comment (vim/vim#19413)

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


Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/c3905860672@github.com>

zeertzjq

unread,
Feb 15, 2026, 11:39:16 PMFeb 15
to vim/vim, Subscribed

@zeertzjq commented on this pull request.


In runtime/doc/builtin.txt:

> +		    '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)

In runtime/doc/builtin.txt:

> +		    '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.Message ID: <vim/vim/pull/19413/review/3806563861@github.com>

zeertzjq

unread,
Feb 15, 2026, 11:43:08 PMFeb 15
to vim/vim, Subscribed

@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.Message ID: <vim/vim/pull/19413/review/3806572910@github.com>

Justin M. Keyes

unread,
Feb 16, 2026, 8:43:47 AMFeb 16
to vim/vim, Subscribed

@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.Message ID: <vim/vim/pull/19413/review/3808712106@github.com>

h_east

unread,
Feb 16, 2026, 9:07:50 AMFeb 16
to vim/vim, Subscribed
h-east left a comment (vim/vim#19413)

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


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/c3908683379@github.com>

Shougo

unread,
Feb 17, 2026, 5:25:15 AMFeb 17
to vim/vim, Push

@Shougo pushed 1 commit.

  • 5f73c74 Fix restoreRedobuff() memory leak

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/before/6f548b722a25b5829974a40bb7798798461effea/after/5f73c74579345434fc4758daada8b340f7e751f9@github.com>

Shougo

unread,
Feb 17, 2026, 5:25:38 AMFeb 17
to vim/vim, Push

@Shougo pushed 1 commit.

  • 9fbf4f9 Update runtime/doc/builtin.txt

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/before/5f73c74579345434fc4758daada8b340f7e751f9/after/9fbf4f934ac3dd04e0d2c6313618b04a799fb8d3@github.com>

Shougo

unread,
Feb 17, 2026, 5:25:48 AMFeb 17
to vim/vim, Push

@Shougo pushed 1 commit.

  • c60fe43 Update runtime/doc/builtin.txt

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/before/9fbf4f934ac3dd04e0d2c6313618b04a799fb8d3/after/c60fe43fb1666876758dfe58f319294a74701ea0@github.com>

Shougo

unread,
Feb 17, 2026, 6:25:25 AMFeb 17
to vim/vim, Subscribed
Shougo left a comment (vim/vim#19413)

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?


Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/c3914137877@github.com>

Justin M. Keyes

unread,
Feb 17, 2026, 7:37:15 AMFeb 17
to vim/vim, Subscribed
justinmk left a comment (vim/vim#19413)

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.Message ID: <vim/vim/pull/19413/c3914475754@github.com>

Shougo

unread,
Feb 17, 2026, 7:56:18 PMFeb 17
to vim/vim, Push

@Shougo pushed 3 commits.

  • eea3e93 Add user commands support in getrepeat()
  • 2a80388 Improve functions documentation
  • 7ee8e21 Fix code style

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/before/c60fe43fb1666876758dfe58f319294a74701ea0/after/7ee8e21a607368ab004be7a636abfe883413981c@github.com>

Shougo

unread,
Feb 17, 2026, 7:57:31 PMFeb 17
to vim/vim, Subscribed
Shougo left a comment (vim/vim#19413)

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.Message ID: <vim/vim/pull/19413/c3917872781@github.com>

zeertzjq

unread,
Feb 17, 2026, 9:48:58 PMFeb 17
to vim/vim, Subscribed

@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.Message ID: <vim/vim/pull/19413/review/3817383551@github.com>

Shougo

unread,
Feb 17, 2026, 10:30:35 PMFeb 17
to vim/vim, Push

@Shougo pushed 3 commits.


View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/before/7ee8e21a607368ab004be7a636abfe883413981c/after/5ced7db8c845c39fd3737aec6a4a68431ad43218@github.com>

Shougo

unread,
Feb 17, 2026, 10:33:42 PMFeb 17
to vim/vim, Subscribed
Shougo left a comment (vim/vim#19413)

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


Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/c3918360495@github.com>

Shougo

unread,
Feb 17, 2026, 10:46:10 PMFeb 17
to vim/vim, Push

@Shougo pushed 2 commits.

  • 6986598 Remove traing white spaces
  • ce64ca5 Clear programmatically setrepeat() after user operations

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/before/5ced7db8c845c39fd3737aec6a4a68431ad43218/after/ce64ca54ac37c5dd4ea0962a9afd895aadecd981@github.com>

Shougo

unread,
Feb 17, 2026, 10:46:37 PMFeb 17
to vim/vim, Subscribed

@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.Message ID: <vim/vim/pull/19413/review/3817564942@github.com>

Shougo

unread,
Feb 17, 2026, 10:50:19 PMFeb 17
to vim/vim, Push

@Shougo pushed 1 commit.


View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/before/ce64ca54ac37c5dd4ea0962a9afd895aadecd981/after/9bb60fafbaca09a5ad2b387f05be4d8142343860@github.com>

h_east

unread,
Feb 18, 2026, 5:16:09 AMFeb 18
to vim/vim, Subscribed
h-east left a comment (vim/vim#19413)

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.Message ID: <vim/vim/pull/19413/c3919918259@github.com>

h_east

unread,
Feb 20, 2026, 8:52:19 AMFeb 20
to vim/vim, Subscribed
h-east left a comment (vim/vim#19413)

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.Message ID: <vim/vim/pull/19413/c3934853022@github.com>

Shougo

unread,
Feb 22, 2026, 5:34:39 AMFeb 22
to vim/vim, Subscribed
Shougo left a comment (vim/vim#19413)

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.


Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/c3940673474@github.com>

Shougo

unread,
Feb 22, 2026, 5:34:54 AMFeb 22
to vim/vim, Push

@Shougo pushed 24 commits.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/before/9bb60fafbaca09a5ad2b387f05be4d8142343860/after/cdb457db6ce73569f95379dcbc700aafe6f018df@github.com>

mattn

unread,
Feb 22, 2026, 8:18:13 AMFeb 22
to vim/vim, Subscribed

@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.Message ID: <vim/vim/pull/19413/review/3837679996@github.com>

Shougo

unread,
Feb 23, 2026, 3:04:45 AMFeb 23
to vim/vim, Push

@Shougo pushed 1 commit.


View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/before/cdb457db6ce73569f95379dcbc700aafe6f018df/after/f485d346b4e57253a9c3d6cc41522cf160b59b2a@github.com>

Shougo

unread,
Feb 23, 2026, 3:04:53 AMFeb 23
to vim/vim, Subscribed

@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.Message ID: <vim/vim/pull/19413/review/3839609386@github.com>

Justin M. Keyes

unread,
Feb 23, 2026, 4:35:55 AMFeb 23
to vim/vim, Subscribed
justinmk left a comment (vim/vim#19413)

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.


Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/c3943630505@github.com>

Shougo

unread,
Feb 23, 2026, 5:15:23 AMFeb 23
to vim/vim, Subscribed
Shougo left a comment (vim/vim#19413)

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.Message ID: <vim/vim/pull/19413/c3943833733@github.com>

Martin Tournoij

unread,
Feb 23, 2026, 7:43:27 AMFeb 23
to vim/vim, Subscribed
arp242 left a comment (vim/vim#19413)

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.Message ID: <vim/vim/pull/19413/c3944546673@github.com>

Shougo

unread,
Feb 23, 2026, 8:36:02 PMFeb 23
to vim/vim, Subscribed
Shougo left a comment (vim/vim#19413)

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


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/c3948365165@github.com>

Christian Brabandt

unread,
Mar 2, 2026, 3:36:45 PMMar 2
to vim/vim, Subscribed

@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.Message ID: <vim/vim/pull/19413/review/3878508094@github.com>

Copilot

unread,
Mar 2, 2026, 3:50:09 PMMar 2
to vim/vim, Subscribed

@Copilot commented on this pull request.

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.


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

⬇️ Suggested change
-	++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.

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

In runtime/doc/builtin.txt:

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

⬇️ Suggested change
-    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.Message ID: <vim/vim/pull/19413/review/3878612397@github.com>

Shougo

unread,
Mar 2, 2026, 10:52:21 PMMar 2
to vim/vim, Push

@Shougo pushed 26 commits.


View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/before/f485d346b4e57253a9c3d6cc41522cf160b59b2a/after/c1bb98bf6badaae1c226738521e81cdfe7fa76a3@github.com>

Shougo

unread,
Mar 2, 2026, 10:52:43 PMMar 2
to vim/vim, Subscribed

@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.Message ID: <vim/vim/pull/19413/review/3880054503@github.com>

Shougo

unread,
Mar 2, 2026, 10:52:50 PMMar 2
to vim/vim, Subscribed

@Shougo commented on this pull request.


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
+    {

Fixed.


Reply to this email directly, view it on GitHub, or unsubscribe.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/review/3880055009@github.com>

Shougo

unread,
Mar 2, 2026, 10:53:03 PMMar 2
to vim/vim, Subscribed

@Shougo 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;

Fixed.


Reply to this email directly, view it on GitHub, or unsubscribe.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/review/3880055504@github.com>

Christian Brabandt

unread,
Mar 4, 2026, 4:26:54 PM (13 days ago) Mar 4
to vim/vim, Subscribed
chrisbra left a comment (vim/vim#19413)

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.


Reply to this email directly, view it on GitHub, or unsubscribe.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/c4000394026@github.com>

Shougo

unread,
Mar 4, 2026, 8:38:09 PM (13 days ago) Mar 4
to vim/vim, Subscribed
Shougo left a comment (vim/vim#19413)

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.


Reply to this email directly, view it on GitHub, or unsubscribe.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/c4001446284@github.com>

Shougo

unread,
Mar 4, 2026, 11:15:57 PM (13 days ago) Mar 4
to vim/vim, Push

@Shougo pushed 27 commits.


View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/before/c1bb98bf6badaae1c226738521e81cdfe7fa76a3/after/702d1578d90cbaa4de4923cb3352b207e5787028@github.com>

Maxim Kim

unread,
Mar 5, 2026, 2:29:25 AM (13 days ago) Mar 5
to vim/vim, Subscribed
habamax left a comment (vim/vim#19413)

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.Message ID: <vim/vim/pull/19413/c4002890977@github.com>

James Mills

unread,
Mar 6, 2026, 6:03:12 AM (12 days ago) Mar 6
to vim/vim, Subscribed
prologic left a comment (vim/vim#19413)

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.Message ID: <vim/vim/pull/19413/c4011065593@github.com>

Shougo

unread,
Mar 6, 2026, 6:44:25 AM (12 days ago) Mar 6
to vim/vim, Push

@Shougo pushed 1 commit.


View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/before/702d1578d90cbaa4de4923cb3352b207e5787028/after/c2c223b3133d04e5f8445b15db63e0c48e91c08b@github.com>

Demi Marie Obenour

unread,
Mar 6, 2026, 9:24:14 AM (12 days ago) Mar 6
to vim/vim, Subscribed
DemiMarie left a comment (vim/vim#19413)

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.Message ID: <vim/vim/pull/19413/c4012042947@github.com>

Martin Tournoij

unread,
Mar 6, 2026, 11:46:25 AM (12 days ago) Mar 6
to vim/vim, Subscribed
arp242 left a comment (vim/vim#19413)

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.Message ID: <vim/vim/pull/19413/c4012827497@github.com>

h_east

unread,
Mar 6, 2026, 12:23:09 PM (12 days ago) Mar 6
to vim/vim, Subscribed
h-east left a comment (vim/vim#19413)

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.Message ID: <vim/vim/pull/19413/c4013007154@github.com>

Justin M. Keyes

unread,
Mar 6, 2026, 12:38:21 PM (12 days ago) Mar 6
to vim/vim, Subscribed
justinmk left a comment (vim/vim#19413)

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.Message ID: <vim/vim/pull/19413/c4013076582@github.com>

Martin Tournoij

unread,
Mar 6, 2026, 1:57:36 PM (11 days ago) Mar 6
to vim/vim, Subscribed
arp242 left a comment (vim/vim#19413)

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.Message ID: <vim/vim/pull/19413/c4013478484@github.com>

James Mills

unread,
Mar 6, 2026, 2:29:47 PM (11 days ago) Mar 6
to vim/vim, Subscribed
prologic left a comment (vim/vim#19413)

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.Message ID: <vim/vim/pull/19413/c4013652362@github.com>

Christian Brabandt

unread,
Mar 6, 2026, 2:57:09 PM (11 days ago) Mar 6
to vim/vim, Subscribed
chrisbra left a comment (vim/vim#19413)

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.Message ID: <vim/vim/pull/19413/c4013824271@github.com>

Christian Clason

unread,
Mar 6, 2026, 3:03:40 PM (11 days ago) Mar 6
to vim/vim, Subscribed
clason left a comment (vim/vim#19413)

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.Message ID: <vim/vim/pull/19413/c4013851055@github.com>

Martin Tournoij

unread,
Mar 6, 2026, 3:12:05 PM (11 days ago) Mar 6
to vim/vim, Subscribed
arp242 left a comment (vim/vim#19413)

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.Message ID: <vim/vim/pull/19413/c4013889440@github.com>

Shougo

unread,
Mar 6, 2026, 10:05:12 PM (11 days ago) Mar 6
to vim/vim, Push

@Shougo pushed 5 commits.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/before/c2c223b3133d04e5f8445b15db63e0c48e91c08b/after/87d886d992e05aac558aded837eb4e5593f5459a@github.com>

Shougo

unread,
Mar 6, 2026, 10:22:30 PM (11 days ago) Mar 6
to vim/vim, Push

@Shougo pushed 2 commits.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/before/87d886d992e05aac558aded837eb4e5593f5459a/after/3090f96f95837056eea747cf3ab64172d7699b00@github.com>

Shougo

unread,
Mar 6, 2026, 10:32:57 PM (11 days ago) Mar 6
to vim/vim, Push

@Shougo pushed 1 commit.

  • 74d1671 Fix dict_has_key() type error

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/before/3090f96f95837056eea747cf3ab64172d7699b00/after/74d16717b9527d746b975b01c0e8df500c710217@github.com>

Shougo

unread,
Mar 6, 2026, 10:43:08 PM (11 days ago) Mar 6
to vim/vim, Push

@Shougo pushed 1 commit.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/before/74d16717b9527d746b975b01c0e8df500c710217/after/7a79374551c4a634fc1bb0f9f5e56aca815a5d1d@github.com>

Shougo

unread,
Mar 8, 2026, 4:53:27 AM (10 days ago) Mar 8
to vim/vim, Push

@Shougo pushed 5 commits.

  • dc0f04e Remove clear_repeat_dict() in nv_operator()
  • 398a60f Update the documentation
  • cf56029 Add backward compatibility tests
  • 35e1d77 More type tests
  • fd9f623 Add basic operator supports

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/before/7a79374551c4a634fc1bb0f9f5e56aca815a5d1d/after/fd9f62367a7d9b8d178dee4818c0a5125a3b2403@github.com>

Shougo

unread,
Mar 8, 2026, 4:54:00 AM (10 days ago) Mar 8
to vim/vim, Subscribed
Shougo left a comment (vim/vim#19413)

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


Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/c4018670067@github.com>

Shougo

unread,
Mar 8, 2026, 4:59:07 AM (10 days ago) Mar 8
to vim/vim, Push

@Shougo pushed 1 commit.


View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19413/before/fd9f62367a7d9b8d178dee4818c0a5125a3b2403/after/cb9e0bb4e72889c643a4bc16de8c6f2c48c24931@github.com>

Folling

unread,
Mar 9, 2026, 9:22:36 AM (9 days ago) Mar 9
to vim/vim, Subscribed
Folling left a comment (vim/vim#19413)

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.Message ID: <vim/vim/pull/19413/c4023755077@github.com>

Christian Brabandt

unread,
Mar 9, 2026, 4:10:30 PM (8 days ago) Mar 9
to vim/vim, Subscribed
chrisbra left a comment (vim/vim#19413)

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.Message ID: <vim/vim/pull/19413/c4026579122@github.com>

Shougo

unread,
Mar 10, 2026, 7:08:30 AM (8 days ago) Mar 10
to vim/vim, Subscribed
Shougo left a comment (vim/vim#19413)

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.Message ID: <vim/vim/pull/19413/c4030581037@github.com>

timkicker

unread,
Mar 16, 2026, 10:50:16 AM (2 days ago) Mar 16
to vim/vim, Subscribed
timkicker left a comment (vim/vim#19413)

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.Message ID: <vim/vim/pull/19413/c4068228294@github.com>

Shougo

unread,
Mar 17, 2026, 8:53:02 PM (5 hours ago) Mar 17
to vim/vim, Subscribed
Shougo left a comment (vim/vim#19413)

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.Message ID: <vim/vim/pull/19413/c4078925279@github.com>

Reply all
Reply to author
Forward
0 new messages