Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Manual whole-line completion with ^X^L inserts the wrong entry into the buffer. #1326

Open
2 tasks done
bagohart opened this issue Nov 25, 2022 · 20 comments
Open
2 tasks done
Labels

Comments

@bagohart
Copy link

FAQ

  • I have checked the FAQ and it didn't resolve my problem.

Announcement

Minimal reproducible full config

if has('vim_starting')
  set encoding=utf-8
endif
scriptencoding utf-8

if &compatible
  set nocompatible
endif

let s:plug_dir = expand('/tmp/plugged/vim-plug')
if !filereadable(s:plug_dir .. '/plug.vim')
  execute printf('!curl -fLo %s/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim', s:plug_dir)
end

execute 'set runtimepath+=' . s:plug_dir
call plug#begin(s:plug_dir)
Plug 'hrsh7th/nvim-cmp'
Plug 'hrsh7th/cmp-buffer'
call plug#end()
PlugInstall | quit

" Setup global configuration. More on configuration below.
lua << EOF
local cmp = require "cmp"
cmp.setup {
  mapping = {
    ['<CR>'] = cmp.mapping.confirm({ select = true })
  },

  sources = cmp.config.sources({
    { name = "nvim_lsp" },
    { name = "buffer" },
  }),
}
EOF

Description

Hi,
Cool plugin! I've tried it this week for using it with LSP and really like it so far.

My only problem is that it breaks manual whole-line completion.
This seems to happen independently of other plugins, filetype, file content and doesn't depend on if the autocompletion menu was open or not.
So when I type (^X^L) to start manual line completion, then the automatic completion menu is dismissed (if it was visible), and the manual line completion menu appears, and then I can use <C-p> and <C-n> to select an entry, but when I press <CR> to accept the selected line, then a different line is inserted. This does not always happen, but in more than 50% of my attempts. I tried it with different settings for 'completeopt' and even tried to override <CR> to be used only when the menu is visible using cmp.core.view:visible(), but it made no difference. Sometimes it seems to be an off-by-one selection, but sometimes the inserted text is farther away. It happens when the line is empty, and if there's something written to complete. I've spent about 2 hours trying to fix this, and have no idea what's happening here.

Steps to reproduce

nvim -u ~/cmp-repro.vim ~/cmp-repro.vim
Insert two empty lines at the beginning of the file, go to first line.
Press ^X^L and use ^P or ^N to choose some entry.
Select the entry with <CR>.

Expected behavior

The selected line should be inserted in the buffer.

Actual behavior

Another line that was not selected is inserted in the buffer.

Additional context

Output of :version:

NVIM v0.8.0
Build type: Release
LuaJIT 2.1.0-beta3
Compiled by builduser

Running on Manjaro XFCE.

@bagohart bagohart added the bug Something isn't working label Nov 25, 2022
@hrsh7th
Copy link
Owner

hrsh7th commented Nov 27, 2022

Hm... To be honest, I can't understand the root cause (but reproduced).

set completeopt=menuone,noselect,noinsert

I think the above option will improve the behavior. Could you try this?

@bagohart
Copy link
Author

I added that line at the beginning of the minimal reproducible config from above, but the bug is still happening.

Is it possible, as a workaround, to completely disable nvim-cmp for only manual line completion?

Another workaround is to use https://github.com/amarakon/nvim-cmp-buffer-lines but that doesn't currently work for large files.

@hrsh7th
Copy link
Owner

hrsh7th commented Nov 27, 2022

if has('vim_starting')
  set encoding=utf-8
endif
scriptencoding utf-8

if &compatible
  set nocompatible
endif

let s:plug_dir = expand('/tmp/plugged/vim-plug')
if !filereadable(s:plug_dir .. '/plug.vim')
  execute printf('!curl -fLo %s/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim', s:plug_dir)
end

execute 'set runtimepath+=' . s:plug_dir
call plug#begin(s:plug_dir)
Plug 'hrsh7th/nvim-cmp'
Plug 'hrsh7th/cmp-buffer'
call plug#end()
PlugInstall | quit

set completeopt=menu,menuone,noselect

" Setup global configuration. More on configuration below.
lua << EOF
local cmp = require "cmp"
cmp.setup {
  mapping = {
    ['<CR>'] = cmp.mapping.confirm({ select = true })
  },

  sources = cmp.config.sources({
    { name = "nvim_lsp" },
    { name = "buffer" },
  }),
}
EOF

In my environment, the above setup solve this problem.

@bagohart
Copy link
Author

