* dev

* Improve sync design

* Support buffer local mapping

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* stylua

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* integration

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* update

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp

* tmp
This commit is contained in:
hrsh7th
2021-08-04 01:07:12 +09:00
committed by GitHub
parent b32a6e7e77
commit d23d3533cf
53 changed files with 4681 additions and 0 deletions

55
lua/cmp/utils/async.lua Normal file
View File

@@ -0,0 +1,55 @@
local async = {}
---@class cmp.AsyncThrottle
---@field public timeout number
---@field public stop function
---@field public __call function
---@param fn function
---@param timeout number
---@return cmp.AsyncThrottle
async.throttle = function(fn, timeout)
local time = nil
local timer = vim.loop.new_timer()
return setmetatable({
timeout = timeout,
stop = function()
time = nil
timer:stop()
end,
}, {
__call = function(self, ...)
local args = { ... }
if time == nil then
time = vim.loop.now()
end
timer:stop()
local delta = math.max(0, self.timeout - (vim.loop.now() - time))
timer:start(delta, 0, vim.schedule_wrap(function()
time = nil
fn(unpack(args))
end))
end
})
end
---Create deduplicated callback
---@return function
async.dedup = function()
local id = 0
return function(callback)
id = id + 1
local current = id
return function(...)
if current == id then
callback(...)
end
end
end
end
return async

View File

@@ -0,0 +1,40 @@
local async = require "cmp.utils.async"
describe('utils.async', function()
it('throttle', function()
local count = 0
local now
local f = async.throttle(function()
count = count + 1
end, 100)
-- 1. delay for 100ms
now = vim.loop.now()
f.timeout = 100
f()
vim.wait(1000, function() return count == 1 end)
assert.is.truthy(math.abs(f.timeout - (vim.loop.now() - now)) < 10)
-- 2. delay for 500ms
now = vim.loop.now()
f.timeout = 500
f()
vim.wait(1000, function() return count == 2 end)
assert.is.truthy(math.abs(f.timeout - (vim.loop.now() - now)) < 10)
-- 4. delay for 500ms and wait 100ms (remain 400ms)
f.timeout = 500
f()
vim.wait(100) -- remain 400ms
-- 5. call immediately (100ms already elapsed from No.4)
now = vim.loop.now()
f.timeout = 100
f()
vim.wait(1000, function() return count == 3 end)
assert.is.truthy(math.abs(vim.loop.now() - now) < 10)
end)
end)

57
lua/cmp/utils/cache.lua Normal file
View File

@@ -0,0 +1,57 @@
---@class cmp.Cache
---@field public entries any
local cache = {}
cache.new = function()
local self = setmetatable({}, { __index = cache })
self.entries = {}
return self
end
---Get cache value
---@param key string
---@return any|nil
cache.get = function(self, key)
key = self:key(key)
if self.entries[key] ~= nil then
return unpack(self.entries[key])
end
return nil
end
---Set cache value explicitly
---@param key string
---@vararg any
cache.set = function(self, key, ...)
key = self:key(key)
self.entries[key] = { ... }
end
---Ensure value by callback
---@param key string
---@param callback fun(): any
cache.ensure = function(self, key, callback)
local value = self:get(key)
if value == nil then
self:set(key, callback())
end
return self:get(key)
end
---Clear all cache entries
cache.clear = function(self)
self.entries = {}
end
---Create key
---@param key string|table
---@return string
cache.key = function(_, key)
if type(key) == 'table' then
return table.concat(key, ':')
end
return key
end
return cache

View File

113
lua/cmp/utils/char.lua Normal file
View File

