]> git.madduck.net Git - etc/vim.git/blobdiff - .vim/bundle/asyncomplete/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:

Merge commit '56df844d3c39ec494dacc69eae34272b27db185a' as '.vim/bundle/asyncomplete'
[etc/vim.git] / .vim / bundle / asyncomplete / autoload / asyncomplete.vim
diff --git a/.vim/bundle/asyncomplete/autoload/asyncomplete.vim b/.vim/bundle/asyncomplete/autoload/asyncomplete.vim
new file mode 100644 (file)
index 0000000..d5e4e4b
--- /dev/null
@@ -0,0 +1,540 @@
+function! asyncomplete#log(...) abort
+    if !empty(g:asyncomplete_log_file)
+        call writefile([json_encode(a:000)], g:asyncomplete_log_file, 'a')
+    endif
+endfunction
+
+" do nothing, place it here only to avoid the message
+augroup asyncomplete_silence_messages
+    au!
+    autocmd User asyncomplete_setup silent
+augroup END
+
+if !has('timers')
+    echohl ErrorMsg
+    echomsg 'Vim/Neovim compiled with timers required for asyncomplete.vim.'
+    echohl NONE
+    if has('nvim')
+        call asyncomplete#log('neovim compiled with timers required.')
+    else
+        call asyncomplete#log('vim compiled with timers required.')
+    endif
+    " Clear augroup so this message is only displayed once.
+    au! asyncomplete_enable *
+    finish
+endif
+
+let s:already_setup = 0
+let s:sources = {}
+let s:matches = {} " { server_name: { incomplete: 1, startcol: 0, items: [], refresh: 0, status: 'idle|pending|success|failure', ctx: ctx } }
+let s:has_complete_info = exists('*complete_info')
+let s:has_matchfuzzypos = exists('*matchfuzzypos')
+
+function! s:setup_if_required() abort
+    if !s:already_setup
+        " register asyncomplete change manager
+        for l:change_manager in g:asyncomplete_change_manager
+            call asyncomplete#log('core', 'initializing asyncomplete change manager', l:change_manager)
+            if type(l:change_manager) == type('')
+                execute 'let s:on_change_manager = function("'. l:change_manager  .'")()'
+            else
+                let s:on_change_manager = l:change_manager()
+            endif
+            if has_key(s:on_change_manager, 'error')
+                call asyncomplete#log('core', 'initializing asyncomplete change manager failed', s:on_change_manager['name'], s:on_change_manager['error'])
+            else
+                call s:on_change_manager.register(function('s:on_change'))
+                call asyncomplete#log('core', 'initializing asyncomplete change manager complete', s:on_change_manager['name'])
+                break
+            endif
+        endfor
+
+        augroup asyncomplete
+            autocmd!
+            autocmd InsertEnter * call s:on_insert_enter()
+            autocmd InsertLeave * call s:on_insert_leave()
+        augroup END
+
+        doautocmd <nomodeline> User asyncomplete_setup
+        let s:already_setup = 1
+    endif
+endfunction
+
+function! asyncomplete#enable_for_buffer() abort
+    call s:setup_if_required()
+    let b:asyncomplete_enable = 1
+endfunction
+
+function! asyncomplete#disable_for_buffer() abort
+    let b:asyncomplete_enable = 0
+endfunction
+
+function! asyncomplete#get_source_names() abort
+    return keys(s:sources)
+endfunction
+
+function! asyncomplete#get_source_info(source_name) abort
+    return s:sources[a:source_name]
+endfunction
+
+function! asyncomplete#register_source(info) abort
+    if has_key(s:sources, a:info['name'])
+        call asyncomplete#log('core', 'duplicate asyncomplete#register_source', a:info['name'])
+        return -1
+    else
+        let s:sources[a:info['name']] = a:info
+        if has_key(a:info, 'events') && has_key(a:info, 'on_event')
+            execute 'augroup asyncomplete_source_event_' . a:info['name']
+            for l:event in a:info['events']
+                let l:exec =  'if get(b:,"asyncomplete_enable",0) | call s:notify_event_to_source("' . a:info['name'] . '", "'.l:event.'",asyncomplete#context()) | endif'
+                if type(l:event) == type('')
+                    execute 'au ' . l:event . ' * ' . l:exec
+                elseif type(l:event) == type([])
+                    execute 'au ' . join(l:event,' ') .' ' .  l:exec
+                endif
+            endfor
+            execute 'augroup end'
+        endif
+
+        if exists('b:asyncomplete_active_sources')
+          unlet b:asyncomplete_active_sources
+          call s:get_active_sources_for_buffer()
+        endif
+
+        if exists('b:asyncomplete_triggers')
+          unlet b:asyncomplete_triggers
+          call s:update_trigger_characters()
+        endif
+
+        return 1
+    endif
+endfunction
+
+function! asyncomplete#unregister_source(info_or_server_name) abort
+    if type(a:info_or_server_name) == type({})
+        let l:server_name = a:info_or_server_name['name']
+    else
+        let l:server_name = a:info_or_server_name
+    endif
+    if has_key(s:sources, l:server_name)
+        let l:server = s:sources[l:server_name]
+        if has_key(l:server, 'unregister')
+            call l:server.unregister()
+        endif
+        unlet s:sources[l:server_name]
+        return 1
+    else
+        return -1
+    endif
+endfunction
+
+function! asyncomplete#context() abort
+    let l:ret = {'bufnr':bufnr('%'), 'curpos':getcurpos(), 'changedtick':b:changedtick}
+    let l:ret['lnum'] = l:ret['curpos'][1]
+    let l:ret['col'] = l:ret['curpos'][2]
+    let l:ret['filetype'] = &filetype
+    let l:ret['filepath'] = expand('%:p')
+    let l:ret['typed'] = strpart(getline(l:ret['lnum']),0,l:ret['col']-1)
+    return l:ret
+endfunction
+
+function! s:on_insert_enter() abort
+    call s:get_active_sources_for_buffer() " call to cache
+    call s:update_trigger_characters()
+endfunction
+
+function! s:on_insert_leave() abort
+    let s:matches = {}
+    if exists('s:update_pum_timer')
+        call timer_stop(s:update_pum_timer)
+        unlet s:update_pum_timer
+    endif
+endfunction
+
+function! s:get_active_sources_for_buffer() abort
+    if exists('b:asyncomplete_active_sources')
+        " active sources were cached for buffer
+        return b:asyncomplete_active_sources
+    endif
+
+    call asyncomplete#log('core', 'computing active sources for buffer', bufnr('%'))
+    let b:asyncomplete_active_sources = []
+    for [l:name, l:info] in items(s:sources)
+        let l:blocked = 0
+
+        if has_key(l:info, 'blocklist')
+            let l:blocklistkey = 'blocklist'
+        else
+            let l:blocklistkey = 'blacklist'
+        endif
+        if has_key(l:info, l:blocklistkey)
+            for l:filetype in l:info[l:blocklistkey]
+                if l:filetype == &filetype || l:filetype is# '*'
+                    let l:blocked = 1
+                    break
+                endif
+            endfor
+        endif
+
+        if l:blocked
+            continue
+        endif
+
+        if has_key(l:info, 'allowlist')
+            let l:allowlistkey = 'allowlist'
+        else
+            let l:allowlistkey = 'whitelist'
+        endif
+        if has_key(l:info, l:allowlistkey)
+            for l:filetype in l:info[l:allowlistkey]
+                if l:filetype == &filetype || l:filetype is# '*'
+                    let b:asyncomplete_active_sources += [l:name]
+                    break
+                endif
+            endfor
+        endif
+    endfor
+
+    call asyncomplete#log('core', 'active source for buffer', bufnr('%'), b:asyncomplete_active_sources)
+
+    return b:asyncomplete_active_sources
+endfunction
+
+function! s:update_trigger_characters() abort
+    if exists('b:asyncomplete_triggers')
+        " triggers were cached for buffer
+        return b:asyncomplete_triggers
+    endif
+    let b:asyncomplete_triggers = {} " { char: { 'sourcea': 1, 'sourceb': 2 } }
+
+    for l:source_name in s:get_active_sources_for_buffer()
+        let l:source_info = s:sources[l:source_name]
+        if has_key(l:source_info, 'triggers') && has_key(l:source_info['triggers'], &filetype)
+            let l:triggers = l:source_info['triggers'][&filetype]
+        elseif has_key(l:source_info, 'triggers') && has_key(l:source_info['triggers'], '*')
+            let l:triggers = l:source_info['triggers']['*']
+        elseif has_key(g:asyncomplete_triggers, &filetype)
+            let l:triggers = g:asyncomplete_triggers[&filetype]
+        elseif has_key(g:asyncomplete_triggers, '*')
+            let l:triggers = g:asyncomplete_triggers['*']
+        else
+            let l:triggers = []
+        endif
+
+        for l:trigger in l:triggers
+            let l:last_char = l:trigger[len(l:trigger) -1]
+            if !has_key(b:asyncomplete_triggers, l:last_char)
+                let b:asyncomplete_triggers[l:last_char] = {}
+            endif
+            if !has_key(b:asyncomplete_triggers[l:last_char], l:source_name)
+                let b:asyncomplete_triggers[l:last_char][l:source_name] = []
+            endif
+            call add(b:asyncomplete_triggers[l:last_char][l:source_name], l:trigger)
+        endfor
+    endfor
+    call asyncomplete#log('core', 'trigger characters for buffer', bufnr('%'), b:asyncomplete_triggers)
+endfunction
+
+function! s:should_skip() abort
+    if mode() isnot# 'i' || !get(b:, 'asyncomplete_enable', 0)
+        return 1
+    else
+        return 0
+    endif
+endfunction
+
+function! asyncomplete#close_popup() abort
+  return pumvisible() ? "\<C-y>" : ''
+endfunction
+
+function! asyncomplete#cancel_popup() abort
+  return pumvisible() ? "\<C-e>" : ''
+endfunction
+
+function! s:get_min_chars(source_name) abort
+  if exists('b:asyncomplete_min_chars')
+    return b:asyncomplete_min_chars
+  elseif has_key(s:sources, a:source_name)
+    return get(s:sources[a:source_name], 'min_chars', g:asyncomplete_min_chars)
+  endif
+  return g:asyncomplete_min_chars
+endfunction
+
+function! s:on_change() abort
+    if s:should_skip() | return | endif
+
+    if !g:asyncomplete_auto_popup
+        return
+    endif
+
+    let l:ctx = asyncomplete#context()
+    let l:last_char = l:ctx['typed'][l:ctx['col'] - 2] " col is 1-indexed, but str 0-indexed
+    if exists('b:asyncomplete_triggers')
+        let l:triggered_sources = get(b:asyncomplete_triggers, l:last_char, {})
+    else
+        let l:triggered_sources = {}
+    endif
+    let l:refresh_pattern = get(b:, 'asyncomplete_refresh_pattern', '\(\k\+$\)')
+    let [l:_, l:startidx, l:endidx] = asyncomplete#utils#matchstrpos(l:ctx['typed'], l:refresh_pattern)
+
+    for l:source_name in get(b:, 'asyncomplete_active_sources', [])
+        " match sources based on the last character if it is a trigger character
+        " TODO: also check for multiple chars instead of just last chars for
+        " languages such as cpp which uses -> and ::
+        if has_key(l:triggered_sources, l:source_name)
+            let l:startcol = l:ctx['col']
+        elseif l:startidx > -1
+            let l:startcol = l:startidx + 1 " col is 1-indexed, but str 0-indexed
+        endif
+        " here we use the existence of `l:startcol` to determine whether to
+        " use this completion source. If `l:startcol` exists, we use the
+        " source. If it does not exist, it means that we cannot get a
+        " meaningful starting point for the current source, and this implies
+        " that we cannot use this source for completion. Therefore, we remove
+        " the matches from the source.
+        if exists('l:startcol') && l:endidx - l:startidx >= s:get_min_chars(l:source_name)
+            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
+                let s:matches[l:source_name] = { 'startcol': l:startcol, 'status': 'idle', 'items': [], 'refresh': 0, 'ctx': l:ctx }
+            endif
+        else
+            if has_key(s:matches, l:source_name)
+                unlet s:matches[l:source_name]
+            endif
+        endif
+    endfor
+
+    call s:trigger(l:ctx)
+    call s:update_pum()
+endfunction
+
+function! s:trigger(ctx) abort
+    " send cancellation request if supported
+    for [l:source_name, l:matches] in items(s:matches)
+        call asyncomplete#log('core', 's:trigger', l:matches)
+        if l:matches['refresh'] || l:matches['status'] ==# 'idle' || l:matches['status'] ==# 'failure'
+            let l:matches['status'] = 'pending'
+            try
+                " TODO: check for min chars
+                call asyncomplete#log('core', 's:trigger.completor()', l:source_name, s:matches[l:source_name], a:ctx)
+                call s:sources[l:source_name].completor(s:sources[l:source_name], a:ctx)
+            catch
+                let l:matches['status'] = 'failure'
+                call asyncomplete#log('core', 's:trigger', 'error', v:exception)
+                continue
+            endtry
+        endif
+    endfor
+endfunction
+
+function! asyncomplete#complete(name, ctx, startcol, items, ...) abort
+    let l:refresh = a:0 > 0 ? a:1 : 0
+    let l:ctx = asyncomplete#context()
+    if !has_key(s:matches, a:name) || l:ctx['lnum'] != a:ctx['lnum'] " TODO: handle more context changes
+        call asyncomplete#log('core', 'asyncomplete#log', 'ignoring due to context chnages', a:name, a:ctx, a:startcol, l:refresh, a:items)
+        call s:update_pum()
+        return
+    endif
+
+    call asyncomplete#log('asyncomplete#complete', a:name, a:ctx, a:startcol, l:refresh, a:items)
+
+    let l:matches = s:matches[a:name]
+    let l:matches['items'] = s:normalize_items(a:items)
+    let l:matches['refresh'] = l:refresh
+    let l:matches['startcol'] = a:startcol
+    let l:matches['status'] = 'success'
+
+    call s:update_pum()
+endfunction
+
+function! s:normalize_items(items) abort
+    if len(a:items) > 0 && type(a:items[0]) ==# type('')
+        let l:items = []
+        for l:item in a:items
+            let l:items += [{'word': l:item }]
+        endfor
+        return l:items
+    else
+        return a:items
+    endif
+endfunction
+
+function! asyncomplete#force_refresh() abort
+    return asyncomplete#menu_selected() ? "\<c-y>\<c-r>=asyncomplete#_force_refresh()\<CR>" : "\<c-r>=asyncomplete#_force_refresh()\<CR>"
+endfunction
+
+function! asyncomplete#_force_refresh() abort
+    if s:should_skip() | return | endif
+
+    let l:ctx = asyncomplete#context()
+    let l:startcol = l:ctx['col']
+    let l:last_char = l:ctx['typed'][l:startcol - 2]
+
+    " 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
+    let l:refresh_pattern = get(b:, 'asyncomplete_refresh_pattern', '\(\k\+$\)')
+    let [l:_, l:startidx, l:endidx] = asyncomplete#utils#matchstrpos(l:ctx['typed'], l:refresh_pattern)
+    " When no word here, startcol is current col
+    let l:startcol = l:startidx == -1 ? col('.') : l:startidx + 1
+
+    let s:matches = {}
+
+    for l:source_name in get(b:, 'asyncomplete_active_sources', [])
+        let s:matches[l:source_name] = { 'startcol': l:startcol, 'status': 'idle', 'items': [], 'refresh': 0, 'ctx': l:ctx }
+    endfor
+
+    call s:trigger(l:ctx)
+    call s:update_pum()
+    return ''
+endfunction
+
+function! s:update_pum() abort
+    if exists('s:update_pum_timer')
+        call timer_stop(s:update_pum_timer)
+        unlet s:update_pum_timer
+    endif
+    call asyncomplete#log('core', 's:update_pum')
+    let s:update_pum_timer = timer_start(g:asyncomplete_popup_delay, function('s:recompute_pum'))
+endfunction
+
+function! s:recompute_pum(...) abort
+    if s:should_skip() | return | endif
+
+    " TODO: add support for remote recomputation of complete items,
+    " Ex: heavy computation such as fuzzy search can happen in a python thread
+
+    call asyncomplete#log('core', 's:recompute_pum')
+
+    if asyncomplete#menu_selected()
+        call asyncomplete#log('core', 's:recomputed_pum', 'ignorning refresh pum due to menu selection')
+        return
+    endif
+
+    let l:ctx = asyncomplete#context()
+
+    let l:startcols = []
+    let l:matches_to_filter = {}
+
+    for [l:source_name, l:match] in items(s:matches)
+        " ignore sources that have been unregistered
+        if !has_key(s:sources, l:source_name) | continue | endif
+        let l:startcol = l:match['startcol']
+        let l:startcols += [l:startcol]
+        let l:curitems = l:match['items']
+
+        if l:startcol > l:ctx['col']
+            call asyncomplete#log('core', 's:recompute_pum', 'ignoring due to wrong start col', l:startcol, l:ctx['col'])
+            continue
+        else
+            let l:matches_to_filter[l:source_name] = l:match
+        endif
+    endfor
+
+    let l:startcol = min(l:startcols)
+    let l:base = l:ctx['typed'][l:startcol - 1:] " col is 1-indexed, but str 0-indexed
+
+    let l:filter_ctx = extend({
+        \ 'base': l:base,
+        \ 'startcol': l:startcol,
+        \ }, l:ctx)
+
+    let l:mode = s:has_complete_info ? complete_info(['mode'])['mode'] : 'unknown'
+    if l:mode ==# '' || l:mode ==# 'eval' || l:mode ==# 'unknown'
+        let l:Preprocessor = empty(g:asyncomplete_preprocessor) ? function('s:default_preprocessor') : g:asyncomplete_preprocessor[0]
+        call l:Preprocessor(l:filter_ctx, l:matches_to_filter)
+    endif
+endfunction
+
+let s:pair = {
+\  '"':  '"',
+\  '''':  '''',
+\}
+
+function! s:default_preprocessor(options, matches) abort
+    let l:items = []
+    let l:startcols = []
+    for [l:source_name, l:matches] in items(a:matches)
+        let l:startcol = l:matches['startcol']
+        let l:base = a:options['typed'][l:startcol - 1:]
+        if has_key(s:sources[l:source_name], 'filter')
+            let l:result = s:sources[l:source_name].filter(l:matches, l:startcol, l:base)
+            let l:items += l:result[0]
+            let l:startcols += l:result[1]
+        else
+            if empty(l:base)
+                for l:item in l:matches['items']
+                    call add(l:items, s:strip_pair_characters(l:base, l:item))
+                    let l:startcols += [l:startcol]
+                endfor
+            elseif s:has_matchfuzzypos && g:asyncomplete_matchfuzzy
+                for l:item in matchfuzzypos(l:matches['items'], l:base, {'key':'word'})[0]
+                    call add(l:items, s:strip_pair_characters(l:base, l:item))
+                    let l:startcols += [l:startcol]
+                endfor
+            else
+                for l:item in l:matches['items']
+                    if stridx(l:item['word'], l:base) == 0
+                        call add(l:items, s:strip_pair_characters(l:base, l:item))
+                        let l:startcols += [l:startcol]
+                    endif
+                endfor
+            endif
+        endif
+    endfor
+
+    let a:options['startcol'] = min(l:startcols)
+
+    call asyncomplete#preprocess_complete(a:options, l:items)
+endfunction
+
+function! s:strip_pair_characters(base, item) abort
+    " Strip pair characters. If pre-typed text is '"', candidates
+    " should have '"' suffix.
+    let l:item = a:item
+    if has_key(s:pair, a:base[0])
+        let [l:lhs, l:rhs, l:str] = [a:base[0], s:pair[a:base[0]], l:item['word']]
+        if len(l:str) > 1 && l:str[0] ==# l:lhs && l:str[-1:] ==# l:rhs
+            let l:item = extend({}, l:item)
+            let l:item['word'] = l:str[:-2]
+        endif
+    endif
+    return l:item
+endfunction
+
+function! asyncomplete#preprocess_complete(ctx, items) abort
+    " TODO: handle cases where this is called asynchronsouly. Currently not supported
+    if s:should_skip() | return | endif
+
+    call asyncomplete#log('core', 'asyncomplete#preprocess_complete')
+
+    if asyncomplete#menu_selected()
+        call asyncomplete#log('core', 'asyncomplete#preprocess_complete', 'ignorning pum update due to menu selection')
+        return
+    endif
+
+    if (g:asyncomplete_auto_completeopt == 1)
+        setl completeopt=menuone,noinsert,noselect
+    endif
+
+    let l:startcol = a:ctx['startcol']
+    call asyncomplete#log('core', 'asyncomplete#preprocess_complete calling complete()', l:startcol, a:items)
+    if l:startcol > 0 " Prevent E578: Not allowed to change text here
+        call complete(l:startcol, a:items)
+    endif
+endfunction
+
+function! asyncomplete#menu_selected() abort
+    " when the popup menu is visible, v:completed_item will be the
+    " current_selected item
+    " if v:completed_item is empty, no item is selected
+    return pumvisible() && !empty(v:completed_item)
+endfunction
+
+function! s:notify_event_to_source(name, event, ctx) abort
+    try
+        if has_key(s:sources, a:name)
+            call s:sources[a:name].on_event(s:sources[a:name], a:ctx, a:event)
+        endif
+    catch
+        call asyncomplete#log('core', 's:notify_event_to_source', 'error', v:exception)
+        return
+    endtry
+endfunction