]> git.madduck.net Git - etc/vim.git/blob - autoload/lsp/omni.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 / omni.vim
1 " vint: -ProhibitUnusedVariable
2
3 " constants {{{
4 let s:t_dict = type({})
5
6 let s:default_completion_item_kinds = {
7             \ '1': 'text',
8             \ '2': 'method',
9             \ '3': 'function',
10             \ '4': 'constructor',
11             \ '5': 'field',
12             \ '6': 'variable',
13             \ '7': 'class',
14             \ '8': 'interface',
15             \ '9': 'module',
16             \ '10': 'property',
17             \ '11': 'unit',
18             \ '12': 'value',
19             \ '13': 'enum',
20             \ '14': 'keyword',
21             \ '15': 'snippet',
22             \ '16': 'color',
23             \ '17': 'file',
24             \ '18': 'reference',
25             \ '19': 'folder',
26             \ '20': 'enum member',
27             \ '21': 'constant',
28             \ '22': 'struct',
29             \ '23': 'event',
30             \ '24': 'operator',
31             \ '25': 'type parameter',
32             \ }
33
34 let s:completion_item_kinds = {}
35
36 let s:completion_status_success = 'success'
37 let s:completion_status_failed = 'failed'
38 let s:completion_status_pending = 'pending'
39
40 let s:is_user_data_support = has('patch-8.0.1493')
41 let s:managed_user_data_key_base = 0
42 let s:managed_user_data_map = {}
43
44 " }}}
45
46 " completion state
47 let s:completion = {'counter': 0, 'status': '', 'matches': []}
48
49 function! lsp#omni#complete(findstart, base) abort
50     let l:info = s:find_complete_servers()
51     if empty(l:info['server_names'])
52         return a:findstart ? -1 : []
53     endif
54
55     if a:findstart
56         return col('.')
57     else
58         if !g:lsp_async_completion
59             let s:completion['status'] = s:completion_status_pending
60         endif
61
62         let l:left = strpart(getline('.'), 0, col('.')-1)
63
64         " Initialize the default startcol. It will be updated if the completion items has textEdit.
65         let s:completion['startcol'] = s:get_startcol(l:left, l:info['server_names'])
66
67         " The `l:info` variable will be filled with completion results after request was finished.
68         call s:send_completion_request(l:info)
69
70         if g:lsp_async_completion
71             " If g:lsp_async_completion == v:true, the `s:display_completions` " will be called by `s:send_completion_request`.
72             redraw
73             return exists('v:none') ? v:none : []
74         else
75             " Wait for finished the textDocument/completion request and then call `s:display_completions` explicitly.
76             call lsp#utils#_wait(-1, {-> s:completion['status'] isnot# s:completion_status_pending || complete_check()}, 10)
77             call timer_start(0, { timer -> s:display_completions(timer, l:info) })
78
79             return exists('v:none') ? v:none : []
80         endif
81     endif
82 endfunction
83
84 function! s:get_filter_label(item) abort
85     return lsp#utils#_trim(a:item['word'])
86 endfunction
87
88 function! s:prefix_filter(item, last_typed_word) abort
89     let l:label = s:get_filter_label(a:item)
90
91     if g:lsp_ignorecase
92         return stridx(tolower(l:label), tolower(a:last_typed_word)) == 0
93     else
94         return stridx(l:label, a:last_typed_word) == 0
95     endif
96 endfunction
97
98 function! s:contains_filter(item, last_typed_word) abort
99     let l:label = s:get_filter_label(a:item)
100
101     if g:lsp_ignorecase
102         return stridx(tolower(l:label), tolower(a:last_typed_word)) >= 0
103     else
104         return stridx(l:label, a:last_typed_word) >= 0
105     endif
106 endfunction
107
108 let s:pair = {
109 \  '"':  '"',
110 \  '''':  '''',
111 \  '{':  '}',
112 \  '(':  ')',
113 \  '[':  ']',
114 \}
115
116 function! s:display_completions(timer, info) abort
117     " TODO: Allow multiple servers
118     let l:server_name = a:info['server_names'][0]
119     let l:server_info = lsp#get_server_info(l:server_name)
120
121     let l:current_line = strpart(getline('.'), 0, col('.') - 1)
122     let l:last_typed_word = strpart(l:current_line, s:completion['startcol'] - 1)
123
124     let l:filter = has_key(l:server_info, 'config') && has_key(l:server_info['config'], 'filter') ? l:server_info['config']['filter'] : { 'name': 'prefix' }
125     if l:filter['name'] ==? 'prefix'
126         let s:completion['matches'] = filter(s:completion['matches'], {_, item -> s:prefix_filter(item, l:last_typed_word)})
127         if has_key(s:pair, l:last_typed_word[0])
128             let [l:lhs, l:rhs] = [l:last_typed_word[0], s:pair[l:last_typed_word[0]]]
129             for l:item in s:completion['matches']
130                 let l:str = l:item['word']
131                 if len(l:str) > 1 && l:str[0] ==# l:lhs && l:str[-1:] ==# l:rhs
132                     let l:item['word'] = l:str[:-2]
133                 endif
134             endfor
135         endif
136     elseif l:filter['name'] ==? 'contains'
137         let s:completion['matches'] = filter(s:completion['matches'], {_, item -> s:contains_filter(item, l:last_typed_word)})
138     endif
139
140     let s:completion['status'] = ''
141
142     if mode() is# 'i'
143         call complete(s:completion['startcol'], s:completion['matches'])
144     endif
145 endfunction
146
147 function! s:handle_omnicompletion(server_name, complete_counter, info, data) abort
148     if s:completion['counter'] != a:complete_counter
149         " ignore old completion results
150         return
151     endif
152
153     if lsp#client#is_error(a:data) || !has_key(a:data, 'response') || !has_key(a:data['response'], 'result')
154         let s:completion['status'] = s:completion_status_failed
155         return
156     endif
157
158     let l:result = s:get_completion_result(a:server_name, a:data)
159     let s:completion['matches'] = l:result['items']
160     let s:completion['startcol'] = min([l:result['startcol'], s:completion['startcol']])
161     let s:completion['status'] = s:completion_status_success
162
163     if g:lsp_async_completion
164         call s:display_completions(0, a:info)
165     endif
166 endfunction
167
168 function! lsp#omni#get_kind_text(completion_item, ...) abort
169     let l:server = get(a:, 1, '')
170     if empty(l:server) " server name
171         let l:completion_item_kinds = s:default_completion_item_kinds
172     else
173         if !has_key(s:completion_item_kinds, l:server)
174             let l:server_info = lsp#get_server_info(l:server)
175             if has_key (l:server_info, 'config') && has_key(l:server_info['config'], 'completion_item_kinds')
176                 let s:completion_item_kinds[l:server] = extend(copy(s:default_completion_item_kinds), l:server_info['config']['completion_item_kinds'])
177             else
178                 let s:completion_item_kinds[l:server] = s:default_completion_item_kinds
179             endif
180         endif
181         let l:completion_item_kinds = s:completion_item_kinds[l:server]
182     endif
183
184     return has_key(a:completion_item, 'kind') && has_key(l:completion_item_kinds, a:completion_item['kind'])
185                 \ ? l:completion_item_kinds[a:completion_item['kind']] : ''
186 endfunction
187
188 function! s:get_kind_text_mappings(server) abort
189         let l:server_name = a:server['name']
190         if has_key(s:completion_item_kinds, l:server_name)
191                 return s:completion_item_kinds[l:server_name]
192         else
193                 if has_key(a:server, 'config') && has_key(a:server['config'], 'completion_item_kinds')
194                         let s:completion_item_kinds[l:server_name] = extend(copy(s:default_completion_item_kinds), a:server['config']['completion_item_kinds'])
195                 else
196                         let s:completion_item_kinds[l:server_name] = s:default_completion_item_kinds
197                 endif
198                 return s:completion_item_kinds[l:server_name]
199         endif
200 endfunction
201
202 " auxiliary functions {{{
203
204 function! s:find_complete_servers() abort
205     let l:server_names = []
206     for l:server_name in lsp#get_allowed_servers()
207         if lsp#capabilities#has_completion_provider(l:server_name)
208             " TODO: support triggerCharacters
209             call add(l:server_names, l:server_name)
210         endif
211     endfor
212
213     return { 'server_names': l:server_names }
214 endfunction
215
216 function! s:send_completion_request(info) abort
217     let s:completion['counter'] = s:completion['counter'] + 1
218     let l:server_name = a:info['server_names'][0]
219     " TODO: support multiple servers
220     call lsp#send_request(l:server_name, {
221         \ 'method': 'textDocument/completion',
222         \ 'params': {
223         \   'textDocument': lsp#get_text_document_identifier(),
224         \   'position': lsp#get_position(),
225         \   'context': { 'triggerKind': 1 },
226         \ },
227         \ 'on_notification': function('s:handle_omnicompletion', [l:server_name, s:completion['counter'], a:info]),
228         \ })
229 endfunction
230
231 function! s:get_completion_result(server_name, data) abort
232     let l:result = a:data['response']['result']
233
234     let l:options = {
235         \ 'server': lsp#get_server_info(a:server_name),
236         \ 'position': lsp#get_position(),
237         \ 'response': a:data['response'],
238         \ }
239
240     return lsp#omni#get_vim_completion_items(l:options)
241 endfunction
242
243 function! s:sort_by_sorttext(i1, i2) abort
244     let l:text1 = get(a:i1, 'sortText')
245     let l:text2 = get(a:i2, 'sortText')
246
247     " sortText is possibly empty string
248     let l:text1 = !empty(l:text1) ? l:text1 : a:i1['label']
249     let l:text2 = !empty(l:text2) ? l:text2 : a:i2['label']
250
251     if g:lsp_ignorecase
252         return l:text1 ==? l:text2 ? 0 : l:text1 >? l:text2 ? 1 : -1
253     else
254         return l:text1 ==# l:text2 ? 0 : l:text1 ># l:text2 ? 1 : -1
255     endif
256 endfunction
257
258 " Create vim's completed items from LSP response.
259 "
260 " options = {
261 "   server: {}, " needs to be server_info and not server_name
262 "   position: lsp#get_position(),
263 "   response: {}, " needs to be the entire lsp response. errors need to be
264 "   handled before calling the fuction
265 " }
266 "
267 " * The returned` startcol` may be the same as the cursor position, in which case you need to decide which one to use.
268 "
269 " @return { 'items': v:completed_item[], 'incomplete': v:t_bool, 'startcol': number }
270 "
271 function! lsp#omni#get_vim_completion_items(options) abort
272     let l:server = a:options['server']
273     let l:server_name = l:server['name']
274     let l:kind_text_mappings = s:get_kind_text_mappings(l:server)
275     let l:complete_position = a:options['position']
276     let l:current_line = getline('.')
277     let l:default_startcol = s:get_startcol(strcharpart(l:current_line, 0, l:complete_position['character']), [l:server_name])
278     let l:default_start_character = strchars(strpart(l:current_line, 0, l:default_startcol - 1))
279     let l:refresh_pattern = s:get_refresh_pattern([l:server_name])
280
281     let l:result = a:options['response']['result']
282     if type(l:result) == type([])
283         let l:items = l:result
284         let l:incomplete = 0
285     elseif type(l:result) == type({})
286         let l:items = l:result['items']
287         let l:incomplete = l:result['isIncomplete']
288     else
289         let l:items = []
290         let l:incomplete = 0
291     endif
292
293     let l:sort = has_key(l:server, 'config') && has_key(l:server['config'], 'sort') ? l:server['config']['sort'] : v:null
294
295     if len(l:items) > 0 && type(l:sort) == s:t_dict && len(l:items) <= l:sort['max']
296       " If first item contains sortText, maybe we can use sortText
297       call sort(l:items, function('s:sort_by_sorttext'))
298     endif
299
300     let l:start_character = l:complete_position['character']
301
302     let l:start_characters = [] " The mapping of item specific start_character.
303     let l:vim_complete_items = []
304     for l:completion_item in l:items
305         let l:expandable = get(l:completion_item, 'insertTextFormat', 1) == 2
306         let l:vim_complete_item = {
307             \ 'kind': get(l:kind_text_mappings, get(l:completion_item, 'kind', '') , ''),
308             \ 'dup': 1,
309             \ 'empty': 1,
310             \ 'icase': 1,
311             \ }
312         let l:range = lsp#utils#text_edit#get_range(get(l:completion_item, 'textEdit', {}))
313         let l:complete_word = ''
314         if has_key(l:completion_item, 'textEdit') && type(l:completion_item['textEdit']) == s:t_dict && !empty(l:range) && has_key(l:completion_item['textEdit'], 'newText')
315             let l:text_edit_new_text = l:completion_item['textEdit']['newText']
316             if has_key(l:completion_item, 'filterText') && !empty(l:completion_item['filterText']) && matchstr(l:text_edit_new_text, '^' . l:refresh_pattern) ==# ''
317                 " Use filterText as word.
318                 let l:complete_word = l:completion_item['filterText']
319             else
320                 " Use textEdit.newText as word.
321                 let l:complete_word = l:text_edit_new_text
322             endif
323
324             let l:item_start_character = l:range['start']['character']
325             let l:start_character = min([l:item_start_character, l:start_character])
326             let l:start_characters += [l:item_start_character]
327         elseif has_key(l:completion_item, 'insertText') && !empty(l:completion_item['insertText'])
328             let l:complete_word = l:completion_item['insertText']
329             let l:start_characters += [l:default_start_character]
330         else
331             let l:complete_word = l:completion_item['label']
332             let l:start_characters += [l:default_start_character]
333         endif
334
335         if l:expandable
336             let l:vim_complete_item['word'] = lsp#utils#make_valid_word(substitute(l:complete_word, '\$[0-9]\+\|\${\%(\\.\|[^}]\)\+}', '', 'g'))
337             let l:vim_complete_item['abbr'] = l:completion_item['label'] . '~'
338         else
339             let l:vim_complete_item['word'] = l:complete_word
340             let l:vim_complete_item['abbr'] = l:completion_item['label']
341         endif
342
343         if s:is_user_data_support
344             let l:vim_complete_item['user_data'] = s:create_user_data(
345                 \ l:completion_item,
346                 \ l:server_name,
347                 \ l:complete_position,
348                 \ l:start_characters[-1],
349                 \ l:complete_word)
350         endif
351
352         let l:vim_complete_items += [l:vim_complete_item]
353     endfor
354
355     " Add the additional text for startcol correction.
356     if l:start_character != l:default_start_character
357         for l:i in range(len(l:start_characters))
358             let l:item_start_character = l:start_characters[l:i]
359             if l:start_character < l:item_start_character
360                 let l:item = l:vim_complete_items[l:i]
361                 let l:item['word'] = strcharpart(l:current_line, l:start_character, l:item_start_character - l:start_character) . l:item['word']
362             endif
363         endfor
364     endif
365     let l:startcol = lsp#utils#position#lsp_character_to_vim('%', { 'line': l:complete_position['line'], 'character': l:start_character })
366
367     return { 'items': l:vim_complete_items, 'incomplete': l:incomplete, 'startcol': l:startcol }
368 endfunction
369
370 "
371 " Clear internal user_data map.
372 "
373 " This function should call at `CompleteDone` only if not empty `v:completed_item`.
374 "
375 function! lsp#omni#_clear_managed_user_data_map() abort
376     let s:managed_user_data_key_base = 0
377     let s:managed_user_data_map = {}
378 endfunction
379
380 "
381 " create item's user_data.
382 "
383 function! s:create_user_data(completion_item, server_name, complete_position, start_character, complete_word) abort
384     let l:user_data_key = s:create_user_data_key(s:managed_user_data_key_base)
385     let s:managed_user_data_map[l:user_data_key] = {
386     \   'complete_position': a:complete_position,
387     \   'server_name': a:server_name,
388     \   'completion_item': a:completion_item,
389     \   'start_character': a:start_character,
390     \   'complete_word': a:complete_word,
391     \ }
392     let s:managed_user_data_key_base += 1
393     return l:user_data_key
394 endfunction
395
396 function! lsp#omni#get_managed_user_data_from_completed_item(completed_item) abort
397     " the item has no user_data.
398     if !has_key(a:completed_item, 'user_data')
399         return {}
400     endif
401
402     let l:user_data_string = get(a:completed_item, 'user_data', '')
403     if type(l:user_data_string) != type('')
404         return {}
405     endif
406
407     " Check managed user_data.
408     if has_key(s:managed_user_data_map, l:user_data_string)
409         return s:managed_user_data_map[l:user_data_string]
410     endif
411
412     " Check json.
413     if stridx(l:user_data_string, '"vim-lsp/key"') != -1
414         try
415             let l:user_data = json_decode(l:user_data_string)
416             if has_key(l:user_data, 'vim-lsp/key')
417                 let l:user_data_key = s:create_user_data_key(l:user_data['vim-lsp/key'])
418                 if has_key(s:managed_user_data_map, l:user_data_key)
419                     return s:managed_user_data_map[l:user_data_key]
420                 endif
421             endif
422         catch /.*/
423         endtry
424     endif
425     return {}
426 endfunction
427
428 function! lsp#omni#get_completion_item_kinds() abort
429     return map(keys(s:default_completion_item_kinds), {idx, key -> str2nr(key)})
430 endfunction
431
432 function! s:create_user_data_key(base) abort
433     return '{"vim-lsp/key":"' . a:base . '"}'
434 endfunction
435
436 function! s:get_startcol(left, server_names) abort
437     " Initialize the default startcol. It will be updated if the completion items has textEdit.
438     let l:startcol = 1 + matchstrpos(a:left, s:get_refresh_pattern(a:server_names))[1]
439     return l:startcol == 0 ? strlen(a:left) + 1 : l:startcol
440 endfunction
441
442 function! s:get_refresh_pattern(server_names) abort
443     for l:server_name in a:server_names
444         let l:server_info = lsp#get_server_info(l:server_name)
445         if has_key(l:server_info, 'config') && has_key(l:server_info['config'], 'refresh_pattern')
446             return l:server_info['config']['refresh_pattern']
447         endif
448     endfor
449     return '\(\k\+$\)'
450 endfunction
451
452 " }}}