X-Git-Url: https://git.madduck.net/etc/vim.git/blobdiff_plain/5a4872f466ebd76ddd532bdf2798554421c53df4..fe3919e725e156d751069662d11e38f7b4791de1:/.vim/bundle/vim-lsp/autoload/lsp/ui/vim/completion.vim diff --git a/.vim/bundle/vim-lsp/autoload/lsp/ui/vim/completion.vim b/.vim/bundle/vim-lsp/autoload/lsp/ui/vim/completion.vim new file mode 100644 index 00000000..5989ba0e --- /dev/null +++ b/.vim/bundle/vim-lsp/autoload/lsp/ui/vim/completion.vim @@ -0,0 +1,299 @@ +" vint: -ProhibitUnusedVariable +" +let s:context = {} + +function! lsp#ui#vim#completion#_setup() abort + augroup lsp_ui_vim_completion + autocmd! + autocmd CompleteDone * call s:on_complete_done() + augroup END +endfunction + +function! lsp#ui#vim#completion#_disable() abort + augroup lsp_ui_vim_completion + autocmd! + augroup END +endfunction + +" +" After CompleteDone, v:complete_item's word has been inserted into the line. +" Yet not inserted commit characters. +" +" below example uses | as cursor position. +" +" 1. `call getbuf|` +" 2. select `getbufline` item. +" 3. Insert commit characters. e.g. `(` +" 4. fire CompleteDone, then the line is `call getbufline|` +" 5. call feedkeys to call `s:on_complete_done_after` +" 6. then the line is `call getbufline(|` in `s:on_complete_done_after` +" +function! s:on_complete_done() abort + " Sometimes, vim occurs `CompleteDone` unexpectedly. + " We try to detect it by checking empty completed_item. + if empty(v:completed_item) || get(v:completed_item, 'word', '') ==# '' && get(v:completed_item, 'abbr', '') ==# '' + doautocmd User lsp_complete_done + return + endif + + " Try to get managed user_data. + let l:managed_user_data = lsp#omni#get_managed_user_data_from_completed_item(v:completed_item) + + " Clear managed user_data. + call lsp#omni#_clear_managed_user_data_map() + + " If managed user_data does not exists, skip it. + if empty(l:managed_user_data) + doautocmd User lsp_complete_done + return + endif + + let s:context['done_line'] = getline('.') + let s:context['done_line_nr'] = line('.') + let s:context['done_position'] = lsp#utils#position#vim_to_lsp('%', getpos('.')[1 : 2]) + let s:context['complete_position'] = l:managed_user_data['complete_position'] + let s:context['server_name'] = l:managed_user_data['server_name'] + let s:context['completion_item'] = l:managed_user_data['completion_item'] + let s:context['start_character'] = l:managed_user_data['start_character'] + let s:context['complete_word'] = l:managed_user_data['complete_word'] + call feedkeys(printf("\=%d_on_complete_done_after()\", s:SID()), 'n') +endfunction + +" +" Apply textEdit or insertText(snippet) and additionalTextEdits. +" +function! s:on_complete_done_after() abort + " Clear message line. feedkeys above leave garbage on message line. + echo '' + + " Ignore process if the mode() is not insert-mode after feedkeys. + if mode(1)[0] !=# 'i' + return '' + endif + + let l:done_line = s:context['done_line'] + let l:done_line_nr = s:context['done_line_nr'] + let l:done_position = s:context['done_position'] + let l:complete_position = s:context['complete_position'] + let l:server_name = s:context['server_name'] + let l:completion_item = s:context['completion_item'] + let l:start_character = s:context['start_character'] + let l:complete_word = s:context['complete_word'] + + " check the commit characters are or . + if line('.') ==# l:done_line_nr && strlen(getline('.')) < strlen(l:done_line) + doautocmd User lsp_complete_done + return '' + endif + + " Do nothing if text_edit is disabled. + if !g:lsp_text_edit_enabled + doautocmd User lsp_complete_done + return '' + endif + + let l:completion_item = s:resolve_completion_item(l:completion_item, l:server_name) + + " clear completed string if need. + let l:is_expandable = s:is_expandable(l:done_line, l:done_position, l:complete_position, l:completion_item, l:complete_word) + if l:is_expandable + call s:clear_auto_inserted_text(l:done_line, l:done_position, l:complete_position) + endif + + " apply additionalTextEdits. + if has_key(l:completion_item, 'additionalTextEdits') && !empty(l:completion_item['additionalTextEdits']) + call lsp#utils#text_edit#apply_text_edits(lsp#utils#get_buffer_uri(bufnr('%')), l:completion_item['additionalTextEdits']) + endif + + " snippet or textEdit. + if l:is_expandable + " At this timing, the cursor may have been moved by additionalTextEdit, so we use overflow information instead of textEdit itself. + if type(get(l:completion_item, 'textEdit', v:null)) == type({}) + let l:range = lsp#utils#text_edit#get_range(l:completion_item['textEdit']) + let l:overflow_before = max([0, l:start_character - l:range['start']['character']]) + let l:overflow_after = max([0, l:range['end']['character'] - l:complete_position['character']]) + let l:text = l:completion_item['textEdit']['newText'] + else + let l:overflow_before = 0 + let l:overflow_after = 0 + let l:text = s:get_completion_text(l:completion_item) + endif + + " apply snipept or text_edit + let l:position = lsp#utils#position#vim_to_lsp('%', getpos('.')[1 : 2]) + let l:range = { + \ 'start': { + \ 'line': l:position['line'], + \ 'character': l:position['character'] - (l:complete_position['character'] - l:start_character) - l:overflow_before, + \ }, + \ 'end': { + \ 'line': l:position['line'], + \ 'character': l:position['character'] + l:overflow_after, + \ } + \ } + + if get(l:completion_item, 'insertTextFormat', 1) == 2 + " insert Snippet. + call lsp#utils#text_edit#apply_text_edits('%', [{ 'range': l:range, 'newText': '' }]) + call cursor(lsp#utils#position#lsp_to_vim('%', l:range['start'])) + if exists('g:lsp_snippet_expand') && len(g:lsp_snippet_expand) > 0 + call g:lsp_snippet_expand[0]({ 'snippet': l:text }) + else + call s:simple_expand_text(l:text) + endif + else + " apply TextEdit. + call lsp#utils#text_edit#apply_text_edits('%', [{ 'range': l:range, 'newText': l:text }]) + + " The VSCode always apply completion word as snippet. + " It means we should place cursor to end of new inserted text as snippet does. + let l:lines = lsp#utils#_split_by_eol(l:text) + let l:start = l:range.start + let l:start.line += len(l:lines) - 1 + let l:start.character += strchars(l:lines[-1]) + call cursor(lsp#utils#position#lsp_to_vim('%', l:start)) + endif + endif + + doautocmd User lsp_complete_done + return '' +endfunction + +" +" is_expandable +" +function! s:is_expandable(done_line, done_position, complete_position, completion_item, complete_word) abort + if get(a:completion_item, 'textEdit', v:null) isnot# v:null + let l:range = lsp#utils#text_edit#get_range(a:completion_item['textEdit']) + if l:range['start']['line'] != l:range['end']['line'] + return v:true + endif + + " compute if textEdit will change text. + let l:completed_before = strcharpart(a:done_line, 0, a:complete_position['character']) + let l:completed_after = strcharpart(a:done_line, a:done_position['character'], strchars(a:done_line) - a:done_position['character']) + let l:completed_line = l:completed_before . l:completed_after + let l:text_edit_before = strcharpart(l:completed_line, 0, l:range['start']['character']) + let l:text_edit_after = strcharpart(l:completed_line, l:range['end']['character'], strchars(l:completed_line) - l:range['end']['character']) + return a:done_line !=# l:text_edit_before . s:trim_unmeaning_tabstop(a:completion_item['textEdit']['newText']) . l:text_edit_after + endif + return s:get_completion_text(a:completion_item) !=# s:trim_unmeaning_tabstop(a:complete_word) +endfunction + +" +" trim_unmeaning_tabstop +" +function! s:trim_unmeaning_tabstop(text) abort + return substitute(a:text, '\%(\$0\|\${0}\)$', '', 'g') +endfunction + +" +" Try `completionItem/resolve` if it possible. +" +function! s:resolve_completion_item(completion_item, server_name) abort + " server_name is not provided. + if empty(a:server_name) + return a:completion_item + endif + + " check server capabilities. + if !lsp#capabilities#has_completion_resolve_provider(a:server_name) + return a:completion_item + endif + + let l:ctx = {} + let l:ctx['response'] = {} + function! l:ctx['callback'](data) abort + let l:self['response'] = a:data['response'] + endfunction + + try + call lsp#send_request(a:server_name, { + \ 'method': 'completionItem/resolve', + \ 'params': a:completion_item, + \ 'sync': 1, + \ 'sync_timeout': g:lsp_completion_resolve_timeout, + \ 'on_notification': function(l:ctx['callback'], [], l:ctx) + \ }) + catch /.*/ + call lsp#log('s:resolve_completion_item', 'request timeout.') + endtry + + if empty(l:ctx['response']) + return a:completion_item + endif + + if lsp#client#is_error(l:ctx['response']) + return a:completion_item + endif + + if empty(l:ctx['response']['result']) + return a:completion_item + endif + + return l:ctx['response']['result'] +endfunction + +" +" Remove additional inserted text +" +" LSP server knows only `complete_position` so we should remove inserted text until complete_position. +" +function! s:clear_auto_inserted_text(done_line, done_position, complete_position) abort + let l:before = strcharpart(a:done_line, 0, a:complete_position['character']) + let l:after = strcharpart(a:done_line, a:done_position['character'], (strchars(a:done_line) - a:done_position['character'])) + call setline(a:done_position['line'] + 1, l:before . l:after) + call cursor([a:done_position['line'] + 1, strlen(l:before) + 1]) +endfunction + +" +" Expand text +" +function! s:simple_expand_text(text) abort + let l:pos = { + \ 'line': line('.') - 1, + \ 'character': lsp#utils#to_char('%', line('.'), col('.')) + \ } + + " Remove placeholders and get first placeholder position that use to cursor position. + " e.g. `|getbufline(${1:expr}, ${2:lnum})${0}` to getbufline(|,) + let l:text = substitute(a:text, '\$\%({[0-9]\+\%(:\(\\.\|[^}]\+\)*\)}\|[0-9]\+\)', '\=substitute(submatch(1), "\\", "", "g")', 'g') + let l:offset = match(a:text, '\$\%({[0-9]\+\%(:\(\\.\|[^}]\+\)*\)}\|[0-9]\+\)') + if l:offset == -1 + let l:offset = strchars(l:text) + endif + + call lsp#utils#text_edit#apply_text_edits(lsp#utils#get_buffer_uri(bufnr('%')), [{ + \ 'range': { + \ 'start': l:pos, + \ 'end': l:pos + \ }, + \ 'newText': l:text + \ }]) + + let l:pos = lsp#utils#position#lsp_to_vim('%', { + \ 'line': l:pos['line'], + \ 'character': l:pos['character'] + l:offset + \ }) + call cursor(l:pos) +endfunction + +" +" Get completion text from CompletionItem. Fallback to label when insertText +" is falsy +" +function! s:get_completion_text(completion_item) abort + let l:text = get(a:completion_item, 'insertText', '') + if empty(l:text) + let l:text = a:completion_item['label'] + endif + return l:text +endfunction + +" +" Get script id that uses to call `s:` function in feedkeys. +" +function! s:SID() abort + return matchstr(expand(''), '\zs\d\+\ze_SID$') +endfunction +