YouCompleteMe 配合 UltiSnips 补全 C/C++ 函数参数

一直在 Vim 上用 YouCompleteMe 进行 C/C++ 自动补全,一个大的缺陷是不能进行函数参数的补全。后来在 GitHub 上搜索到了这个 issue 中的一个评论,解决了一部分问题,然而仍有一些问题:

  1. 在有些时候选中了结果,但并不希望进行函数参数补全,比如输入 C++ 的 I/O manipulator 的时候。因为 std::endl 之类的 I/O manipulator 实际上是个函数,但 std::ios_baseoperator<< 是接受了一个函数指针作为参数,因此使用的时候只需 std::cout << "xxx" << std::endl 不需要写 std::endl 的参数。而此时如果你用了这个方法,选中了补全结果后再输入任何键它都会进行参数列表的展开。

  2. 在选中补全结果的时候必须使用 Ctrl-Y 进行 snippet 展开。如果直接输入左括号,则会出现多输入一个括号的现象。

  3. 无法正确处理参数是函数指针的函数。

经过我一番摸索,改进了这个方法,并在那个 issue 下面贴了改进后的代码。也在这里贴一下:

function! s:onCompleteDone()
let abbr = v:completed_item.abbr
let startIdx = stridx(abbr,"(")
if startIdx < 0
return abbr
endif
let endIdx = strridx(abbr,")")
if endIdx - startIdx > 1
let argsStr = strpart(abbr, startIdx+1, endIdx - startIdx -1)
"let argsList = split(argsStr, ",")

let argsList = []
let arg = ''
let countParen = 0
for i in range(strlen(argsStr))
if argsStr[i] == ',' && countParen == 0
call add(argsList, arg)
let arg = ''
elseif argsStr[i] == '('
let countParen += 1
let arg = arg . argsStr[i]
elseif argsStr[i] == ')'
let countParen -= 1
let arg = arg . argsStr[i]
else
let arg = arg . argsStr[i]
endif
endfor
if arg != '' && countParen == 0
call add(argsList, arg)
endif
else
let argsList = []
endif

let snippet = '('
let c = 1
for i in argsList
if c > 1
let snippet = snippet . ", "
endif
" strip space
let arg = substitute(i, '^\s*\(.\{-}\)\s*$', '\1', '')
let snippet = snippet . '${' . c . ":" . arg . '}'
let c += 1
endfor
let snippet = snippet . ')' . "$0"
return UltiSnips#Anon(snippet)
endfunction

autocmd VimEnter * imap <expr> (
\ pumvisible() && exists('v:completed_item') && !empty(v:completed_item) &&
\ v:completed_item.word != '' && (v:completed_item.kind == 'f' \|\|
\ v:completed_item.kind == 'm') ?
\ "\<C-R>=\<SID>onCompleteDone()\<CR>" : "<Plug>delimitMate("

使用改进后的方法后,输入左括号时才会进行函数参数的补全,并且部分解决了函数参数是函数指针的问题。不过由于这个问题我并不是依靠 Clang 的语义分析来解决的,而是直接进行左右括号的匹配,一定会存在仍不能解决的情况。不过大部分情况应该都没问题了。另外我使用了 delimitMate,上面这个配置也兼容 delimitMate。

另外再提供两个 Vim 的实用配置。

用 Tab 键进行 delimitMate 的光标跳转(也就是说,输入左括号后使用 Tab 键就可跳转到 delimitMate 生成的右括号的右边,而无需 <S-TAB>),且不破坏 UltiSnips 的 Tab 键展开,同时禁用 delimitMate 自带的 <S-TAB>

autocmd VimEnter * imap <silent> <expr> <TAB> delimitMate#ShouldJump() ? delimitMate#JumpAny() : "\<C-r>=UltiSnips#ExpandSnippetOrJump()\<CR>"
autocmd VimEnter * inoremap <S-TAB> <S-TAB>

让补全下拉菜单支持用回车键选择补全结果,同时不破坏 endwise 的回车键:

autocmd VimEnter * imap <expr> <CR>
\ pumvisible() ?
\ (exists('v:completed_item') && !empty(v:completed_item) &&
\ v:completed_item.word != '' && (v:completed_item.kind == 'f' \|\|
\ v:completed_item.kind == 'm')) ?
\ "\<C-R>=\<SID>onCompleteDone()\<CR>" :
\ "\<C-y>" :
\ "\<Plug>delimitMateCR\<Plug>DiscretionaryEnd"

好了,这样我的 Vim 终于可以像 IDE 一样愉快地进行补全了。