Merge pull request #49 from prabirshrestha/TextChangedP

[v2] add support for fuzzy search using lua and use TextChangedP
This commit is contained in:
Prabir Shrestha
2018-03-08 08:12:16 -08:00
committed by GitHub
3 changed files with 560 additions and 273 deletions
+388 -273
View File
@@ -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('<sfile>: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! * <buffer>
autocmd InsertEnter <buffer> call s:python_cm_insert_enter()
autocmd InsertEnter <buffer> call s:change_tick_start()
autocmd InsertLeave <buffer> 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 <buffer> call s:check_changes()
augroup END
if exists('##TextChangedP')
augroup ayncomplete
autocmd! * <buffer>
autocmd InsertEnter <buffer> call s:on_insert_enter()
autocmd InsertLeave <buffer> call s:on_insert_leave()
autocmd TextChangedI <buffer> call s:on_text_changed()
autocmd TextChangedP <buffer> call s:on_text_changed()
augroup END
else
augroup ayncomplete
autocmd! * <buffer>
autocmd InsertEnter <buffer> call s:on_insert_enter()
autocmd InsertLeave <buffer> call s:on_insert_leave()
autocmd InsertEnter <buffer> call s:change_tick_start()
autocmd InsertLeave <buffer> 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 <buffer> 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
+156
View File
@@ -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
+16
View File
@@ -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 <c-space> <Plug>(asyncomplete_force_refresh)
inoremap <silent> <expr> <Plug>(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