@@ -0,0 +1,113 @@
local alpha = {}
string.gsub('abcdefghijklmnopqrstuvwxyz', '.', function(char)
alpha[string.byte(char)] = true
end)
local ALPHA = {}
string.gsub('ABCDEFGHIJKLMNOPQRSTUVWXYZ', '.', function(char)
ALPHA[string.byte(char)] = true
end)
local digit = {}
string.gsub('1234567890', '.', function(char)
digit[string.byte(char)] = true
end)
local white = {}
string.gsub(' \t\n', '.', function(char)
white[string.byte(char)] = true
end)
local char = {}
---@param byte number
---@return boolean
char.is_upper = function(byte)
return ALPHA[byte]
end
---@param byte number
---@return boolean
char.is_alpha = function(byte)
return alpha[byte] or ALPHA[byte]
end
---@param byte number
---@return boolean
char.is_digit = function(byte)
return digit[byte]
end
---@param byte number
---@return boolean
char.is_white = function(byte)
return white[byte]
end
---@param byte number
---@return boolean
char.is_symbol = function(byte)
return not (char.is_alnum(byte) or char.is_white(byte))
end
---@param byte number
---@return boolean
char.is_printable = function(byte)
return string.match(string.char(byte), '^%c$') == nil
end
---@param byte number
---@return boolean
char.is_alnum = function(byte)
return char.is_alpha(byte) or char.is_digit(byte)
end
---@param text string
---@param index number
---@return boolean
char.is_semantic_index = function(text, index)
if index <= 1 then
return true
end
local prev = string.byte(text, index - 1)
local curr = string.byte(text, index)
if not char.is_upper(prev) and char.is_upper(curr) then
return true
end
if char.is_symbol(curr) or char.is_white(curr) then
return true
end
if not char.is_alpha(prev) and char.is_alpha(curr) then
return true
end
return false
end
---@param text string
---@param current_index number
---@return boolean
char.get_next_semantic_index = function(text, current_index)
for i = current_index + 1, #text do
if char.is_semantic_index(text, i) then
return i
end
end
return #text + 1
end
---Ignore case match
---@param byte1 number
---@param byte2 number
---@return boolean
char.match = function(byte1, byte2)
if not char.is_alpha(byte1) or not char.is_alpha(byte2) then
return byte1 == byte2
end
local diff = byte1 - byte2
return diff == 0 or diff == 32 or diff == -32
end
return char

20
lua/cmp/utils/debug.lua Normal file
View File

@@ -0,0 +1,20 @@
local debug = {}
local flag = false
---Print log
---@vararg any
debug.log = function(...)
if flag then
local data = {}
for _, v in ipairs({ ... }) do
if not vim.tbl_contains({ 'string', 'number', 'boolean' }, type(v)) then
v = vim.inspect(v)
end
table.insert(data, v)
end
print(table.concat(data, '\t'))
end
end
return debug

134
lua/cmp/utils/keymap.lua Normal file
View File

