This bug was originally reported at neovim/neovim#14133, but I'm reporting it here too since it affects Vim as well.
function! X()
let line = line('.')
let col = col('.')
echomsg "Pressed x at (".l:line.", ".l:col.")"
return "x"
endfunction
nnoremap <expr> x X()
fpx.:messages you see "Pressed x at (8, 8)."nmap ! fpx!.Environment (please complete the following information):
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub, or unsubscribe.![]()
Relevant todo item:
Using an insert mode expression mapping, cursor is not in the expected
position. (ZyX, 2010 Aug 29)
After sourcing this script:
let g:pos = [] fu Func() call add(g:pos, col('.')) return 'a' endfu ino <expr> a Func() norm aaaaa echom pos echo ''
g:pos contains this list:
[1, 1, 1, 1]
While one would expect:
[1, 1, 2, 3]
As a workaround, <Ignore> can be prepended to the sequence returned by Func():
let g:pos = [] fu Func() call add(g:pos, col('.')) return "\<ignore>a" endfu ino <expr> a Func() norm aaaaa echom pos echo ''
[1, 1, 2, 3]
While one would expect:
[1, 1, 2, 3]
Ah no, one would expect:
[1, 2, 3, 4]
As a workaround, can be prepended to the sequence returned by Func():
No, <Ignore> should be appended, not prepended:
let g:pos = [] fu Func
()
let g:pos += [col('.')]
return "a\<ignore>"endfu ino <expr> a Func() norm aaaaa echom pos echo ''
[1, 2, 3, 4]
Although, in the original example, <Ignore> can't help:
vim9script def g:Func(): string echom 'Pressed x at col: ' .. col('.') return "\<ignore>x" enddef nno <expr> x g:Func() setline(1, 'aaa p aaa') norm fpx
Pressed x at col: 1
I think that's because :norm writes fpx in the typeahead buffer, then inside the latter, Vim remaps the key x into <Ignore>x. To get this sequence, Func() has to be evaluated; but when it is, the cursor has not been moved yet; fp has not been executed. I think the OP expects their X() function to be evaluated after fp has been executed. This is the case during an interactive usage; but not in a non-interactive one (like a macro or a mapping).
This is confirmed using feedkeys():
vim9script def g:Func(): string echom 'Pressed x at col: ' .. col('.') return 'x' enddef nno <expr> x g:Func() setline(1, 'aaa p aaa') feedkeys('fpx')
Pressed x at col: 1
Notice that the column was 1 when Func() was evaluated. That's because fp was still in the typeahead when x was remapped.
Now, watch this:
vim9script def g:Func(): string echom 'Pressed x at col: ' .. col('.') return 'x' enddef nno <expr> x g:Func() setline(1, 'aaa p aaa') feedkeys('fp', 'x') feedkeys('x')
Pressed x at col: 5
This time, the column is 5, because we've asked feedkeys() to execute the contents of the typeahead buffer (via the second optional flag argument x), before adding the key x.
So, is that really a bug? It seems it's working as intended.
In your case, one workaround is to force Vim to execute fp before X() is evaluated by first executing a no-op, like CTRL-\_CTRL-N:
vim9script def g:Func(): string echom 'Pressed x at col: ' .. col('.') return 'x' enddef nno <expr> x g:Func() setline(1, 'aaa p aaa')
exe "norm fp\<c-\>\<c-n>x"
Pressed x at col: 5
That is, during the recording, don't press this:
f p x
Press this:
f p CTRL-\ CTRL-N x
@lacygoill thanks for the thorough reply! I have a couple questions.
fp before evaluating X()? I see that it works, but I don't understand why.set nocompatible
function! X()
call feedkeys(":echomsg 'Typeahead: ","ni")
call feedkeys("'\<CR>", "nx")
echomsg "Pressed x at col ".col('.')
return "x"
endfunction
nnoremap <expr> x X()
nmap ! fpxFr
then I see "Fr" appear (check :messages) but column 1 is still displayed. This surprises me because based on your answer above I would have expected "fp" to still be in the typeahead buffer if it hasn't executed yet.[1]: I can't find a good way to inspect current type-ahead -- only ways to save, restore, and execute it. The closest I can find is input() but that blocks on the user's input.
Why does a no-op force Vim to execute fp before evaluating X()? I see that it works, but I don't understand why.
I'm not a dev, so I don't know. I've just noticed that fp didn't seem to have been executed when X() was evaluated, and I remembered a similar issue which I've documented here. Basically, it boiled down to this: when executing the contents of a register which changes the current mode, some keys might be executed in the wrong mode. The solution which I found was to execute a no-op to force Vim to change the current mode when I expected it would already have been automatically done. Your issue sounded similar, so I applied the same solution:
the current mode should have been changed
=> it did not
=> execute a no-op to make Vim change it now
the keys should have been executed
=> they were not
=> execute a no-op to make Vim execute them now
then I see "Fr" appear (check :messages) but column 1 is still displayed. This surprises me because based on your answer above I would have expected "fp" to still be in the typeahead buffer if it hasn't executed yet.
I agree, it looks unexpected. But maybe fp has been removed from the typeahead because Vim has noticed that these keys could not be remapped further (you have no f mapping), and that they form a complete and valid command (f expects an argument, here provided by p). And maybe the keys which are removed from the typeahead are not immediately executed; there might be some kind of overhead which requires a little more time. This time might be small enough that it cannot be perceived during an interactive use, but could be noticeable in a non-interactive one (like a mapping or a macro), because all the keys are typed almost instantaneously.
That's all speculation on my part. You'll have to wait for a dev to comment to get more reliable information.
[1]: I can't find a good way to inspect current type-ahead -- only ways to save, restore, and execute it. The closest I can find is input() but that blocks on the user's input.
You could add a breakpoint at the start of the function:
breakadd func X
Then, as you said, you can invoke input('') which will consume the typeahead, and put it on the command line. Obviously, this breaks the original sequence, but for a one-time debugging session, I guess it's good enough to let you peek at the typeahead's contents.
There are a couple of passages in vim's :help map-<expr> which sort of sideways mention exactly what @lacygoill is saying:
Be very careful about side effects! The expression is evaluated while
obtaining characters, you may very well make the command dysfunctional.
...
If something changed that requires Vim to
go through the main loop (e.g. to update the display), return "\<Ignore>".
This is similar to "nothing" but makes Vim return from the loop that waits for input.
But neither are so explicit, and the problem remains. If a person is writing an <expr> map which needs line/col, they'll be bitten eventually when someone tries to create a map or macro on it. Unless they add a "sequence point" like this
nnoremap <expr> <plug>(my-x) X()
nmap x <ignore><plug>(my-x)
nmap <space> fpx