I've just tried it with that file, i.e.
nvim --clean -u init.vim.reproduce_bug_in_nvim_cmp_2 init.vim.reproduce_bug_in_nvim_cmp_2
but the bug still happens.
On my first few attempts, the correct line was selected, but after about five attempts, the wrong line began to be selected.

Sometimes, another weird thing happens: I press to insert a line, but nothing happens, as if I hadn't pressed any key at all.

@bagohart
Copy link
Author

bagohart commented Nov 28, 2022

I just noticed that the same problem (the wrong entry gets selected) occurs when using repeated ^X^P selection, i.e. ^X^P^X^P^X^P to select words that appear in the file in a sequence. In my books, this is a much more serious problem than ^X^L not working.

On the plus, side, I think I found a workaround that actually works: when the entry is already selected, I can just keep writing, and the correct selection keeps being inserted. In other words, as long as I avoid the Enter key, nothing bad happens. Obviously, that makes using <Up> and <Down> inconvenient, which is a bit annoying. Maybe <CR> could be made to work by a mapping that just does <C-n><C-p><Space><Backspace>, but that does seem annoyingly hacky.

Another idea how to solve this: would it be possible to completely disable nvim-cmp for manual completion? I think, this would be an intuitive solution for everyone who is already used to manual completion.

@bagohart
Copy link
Author

bagohart commented Nov 30, 2022

Okay, so here's a hacky workaround that seems to work for me for now and actually lets me use <CR>:

['<CR>'] = function(fallback) -- ugly workaround because manual completion (not only ^X^L) is broken!
    if cmp.visible() and cmp.get_active_entry() then
        cmp.select_next_item({ behavior = cmp.SelectBehavior.Insert })
        cmp.select_prev_item({ behavior = cmp.SelectBehavior.Insert })
        vim.cmd([[call feedkeys("\<Space>\<BS>")]])
    else
        fallback()
    end
end,

@bagohart
Copy link
Author

bagohart commented Dec 2, 2022

Sorry to spam this issue with workarounds instead of fixing it, but I noticed that my last attempt at a workaround isn't suitable, because it destroys i.a. the snippet functionality. Here's a better way which so far seems to work perfectly:

['<CR>'] = function(fallback) -- ugly workaround for nvim-cmp bug that selects wrong entry for manual completion
    if vim.fn.complete_info().mode ~= '' then
        cmp.select_next_item({ behavior = cmp.SelectBehavior.Insert })
        cmp.select_prev_item({ behavior = cmp.SelectBehavior.Insert })
        vim.cmd([[call feedkeys("\<Space>\<BS>")]])
    else
        if not cmp.confirm({ select = false }) then fallback() end
    end
end,

I didn't find a way to check if cmp is active, but the check on complete_info() seems to be suitable to check if manual completion is active, because when I tested it with an open autocompletion menu of cmp, the return value of complete_info() was always empty.

@hrsh7th
Copy link
Owner

hrsh7th commented Dec 27, 2022

Could you please provide detailed steps to reproduce?
Apparently I don't understand your actual problem

@bagohart
Copy link
Author

Ok, I'll try again:
Autocompletion works fine.
Manual completion invoked with ^X^L and ^X^P does not:
The problem is that using <CR> to input a selected line into the buffer, sometimes a different line is inserted into the buffer instead.
It seems weirdly indeterministic, and does not happen every time, but it happens quite often, probably >50% of my attempts.
I find it very surprising that no one has reported this so far, since ^X^L and ^X^P^X^P^X^P are useful features even if autocompletion is available.

Here's an exact sequence of keystrokes which I can use to reproduce. I've tried this several times and the result is the same every time.
Save the minimal reproducible full config from my first post as init.vim.nvim-cmp-1326-reproduce.
Save the following content as file nvim-cmp-1326-input-file:

111
222
333

where the last line is blank.

Now, in the shell, input:

nvim --clean -u init.vim.nvim-cmp-1326-reproduce nvim-cmp-1326-input-file

and then in neovim, the following keypresses:
Gi<C-x><C-l><CR>
After pressing <C-x><C-l> the buffer contains 333 (as expected), but after pressing <CR>, instead 111 will be inserted into the buffer.

Instead of <C-x><C-l> if I try it with <C-x><C-p>, the same thing happens.

@hrsh7th
Copy link
Owner

hrsh7th commented Dec 28, 2022

@bagohart Yes. Your reproduction steps can be reproduced the problem.

I think it's depending on to the completeopt value. Can you try to change the completeopt?
In my environment, the set completeopt=menu,menuone,noselect solves the problem.