@@ -0,0 +1,134 @@
local misc = require('cmp.utils.misc')
local cache = require('cmp.utils.cache')
local keymap = {}
---The mapping of vim notation and chars.
keymap._table = {
['<CR>'] = { '\n', '\r', '\r\n' },
['<Tab>'] = { '\t' },
['<BSlash>'] = { '\\' },
['<Bar>'] = { '|' },
['<Space>'] = { ' ' },
}
---Shortcut for nvim_replace_termcodes
---@param keys string
---@return string
keymap.t = function(keys)
return vim.api.nvim_replace_termcodes(keys, true, true, true)
end
---Return vim notation keymapping (simple conversion).
---@param s string
---@return string
keymap.to_keymap = function(s)
return string.gsub(s, '.', function(c)
for key, chars in pairs(keymap._table) do
if vim.tbl_contains(chars, c) then
return key
end
end
return c
end)
end
---Feedkeys with callback
keymap.feedkeys = setmetatable({
callbacks = {},
}, {
__call = function(self, keys, mode, callback)
vim.fn.feedkeys(keymap.t(keys), mode)
if callback then
local current_mode = string.sub(vim.api.nvim_get_mode().mode, 1, 1)
local id = misc.id('cmp.utils.keymap.feedkeys')
local cb = ('<Plug>(cmp-utils-keymap-feedkeys:%s)'):format(id)
self.callbacks[id] = function()
callback()
vim.api.nvim_buf_del_keymap(0, current_mode, cb)
return keymap.t('<Ignore>')
end
vim.api.nvim_buf_set_keymap(0, current_mode, cb, ('v:lua.cmp.utils.keymap.feedkeys.expr(%s)'):format(id), {
expr = true,
nowait = true,
silent = true,
})
vim.fn.feedkeys(keymap.t(cb), '')
end
end
})
misc.set(_G, { 'cmp', 'utils', 'keymap', 'feedkeys', 'expr' }, function(id)
if keymap.feedkeys.callbacks[id] then
keymap.feedkeys.callbacks[id]()
end
return keymap.t('<Ignore>')
end)
---Register keypress handler.
keymap.listen = setmetatable({
cache = cache.new(),
}, {
__call = function(_, keys, callback)
keys = keymap.to_keymap(keys)
local bufnr = vim.api.nvim_get_current_buf()
if keymap.listen.cache:get({ bufnr, keys }) then
return
end
local existing = nil
for _, map in ipairs(vim.api.nvim_buf_get_keymap(0, 'i')) do
if existing then
break
end
if map.lhs == keys then
existing = map
end
end
for _, map in ipairs(vim.api.nvim_get_keymap('i')) do
if existing then
break
end
if map.lhs == keys then
existing = map
break
end
end
existing = existing or {
lhs = keys,
rhs = keys,
expr = 0,
nowait = 0,
noremap = 1,
}
keymap.listen.cache:set({ bufnr, keys }, {
existing = existing,
callback = callback,
})
vim.api.nvim_buf_set_keymap(0, 'i', keys, ('v:lua.cmp.utils.keymap.expr("%s")'):format(keys), {
expr = true,
nowait = true,
noremap = true,
})
end,
})
misc.set(_G, { 'cmp', 'utils', 'keymap', 'expr' }, function(keys)
keys = keymap.to_keymap(keys)
local bufnr = vim.api.nvim_get_current_buf()
local existing = keymap.listen.cache:get({ bufnr, keys }).existing
local callback = keymap.listen.cache:get({ bufnr, keys }).callback
callback(keys, function()
vim.api.nvim_buf_set_keymap(0, 'i', '<Plug>(cmp-utils-keymap:_)', existing.rhs, {
expr = existing.expr == 1,
noremap = existing.noremap == 1,
})
vim.fn.feedkeys(keymap.t('<Plug>(cmp-utils-keymap:_)'), 'i')
end)
return keymap.t('<Ignore>')
end)
return keymap

View File

@@ -0,0 +1,13 @@
local spec = require('cmp.utils.spec')
local keymap = require('cmp.utils.keymap')
describe('keymap', function()
before_each(spec.before)
it('to_keymap', function()
assert.are.equal(keymap.to_keymap('\n'), '<CR>')
assert.are.equal(keymap.to_keymap('<CR>'), '<CR>')
assert.are.equal(keymap.to_keymap('|'), '<Bar>')
end)
end)

112
lua/cmp/utils/misc.lua Normal file
View File

@@ -0,0 +1,112 @@
local misc = {}
---Return concatenated list
---@param list1 any[]
---@param list2 any[]
---@return any[]
misc.concat = function(list1, list2)
local new_list = {}
for _, v in ipairs(list1) do
table.insert(new_list, v)
end
for _, v in ipairs(list2) do
table.insert(new_list, v)
end
return new_list
end
---Merge two tables recursively
---@generic T
---@param v1 T
---@param v2 T
---@return T
misc.merge = function(v1, v2)
local merge1 = type(v1) == "table" and not vim.tbl_islist(v1)
local merge2 = type(v2) == "table" and not vim.tbl_islist(v1)
if merge1 and merge2 then
local new_tbl = {}
for k, v in pairs(v2) do
new_tbl[k] = misc.merge(v1[k], v)
end
for k, v in pairs(v1) do
if v2[k] == nil then
new_tbl[k] = v
end
end
return new_tbl
end
return v1 or v2
end
---Generate id for group name
misc.id = setmetatable({
group = {}
}, {
__call = function(_, group)
misc.id.group[group] = misc.id.group[group] or 0
misc.id.group[group] = misc.id.group[group] + 1
return misc.id.group[group]
end
})
---Check the value is nil or not.
---@param v boolean
---@return boolean
misc.safe = function(v)
if v == nil or v == vim.NIL then
return nil
end
return v
end
---Treat 1/0 as bool value
---@param v boolean|"1"|"0"
---@param def boolean
---@return boolean
misc.bool = function(v, def)
if misc.safe(v) == nil then
return def
end
return v == true or v == 1
end
---Set value to deep object
---@param t table
---@param keys string[]
---@param v any
misc.set = function(t, keys, v)
local c = t
for i = 1, #keys - 1 do
local key = keys[i]
c[key] = misc.safe(c[key]) or {}
c = c[key]
end
c[keys[#keys]] = v
end
---Copy table
---@generic T
---@param tbl T
---@return T
misc.copy = function(tbl)
if type(tbl) ~= 'table' then
return tbl
end
if vim.tbl_islist(tbl) then
local copy = {}
for i, value in ipairs(tbl) do
copy[i] = misc.copy(value)
end
return copy
end
local copy = {}
for key, value in pairs(tbl) do
copy[key] = misc.copy(value)
end
return copy
end
return misc

32
lua/cmp/utils/patch.lua Normal file
View File

@@ -0,0 +1,32 @@
local keymap = require('cmp.utils.keymap')
local types = require('cmp.types')
local patch = {}
---@type table<number, function>
patch.callbacks = {}
---Apply oneline textEdit
---@param ctx cmp.Context
---@param range lsp.Range
---@param word string
---@param callback function
patch.apply = function(ctx, range, word, callback)
local ok = true
ok = ok and range.start.line == ctx.cursor.row - 1
ok = ok and range.start.line == range['end'].line
if not ok then
error("text_edit's range must be current one line.")
end
range = types.lsp.Range.to_vim(ctx.bufnr, range)
local before = string.sub(ctx.cursor_before_line, range.start.col)
local after = string.sub(ctx.cursor_after_line, ctx.cursor.col, range['end'].col)
local before_len = vim.fn.strchars(before)
local after_len = vim.fn.strchars(after)
local keys = string.rep('<Left>', after_len) .. string.rep('<BS>', after_len + before_len) .. word
keymap.feedkeys(keys, 'n', callback)
end
return patch

21
lua/cmp/utils/pattern.lua Normal file
View File

@@ -0,0 +1,21 @@
local pattern = {}
pattern._regexes = {}
pattern.regex = function(p)
if not pattern._regexes[p] then
pattern._regexes[p] = vim.regex(p)
end
return pattern._regexes[p]
end
pattern.offset = function(p, text)
local s, e = pattern.regex(p):match_str(text)
if s then
return s + 1, e + 1
end
return nil, nil
end
return pattern

42
lua/cmp/utils/spec.lua Normal file
View File

@@ -0,0 +1,42 @@
local context = require'cmp.context'
local source = require 'cmp.source'
local types = require('cmp.types')
local spec = {}
spec.before = function()
vim.cmd [[
bdelete!
enew!
setlocal virtualedit=all
]]
end
spec.state = function(text, row, col)
vim.fn.setline(1, text)
vim.fn.cursor(row, col)
local ctx = context.empty()
local s = source.new('spec', {
complete = function()
end
})
return {
context = function()
return ctx
end,
source = function()
return s
end,
press = function(char)
vim.fn.feedkeys(('i%s'):format(char), 'nx')
vim.fn.feedkeys(('l'):format(char), 'nx')
ctx.prev_context = nil
ctx = context.new(ctx, { reason = types.cmp.ContextReason.Manual })
s:complete(ctx, function() end)
return ctx
end
}
end
return spec

150
lua/cmp/utils/str.lua Normal file
View File

@@ -0,0 +1,150 @@
local char = require'cmp.utils.char'
local pattern = require 'cmp.utils.pattern'
local str = {}
local INVALID_CHARS = {}
INVALID_CHARS[string.byte('=')] = true
INVALID_CHARS[string.byte('$')] = true
INVALID_CHARS[string.byte('(')] = true
INVALID_CHARS[string.byte('[')] = true
INVALID_CHARS[string.byte('"')] = true
INVALID_CHARS[string.byte("'")] = true
INVALID_CHARS[string.byte("\n")] = true
local PAIR_CHARS = {}
PAIR_CHARS[string.byte('[')] = string.byte(']')
PAIR_CHARS[string.byte('(')] = string.byte(')')
PAIR_CHARS[string.byte('<')] = string.byte('>')
---Return if specified text has prefix or not
---@param text string
---@param prefix string
---@return boolean
str.has_prefix = function(text, prefix)
if #text < #prefix then
return false
end
for i = 1, #prefix do
if not char.match(string.byte(text, i), string.byte(prefix, i)) then
return false
end
end
return true
end
---Remove suffix
---@param text string
---@param suffix string
---@return string
str.remove_suffix = function(text, suffix)
if #text < #suffix then
return text
end
local i = 0
while i < #suffix do
if string.byte(text, #text - i) ~= string.byte(suffix, #suffix - i) then
return text
end
i = i + 1
end
return string.sub(text, 1, -#suffix - 1)
end
---strikethrough
---@param text string
---@return string
str.strikethrough = function(text)
local r = pattern.regex('.')
local buffer = ''
while text ~= '' do
local s, e = r:match_str(text)
if not s then
break
end
buffer = buffer .. string.sub(text, s, e) .. '̶'
text = string.sub(text, e + 1)
end
return buffer
end
---omit
---@param text string
---@param width number
---@return string
str.omit = function(text, width)
if width == 0 then
return ''
end
if not text then
text = ''
end
if #text > width then
return string.sub(text, 1, width + 1) .. '...'
end
return text
end
---trim
---@param text string
---@return string
str.trim = function(text)
local s = 1
for i = 1, #text do
if not char.is_white(string.byte(text, i)) then
s = i
break
end
end
local e = #text
for i = #text, 1, -1 do
if not char.is_white(string.byte(text, i)) then
e = i
break
end
end
if s == 1 and e == #text then
return text
end
return string.sub(text, s, e)
end
---get_word
---@param text string
---@return string
str.get_word = function(text, stop_char)
local valids = {}
local has_valid = false
for idx = 1, #text do
local c = string.byte(text, idx)
local invalid = INVALID_CHARS[c] and not (valids[c] and stop_char ~= c)
if has_valid and invalid then
return string.sub(text, 1, idx - 1)
end
valids[c] = true
if PAIR_CHARS[c] then
valids[PAIR_CHARS[c]] = true
end
has_valid = has_valid or not invalid
end
return text
end
---Oneline
---@param text string
---@return string
str.oneline = function(text)
for i = 1, #text do
if string.byte(text, i) == string.byte('\n', 1) then
return string.sub(text, 1, i - 1)
end
end
return text
end
return str

View File

@@ -0,0 +1,26 @@
local str = require "cmp.utils.str"
describe('utils.str', function()
it('get_word', function()
assert.are.equal(str.get_word('print'), 'print')
assert.are.equal(str.get_word('$variable'), '$variable')
assert.are.equal(str.get_word('print()'), 'print')
assert.are.equal(str.get_word('["cmp#confirm"]'), '["cmp#confirm"]')
assert.are.equal(str.get_word('"devDependencies":', string.byte('"')), '"devDependencies')
end)
it('strikethrough', function()
assert.are.equal(str.strikethrough('あいうえお'), 'あ̶い̶う̶え̶お̶')
end)
it('remove_suffix', function()
assert.are.equal(str.remove_suffix('log()', '$0'), 'log()')
assert.are.equal(str.remove_suffix('log()$0', '$0'), 'log()')
assert.are.equal(str.remove_suffix('log()${0}', '${0}'), 'log()')
assert.are.equal(str.remove_suffix('log()${0:placeholder}', '${0}'), 'log()${0:placeholder}')
end)
end)