Skip to content

Instantly share code, notes, and snippets.

@mg979
Created January 24, 2021 15:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mg979/07a8d24a5d2101ffcd2edfefd45ac028 to your computer and use it in GitHub Desktop.
Save mg979/07a8d24a5d2101ffcd2edfefd45ac028 to your computer and use it in GitHub Desktop.
Score fuzzy matches
M = {}
function M.score()
local lines = {}
local self = function(x, ...) return vim.fn.get(vim.fn.eval('self'), x, ...) end
local re = function(x, ...) return vim.fn.get(vim.fn.eval('self.re'), x, ...) end
local find = string.find
local vmatch = vim.fn.match
local vmatchend = vim.fn.matchend
local fileFinder = self('file_finder') == 1
local slash = vim.fn.has('win32') == 1 and '.*[\\/]' or '.*/'
local longFirst = self('score_longest_first', 1) == 1
-- the only option that we consider when evaluating score is the case
local filter = table.concat(self('filter'))
local case = vim.fn.matchstr(re('opts'), '\\[cC]')
local opts = '\\V' .. case .. re('pre')
local post = re('post')
local smartcase = not find(opts, '\\[cC]') and not find(filter, '%u')
local pattern = opts .. re('pattern') .. post -- the basic pattern
local wpattern = opts .. '\\<' .. re('pattern') .. post -- the pattern at begin of word
local xpattern = opts .. filter .. post -- the exact pattern
local fromCol = string.match(re('opts'), '\\%%>(%d+)c')
fromCol = fromCol and tonumber(fromCol) or 0
local filterLen = #filter
local Count = 0
local fastScore = filterLen < 3 and vim.fn.line('$') > 1000
-- used to search for case sensitive exact matches
local caseAfterSlash = not self('stop_at_tab') and string.find or
function(v, p, ...) local s = ... and ... - 1 or 0; return vmatch(v, '\\C' .. p, s) end
local sameCase = not self('stop_at_tab') and string.find or
function(v, p, ...) local s = ... and ... - 1 or 0; return vmatch(v, '\\C' .. p, s) ~= -1 end
for _,v in ipairs(vim.fn.getline(1, '$')) do
local firstMatch = vmatch(v, pattern, fromCol)
local matchLength = fastScore and 0 or vmatchend(v, pattern) - filterLen
local perfectMatch = not fastScore and vmatch(v, xpattern, fromCol) >= 0
-- if longFirst, the farthest the first match, the higher the score
-- the longer the match, the less exact it is, so subtract it from score
local score = ( longFirst and firstMatch or -firstMatch ) - matchLength
local lastSlash, afterSlash, beginOfLine, beginOfBasename, beginOfWord, exactAfterSlash
-- remember that vim matches are 0-indexed, lua's are 1-indexed
if fileFinder then
_, lastSlash = find(v, slash)
afterSlash = lastSlash and lastSlash < firstMatch
beginOfLine = not lastSlash and firstMatch == fromCol
beginOfBasename = lastSlash and
vmatch(v, pattern, lastSlash - 1) == lastSlash
beginOfWord = not fastScore and
not beginOfLine and
not beginOfBasename and
vmatch(v, wpattern, fromCol) > fromCol
exactAfterSlash = perfectMatch and
beginOfBasename and
vmatch(v, xpattern, lastSlash - 1) == lastSlash
else
beginOfLine = firstMatch == fromCol
beginOfWord = not beginOfLine and
vmatch(v, wpattern, fromCol) > fromCol
end
-- prefer exact case if smartcase
if smartcase and not fastScore then
if exactAfterSlash then if caseAfterSlash(v, filter, lastSlash) ~= lastSlash
then score = score - 100 end
elseif perfectMatch then if not sameCase(v, filter, fromCol + 1)
then score = score - 75 end
elseif vmatch(v, '\\C' .. pattern, fromCol) == -1
then score = score - 50 end
end
if perfectMatch then score = score + 200 end -- perfect filter match increases score
if exactAfterSlash then score = score + 100 end -- perfect match at start of basename
if beginOfBasename then score = score + 100 end -- match at start of basename
if not lastSlash then score = score + 50 end -- no slashes, file is in current directory
if beginOfLine then score = score + 50 end -- match at start of line
if afterSlash then score = score + 20 end -- match starts after last slash
if beginOfWord then score = score + 20 end -- match at start of word
Count = Count + 1
lines[Count] = {v, score}
end
-- sort by score
table.sort(lines, function(a, b) return a[2] > b[2] end )
-- update buffer
vim.fn.execute('%d _')
for i,line in ipairs(lines) do
vim.fn.setline(i, line[1])
end
end
return M
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment