3

I've been trying to write a simple complete function for Vim that tries multiple values for the base argument.

Here is a version that works but which I find unsatisfactory:

function! CompleteFunc(findstart, base)
  if a:findstart
    let start = AmbitiousFindStart()
    " -1 to exclude cursor, -1 again to convert column to index
    let fake_base = getline('.')[start:col('.')-2]
    if empty(GetCompletions(fake_base))
      let start = FallbackFindStart()
    endif
    return start
  else
    return GetCompletions(a:base)
  endif
endfunction
set completefunc=CompleteFunc

Specifically in my case, AmbitiousFindStart() tries to set start so as to consume all non-whitespace characters, but if this fails, I want to use FallbackFindStart(), which only tries to consume word characters.

Here are the problems I see with this approach:

  1. Even in the case when AmbitiousFindStart() succeeds (i.e. produces matches when calling GetCompletions(fake_base)), GetCompletions() is called twice.
  2. The fake_base approach seems inelegant to me; Vim is the one who is suppose to provide me with base, rather than me going out of my way to try to calculate what base would be. Additionally I'm not even sure if my base calculation is correct (it seems to work in the cases I've tried, but there might be problems with e.g. multi-byte characters). I've therefore called it the "fake" base to distinguish it from the "real" base that Vim would provide.

My first approach to fix problem (1) was the following, which does not work:

function! BadCompleteFunc(findstart, base)
  if a:findstart
    let start = AmbitiousFindStart()
    return start
  else
    let compl_lst = GetCompletions(a:base)
    if empty(compl_lst)
      let fake_base = getline('.')[start:col('.')-2]
      let compl_lst = GetCompletions(fake_base)
    endif
    return compl_lst
  endif
endfunction

The problem with this is that Vim uses the start value returned from the first time BadCompleteFunc() is called to decide what to replace the completion with, rather than the string value of base.

The second approach I came up with is to use complete(), which has problems of its own:

function! Complete()
  let startidx = AmbitiousFindStart()
  let startcol = startidx + 1
  let currcol = col('.')
  let curridx = currcol - 1
  let fake_base = getline('.')[startidx:curridx-1]
  let matches = GetCompletions(fake_base)
  if !empty(matches)
    call complete(startcol, matches)
  else
    let startidx = FallbackFindStart()
    let startcol = startidx + 1
    let fake_base = getline('.')[startidx:curridx-1]
    let matches = GetCompletions(fake_base)
    call complete(startcol, matches)
  endif
  return ''
endfunction

The problem here is that this assumes a usage through a custom insert mode mapping, such as inoremap <C-X><C-U> <C-R>=Complete()<CR>, rather than being able to set completefunc.

What is the right way to approach this sort of complete function?

1

'completefunc' assumes that you can determine the base just by looking at the text before the cursor; then, the call sequence is just fine. In your case, you want a fallback with a different base in case the original base does not yield any completion candidates. For that, you have to call GetCompletions() to find out. The only way you could optimize this is storing the result of that first call in a script-local variable, and reusing that when your completion function is invoked the second time around (with findstart = 0).

Another way I've solved a similar problem is triggering the fallback only on a second invocation at the same cursor position. So, the user invokes completion, either gets a list of matches or a "no matches" error, and then retriggers the completion. It's both more work, but also offers more control to the user.

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.