31

How do I create my own list of auto-completion for certain filetypes?

For example I would like css and html to autocomplete from the list of css classes in FontAwesome.

0

3 Answers 3

25

Dictionary completion would be a cheap and non-intrusive solution:

  1. save fontawesome.txt somewhere on your machine,

  2. put this in after/ftplugin/css.vim (create the file if it doesn't exist):

    setlocal complete+=k
    setlocal dictionary+=/path/to/fontawesome.txt
    setlocal iskeyword+=-
    
  3. start a new session or do :e in a CSS buffer to forcefully source the file above,

  4. press <C-n> or <C-p> in insert mode,

  5. enjoy!

    dictionary completion

Reference:

:help ins-completion
:help 'complete'
:help 'dictionary'
:help 'iskeyword'
20

EDIT Thanks to romainl's comment I've edited my answer to show how to create a user-defined completion function. In the previous version I was overriding the buil-in omni-completion which wasn't good because the user would have loose the default completion which is pretty powerful.


A vimscript solution

One solution is to use vimscript and the fact that vim let's you create a customized completion function.

The advantage of this solution is that you don't need an additional plugin you can simply create a user-defined completion function and use the built-in completion feature.

I'll use your example of the fontAwesome classes in css files:

Create the file ~/.vim/autoload/csscomplete.vim and put the following lines in it:

let s:matches=".fa-lg .fa-2x .fa-3x .fa-4x .fa-5x .fa-fw .fa-ul .fa-ul .fa-li .fa-li.fa-lg .fa-border .fa-pull-left .fa-pull-right .fa.fa-pull-left"

function! csscomplete#CompleteFA(findstart, base)
    if a:findstart
        " locate the start of the word
        let line = getline('.')
        let start = col('.') - 1
        while start > 0 && (line[start - 1] =~ '\a' || line[start - 1] =~ '.' || line[start - 1] =~ '-')
            let start -= 1
        endwhile
        return start
    else
        " find classes matching "a:base"
        let res = []
        for m in split(s:matches)
            if m =~ '^' . a:base
                call add(res, m)
            endif
        endfor
        return res
    endif
endfun

Then create the file ~/.vim/after/ftplugin/css.vim and put in it the following line:

setlocal completefunc=csscomplete#CompleteFA

Then you can trigger your user-defined completion function with <C-x><C-u>. It will try to find a match to the last typed word.

In the screenshot I typed .fa-l and then triggered the function with <C-x><C-u>:

enter image description here


How does it work?

First here are some relevant documentation topics:

If you want to create a custom completion for a particular filetype you have to put it in the file $HOME/.vim/autoload/{FILETYPE}complete.vim.

Then in this file you have to create your completion function which is called twice:

  • On first call its first argument is the current position of the cursor and the function will determine the word to complete. In my function I used 3 comparisons to get the word to complete because the class can be composed of letters, . and - (I think it is possible to improve this matching but I'm really bad with regex)

  • On second call the second argument is the word to match and then the function compares it to the list of possible classes to match 1. Here you see that I return a dictionary which will populate the completion menu but when you'll read the documentation you'll see that you can create a much more complex dictionary to customize even more the behavior of your function.

You also have to tell to Vim "for this kind of file, use this complete function I created". To do so you have to set completefunc to the name of the function you created (here csscomplete#CompleteFA) and this setting must be done in the file $HOME/.vim/after/ftplugin/{FILETYPE}.vim.

1 In my function the variable s:matches contains all the possible matches. Here I only put some suggestions for readability but you can put all the classes offered by FontAwesome (You can find the complete list in this file created by romainl).


What if I don't like Vimscript?

One possibility is to use the plugin YoucompleteMe which offers an API to play with the completion menu. You can create python function which will do the matching job and will use the API to populate Vim interface. More details here.

2
  • 2
    The default omni completion for CSS and HTML is already quite useful and you can only have one at a time so I would suggest using "user-defined completion" instead. See :help i_ctrl-x_ctrl-u.
    – romainl
    Aug 30, 2015 at 8:53
  • @romainl: You're absolutely right, I'll see how to adapt my answer.
    – statox
    Aug 30, 2015 at 18:29
8

Sometimes you want a custom auto-completion that doesn't interfere with any of the built-in or user-defined autocompletions at all. This is especially useful for scripts or plugins that are intended to work for a wide range of filetypes.

This can be done fairly easy with the complete() function and a simple wrapper; most of the procedure is the same as described in :help complete-functions and Statox's answer, except that you need to define your own mappings and have to call complete() yourself instead of returning a value.

Here is a basic example, the comments should explain how it works.

" Map <C-x><C-m> for our custom completion
inoremap <C-x><C-m> <C-r>=MyComplete()<CR>

" Make subsequent <C-m> presses after <C-x><C-m> go to the next entry (just like
" other <C-x>* mappings)
inoremap <expr> <C-m> pumvisible() ?  "\<C-n>" : "\<C-m>"

" Complete function for addresses; we match the name & address
fun! MyComplete()
    " The data. In this example it's static, but you could read it from a file,
    " get it from a command, etc.
    let l:data = [
        \ ["Elmo the Elk", "[email protected]"],
        \ ["Eek the Cat", "[email protected]"]
    \ ]

    " Locate the start of the word and store the text we want to match in l:base
    let l:line = getline('.')
    let l:start = col('.') - 1
    while l:start > 0 && l:line[l:start - 1] =~ '\a'
        let l:start -= 1
    endwhile
    let l:base = l:line[l:start : col('.')-1]

    " Record what matches − we pass this to complete() later
    let l:res = []

    " Find matches
    for m in l:data
        " Check if it matches what we're trying to complete; in this case we
        " want to match against the start of both the first and second list
        " entries (i.e. the name and email address)
        if l:m[0] !~? '^' . l:base && l:m[1] !~? '^' . l:base
            " Doesn't match
            continue
        endif

        " It matches! See :help complete() for the full docs on the key names
        " for this dict.
        call add(l:res, {
            \ 'icase': 1,
            \ 'word': l:m[0] . ' <' . l:m[1] . '>, ',
            \ 'abbr': l:m[0],
            \ 'menu': l:m[1],
            \ 'info': len(l:m) > 2 ? join(l:m[2:], "\n") : '',
        \ })
    endfor

    " Now call the complete() function
    call complete(l:start + 1, l:res)
    return ''
endfun
1
  • A small but important correction: let l:base = l:line[l:start : col('.')-1] should be let l:base = l:line[l:start : col('.')-2]. If I understand correctly, the -2 is due to two things: (1) 1- to 0-based indexing, and (2) the cursor being to the right of the text to be completed. If only -1 was used, completion would not work as expected if there are characters to the right of the cursor.... In any case, a helpful answer that allowed me to create my own custom completion on two occassions :)
    – husB
    Nov 2, 2022 at 17:41

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.