diff --git a/autoload/asyncomplete.vim b/autoload/asyncomplete.vim index f5c0218..a0ebeb0 100644 --- a/autoload/asyncomplete.vim +++ b/autoload/asyncomplete.vim @@ -2,20 +2,20 @@ 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 + call asyncomplete#log('vim/neovim compiled with timers required.') finish endif let s:sources = {} +let s:matches = {} +let s:already_setup = 0 let s:change_timer = -1 let s:last_tick = [] -let s:has_popped_up = 0 -let s:complete_timer_ctx = {} -let s:already_setup = 0 +let s:startcol = -1 +let s:candidates = [] +let s:script_path = expand(':p:h') +let s:has_lua = has('lua') || has('neovim-0.2.2') +let s:supports_smart_completion = s:has_lua && exists('##TextChangedP') function! asyncomplete#log(...) abort if !empty(g:asyncomplete_log_file) @@ -36,17 +36,92 @@ function! asyncomplete#enable_for_buffer() abort endif let b:asyncomplete_enable = 1 - augroup ayncomplete - autocmd! * - autocmd InsertEnter call s:python_cm_insert_enter() - autocmd InsertEnter call s:change_tick_start() - autocmd InsertLeave call s:change_tick_stop() - " working together with timer, the timer is for detecting changes - " popup menu is visible. TextChangedI will not be triggered when popup - " menu is visible, but TextChangedI is more efficient and faster than - " timer when popup menu is not visible. - autocmd TextChangedI call s:check_changes() - augroup END + if exists('##TextChangedP') + augroup ayncomplete + autocmd! * + autocmd InsertEnter call s:on_insert_enter() + autocmd InsertLeave call s:on_insert_leave() + autocmd TextChangedI call s:on_text_changed() + autocmd TextChangedP call s:on_text_changed() + augroup END + else + augroup ayncomplete + autocmd! * + autocmd InsertEnter call s:on_insert_enter() + autocmd InsertLeave call s:on_insert_leave() + autocmd InsertEnter call s:change_tick_start() + autocmd InsertLeave call s:change_tick_stop() + " working together with timer, the timer is for detecting changes + " popup menu is visible. TextChangedI will not be triggered when popup + " menu is visible, but TextChangedI is more efficient and faster than + " timer when popup menu is not visible. + autocmd TextChangedI call s:check_changes() + augroup END + endif +endfunction + +function! s:on_insert_enter() abort + call s:reset() +endfunction + +function! s:on_insert_leave() abort + call s:reset() +endfunction + +function! s:reset() abort + let s:matches = {} + let s:startcol = -1 + let s:candidates = [] +endfunction + +function! s:on_text_changed() abort + let l:ctx = asyncomplete#context() + call s:notify_sources_to_refresh(l:ctx, 0) + if s:supports_smart_completion() && pumvisible() && !empty(s:candidates) + " TODO: delay s:update_pum() since it is expensive due to filtering candidates + call s:update_pum(l:ctx, s:startcol, s:candidates) + endif +endfunction + +function! s:change_tick() abort + return [b:changedtick, getcurpos()] +endfunction + +function! s:change_tick_start() abort + if s:change_timer != -1 + return + endif + let s:last_tick = s:change_tick() + " changes every 30ms, which is 0.03s, it should be fast enough + let s:change_timer = timer_start(30, function('s:check_changes'), { 'repeat': -1 }) + call s:on_changed() +endfunction + +function! s:change_tick_stop() abort + if s:change_timer == -1 + return + endif + call timer_stop(s:change_timer) + let s:last_tick = [] + let s:change_timer = -1 +endfunction + +function! s:check_changes(...) abort + let l:tick = s:change_tick() + if l:tick != s:last_tick + let s:last_tick = l:tick + call s:on_changed() + endif +endfunction + +function! s:on_changed() abort + if exists('s:complete_timer') + call timer_stop(s:complete_timer) + unlet s:complete_timer + endif + + let l:ctx = asyncomplete#context() + call s:notify_sources_to_refresh(l:ctx, 0) endfunction function! asyncomplete#register_source(info) abort @@ -57,7 +132,7 @@ function! asyncomplete#register_source(info) abort 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:python_cm_event("' . a:info['name'] . '", "'.l:event.'",asyncomplete#context()) | endif' + let l:exec = 'if get(b:,"asyncomplete_enable",0) | call s:notify_source_event("' . a:info['name'] . '", "'.l:event.'",asyncomplete#context()) | endif' if type(l:event) == type('') execute 'au ' . l:event . ' * ' . l:exec elseif type(l:event) == type([]) @@ -80,18 +155,300 @@ function! asyncomplete#unregister_source(name) abort endtry endfunction -function! asyncomplete#complete(name, ctx, startcol, matches, ...) abort - let l:refresh = a:0 > 0 ? a:1 : 0 - - " ignore the request if context has changed - if asyncomplete#context_changed(a:ctx) - if g:asyncomplete_force_refresh_on_context_changed - call s:python_cm_complete(a:name, a:ctx, a:startcol, a:matches, l:refresh, 1) - endif +function! s:is_enabled() abort + if !get(b:, 'asyncomplete_enable') || mode() isnot# 'i' || &paste + return 0 + else return 1 endif +endfunction - call s:python_cm_complete(a:name, a:ctx, a:startcol, a:matches, l:refresh, 0) +function! s:notify_sources_to_refresh(ctx, force) abort + if !s:is_enabled() + return + endif + + if !pumvisible() && !g:asyncomplete_auto_popup + if !a:force + return + endif + endif + + let l:typed = a:ctx['typed'] + + for l:source_name in s:get_active_sources_for_buffer() + let l:refresh = a:force + let l:source = s:sources[l:source_name] + if !a:force + if has_key(s:matches, l:source_name) + if s:matches[l:source_name]['incomplete'] + let l:refresh = 1 + else + let l:matchpos = s:get_matchpos(s:sources[l:source_name], a:ctx) + let l:startpos = l:matchpos[1] + let l:endpos = l:matchpos[2] + let l:typed_len = l:endpos - l:startpos + let l:startcol = len(l:typed[:len(l:typed) - l:typed_len]) + if s:matches[l:source_name]['startcol'] == l:startcol + if s:matches[l:source_name]['pending'] + call s:queue_compute_candidates() + else + if s:supports_smart_completion() + call s:queue_compute_candidates() + endif + endif + else + if s:matches[l:source_name]['pending'] + call s:queue_compute_candidates() + else + let l:typed_len = l:endpos - l:startpos + let l:min_chars = get(l:source, 'min_chars', g:asyncomplete_min_chars) + if l:typed_len >= l:min_chars + let l:refresh = 1 + let s:matches[l:source_name] = { + \ 'pending': 1, + \ 'startcol': l:matchpos[1], + \ 'incomplete': 0, + \ 'candidates': [], + \ } + endif + call s:queue_compute_candidates() + endif + endif + endif + else + let l:matchpos = s:get_matchpos(s:sources[l:source_name], a:ctx) + let l:startpos = l:matchpos[1] + let l:endpos = l:matchpos[2] + let l:typed_len = l:endpos - l:startpos + let l:min_chars = get(l:source, 'min_chars', g:asyncomplete_min_chars) + if l:typed_len >= l:min_chars + let l:refresh = 1 + let s:matches[l:source_name] = { + \ 'pending': 1, + \ 'startcol': len(l:typed[:len(l:typed) - l:typed_len -1]), + \ 'typed': l:typed[:l:matchpos[1]], + \ 'incomplete': 0, + \ 'candidates': [], + \ } + endif + endif + endif + if l:refresh + try + call asyncomplete#log('core.s:notify_sources_to_refresh', 'completor()', l:source_name, a:ctx) + call s:sources[l:source_name].completor(s:sources[l:source_name], a:ctx) + catch + call asyncomplete#log('core.s:notify_sources_to_refresh', 'completor()', 'error', v:exception) + continue + endtry + endif + endfor +endfunction + +function! s:get_matchpos(source_info, ctx) abort + if has_key(a:source_info, 'refresh_pattern') + let l:refresh_pattern = a:source_info['refresh_pattern'] + if (type(l:refresh_pattern) != type('')) + let l:refresh_pattern = l:refresh_pattern() + endif + else + let l:refresh_pattern = g:asyncomplete_default_refresh_pattern + endif + + return s:matchstrpos(a:ctx['typed'], l:refresh_pattern) +endfunction + +function! asyncomplete#complete(name, ctx, startcol, candidates, ...) abort + let l:incomplete = a:0 > 0 ? a:1 : 0 + let l:current_context = asyncomplete#context() + call asyncomplete#log('core#complete', a:name, a:startcol, len(a:candidates), l:incomplete, a:ctx, l:current_context) + + " handle context_changed scenarios, add more scenarios + if l:current_context['lnum'] != a:ctx['lnum'] || l:current_context['filetype'] != a:ctx['filetype'] + call asyncomplete#log('core#complete', a:name, 'ignoring since context changed', l:current_context, a:ctx) + return + endif + + let s:matches[a:name] = { + \ 'pending': 0, + \ 'startcol': a:startcol, + \ 'incomplete': l:incomplete, + \ 'candidates': s:normalize_candidates(a:name, a:candidates), + \ 'typed': '', + \ 'ctx': a:ctx, + \ } + + call s:queue_compute_candidates() +endfunction + +function! s:queue_compute_candidates() abort + " call s:compute_candidates() at the end of the event loop to avoid calling expensive compute multiple times + if exists('s:compute_timer_candidate') + call timer_stop(s:compute_timer_candidate) + unlet s:compute_timer_candidate + endif + let s:compute_timer_candidate = timer_start(0, function('s:compute_candidates')) +endfunction + +function! s:normalize_candidates(name, candidates) abort + let l:normalizedcurcandidates = [] + if get(s:sources[a:name], 'normalize_completion_items', g:asyncomplete_normalize_completion_items) + call asyncomplete#log('s:normalize_candidates', 'normalizing all candidates', a:name) + for l:item in a:candidates + let l:e = {} + if type(l:item) == type('') + let l:e['word'] = l:item + else + let l:e = copy(l:item) + endif + call add(l:normalizedcurcandidates, l:e) + endfor + else + if !empty(a:candidates) + if type(a:candidates[0]) == type('') + call asyncomplete#log('s:normalize_candidates', 'normalizing string candidates', a:name) + for l:item in a:candidates + call add(l:normalizedcurcandidates, { 'word': l:item }) + endfor + else + call asyncomplete#log('s:normalize_candidates', 'ignoring candidates normalization', a:name) + let l:normalizedcurcandidates = a:candidates + endif + endif + endif + return l:normalizedcurcandidates +endfunction + +function! s:compute_candidates(...) abort + if !s:is_enabled() + return + endif + + call asyncomplete#log('core.s:compute_candidates()') + + " find mimnimal startcol from all matches + let l:startcols = [] + for l:item in values(s:matches) + if !l:item['pending'] + let l:startcols += [l:item['startcol']] + endif + endfor + let l:startcol = min(l:startcols) + + let l:ctx = asyncomplete#context() + let l:base = l:ctx['typed'][l:startcol-1:] + + " sort sources by priority + let l:sources = sort(keys(s:matches), function('s:sort_sources_by_priority')) + + " remove duplicates if enabled + if g:asyncomplete_remove_duplicates + let l:sources = filter(copy(l:sources), 'index(l:sources, v:val, v:key+1) == -1') + endif + + let l:candidates = [] + + " normalize + for l:name in l:sources + let l:info = s:matches[l:name] + let l:curstartcol = l:info['startcol'] + + if l:curstartcol > l:ctx['col'] + " wrong start col + continue + endif + + let l:candidates += l:info['candidates'] + endfor + + let s:startcol = l:startcol + let s:candidates = l:candidates + call s:update_pum(ctx, l:startcol, l:candidates) +endfunction + +function! s:update_pum(ctx, startcol, candidates) abort + if !s:is_enabled() + return + endif + + if asyncomplete#menu_selected() + return 0 + endif + + setlocal completeopt-=longest + setlocal completeopt+=menuone + setlocal completeopt-=menu + if &completeopt !~# 'noinsert\|noselect' + setlocal completeopt+=noselect + endif + + let l:prefix = a:ctx['typed'][a:startcol-1 : col('.') - 1] + + call asyncomplete#log('update pum') + + " filter candidates + let l:candidates = s:supports_smart_completion() ? s:filter_completion_items_lua(l:prefix, a:candidates) : a:candidates + + call complete(a:startcol, l:candidates) +endfunction + +function! s:supports_smart_completion() abort + return s:supports_smart_completion && g:asyncomplete_smart_completion +endfunction + +function! s:filter_completion_items_lua(prefix, matches) abort + let l:tmpmatches = [] + lua << EOF + function spairs(t, order) + -- collect the keys + local keys = {} + for k in pairs(t) do keys[#keys+1] = k end + + -- if order function given, sort by it by passing the table and keys a, b, + -- otherwise just sort the keys + if order then + table.sort(keys, function(a,b) return order(t, a, b) end) + else + table.sort(keys) + end + + -- return the iterator function + local i = 0 + return function() + i = i + 1 + if keys[i] then + return keys[i], t[keys[i]] + end + end + end + + local prefix = vim.eval('a:prefix') + local matches = vim.eval('a:matches') + local tmpmatches = vim.eval('l:tmpmatches') + if asyncomplete.fts == nil then + local fts_fuzzy_match_script_path = vim.eval('s:script_path') .. '/fts_fuzzy_match.lua' + asyncomplete.fts = dofile(fts_fuzzy_match_script_path) + vim.eval("asyncomplete#log('fts_fuzzy_match loaded')") + end + local index = 0 + local unsorted_matches = {} + for i = 0, #matches - 1 do + local word = matches[i].word + local matched, score, matchedIndices = asyncomplete.fts.fuzzy_match(prefix, word) + if matched == true then + table.insert(unsorted_matches, { score = score, match = matches[i] }) + end + -- local matched = asyncomplete.fts.fuzzy_match_simple(prefix, word) + -- if matched == true then + -- tmpmatches:add(matches[i]) + -- end + end + for k,v in spairs(unsorted_matches, function(t,a,b) return t[b].score < t[a].score end) do + tmpmatches:add(v.match) + end +EOF + return l:tmpmatches endfunction function! asyncomplete#force_refresh() abort @@ -99,9 +456,7 @@ function! asyncomplete#force_refresh() abort endfunction function! asyncomplete#_force_refresh() abort - if get(b:, 'asyncomplete_enable') - call s:python_cm_refresh(asyncomplete#context(), 1) - endif + call s:notify_sources_to_refresh(asyncomplete#context(), 1) return '' endfunction @@ -122,90 +477,6 @@ function! asyncomplete#context_changed(ctx) abort return getcurpos() != a:ctx['curpos'] endfunction -function! s:python_cm_insert_enter() abort - call asyncomplete#log('core', 'python_cm_insert_enter') - let s:matches = {} -endfunction - -" function! s:python_cm_insert_leave() abort -" endfunction - -function! s:change_tick_start() abort - if s:change_timer != -1 - return - endif - let s:last_tick = s:change_tick() - " changes every 30ms, which is 0.03s, it should be fast enough - let s:change_timer = timer_start(30, function('s:check_changes'), { 'repeat': -1 }) - call s:on_changed() -endfunction - -function! s:change_tick_stop() abort - if s:change_timer == -1 - return - endif - call timer_stop(s:change_timer) - let s:last_tick = [] - let s:change_timer = -1 -endfunction - -function! s:on_changed() abort - if !get(b:, 'asyncomplete_enable') || mode() isnot# 'i' || &paste - return - endif - - if exists('s:complete_timer') - call timer_stop(s:complete_timer) - unlet s:complete_timer - endif - - let l:ctx = asyncomplete#context() - - call s:python_cm_refresh(l:ctx, 0) -endfunction - -function! s:check_changes(...) abort - let l:tick = s:change_tick() - if l:tick != s:last_tick - let s:last_tick = l:tick - call s:on_changed() - endif -endfunction - -function! s:change_tick() abort - return [b:changedtick, getcurpos()] -endfunction - -function! s:python_cm_complete(name, ctx, startcol, matches, refresh, outdated) abort - call asyncomplete#log('core', 's:python_cm_complete', a:name, a:ctx, a:startcol, a:refresh, a:outdated) - if a:outdated - call s:notify_sources_to_refresh([a:name], asyncomplete#context()) - return - endif - - if !has_key(s:matches, a:name) - let s:matches[a:name] = {} - endif - if empty(a:matches) - unlet s:matches[a:name] - else - let s:matches[a:name]['startcol'] = a:startcol - let s:matches[a:name]['matches'] = a:matches - let s:matches[a:name]['refresh'] = a:refresh - endif - - if s:has_popped_up - call s:python_refresh_completions(asyncomplete#context()) - endif -endfunction - -function! s:python_cm_complete_timeout(srcs, ctx) abort - if !s:has_popped_up - call s:python_refresh_completions(a:ctx) - let s:has_popped_up = 1 - endif -endfunction - function! s:get_active_sources_for_buffer() abort " TODO: cache active sources per buffer let l:active_sources = [] @@ -248,126 +519,12 @@ else endfunction endif -function! s:python_cm_refresh(ctx, force) abort - let l:has_popped_up = 0 - if a:force - call s:notify_sources_to_refresh(s:get_active_sources_for_buffer(), a:ctx) - return - endif - - if !pumvisible() && !g:asyncomplete_auto_popup - return - endif - - let l:typed = a:ctx['typed'] - let l:sources_to_notify = [] - - for l:name in s:get_active_sources_for_buffer() - let l:source = s:sources[l:name] - if has_key(l:source, 'refresh_pattern') - let l:refresh_pattern = l:source['refresh_pattern'] - else - let l:refresh_pattern = '\k\+$' - endif - let l:matchpos = s:matchstrpos(l:typed, l:refresh_pattern) - let l:startpos = l:matchpos[1] - let l:endpos = l:matchpos[2] - - call asyncomplete#log('core', 's:python_cm_refresh', l:matchpos, a:ctx) - - let l:typed_len = l:endpos - l:startpos - if l:typed_len == 1 - call add(l:sources_to_notify, l:name) - elseif has_key(s:matches, l:name) && s:matches[l:name]['refresh'] - call add(l:sources_to_notify, l:name) - endif - endfor - - call s:notify_sources_to_refresh(l:sources_to_notify, a:ctx) -endfunction - -function! s:notify_sources_to_refresh(sources, ctx) abort - if exists('s:complete_timer') - call timer_stop(s:complete_timer) - unlet s:complete_timer - endif - - let s:complete_timer = timer_start(g:asyncomplete_completion_delay, function('s:complete_timeout')) - let s:complete_timer_ctx = a:ctx - - for l:name in a:sources - try - call asyncomplete#log('core', 'completor()', l:name, a:ctx) - call s:sources[l:name].completor(s:sources[l:name], a:ctx) - catch - call asyncomplete#log('core', 'notify_sources_to_refresh', 'error', v:exception) - continue - endtry - endfor -endfunction - function! s:sort_sources_by_priority(source1, source2) abort let l:priority1 = get(get(s:sources, a:source1, {}), 'priority', 0) let l:priority2 = get(get(s:sources, a:source2, {}), 'priority', 0) return l:priority1 > l:priority2 ? -1 : (l:priority1 != l:priority2) endfunction -function! s:python_refresh_completions(ctx) abort - let l:matches = [] - - let l:names = keys(s:matches) - - if empty(l:names) - call s:python_complete(a:ctx, a:ctx['col'], []) - return - endif - - let l:startcols = [] - for l:item in values(s:matches) - let l:startcols += [l:item['startcol']] - endfor - - let l:startcol = min(l:startcols) - let l:base = a:ctx['typed'][l:startcol-1:] - - let l:filtered_matches = [] - - let l:sources = sort(keys(s:matches), function('s:sort_sources_by_priority')) - - if g:asyncomplete_remove_duplicates - let l:sources = filter(copy(l:sources), 'index(l:sources, v:val, v:key+1) == -1') - endif - - for l:name in l:sources - let l:info = s:matches[l:name] - let l:curstartcol = l:info['startcol'] - let l:curmatches = l:info['matches'] - - if l:curstartcol > a:ctx['col'] - " wrong start col - continue - endif - - let l:prefix = a:ctx['typed'][l:startcol-1 : col('.') -1] - - let l:normalizedcurmatches = [] - for l:item in l:curmatches - let l:e = {} - if type(l:item) == type('') - let l:e['word'] = l:item - else - let l:e = copy(l:item) - let l:e['word'] = l:e['word'] - endif - let l:normalizedcurmatches += [l:e] - endfor - - let l:filtered_matches += s:filter_completion_items(l:prefix, l:normalizedcurmatches) - endfor - - call s:core_complete(a:ctx, l:startcol, l:filtered_matches, s:matches) -endfunction - function! s:filter_completion_items(prefix, matches) abort let l:tmpmatches = [] for l:item in a:matches @@ -378,15 +535,7 @@ function! s:filter_completion_items(prefix, matches) abort return l:tmpmatches endfunction -function! s:python_complete(ctx, startcol, matches) abort - if empty(a:matches) - " no need to fire complete message - return - endif - call s:core_complete(a:ctx, a:startcol, a:matches, s:matches) -endfunction - -function! s:python_cm_event(name, event, ctx) abort +function! s:notify_source_event(name, event, ctx) abort try call s:sources[a:name].on_event(s:sources[a:name], a:ctx, a:event) catch @@ -394,40 +543,6 @@ function! s:python_cm_event(name, event, ctx) abort endtry endfunction -function! s:core_complete(ctx, startcol, matches, allmatches) abort - if !get(b:, 'asyncomplete_enable', 0) - return 2 - endif - - " ignore the request if context has changed - if (a:ctx != asyncomplete#context()) || (mode() isnot# 'i') - return 1 - endif - - " something selected by user, do not refresh the menu - if asyncomplete#menu_selected() - return 0 - endif - - setlocal completeopt-=longest - setlocal completeopt+=menuone - setlocal completeopt-=menu - if &completeopt !~# 'noinsert\|noselect' - setlocal completeopt+=noselect - endif - - call complete(a:startcol, a:matches) -endfunction - -function! s:complete_timeout(timer) abort - " finished, clean variable - unlet! s:complete_timer - if s:complete_timer_ctx != asyncomplete#context() - return - endif - call s:python_cm_complete_timeout(s:sources, s:complete_timer_ctx) -endfunction - function! asyncomplete#menu_selected() abort " when the popup menu is visible, v:completed_item will be the " current_selected item diff --git a/autoload/fts_fuzzy_match.lua b/autoload/fts_fuzzy_match.lua new file mode 100644 index 0000000..5ae86c8 --- /dev/null +++ b/autoload/fts_fuzzy_match.lua @@ -0,0 +1,156 @@ +-- LICENSE +-- +-- This software is dual-licensed to the public domain and under the following +-- license: you are granted a perpetual, irrevocable license to copy, modify, +-- publish, and distribute this file as you see fit. + +-- VERSION 0.1.0 + +-- Author: Forrest Smith (github.com/forrestthewoods/lib_fts) +-- Translated to Lua by Blake Mealey (github.com/blake-mealey) + +local module = {} + +-- Returns true if each character in pattern is found sequentially within str +function module.fuzzy_match_simple(pattern, str) + local patternIdx = 1 + local strIdx = 1 + local patternLength = #pattern + local strLength = #str + + while (patternIdx <= patternLength and strIdx <= strLength) do + local patternChar = pattern:sub(patternIdx, patternIdx):lower() + local strChar = str:sub(strIdx, strIdx):lower() + if patternChar == strChar then + patternIdx = patternIdx + 1 + end + strIdx = strIdx + 1 + end + + return patternLength ~= 0 and strLength ~= 0 and (patternIdx - 1) == patternLength +end + +-- Returns [bool, score, matchedIndices] +-- bool: true if each character in pattern is found sequentially within str +-- score: integer; higher is better match. Value has no intrinsic meaning. Range localies with pattern. +-- Can only compare scores with same search pattern. +-- matchedIndices: the indices of characters that were matched in str +function module.fuzzy_match(pattern, str) + + -- Score consts + local adjacency_bonus = 5 -- bonus for adjacent matches + local separator_bonus = 10 -- bonus if match occurs after a separator + local camel_bonus = 10 -- bonus if match is uppercase and prev is lower + local leading_letter_penalty = -3 -- penalty applied for every letter in str before the first match + local max_leading_letter_penalty = -9 -- maximum penalty for leading letters + local unmatched_letter_penalty = -1 -- penalty for every letter that doesn't matter + + -- Loop localiables + local score = 0 + local patternIdx = 1 + local patternLength = #pattern + local strIdx = 1 + local strLength = #str + local prevMatched = false + local prevLower = false + local prevSeparator = true -- true so if first letter match gets separator bonus + + -- Use "best" matched letter if multiple string letters match the pattern + local bestLetter = nil + local bestLower = nil + local bestLetterIdx = nil + local bestLetterScore = 0 + + local matchedIndices = {} + + -- Loop over strings + while (strIdx <= strLength) do + local patternChar = patternIdx <= patternLength and pattern:sub(patternIdx, patternIdx) or nil + local strChar = str:sub(strIdx, strIdx) + + local patternLower = patternChar and patternChar:lower() or nil + local strLower = strChar:lower() + local strUpper = strChar:upper() + + local nextMatch = patternChar and patternLower == strLower + local rematch = bestLetter and bestLower == strLower + + local advanced = nextMatch and bestLetter + local patternRepeat = bestLetter and patternChar and bestLower == patternLower + if advanced or patternRepeat then + score = score + bestLetterScore + table.insert(matchedIndices, bestLetterIdx) + bestLetter = nil + bestLower = nil + bestLetterIdx = nil + bestLetterScore = 0 + end + + if nextMatch or rematch then + local newScore = 0 + + -- Apply penalty for each letter before the first pattern match + -- Note: std::max because penalties are negative values. So max is smallest penalty. + if patternIdx == 0 then + local penalty = math.max(strIdx * leading_letter_penalty, max_leading_letter_penalty) + score = score + penalty + end + + -- Apply bonus for consecutive bonuses + if prevMatched then + newScore = newScore + adjacency_bonus + end + + -- Apply bonus for matches after a separator + if prevSeparator then + newScore = newScore + separator_bonus + end + + -- Apply bonus across camel case boundaries. Includes "clever" isLetter check. + if prevLower and strChar == strUpper and strLower ~= strUpper then + newScore = newScore + camel_bonus + end + + -- Update patter index IFF the next pattern letter was matched + if nextMatch then + patternIdx = patternIdx + 1 + end + + -- Update best letter in str which may be for a "next" letter or a "rematch" + if newScore >= bestLetterScore then + + -- Apply penalty for now skipped letter + if bestLetter then + score = score + unmatched_letter_penalty + end + + bestLetter = strChar + bestLower = bestLetter:lower() + bestLetterIdx = strIdx + bestLetterScore = newScore + end + + prevMatched = true + else + score = score + unmatched_letter_penalty + prevMatched = false + end + + -- Includes "clever" isLetter check. + prevLower = strChar == strLower and strLower ~= strUpper + prevSeparator = strChar == '_' or strChar == ' ' + + strIdx = strIdx + 1 + end + + -- Apply score for last match + if bestLetter then + score = score + bestLetterScore + table.insert(matchedIndices, bestLetterIdx) + end + + local matched = patternIdx - 1 == patternLength + return matched, score, matchedIndices +end + +return module diff --git a/plugin/asyncomplete.vim b/plugin/asyncomplete.vim index 5c39e1e..7ed8bcb 100644 --- a/plugin/asyncomplete.vim +++ b/plugin/asyncomplete.vim @@ -3,6 +3,8 @@ if exists('g:asyncomplete_loaded') endif let g:asyncomplete_loaded = 1 +let s:has_lua = has('lua') || has('neovim-0.2.2') + if get(g:, 'asyncomplete_enable_for_all', 1) augroup asyncomplete_enable au! @@ -10,10 +12,14 @@ if get(g:, 'asyncomplete_enable_for_all', 1) augroup END endif +let g:asyncomplete_min_chars = get(g:, 'asyncomplete_min_chars', 1) let g:asyncomplete_auto_popup = get(g:, 'asyncomplete_auto_popup', 1) let g:asyncomplete_completion_delay = get(g:, 'asyncomplete_completion_delay', 100) let g:asyncomplete_log_file = get(g:, 'asyncomplete_log_file', '') let g:asyncomplete_remove_duplicates = get(g:, 'asyncomplete_remove_duplicates', 0) +let g:asyncomplete_smart_completion = get(g:, 'asyncomplete_smart_completion', 0) " s:has_lua && exists('##TextChangedP') +let g:asyncomplete_default_refresh_pattern = get(g:, 'asyncomplete_default_refresh_pattern', '\(\k\+$\|\.$\|:$\)') +let g:asyncomplete_normalize_completion_items = get(g:, 'asyncomplete_normalize_completion_items', 0) " Setting it to true may slow/hang vim especially on slow are sources such as asyncomplete-lsp.vim " use asyncomplete_force_refersh to retrive the latest autocomplete results instead. @@ -21,3 +27,13 @@ let g:asyncomplete_force_refresh_on_context_changed = get(g:, 'asyncomplete_forc " imap (asyncomplete_force_refresh) inoremap (asyncomplete_force_refresh) asyncomplete#force_refresh() + +function! s:init_lua() abort + lua << EOF + asyncomplete = {} +EOF +endfunction + +if s:has_lua + call s:init_lua() +endif