]> git.madduck.net Git - etc/vim.git/blob - autoload/asyncomplete.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/asyncomplete/' content from commit 016590d2
[etc/vim.git] / autoload / asyncomplete.vim
1 function! asyncomplete#log(...) abort
2     if !empty(g:asyncomplete_log_file)
3         call writefile([json_encode(a:000)], g:asyncomplete_log_file, 'a')
4     endif
5 endfunction
6
7 " do nothing, place it here only to avoid the message
8 augroup asyncomplete_silence_messages
9     au!
10     autocmd User asyncomplete_setup silent
11 augroup END
12
13 if !has('timers')
14     echohl ErrorMsg
15     echomsg 'Vim/Neovim compiled with timers required for asyncomplete.vim.'
16     echohl NONE
17     if has('nvim')
18         call asyncomplete#log('neovim compiled with timers required.')
19     else
20         call asyncomplete#log('vim compiled with timers required.')
21     endif
22     " Clear augroup so this message is only displayed once.
23     au! asyncomplete_enable *
24     finish
25 endif
26
27 let s:already_setup = 0
28 let s:sources = {}
29 let s:matches = {} " { server_name: { incomplete: 1, startcol: 0, items: [], refresh: 0, status: 'idle|pending|success|failure', ctx: ctx } }
30 let s:has_complete_info = exists('*complete_info')
31 let s:has_matchfuzzypos = exists('*matchfuzzypos')
32
33 function! s:setup_if_required() abort
34     if !s:already_setup
35         " register asyncomplete change manager
36         for l:change_manager in g:asyncomplete_change_manager
37             call asyncomplete#log('core', 'initializing asyncomplete change manager', l:change_manager)
38             if type(l:change_manager) == type('')
39                 execute 'let s:on_change_manager = function("'. l:change_manager  .'")()'
40             else
41                 let s:on_change_manager = l:change_manager()
42             endif
43             if has_key(s:on_change_manager, 'error')
44                 call asyncomplete#log('core', 'initializing asyncomplete change manager failed', s:on_change_manager['name'], s:on_change_manager['error'])
45             else
46                 call s:on_change_manager.register(function('s:on_change'))
47                 call asyncomplete#log('core', 'initializing asyncomplete change manager complete', s:on_change_manager['name'])
48                 break
49             endif
50         endfor
51
52         augroup asyncomplete
53             autocmd!
54             autocmd InsertEnter * call s:on_insert_enter()
55             autocmd InsertLeave * call s:on_insert_leave()
56         augroup END
57
58         doautocmd <nomodeline> User asyncomplete_setup
59         let s:already_setup = 1
60     endif
61 endfunction
62
63 function! asyncomplete#enable_for_buffer() abort
64     call s:setup_if_required()
65     let b:asyncomplete_enable = 1
66 endfunction
67
68 function! asyncomplete#disable_for_buffer() abort
69     let b:asyncomplete_enable = 0
70 endfunction
71
72 function! asyncomplete#get_source_names() abort
73     return keys(s:sources)
74 endfunction
75
76 function! asyncomplete#get_source_info(source_name) abort
77     return s:sources[a:source_name]
78 endfunction
79
80 function! asyncomplete#register_source(info) abort
81     if has_key(s:sources, a:info['name'])
82         call asyncomplete#log('core', 'duplicate asyncomplete#register_source', a:info['name'])
83         return -1
84     else
85         let s:sources[a:info['name']] = a:info
86         if has_key(a:info, 'events') && has_key(a:info, 'on_event')
87             execute 'augroup asyncomplete_source_event_' . a:info['name']
88             for l:event in a:info['events']
89                 let l:exec =  'if get(b:,"asyncomplete_enable",0) | call s:notify_event_to_source("' . a:info['name'] . '", "'.l:event.'",asyncomplete#context()) | endif'
90                 if type(l:event) == type('')
91                     execute 'au ' . l:event . ' * ' . l:exec
92                 elseif type(l:event) == type([])
93                     execute 'au ' . join(l:event,' ') .' ' .  l:exec
94                 endif
95             endfor
96             execute 'augroup end'
97         endif
98
99         if exists('b:asyncomplete_active_sources')
100           unlet b:asyncomplete_active_sources
101           call s:get_active_sources_for_buffer()
102         endif
103
104         if exists('b:asyncomplete_triggers')
105           unlet b:asyncomplete_triggers
106           call s:update_trigger_characters()
107         endif
108
109         return 1
110     endif
111 endfunction
112
113 function! asyncomplete#unregister_source(info_or_server_name) abort
114     if type(a:info_or_server_name) == type({})
115         let l:server_name = a:info_or_server_name['name']
116     else
117         let l:server_name = a:info_or_server_name
118     endif
119     if has_key(s:sources, l:server_name)
120         let l:server = s:sources[l:server_name]
121         if has_key(l:server, 'unregister')
122             call l:server.unregister()
123         endif
124         unlet s:sources[l:server_name]
125         return 1
126     else
127         return -1
128     endif
129 endfunction
130
131 function! asyncomplete#context() abort
132     let l:ret = {'bufnr':bufnr('%'), 'curpos':getcurpos(), 'changedtick':b:changedtick}
133     let l:ret['lnum'] = l:ret['curpos'][1]
134     let l:ret['col'] = l:ret['curpos'][2]
135     let l:ret['filetype'] = &filetype
136     let l:ret['filepath'] = expand('%:p')
137     let l:ret['typed'] = strpart(getline(l:ret['lnum']),0,l:ret['col']-1)
138     return l:ret
139 endfunction
140
141 function! s:on_insert_enter() abort
142     call s:get_active_sources_for_buffer() " call to cache
143     call s:update_trigger_characters()
144 endfunction
145
146 function! s:on_insert_leave() abort
147     let s:matches = {}
148     if exists('s:update_pum_timer')
149         call timer_stop(s:update_pum_timer)
150         unlet s:update_pum_timer
151     endif
152 endfunction
153
154 function! s:get_active_sources_for_buffer() abort
155     if exists('b:asyncomplete_active_sources')
156         " active sources were cached for buffer
157         return b:asyncomplete_active_sources
158     endif
159
160     call asyncomplete#log('core', 'computing active sources for buffer', bufnr('%'))
161     let b:asyncomplete_active_sources = []
162     for [l:name, l:info] in items(s:sources)
163         let l:blocked = 0
164
165         if has_key(l:info, 'blocklist')
166             let l:blocklistkey = 'blocklist'
167         else
168             let l:blocklistkey = 'blacklist'
169         endif
170         if has_key(l:info, l:blocklistkey)
171             for l:filetype in l:info[l:blocklistkey]
172                 if l:filetype == &filetype || l:filetype is# '*'
173                     let l:blocked = 1
174                     break
175                 endif
176             endfor
177         endif
178
179         if l:blocked
180             continue
181         endif
182
183         if has_key(l:info, 'allowlist')
184             let l:allowlistkey = 'allowlist'
185         else
186             let l:allowlistkey = 'whitelist'
187         endif
188         if has_key(l:info, l:allowlistkey)
189             for l:filetype in l:info[l:allowlistkey]
190                 if l:filetype == &filetype || l:filetype is# '*'
191                     let b:asyncomplete_active_sources += [l:name]
192                     break
193                 endif
194             endfor
195         endif
196     endfor
197
198     call asyncomplete#log('core', 'active source for buffer', bufnr('%'), b:asyncomplete_active_sources)
199
200     return b:asyncomplete_active_sources
201 endfunction
202
203 function! s:update_trigger_characters() abort
204     if exists('b:asyncomplete_triggers')
205         " triggers were cached for buffer
206         return b:asyncomplete_triggers
207     endif
208     let b:asyncomplete_triggers = {} " { char: { 'sourcea': 1, 'sourceb': 2 } }
209
210     for l:source_name in s:get_active_sources_for_buffer()
211         let l:source_info = s:sources[l:source_name]
212         if has_key(l:source_info, 'triggers') && has_key(l:source_info['triggers'], &filetype)
213             let l:triggers = l:source_info['triggers'][&filetype]
214         elseif has_key(l:source_info, 'triggers') && has_key(l:source_info['triggers'], '*')
215             let l:triggers = l:source_info['triggers']['*']
216         elseif has_key(g:asyncomplete_triggers, &filetype)
217             let l:triggers = g:asyncomplete_triggers[&filetype]
218         elseif has_key(g:asyncomplete_triggers, '*')
219             let l:triggers = g:asyncomplete_triggers['*']
220         else
221             let l:triggers = []
222         endif
223
224         for l:trigger in l:triggers
225             let l:last_char = l:trigger[len(l:trigger) -1]
226             if !has_key(b:asyncomplete_triggers, l:last_char)
227                 let b:asyncomplete_triggers[l:last_char] = {}
228             endif
229             if !has_key(b:asyncomplete_triggers[l:last_char], l:source_name)
230                 let b:asyncomplete_triggers[l:last_char][l:source_name] = []
231             endif
232             call add(b:asyncomplete_triggers[l:last_char][l:source_name], l:trigger)
233         endfor
234     endfor
235     call asyncomplete#log('core', 'trigger characters for buffer', bufnr('%'), b:asyncomplete_triggers)
236 endfunction
237
238 function! s:should_skip() abort
239     if mode() isnot# 'i' || !get(b:, 'asyncomplete_enable', 0)
240         return 1
241     else
242         return 0
243     endif
244 endfunction
245
246 function! asyncomplete#close_popup() abort
247   return pumvisible() ? "\<C-y>" : ''
248 endfunction
249
250 function! asyncomplete#cancel_popup() abort
251   return pumvisible() ? "\<C-e>" : ''
252 endfunction
253
254 function! s:get_min_chars(source_name) abort
255   if exists('b:asyncomplete_min_chars')
256     return b:asyncomplete_min_chars
257   elseif has_key(s:sources, a:source_name)
258     return get(s:sources[a:source_name], 'min_chars', g:asyncomplete_min_chars)
259   endif
260   return g:asyncomplete_min_chars
261 endfunction
262
263 function! s:on_change() abort
264     if s:should_skip() | return | endif
265
266     if !g:asyncomplete_auto_popup
267         return
268     endif
269
270     let l:ctx = asyncomplete#context()
271     let l:last_char = l:ctx['typed'][l:ctx['col'] - 2] " col is 1-indexed, but str 0-indexed
272     if exists('b:asyncomplete_triggers')
273         let l:triggered_sources = get(b:asyncomplete_triggers, l:last_char, {})
274     else
275         let l:triggered_sources = {}
276     endif
277     let l:refresh_pattern = get(b:, 'asyncomplete_refresh_pattern', '\(\k\+$\)')
278     let [l:_, l:startidx, l:endidx] = asyncomplete#utils#matchstrpos(l:ctx['typed'], l:refresh_pattern)
279
280     for l:source_name in get(b:, 'asyncomplete_active_sources', [])
281         " match sources based on the last character if it is a trigger character
282         " TODO: also check for multiple chars instead of just last chars for
283         " languages such as cpp which uses -> and ::
284         if has_key(l:triggered_sources, l:source_name)
285             let l:startcol = l:ctx['col']
286         elseif l:startidx > -1
287             let l:startcol = l:startidx + 1 " col is 1-indexed, but str 0-indexed
288         endif
289         " here we use the existence of `l:startcol` to determine whether to
290         " use this completion source. If `l:startcol` exists, we use the
291         " source. If it does not exist, it means that we cannot get a
292         " meaningful starting point for the current source, and this implies
293         " that we cannot use this source for completion. Therefore, we remove
294         " the matches from the source.
295         if exists('l:startcol') && l:endidx - l:startidx >= s:get_min_chars(l:source_name)
296             if !has_key(s:matches, l:source_name) || s:matches[l:source_name]['ctx']['lnum'] !=# l:ctx['lnum'] || s:matches[l:source_name]['startcol'] !=# l:startcol
297                 let s:matches[l:source_name] = { 'startcol': l:startcol, 'status': 'idle', 'items': [], 'refresh': 0, 'ctx': l:ctx }
298             endif
299         else
300             if has_key(s:matches, l:source_name)
301                 unlet s:matches[l:source_name]
302             endif
303         endif
304     endfor
305
306     call s:trigger(l:ctx)
307     call s:update_pum()
308 endfunction
309
310 function! s:trigger(ctx) abort
311     " send cancellation request if supported
312     for [l:source_name, l:matches] in items(s:matches)
313         call asyncomplete#log('core', 's:trigger', l:matches)
314         if l:matches['refresh'] || l:matches['status'] ==# 'idle' || l:matches['status'] ==# 'failure'
315             let l:matches['status'] = 'pending'
316             try
317                 " TODO: check for min chars
318                 call asyncomplete#log('core', 's:trigger.completor()', l:source_name, s:matches[l:source_name], a:ctx)
319                 call s:sources[l:source_name].completor(s:sources[l:source_name], a:ctx)
320             catch
321                 let l:matches['status'] = 'failure'
322                 call asyncomplete#log('core', 's:trigger', 'error', v:exception)
323                 continue
324             endtry
325         endif
326     endfor
327 endfunction
328
329 function! asyncomplete#complete(name, ctx, startcol, items, ...) abort
330     let l:refresh = a:0 > 0 ? a:1 : 0
331     let l:ctx = asyncomplete#context()
332     if !has_key(s:matches, a:name) || l:ctx['lnum'] != a:ctx['lnum'] " TODO: handle more context changes
333         call asyncomplete#log('core', 'asyncomplete#log', 'ignoring due to context chnages', a:name, a:ctx, a:startcol, l:refresh, a:items)
334         call s:update_pum()
335         return
336     endif
337
338     call asyncomplete#log('asyncomplete#complete', a:name, a:ctx, a:startcol, l:refresh, a:items)
339
340     let l:matches = s:matches[a:name]
341     let l:matches['items'] = s:normalize_items(a:items)
342     let l:matches['refresh'] = l:refresh
343     let l:matches['startcol'] = a:startcol
344     let l:matches['status'] = 'success'
345
346     call s:update_pum()
347 endfunction
348
349 function! s:normalize_items(items) abort
350     if len(a:items) > 0 && type(a:items[0]) ==# type('')
351         let l:items = []
352         for l:item in a:items
353             let l:items += [{'word': l:item }]
354         endfor
355         return l:items
356     else
357         return a:items
358     endif
359 endfunction
360
361 function! asyncomplete#force_refresh() abort
362     return asyncomplete#menu_selected() ? "\<c-y>\<c-r>=asyncomplete#_force_refresh()\<CR>" : "\<c-r>=asyncomplete#_force_refresh()\<CR>"
363 endfunction
364
365 function! asyncomplete#_force_refresh() abort
366     if s:should_skip() | return | endif
367
368     let l:ctx = asyncomplete#context()
369     let l:startcol = l:ctx['col']
370     let l:last_char = l:ctx['typed'][l:startcol - 2]
371
372     " loop left and find the start of the word or trigger chars and set it as the startcol for the source instead of refresh_pattern
373     let l:refresh_pattern = get(b:, 'asyncomplete_refresh_pattern', '\(\k\+$\)')
374     let [l:_, l:startidx, l:endidx] = asyncomplete#utils#matchstrpos(l:ctx['typed'], l:refresh_pattern)
375     " When no word here, startcol is current col
376     let l:startcol = l:startidx == -1 ? col('.') : l:startidx + 1
377
378     let s:matches = {}
379
380     for l:source_name in get(b:, 'asyncomplete_active_sources', [])
381         let s:matches[l:source_name] = { 'startcol': l:startcol, 'status': 'idle', 'items': [], 'refresh': 0, 'ctx': l:ctx }
382     endfor
383
384     call s:trigger(l:ctx)
385     call s:update_pum()
386     return ''
387 endfunction
388
389 function! s:update_pum() abort
390     if exists('s:update_pum_timer')
391         call timer_stop(s:update_pum_timer)
392         unlet s:update_pum_timer
393     endif
394     call asyncomplete#log('core', 's:update_pum')
395     let s:update_pum_timer = timer_start(g:asyncomplete_popup_delay, function('s:recompute_pum'))
396 endfunction
397
398 function! s:recompute_pum(...) abort
399     if s:should_skip() | return | endif
400
401     " TODO: add support for remote recomputation of complete items,
402     " Ex: heavy computation such as fuzzy search can happen in a python thread
403
404     call asyncomplete#log('core', 's:recompute_pum')
405
406     if asyncomplete#menu_selected()
407         call asyncomplete#log('core', 's:recomputed_pum', 'ignorning refresh pum due to menu selection')
408         return
409     endif
410
411     let l:ctx = asyncomplete#context()
412
413     let l:startcols = []
414     let l:matches_to_filter = {}
415
416     for [l:source_name, l:match] in items(s:matches)
417         " ignore sources that have been unregistered
418         if !has_key(s:sources, l:source_name) | continue | endif
419         let l:startcol = l:match['startcol']
420         let l:startcols += [l:startcol]
421         let l:curitems = l:match['items']
422
423         if l:startcol > l:ctx['col']
424             call asyncomplete#log('core', 's:recompute_pum', 'ignoring due to wrong start col', l:startcol, l:ctx['col'])
425             continue
426         else
427             let l:matches_to_filter[l:source_name] = l:match
428         endif
429     endfor
430
431     let l:startcol = min(l:startcols)
432     let l:base = l:ctx['typed'][l:startcol - 1:] " col is 1-indexed, but str 0-indexed
433
434     let l:filter_ctx = extend({
435         \ 'base': l:base,
436         \ 'startcol': l:startcol,
437         \ }, l:ctx)
438
439     let l:mode = s:has_complete_info ? complete_info(['mode'])['mode'] : 'unknown'
440     if l:mode ==# '' || l:mode ==# 'eval' || l:mode ==# 'unknown'
441         let l:Preprocessor = empty(g:asyncomplete_preprocessor) ? function('s:default_preprocessor') : g:asyncomplete_preprocessor[0]
442         call l:Preprocessor(l:filter_ctx, l:matches_to_filter)
443     endif
444 endfunction
445
446 let s:pair = {
447 \  '"':  '"',
448 \  '''':  '''',
449 \}
450
451 function! s:default_preprocessor(options, matches) abort
452     let l:items = []
453     let l:startcols = []
454     for [l:source_name, l:matches] in items(a:matches)
455         let l:startcol = l:matches['startcol']
456         let l:base = a:options['typed'][l:startcol - 1:]
457         if has_key(s:sources[l:source_name], 'filter')
458             let l:result = s:sources[l:source_name].filter(l:matches, l:startcol, l:base)
459             let l:items += l:result[0]
460             let l:startcols += l:result[1]
461         else
462             if empty(l:base)
463                 for l:item in l:matches['items']
464                     call add(l:items, s:strip_pair_characters(l:base, l:item))
465                     let l:startcols += [l:startcol]
466                 endfor
467             elseif s:has_matchfuzzypos && g:asyncomplete_matchfuzzy
468                 for l:item in matchfuzzypos(l:matches['items'], l:base, {'key':'word'})[0]
469                     call add(l:items, s:strip_pair_characters(l:base, l:item))
470                     let l:startcols += [l:startcol]
471                 endfor
472             else
473                 for l:item in l:matches['items']
474                     if stridx(l:item['word'], l:base) == 0
475                         call add(l:items, s:strip_pair_characters(l:base, l:item))
476                         let l:startcols += [l:startcol]
477                     endif
478                 endfor
479             endif
480         endif
481     endfor
482
483     let a:options['startcol'] = min(l:startcols)
484
485     call asyncomplete#preprocess_complete(a:options, l:items)
486 endfunction
487
488 function! s:strip_pair_characters(base, item) abort
489     " Strip pair characters. If pre-typed text is '"', candidates
490     " should have '"' suffix.
491     let l:item = a:item
492     if has_key(s:pair, a:base[0])
493         let [l:lhs, l:rhs, l:str] = [a:base[0], s:pair[a:base[0]], l:item['word']]
494         if len(l:str) > 1 && l:str[0] ==# l:lhs && l:str[-1:] ==# l:rhs
495             let l:item = extend({}, l:item)
496             let l:item['word'] = l:str[:-2]
497         endif
498     endif
499     return l:item
500 endfunction
501
502 function! asyncomplete#preprocess_complete(ctx, items) abort
503     " TODO: handle cases where this is called asynchronsouly. Currently not supported
504     if s:should_skip() | return | endif
505
506     call asyncomplete#log('core', 'asyncomplete#preprocess_complete')
507
508     if asyncomplete#menu_selected()
509         call asyncomplete#log('core', 'asyncomplete#preprocess_complete', 'ignorning pum update due to menu selection')
510         return
511     endif
512
513     if (g:asyncomplete_auto_completeopt == 1)
514         setl completeopt=menuone,noinsert,noselect
515     endif
516
517     let l:startcol = a:ctx['startcol']
518     call asyncomplete#log('core', 'asyncomplete#preprocess_complete calling complete()', l:startcol, a:items)
519     if l:startcol > 0 " Prevent E578: Not allowed to change text here
520         call complete(l:startcol, a:items)
521     endif
522 endfunction
523
524 function! asyncomplete#menu_selected() abort
525     " when the popup menu is visible, v:completed_item will be the
526     " current_selected item
527     " if v:completed_item is empty, no item is selected
528     return pumvisible() && !empty(v:completed_item)
529 endfunction
530
531 function! s:notify_event_to_source(name, event, ctx) abort
532     try
533         if has_key(s:sources, a:name)
534             call s:sources[a:name].on_event(s:sources[a:name], a:ctx, a:event)
535         endif
536     catch
537         call asyncomplete#log('core', 's:notify_event_to_source', 'error', v:exception)
538         return
539     endtry
540 endfunction