]> git.madduck.net Git - etc/vim.git/blob - autoload/lsp/ui/vim/completion.vim

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

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.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Squashed '.vim/bundle/vim-lsp/' content from commit 04428c92
[etc/vim.git] / autoload / lsp / ui / vim / completion.vim
1 " vint: -ProhibitUnusedVariable
2 "
3 let s:context = {}
4
5 function! lsp#ui#vim#completion#_setup() abort
6   augroup lsp_ui_vim_completion
7     autocmd!
8     autocmd CompleteDone * call s:on_complete_done()
9   augroup END
10 endfunction
11
12 function! lsp#ui#vim#completion#_disable() abort
13   augroup lsp_ui_vim_completion
14     autocmd!
15   augroup END
16 endfunction
17
18 "
19 " After CompleteDone, v:complete_item's word has been inserted into the line.
20 " Yet not inserted commit characters.
21 "
22 " below example uses | as cursor position.
23 "
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`
30 "
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
36     return
37   endif
38
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)
41
42   " Clear managed user_data.
43   call lsp#omni#_clear_managed_user_data_map()
44
45   " If managed user_data does not exists, skip it.
46   if empty(l:managed_user_data)
47     doautocmd <nomodeline> User lsp_complete_done
48     return
49   endif
50
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')
60 endfunction
61
62 "
63 " Apply textEdit or insertText(snippet) and additionalTextEdits.
64 "
65 function! s:on_complete_done_after() abort
66   " Clear message line. feedkeys above leave garbage on message line.
67   echo ''
68
69   " Ignore process if the mode() is not insert-mode after feedkeys.
70   if mode(1)[0] !=# 'i'
71     return ''
72   endif
73
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']
82
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
86     return ''
87   endif
88
89   " Do nothing if text_edit is disabled.
90   if !g:lsp_text_edit_enabled
91     doautocmd <nomodeline> User lsp_complete_done
92     return ''
93   endif
94
95   let l:completion_item = s:resolve_completion_item(l:completion_item, l:server_name)
96
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)
99   if l:is_expandable
100     call s:clear_auto_inserted_text(l:done_line, l:done_position, l:complete_position)
101   endif
102
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'])
106   endif
107
108   " snippet or textEdit.
109   if l:is_expandable
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']
116     else
117       let l:overflow_before = 0
118       let l:overflow_after = 0
119       let l:text = s:get_completion_text(l:completion_item)
120     endif
121
122     " apply snipept or text_edit
123     let l:position = lsp#utils#position#vim_to_lsp('%', getpos('.')[1 : 2])
124     let l:range = {
125     \   'start': {
126     \     'line': l:position['line'],
127     \     'character': l:position['character'] - (l:complete_position['character'] - l:start_character) - l:overflow_before,
128     \   },
129     \   'end': {
130     \     'line': l:position['line'],
131     \     'character': l:position['character'] + l:overflow_after,
132     \   }
133     \ }
134
135     if get(l:completion_item, 'insertTextFormat', 1) == 2
136       " insert Snippet.
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 })
141       else
142         call s:simple_expand_text(l:text)
143       endif
144     else
145       " apply TextEdit.
146       call lsp#utils#text_edit#apply_text_edits('%', [{ 'range': l:range, 'newText': l:text }])
147
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))
155     endif
156   endif
157
158   doautocmd <nomodeline> User lsp_complete_done
159   return ''
160 endfunction
161
162 "
163 " is_expandable
164 "
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']
169       return v:true
170     endif
171
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
179   endif
180   return s:get_completion_text(a:completion_item) !=# s:trim_unmeaning_tabstop(a:complete_word)
181 endfunction
182
183 "
184 " trim_unmeaning_tabstop
185 "
186 function! s:trim_unmeaning_tabstop(text) abort
187   return substitute(a:text, '\%(\$0\|\${0}\)$', '', 'g')
188 endfunction
189
190 "
191 " Try `completionItem/resolve` if it possible.
192 "
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
197   endif
198
199   " check server capabilities.
200   if !lsp#capabilities#has_completion_resolve_provider(a:server_name)
201     return a:completion_item
202   endif
203
204   let l:ctx = {}
205   let l:ctx['response'] = {}
206   function! l:ctx['callback'](data) abort
207     let l:self['response'] = a:data['response']
208   endfunction
209
210   try
211     call lsp#send_request(a:server_name, {
212           \   'method': 'completionItem/resolve',
213           \   'params': a:completion_item,
214           \   'sync': 1,
215           \   'sync_timeout': g:lsp_completion_resolve_timeout,
216           \   'on_notification': function(l:ctx['callback'], [], l:ctx)
217           \ })
218   catch /.*/
219     call lsp#log('s:resolve_completion_item', 'request timeout.')
220   endtry
221
222   if empty(l:ctx['response'])
223     return a:completion_item
224   endif
225
226   if lsp#client#is_error(l:ctx['response'])
227     return a:completion_item
228   endif
229
230   if empty(l:ctx['response']['result'])
231     return a:completion_item
232   endif
233
234   return l:ctx['response']['result']
235 endfunction
236
237 "
238 " Remove additional inserted text
239 "
240 " LSP server knows only `complete_position` so we should remove inserted text until complete_position.
241 "
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])
247 endfunction
248
249 "
250 " Expand text
251 "
252 function! s:simple_expand_text(text) abort
253   let l:pos = {
254         \   'line': line('.') - 1,
255         \   'character': lsp#utils#to_char('%', line('.'), col('.'))
256         \ }
257
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]\+\)')
262   if l:offset == -1
263     let l:offset = strchars(l:text)
264   endif
265
266   call lsp#utils#text_edit#apply_text_edits(lsp#utils#get_buffer_uri(bufnr('%')), [{
267         \   'range': {
268         \     'start': l:pos,
269         \     'end': l:pos
270         \   },
271         \   'newText': l:text
272         \ }])
273
274   let l:pos = lsp#utils#position#lsp_to_vim('%', {
275         \   'line': l:pos['line'],
276         \   'character': l:pos['character'] + l:offset
277         \ })
278   call cursor(l:pos)
279 endfunction
280
281 "
282 " Get completion text from CompletionItem. Fallback to label when insertText
283 " is falsy
284 "
285 function! s:get_completion_text(completion_item) abort
286   let l:text = get(a:completion_item, 'insertText', '')
287   if empty(l:text)
288     let l:text = a:completion_item['label']
289   endif
290   return l:text
291 endfunction
292
293 "
294 " Get script id that uses to call `s:` function in feedkeys.
295 "
296 function! s:SID() abort
297   return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze_SID$')
298 endfunction
299