All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
1 " vint: -ProhibitUnusedVariable
5 function! lsp#ui#vim#completion#_setup() abort
6 augroup lsp_ui_vim_completion
8 autocmd CompleteDone * call s:on_complete_done()
12 function! lsp#ui#vim#completion#_disable() abort
13 augroup lsp_ui_vim_completion
19 " After CompleteDone, v:complete_item's word has been inserted into the line.
20 " Yet not inserted commit characters.
22 " below example uses | as cursor position.
24 " 1. `call getbuf|`<C-x><C-o>
25 " 2. select `getbufline` item.
26 " 3. Insert commit characters. e.g. `(`
27 " 4. fire CompleteDone, then the line is `call getbufline|`
28 " 5. call feedkeys to call `s:on_complete_done_after`
29 " 6. then the line is `call getbufline(|` in `s:on_complete_done_after`
31 function! s:on_complete_done() abort
32 " Sometimes, vim occurs `CompleteDone` unexpectedly.
33 " We try to detect it by checking empty completed_item.
34 if empty(v:completed_item) || get(v:completed_item, 'word', '') ==# '' && get(v:completed_item, 'abbr', '') ==# ''
35 doautocmd <nomodeline> User lsp_complete_done
39 " Try to get managed user_data.
40 let l:managed_user_data = lsp#omni#get_managed_user_data_from_completed_item(v:completed_item)
42 " Clear managed user_data.
43 call lsp#omni#_clear_managed_user_data_map()
45 " If managed user_data does not exists, skip it.
46 if empty(l:managed_user_data)
47 doautocmd <nomodeline> User lsp_complete_done
51 let s:context['done_line'] = getline('.')
52 let s:context['done_line_nr'] = line('.')
53 let s:context['done_position'] = lsp#utils#position#vim_to_lsp('%', getpos('.')[1 : 2])
54 let s:context['complete_position'] = l:managed_user_data['complete_position']
55 let s:context['server_name'] = l:managed_user_data['server_name']
56 let s:context['completion_item'] = l:managed_user_data['completion_item']
57 let s:context['start_character'] = l:managed_user_data['start_character']
58 let s:context['complete_word'] = l:managed_user_data['complete_word']
59 call feedkeys(printf("\<C-r>=<SNR>%d_on_complete_done_after()\<CR>", s:SID()), 'n')
63 " Apply textEdit or insertText(snippet) and additionalTextEdits.
65 function! s:on_complete_done_after() abort
66 " Clear message line. feedkeys above leave garbage on message line.
69 " Ignore process if the mode() is not insert-mode after feedkeys.
74 let l:done_line = s:context['done_line']
75 let l:done_line_nr = s:context['done_line_nr']
76 let l:done_position = s:context['done_position']
77 let l:complete_position = s:context['complete_position']
78 let l:server_name = s:context['server_name']
79 let l:completion_item = s:context['completion_item']
80 let l:start_character = s:context['start_character']
81 let l:complete_word = s:context['complete_word']
83 " check the commit characters are <BS> or <C-w>.
84 if line('.') ==# l:done_line_nr && strlen(getline('.')) < strlen(l:done_line)
85 doautocmd <nomodeline> User lsp_complete_done
89 " Do nothing if text_edit is disabled.
90 if !g:lsp_text_edit_enabled
91 doautocmd <nomodeline> User lsp_complete_done
95 let l:completion_item = s:resolve_completion_item(l:completion_item, l:server_name)
97 " clear completed string if need.
98 let l:is_expandable = s:is_expandable(l:done_line, l:done_position, l:complete_position, l:completion_item, l:complete_word)
100 call s:clear_auto_inserted_text(l:done_line, l:done_position, l:complete_position)
103 " apply additionalTextEdits.
104 if has_key(l:completion_item, 'additionalTextEdits') && !empty(l:completion_item['additionalTextEdits'])
105 call lsp#utils#text_edit#apply_text_edits(lsp#utils#get_buffer_uri(bufnr('%')), l:completion_item['additionalTextEdits'])
108 " snippet or textEdit.
110 " At this timing, the cursor may have been moved by additionalTextEdit, so we use overflow information instead of textEdit itself.
111 if type(get(l:completion_item, 'textEdit', v:null)) == type({})
112 let l:range = lsp#utils#text_edit#get_range(l:completion_item['textEdit'])
113 let l:overflow_before = max([0, l:start_character - l:range['start']['character']])
114 let l:overflow_after = max([0, l:range['end']['character'] - l:complete_position['character']])
115 let l:text = l:completion_item['textEdit']['newText']
117 let l:overflow_before = 0
118 let l:overflow_after = 0
119 let l:text = s:get_completion_text(l:completion_item)
122 " apply snipept or text_edit
123 let l:position = lsp#utils#position#vim_to_lsp('%', getpos('.')[1 : 2])
126 \ 'line': l:position['line'],
127 \ 'character': l:position['character'] - (l:complete_position['character'] - l:start_character) - l:overflow_before,
130 \ 'line': l:position['line'],
131 \ 'character': l:position['character'] + l:overflow_after,
135 if get(l:completion_item, 'insertTextFormat', 1) == 2
137 call lsp#utils#text_edit#apply_text_edits('%', [{ 'range': l:range, 'newText': '' }])
138 call cursor(lsp#utils#position#lsp_to_vim('%', l:range['start']))
139 if exists('g:lsp_snippet_expand') && len(g:lsp_snippet_expand) > 0
140 call g:lsp_snippet_expand[0]({ 'snippet': l:text })
142 call s:simple_expand_text(l:text)
146 call lsp#utils#text_edit#apply_text_edits('%', [{ 'range': l:range, 'newText': l:text }])
148 " The VSCode always apply completion word as snippet.
149 " It means we should place cursor to end of new inserted text as snippet does.
150 let l:lines = lsp#utils#_split_by_eol(l:text)
151 let l:start = l:range.start
152 let l:start.line += len(l:lines) - 1
153 let l:start.character += strchars(l:lines[-1])
154 call cursor(lsp#utils#position#lsp_to_vim('%', l:start))
158 doautocmd <nomodeline> User lsp_complete_done
165 function! s:is_expandable(done_line, done_position, complete_position, completion_item, complete_word) abort
166 if get(a:completion_item, 'textEdit', v:null) isnot# v:null
167 let l:range = lsp#utils#text_edit#get_range(a:completion_item['textEdit'])
168 if l:range['start']['line'] != l:range['end']['line']
172 " compute if textEdit will change text.
173 let l:completed_before = strcharpart(a:done_line, 0, a:complete_position['character'])
174 let l:completed_after = strcharpart(a:done_line, a:done_position['character'], strchars(a:done_line) - a:done_position['character'])
175 let l:completed_line = l:completed_before . l:completed_after
176 let l:text_edit_before = strcharpart(l:completed_line, 0, l:range['start']['character'])
177 let l:text_edit_after = strcharpart(l:completed_line, l:range['end']['character'], strchars(l:completed_line) - l:range['end']['character'])
178 return a:done_line !=# l:text_edit_before . s:trim_unmeaning_tabstop(a:completion_item['textEdit']['newText']) . l:text_edit_after
180 return s:get_completion_text(a:completion_item) !=# s:trim_unmeaning_tabstop(a:complete_word)
184 " trim_unmeaning_tabstop
186 function! s:trim_unmeaning_tabstop(text) abort
187 return substitute(a:text, '\%(\$0\|\${0}\)$', '', 'g')
191 " Try `completionItem/resolve` if it possible.
193 function! s:resolve_completion_item(completion_item, server_name) abort
194 " server_name is not provided.
195 if empty(a:server_name)
196 return a:completion_item
199 " check server capabilities.
200 if !lsp#capabilities#has_completion_resolve_provider(a:server_name)
201 return a:completion_item
205 let l:ctx['response'] = {}
206 function! l:ctx['callback'](data) abort
207 let l:self['response'] = a:data['response']
211 call lsp#send_request(a:server_name, {
212 \ 'method': 'completionItem/resolve',
213 \ 'params': a:completion_item,
215 \ 'sync_timeout': g:lsp_completion_resolve_timeout,
216 \ 'on_notification': function(l:ctx['callback'], [], l:ctx)
219 call lsp#log('s:resolve_completion_item', 'request timeout.')
222 if empty(l:ctx['response'])
223 return a:completion_item
226 if lsp#client#is_error(l:ctx['response'])
227 return a:completion_item
230 if empty(l:ctx['response']['result'])
231 return a:completion_item
234 return l:ctx['response']['result']
238 " Remove additional inserted text
240 " LSP server knows only `complete_position` so we should remove inserted text until complete_position.
242 function! s:clear_auto_inserted_text(done_line, done_position, complete_position) abort
243 let l:before = strcharpart(a:done_line, 0, a:complete_position['character'])
244 let l:after = strcharpart(a:done_line, a:done_position['character'], (strchars(a:done_line) - a:done_position['character']))
245 call setline(a:done_position['line'] + 1, l:before . l:after)
246 call cursor([a:done_position['line'] + 1, strlen(l:before) + 1])
252 function! s:simple_expand_text(text) abort
254 \ 'line': line('.') - 1,
255 \ 'character': lsp#utils#to_char('%', line('.'), col('.'))
258 " Remove placeholders and get first placeholder position that use to cursor position.
259 " e.g. `|getbufline(${1:expr}, ${2:lnum})${0}` to getbufline(|,)
260 let l:text = substitute(a:text, '\$\%({[0-9]\+\%(:\(\\.\|[^}]\+\)*\)}\|[0-9]\+\)', '\=substitute(submatch(1), "\\", "", "g")', 'g')
261 let l:offset = match(a:text, '\$\%({[0-9]\+\%(:\(\\.\|[^}]\+\)*\)}\|[0-9]\+\)')
263 let l:offset = strchars(l:text)
266 call lsp#utils#text_edit#apply_text_edits(lsp#utils#get_buffer_uri(bufnr('%')), [{
274 let l:pos = lsp#utils#position#lsp_to_vim('%', {
275 \ 'line': l:pos['line'],
276 \ 'character': l:pos['character'] + l:offset
282 " Get completion text from CompletionItem. Fallback to label when insertText
285 function! s:get_completion_text(completion_item) abort
286 let l:text = get(a:completion_item, 'insertText', '')
288 let l:text = a:completion_item['label']
294 " Get script id that uses to call `s:` function in feedkeys.
296 function! s:SID() abort
297 return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze_SID$')