Repository: neovim
Issue: Async macros or make macros work with events
Labels: enhancement,event-loop,macros

### Problem

The current macro replay mechanism doesn’t work at all with events.

For a simple example see the plugin [better-escape.nvim](https://github.com/max397574/better-escape.nvim) that “maps” `jk` to `<esc>` using autocmds and [timers](https://github.com/max397574/better-escape.nvim/blob/7031dc734add47bb71c010e0551829fa5799375f/lua/better_escape.lua#L56) instead of classic mappings. 
 
When using `jk` in a macro, the replay kinda breaks and instead it inserts `jk` and the remaining commands into the buffer. max397574/better-escape.nvim#21


### Expected behavior

Replay should *truly* mirror the actions that have happened when recording.
If the final fix somehow breaks too much compatibility, then it could be added as a new bool option or new operator.
# Proposed mechanism

1. The event loop should run after each executed key read from the replayed sequence:
    1. This should emulate how `state.c` loop iterates when keys are typed by the user. There is a small delay when the user types keys, and events are processed between each key stroke, this should also happen when replaying macros. We don’t need the time delay for replaying, the `state.c` loop just needs to alternate every other iteration between key and event processing.
2. Keys should be fed one at a time to the typebuffer, instead of the current replay mechanism where the entire register to be replayed is stuffed into the typebuffer, which has the undesired effect that:
    1. Events will be processed only after the macro sequence is replayed since (I believe) `K_EVENT` will be appended to the end of typebuffer.
    2. Calls to `nvim_feedkeys` will not be processed correctly.
    Take for example how better-escape.nvim works. It calls [`feed(”<bs><bs>”)`](https://github.com/max397574/better-escape.nvim/blob/7031dc734add47bb71c010e0551829fa5799375f/lua/better_escape.lua#L68) to delete the “mapped” chars (usually `jk`) and [`feed(”<esc>”)`](https://github.com/max397574/better-escape.nvim/blob/7031dc734add47bb71c010e0551829fa5799375f/lua/better_escape.lua#L69) to exit insert mode. These keys will be appended to the typebuffer.
    For the input sequence `ijkijkhhh`, nvim enters and exists insert mode 2 times and moves the cursor left, the recording register looks the same: `ijkijkhhh`. 
    When the user types the keys in real time, the typebuffer will look like `ijk<bs><bs><esc>ijk<bs><bs><esc>hhh`. 
    When the keys are replayed using the current mechanism, the typebuffer will look like `ijkijkhhh<bs><bs><esc>`, with the letters `jkijkh` being written in the buffer.

## Proof implementation

Here is a [proof of concept implementation](https://github.com/BaraTudor2025/neovim/commit/ca71cf86621dcb87e47c7c64ba0cc56dec1ad69b) that seems to "just work" for the better-escape test case

- The register to be replayed is stuffed into a replay buffer that is then read into the typebuffer one key at a time.
- When the typebuffer is empty, schedule on the event-loop to add one key from the replay buffer (I tried different approaches, but only scheduling worked for me. Another idea would be to add an `K_EVENT` key to the typebuffer, but I didn’t manage to make it work)

I used this when running manual tests

```lua
require("better_escape").setup()
vim.fn.setreg("q", "ijkijkhhh")
vim.keymap.set("n", "Q", "@q")
```

The C implementation is somewhat inspired from the following lua snippet that also seems to work with better-escape:

```lua
local plenary = require "plenary.async"
 
function replay_macro_async(reg, delay_ms)
  if reg == "" then
    return
  end
  local keys = vim.fn.getreg(reg)
  local delay = delay_ms or 50
  local run = plenary.void(function()
    local i = 1
    while i <= #keys do
      plenary.util.sleep(delay)
      plenary.util.scheduler()
      if keys:byte(i) == 0x80 then -- k_special bytes
        local seq = keys:sub(i, i + 2)
        vim.api.nvim_feedkeys(seq, "", false)
        i = i + 3
      else -- doesn't handle unicode, probably should
        vim.api.nvim_feedkeys(keys:sub(i,i), "", false)
        i = i + 1
      end
    end
  end)
  run()
end

vim.keymap.set("n", "Q", function() replay_macro_async(vim.fn.reg_recorded(), 50) end)
```