2

When I use the completion menu (<C-n>, <C-x><C-i>), sometimes the popup menu gives me a lot of options to choose from.

Sometimes I know parts of what I'm looking for at the end of the word, e.g. I want to find doFoobarOnObj, and I remember that the word begins with 'do' and ends with 'Obj'. If many other words also start with 'do', I may have to spend some time scanning the (potentially quite big) list to find what I'm looking for.

Is there a way I could jump and search within the popup menu to find what I'm looking for faster?

2 Answers 2

1

For the text completion you can have that feature using the completeopt option:

set completeopt+=fuzzy

For command line completion you can have that feature using the wildoptions option:

set wildoptions+=fuzzy

Remark: For text completion this feature is also part of the coc.nvim plugins.

3
  • 1
    I was looking for a way to jump within the popup menu, rather than drastic as changing how the completion matching works; although I can see how your solution might work for some people. Commented Sep 20 at 21:56
  • Thanks for your feedback. I wonder a bit how the system will decide when to filter the list and when to navigate it. Unless some kind of sorting would apply to the fuzzy matching. Commented Sep 21 at 8:47
  • 1
    Hmm let me attempt to hack something up using complete_info() and complete(). Commented Sep 21 at 15:30
1

As I suspected, this is possible. We use complete_info() to find the existing matches, ask the user to type his desired filter using getchar(), and replace the completions with complete().

I mapped this to <C-/>/<C-_>. To use this, after getting a lot of completions (e.g. from <C-n> or <C-x><C-i>), invoke <C-/> and start typing to search for the completion you need. You will see your search at the bottom of the screen (like the usual search prompt). You can see the completions being filtered as you type.

You can use all normal vim regex patterns here (like the magic mode). Press backspace to delete the last character or <C-u> to delete your whole search. Press <C-/> to accept this filter and begin another search. Press Enter to leave the search and go back to the completion. The usual completion bindings <C-e>, <C-y>, <C-n>, <C-p>, <C-h>, <C-l>, <Up>, <Down>, <Esc> also work, and automatically leave the completion search.

Some more subtleties about the implementation:

  1. We need to get the "startcol", that is, the start of the text being replaced, so that selecting the completions replaces that text, rather than appending to the end. We find this by <C-n> (to ensure some completion is selected), get the cursor position after the selection and finding the length of the selected completion by complete_info(), and compensating the <C-n> with a <C-p>. The "startcol" is the cursor position after selection minus the length of the selected completion.
  2. Naively passing <C-e> back into the mapping may not work, because the popup menu state is reset when we call complete(), so we have to save the value ourselves and restore it. Similarly, we should persist this state when we perform <C-/> to refine the search. We find the state by performing <C-e> and saving the result before starting the completion search.
  3. Only apply the mapping when pum is active.

This is the result:

function! Prepareearchpum ()
    let g:compstartcol = col('.')
    let g:compinf = complete_info()
    
    if len(g:compinf.items) && (g:compinf.selected == -1)
        return "\<C-n>\<C-r>=Prepareearchpum()\<CR>\<C-p>"
    else
        return ""
        endif
    endfunction
function! Searchpum ()
    let words = []
    for item in g:compinf.items
        call add(words, item.word)
        endfor
        
    let filter = ""

    if len(words)
        let startcol = g:compstartcol - len(g:compinf.items[g:compinf.selected].word)
        
        if exists('g:lastexittext')
            let exittext = g:lastexittext
            unlet g:lastexittext
        else
            let exitcol = col('.')
            let exittext = getline('.')[startcol-1:exitcol-2]
            endif
        
        while 1
            let matchwords = []
            for word in words
                if word =~ filter
                    call add (matchwords, word)
                    endif
                endfor
            if len(matchwords) == 0
                call add (matchwords, exittext)
                endif
                
            call complete(startcol, matchwords)
            
            echomsg '/' .. filter
            
            try
                let char = getchar()
            catch /^Vim:Interrupt$/
                return "\<C-c>"
                endtry

            if type(char) == 0
                let char = nr2char(char)
                endif

            if char == "\<C-e>"
                call complete(startcol, [exittext])
                return "\<C-y>"
            elseif char == "\<C-y>"
                return "\<C-y>"
            elseif char == "\<C-n>"
                return "\<C-n>"
            elseif char == "\<C-p>"
                return "\<C-p>"
            elseif char == "\<C-l>"
                return "\<C-l>"
            elseif char == "\<C-h>"
                return "\<C-h>"
            elseif char == "\<Up>"
                return "\<Up>"
            elseif char == "\<Down>"
                return "\<Down>"
            elseif char == "\<Esc>"
                return "\<Esc>"
            elseif char == "\<C-_>"
                let g:lastexittext = exittext
                call feedkeys ("\<C-_>")
                return ""
            elseif char == "\<CR>"
                return ""
            elseif char == "\<BS>"
                let filter = filter[:-2]
            elseif char == "\<C-u>"
                let filter = ""
            else
                let filter = filter .. char
                endif
            endwhile
        endif
    endfunction
inoremap <expr> <C-_> pumvisible() ? Prepareearchpum () . '<C-e><C-r>=Searchpum()<CR>' : '<C-_>'
3
  • A version of this might be worth including in the distribution as an optional plugin
    – D. Ben Knoble
    Commented Nov 14 at 14:50
  • Now I wish I could use this for the wildmenu pum as well, but complete_info() and complete() don't apply to the wildmenu pum... Hm... Commented Nov 14 at 16:19
  • Okay, while not directly workable with the wildmenu pum, with some UX adjustments its possible to adapt a similar workflow to the normal wildmenu search, by extending existing wildmenu searches with *moresearch, to get something similar. The only issue is that by default, vim immediately replaces the search string with matches, which makes extending the wild search difficult. With a bit of effort, I've made vim not do so. Commented Nov 14 at 17:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.