(Sorry. In my previous post, I wrote set completeopt=menu,menuone,noinsert but it's just wrong)

@bagohart
Copy link
Author

The bug is happening with this setting for completeopt, too.
Minimal reproducible full config: (same as above, but with added completeopt value)

if has('vim_starting')
  set encoding=utf-8
endif
scriptencoding utf-8

if &compatible
  set nocompatible
endif

let s:plug_dir = expand('/tmp/plugged/vim-plug')
if !filereadable(s:plug_dir .. '/plug.vim')
  execute printf('!curl -fLo %s/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim', s:plug_dir)
end

execute 'set runtimepath+=' . s:plug_dir
call plug#begin(s:plug_dir)
Plug 'hrsh7th/nvim-cmp'
Plug 'hrsh7th/cmp-buffer'
call plug#end()
PlugInstall | quit

set completeopt=menu,menuone,noselect

" Setup global configuration. More on configuration below.
lua << EOF
local cmp = require "cmp"
cmp.setup {
  mapping = {
    ['<CR>'] = cmp.mapping.confirm({ select = true })
  },

  sources = cmp.config.sources({
    { name = "nvim_lsp" },
    { name = "buffer" },
  }),
}
EOF

To reproduce:
Save that file as init.vim.nvim-cmp-1326-reproduce-menu_menuone_noselect
Execute nvim --clean -u init.vim.nvim-cmp-1326-reproduce-menu_menuone_noselect nvim-cmp-1326-input-file.
Press Gi<C-x><C-l><C-p><CR>.
Again, 333 should be inserted, but 111 is inserted. This works for both ^X^L and ^X^P.

@hmrks
Copy link

hmrks commented Feb 3, 2023

Hi! I am facing the same issue. I find the bug is happening if you move backward in any native completion menu.

As an example, if you press <C-n> in insert mode, then move around with <C-n> and <C-p>, completion works fine. However if you start with <C-p>, or start any kind of completion by moving backwards in the completion menu (for example <C-x> <C-l> <C-p>), completion breaks. It turns out completion still starts at the top. So if you press <C-p> twice, it will paste the second suggestion from the top, not from the bottom.

I tried playing around with completeopt, to no avail, it does not seem to have any effect on native completion as a matter of fact. Note that I am using this with lsp-zero.nvim, where I can disablenvim-cmp, which resolves the issue. Of course then you will not have completion for LSP, however it confirms that the issue is with the configuration of nvim-cmp.

Running Neovim 0.8.2 on Fedora:

NVIM v0.8.2
Build type: RelWithDebInfo
LuaJIT 2.1.0-beta3
Compilation: /usr/bin/gcc -O2 -flto=auto -ffat-lto-objects -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -fstack-protector-strong -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -DNVIM_TS_HAS_SET_MATCH_LIMIT -DNVIM_TS_HAS_SET_ALLOCATOR -O2 -g -Og -g -Wall -Wextra -pedantic -Wno-unused-parameter -Wstrict-prototypes -std=gnu99 -Wshadow -Wconversion -Wdouble-promotion -Wmissing-noreturn -Wmissing-format-attribute -Wmissing-prototypes -Wimplicit-fallthrough -Wvla -fstack-protector-strong -fno-common -fdiagnostics-color=auto -DINCLUDE_GENERATED_DECLARATIONS -D_GNU_SOURCE -DNVIM_MSGPACK_HAS_FLOAT32 -DNVIM_UNIBI_HAS_VAR_FROM -DMIN_LOG_LEVEL=3 -I/builddir/build/BUILD/neovim-0.8.2/redhat-linux-build/cmake.config -I/builddir/build/BUILD/neovim-0.8.2/src -I/usr/include -I/usr/include/luajit-2.1 -I/builddir/build/BUILD/neovim-0.8.2/redhat-linux-build/src/nvim/auto -I/builddir/build/BUILD/neovim-0.8.2/redhat-linux-build/include
Compiled by mockbuild@koji

Features: +acl +iconv +tui
See ":help feature-compile"

   system vimrc file: "$VIM/sysinit.vim"
  fall-back for $VIM: "/usr/share/nvim"

Run :checkhealth for more info

@d-r-a-b
Copy link

d-r-a-b commented Apr 2, 2023

@bagohart, is this still reproducing for you using your minimal config from #1326 (comment) on latest main branch (commit 777450f)? I had the same issue as you, still repro with your first config, but found that your second config with the completeopt set did not repro the issue -- i.e. completeopt with noselect fixed my issue. Why noselect fixes the behavior I'm observing and why it only affects a few native modes (<C-X><C-L>,<C-X><C-P>) is a deeper question.

In my observation, the base error also doesn't seem to be an off-by-1 error, but rather a sign error (it seems to reliably reverse top/bottom of menu). It can be hard to notice because the order of the menu changes and this can make it seem more like an off-by-n. I detail more about that in the duplicate issue I filed before finding this one. This is close to the behavior described by @hmrks but is different in that the behavior I observe does not occur with <C-N> or <C-P> completion, but rather only the two <C-X> modes described above, and does not depend on initial <C-P>.

@hmrks As another user, I do not see the same behavior with <C-N> or <C-P> completion. Simply disabling nvim-cmp in lsp-zero.nvim and seeing the issue disappear is useful information, but ultimately insufficient evidence that nvim-cmp is the culprit. It only shows that lsp-zero and nvim-cmp together can cause the issue. It very well might still be nvim-cmp, but without a minimal reproducible workflow there is nothing to help the maintainer or a contributor reproduce or trace the issue to begin troubleshooting.


NVIM v0.8.3
Build type: Release
LuaJIT 2.1.0-beta3
Compiled by builduser

Features: +acl +iconv +tui
See ":help feature-compile"

OS: WSL2 running Arch Linux on Win 11

@bagohart
Copy link
Author

bagohart commented Apr 4, 2023

@bagohart, is this still reproducing for you using your minimal config from #1326 (comment) on latest main branch (commit 777450f)? I had the same issue as you, still repro with your first config, but found that your second config with the completeopt set did not repro the issue -- i.e. completeopt with noselect fixed my issue. Why noselect fixes the behavior I'm observing and why it only affects a few native modes (<C-X><C-L>,<C-X><C-P>) is a deeper question.

Yes, it still reproduces with the same example and procedure described above, still using the same file

111
222
333

I think I am on the same nvim version:

NVIM v0.8.3
Build type: Release
LuaJIT 2.1.0-beta3
Compiled by builduser

Features: +acl +iconv +tui
See ":help feature-compile"

   system vimrc file: "$VIM/sysinit.vim"
  fall-back for $VIM: "/usr/share/nvim"

Run :checkhealth for more info

@d-r-a-b
Copy link

d-r-a-b commented Apr 5, 2023

Snippets for debugging.

:imap \ <cmd>put =execute('lua print(vim.fn.complete_info({''selected''}).selected)')<CR>
imap [ <cmd>lua require('cmp.utils.feedkeys').call(require('cmp.utils.keymap').t(string.rep('<C-P>',1)),'in')<CR>

With the above mapping, you can hit \ in insert mode and it will show you what nvim thinks is selected in the native pumenu. You can also use [ navigate up, mimicking the feedkeys behavior that nvim-cmp is using to navigate native pumenu.

In lua/cmp/init.lua, nvim-cmp differentiates between cmp menus and native pumenus. cmp menu is checked with cmp.core.view:visible() and native menus checked with vim.fn.pumvisible(). Behavior is different depending on which type of menu is visible.

  • For pumvisible() native menu:
    • to navigate the menu up, select_prev_item uses feedkeys.... The mapping above for [ mimics this to make debugging clearer.
    • cmp.confirm calls complete_info. This normally gives the correct index and can be checked with the \ mapping above. If feedkeys.... <C-P>... is called first, e.g. with [ mapping above, then the selected index from complete_info is wrong.

Testing just the \ mapping in nvim --clean, I was unable to get wrong index, so my initial hunch is that this isn't an upstream issue but I can't rule it out completely. I am not a lua or nvim expert at all, so not sure if I'll be able to tackle this but hopefully tracking down this far will help.

@d-r-a-b
Copy link

d-r-a-b commented Apr 5, 2023

Repro config

CLICK ON ME! Click to expand and save as `repro_nvim_cmp_vimrc`
if has('vim_starting')
  set encoding=utf-8
endif
scriptencoding utf-8

if &compatible
  set nocompatible
endif

let s:plug_dir = expand('/tmp/plugged/vim-plug')
if !filereadable(s:plug_dir .. '/plug.vim')
  execute printf('!curl -fLo %s/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim', s:plug_dir)
end

execute 'set runtimepath+=' . s:plug_dir
call plug#begin(s:plug_dir)
Plug 'hrsh7th/nvim-cmp'
Plug 'hrsh7th/cmp-buffer'
call plug#end()
PlugInstall | quit

set completeopt=menu,menuone,noselect

" Setup global configuration. More on configuration below.
lua << EOF
local cmp = require "cmp"
cmp.setup {
  mapping = cmp.mapping.preset.insert({
    ['<CR>'] = cmp.mapping.confirm({ select = true })
  }),

  sources = cmp.config.sources({
    { name = "nvim_lsp" },
    { name = "buffer" },
  }),
}
EOF

imap \ <cmd>put =execute('lua print(vim.fn.complete_info({''selected''}).selected)')<CR>
imap [ <cmd>lua require('cmp.utils.feedkeys').call(require('cmp.utils.keymap').t(string.rep('<C-P>',1)),'in')<CR>
nmap \\ iaaa<CR>bbb<CR>ccc<CR>ddd<CR>eee<CR>

Note the 3 maps, \ to print the complete_info index, [ to mimic the select_prev_item behavior on pumenu, and \\ to make it fast to get to an example file that shows the error.

Repro Steps

  • download the config above to repro_nvim_cmp_vimrc
  • nvim --clean -u repro_nvim_cmp_vimrc and wait to get an empty buffer
  • in normal mode, hit \\ which is mapped to create the example file:
    aaa
    bbb
    ccc
    ddd
    eee
    
    and will put you on a newline under "eee" in insert mode
  • in insert mode, <C-X><C-L> to pull up the native line completion pumenu, which should have 5 lines in order from "aaa" at top to "eee" at bottom
  • in insert mode, [[[[ to navigate backwards in the menu 4 times, apparently selecting the "bbb" option
  • in insert mode, with the pumenu still visible, \\\\ to see the selected item index be put in the buffer 4 times
    • (I decided to keep hitting \ binding so that it goes down far enough and is not hidden by the completion menu)
  • Notice that the index that it is showing is 3, when it should be 1
  • in insert mode, <CR> to select what looks like "bbb", and see "ddd" get inserted instead. Wrong behavior, but corresponds to the complete_info index.

@d-r-a-b
Copy link

d-r-a-b commented Apr 5, 2023

Some more investigation. I think this is a subtle upstream bug affecting the scripting interface for complete_info. Linked this issue in the upstream report, follow there to repro and see that complete_info is not behaving as documented.

@d-r-a-b
Copy link

d-r-a-b commented Apr 5, 2023

for now, here's a different, only slightly less hacky workaround than sending <Space><BS> that relies on <C-X><C-Z> stop completion.

mapping = cmp.mapping.preset.insert({
    -- cmp.mapping.preset.insert provides <C-N>, <C-P>, etc
    ['<C-d>'] = cmp.mapping.scroll_docs(-4),
    ['<C-f>'] = cmp.mapping.scroll_docs(4),
    ['<C-Space>'] = cmp.mapping.complete(),
    ['<CR>'] = function(fallback)
        if vim.fn.pumvisible() == 1 then
            -- native pumenu
            -- workaround for neovim/neovim#22892
            if vim.fn.complete_info({'selected'}).selected == -1 then
                -- nothing selected, insert newline
                feedkeys.call(keymap.t('<CR>'), 'in')
            else
                -- something selected, confirm selection by stopping Ctrl-X mode
                -- :h i_CTRL-X_CTRL-Z*
                feedkeys.call(keymap.t('<C-X><C-Z>'), 'in')
            end
        else
            -- `nvim-cmp` default confirm action
            -- Accept currently selected item.
            -- Set `select` to `false` to only confirm explicitly selected items.
            cmp.mapping.confirm({ select = false })(fallback)  
        end
    end
}),

You'll need to put the following requires somewhere sensible also:

local cmp = require'cmp'
local feedkeys = require('cmp.utils.feedkeys')
local keymap = require('cmp.utils.keymap')

Alright, now I'll stop spamming this thread. Hopefully upstream will fix the underlying issue and then we can get a better sense of if anything needs to change in nvim-cmp.

@hrsh7th
Copy link
Owner

hrsh7th commented Apr 26, 2023

@d-r-a-b is great. I checked the vim issue thread. thank you!

arusahni added a commit to arusahni/dotfiles that referenced this issue May 20, 2023
IlyasYOY added a commit to IlyasYOY/dotfiles that referenced this issue Jun 4, 2023
haunt98 added a commit to haunt98/dotfiles that referenced this issue Sep 25, 2023
@bagohart
Copy link
Author

bagohart commented Nov 19, 2023

@d-r-a-b I'm now on neovim 0.9.4 and cannot reproduce the bug anymore, with neither ^x^l nor ^x^p. Same for you? If so, I guess this issue can finally be closed.

Edit: nevermind, it still doesn't work. I tried to remove the mapping, but didn't realize that it was then shadowed by another plugin, and that seemed to make the bug disappear. But it's still there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants