12

I'm trying to enhance an old completion function. I'd like it to update the choices displayed in the popup menu as I type new characters

My completion function is

function! lh#icomplete#ecm(findstart, base) abort
  if a:findstart
    let l = getline('.')
    let startcol = match(l[0:col('.')-1], '\v\S+$')
    if startcol == -1
      let startcol = col('.')-1
    endif
    " let g:debug+= ["findstart(".a:base.") -> ".(startcol)]
    return startcol
  else
    " let g:debug += ["matching(".a:base.")"]
    let words = ['un', 'deux', 'trois', 'trente-deux', 'unité']
    call filter(words, 'v:val =~ a:base')
    " return { 'words' : words}
    return { 'words' : words, 'refresh' : 'always'}
  endif
endfunction

That I use with

:set completefunc=lh#icomplete#ecm
:inoremap µ <c-x><c-u><c-p>

From my understanding of the documentation, the fact I use <c-p>, I go into the third state (according to |ins-completion-menu|), and when I type "any printable, non-white character" I should be able to "Add this character and reduce the number of matches."

When I type in insert mode , the completion menu pops-up as expected. Alas when I type x (just after the µ), I'm out of completion mode and ux is what I get in my buffer.

What did I do wrong or missed in the documentation?

NB: I've seen that without refresh=always, results are filtered, except I'd like to call the function again in order to apply a custom filter.

(Just in case, I'm using gvim 7.4-908)

  • This seems like a bug. It doesn't work for me either (I'm on Vim 7.4.944). – Karl Yngve Lervåg Dec 15 '15 at 7:55
  • Should it work? That's what I don't know. – Luc Hermitte Dec 15 '15 at 8:21
  • The documentation seems to imply that it should work. – Karl Yngve Lervåg Dec 15 '15 at 11:48
3

After further investigations (and some reverse engineering).

I cannot explain why completion doesn't strictly follow the documentation. I'll have to ask on vim_dev I guess.

Anyway, it seems the way to do it consists in registering a listener on CursorMovedI that'll trigger completion again every time a character is inserted.

The difficulty is then to know when to stop.

  • CompletionDone is of no use as it'll be triggered after every key hit.
  • InsertLeave is a good start, but it doesn't cover all cases, i.e.
    • When a character for which there is no more match is typed, we have to stop
    • When the end-user selects one menu-item, we have to stop as well.
      I've found no way other than overriding <cr>, and <c-y>.

Other difficulties consist in detecting when nothing has changed in order to avoid kind of infinite loops and so on.

Any way, here is my current code (that'll be used in other plugins). The last version will be maintained here. It's quite long, but here it is:

" ## Smart completion {{{2
" Function: lh#icomplete#new(startcol, matches, hook) {{{3
function! lh#icomplete#new(startcol, matches, hook) abort
  silent! unlet b:complete_data
  let augroup = 'IComplete'.bufnr('%').'Done'
  let b:complete_data = lh#on#exit()
        \.restore('&completefunc')
        \.restore('&complete')
        \.restore('&omnifunc')
        \.register('au! '.augroup)
        \.register('call self.logger.log("finalized! (".getline(".").")")')
  set complete=
  let b:complete_data.startcol        = a:startcol
  let b:complete_data.all_matches     = map(copy(a:matches), 'type(v:val)==type({}) ? v:val : {"word": v:val}')
  let b:complete_data.matches         = {'words': [], 'refresh': 'always'}
  let b:complete_data.hook            = a:hook
  let b:complete_data.cursor_pos      = []
  let b:complete_data.last_content    = [line('.'), getline('.')]
  let b:complete_data.no_more_matches = 0
  let b:complete_data.logger          = s:logger.reset()

  " keybindings {{{4
  call b:complete_data
        \.restore_buffer_mapping('<cr>', 'i')
        \.restore_buffer_mapping('<c-y>', 'i')
        \.restore_buffer_mapping('<esc>', 'i')
        \.restore_buffer_mapping('<tab>', 'i')
  inoremap <buffer> <silent> <cr>  <c-y><c-\><c-n>:call b:complete_data.conclude()<cr>
  inoremap <buffer> <silent> <c-y> <c-y><c-\><c-n>:call b:complete_data.conclude()<cr>
  " Unlike usual <tab> behaviour, this time, <tab> inserts the next match
  inoremap <buffer> <silent> <tab> <down><c-y><c-\><c-n>:call b:complete_data.conclude()<cr>
  " <c-o><Nop> doesn't work as expected...
  " To stay in INSERT-mode:
  " inoremap <silent> <esc> <c-e><c-o>:<cr>
  " To return into NORMAL-mode:
  inoremap <buffer> <silent> <esc> <c-e><esc>
  " TODO: see to have <Left>, <Right>, <Home>, <End> abort

  " Group {{{4
  exe 'augroup '.augroup
    au!
    " Emulate InsertCharPost
    " au CompleteDone <buffer> call b:complete_data.logger.log("Completion done")
    au InsertLeave  <buffer> call b:complete_data.finalize()
    au CursorMovedI <buffer> call b:complete_data.cursor_moved()
  augroup END

  function! s:cursor_moved() abort dict "{{{4
    if self.no_more_matches
      call self.finalize()
      return
    endif
    if !self.has_text_changed_since_last_move()
      call s:logger.log(lh#fmt#printf("cursor %1 just moved (text hasn't changed)", string(getpos('.'))))
      return
    endif
    call s:logger.log(lh#fmt#printf('cursor moved %1 and text has changed -> relaunch completion', string(getpos('.'))))
    call feedkeys( "\<C-X>\<C-O>\<C-P>\<Down>", 'n' )
  endfunction
  let b:complete_data.cursor_moved = function('s:cursor_moved')

  function! s:has_text_changed_since_last_move() abort dict "{{{4
    let l = line('.')
    let line = getline('.')
    try
      if l != self.last_content[0]  " moved vertically
        let self.no_more_matches = 1
        call s:logger.log("Vertical move => stop")
        return 0
        " We shall leave complete mode now!
      endif
      call s:logger.log(lh#fmt#printf("line was: %1, and becomes: %2; has_changed?%3", self.last_content[1], line, line != self.last_content[1]))
      return line != self.last_content[1] " text changed
    finally
      let self.last_content = [l, line]
    endtry
  endfunction
  let b:complete_data.has_text_changed_since_last_move = function('s:has_text_changed_since_last_move')

  function! s:complete(findstart, base) abort dict "{{{4
    call s:logger.log(lh#fmt#printf('findstart?%1 -> %2', a:findstart, a:base))
    if a:findstart
      if self.no_more_matches
        call s:logger.log("no more matches -> -3")
        return -3
        call self.finalize()
      endif
      if self.cursor_pos == getcurpos()
        call s:logger.log("cursor hasn't moved -> -2")
        return -2
      endif
      let self.cursor_pos = getcurpos()
      return self.startcol
    else
      return self.get_completions(a:base)
    endif
  endfunction
  let b:complete_data.complete = function('s:complete')

  function! s:get_completions(base) abort dict "{{{4
    let matching = filter(copy(self.all_matches), 'v:val.word =~ join(split(a:base, ".\\zs"), ".*")')
    let self.matches.words = matching
    call s:logger.log(lh#fmt#printf("'%1' matches: %2", a:base, string(self.matches)))
    if empty(self.matches.words)
      call s:logger.log("No more matches...")
      let self.no_more_matches = 1
    endif
    return self.matches
  endfunction
  let b:complete_data.get_completions = function('s:get_completions')

  function! s:conclude() abort dict " {{{4
    let selection = getline('.')[self.startcol : col('.')-1]
    call s:logger.log("Successful selection of <".selection.">")
    if !empty(self.hook)
      call lh#function#execute(self.hook, selection)
    endif
    " call self.hook()
    call self.finalize()
  endfunction
  let b:complete_data.conclude = function('s:conclude')

  " Register {{{4
  " call b:complete_data
        " \.restore('b:complete_data')
  " set completefunc=lh#icomplete#func
  set omnifunc=lh#icomplete#func
endfunction

" Function: lh#icomplete#new_on(pattern, matches, hook) {{{3
function! lh#icomplete#new_on(pattern, matches, hook) abort
  let l = getline('.')
  let startcol = match(l[0:col('.')-1], '\v'.a:pattern.'+$')
  if startcol == -1
    let startcol = col('.')-1
  endif
  call lh#icomplete#new(startcol, a:matches, a:hook)
endfunction

" Function: lh#icomplete#func(startcol, base) {{{3
function! lh#icomplete#func(findstart, base) abort
  return b:complete_data.complete(a:findstart, a:base)
endfunction

Which can be used with:

let entries = [
  \ {'word': 'un', 'menu': 1},
  \ {'word': 'deux', 'menu': 2},
  \ {'word': 'trois', 'menu': 3},
  \ {'word': 'trentre-deux', 'menu': 32},
  \ 'unité'
  \ ]
inoremap <silent> <buffer> µ <c-o>:call lh#icomplete#new_on('\w', entries, 'lh#common#warning_msg("nominal: ".v:val)')<cr><c-x><c-O><c-p><down>

You should be able to observe (indirectly) the result applied to C++ snippets selection for my template expander plugin on this screencast.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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