dev (#1)
* 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:
55
lua/cmp/utils/async.lua
Normal file
55
lua/cmp/utils/async.lua
Normal 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
|
||||
|
||||
40
lua/cmp/utils/async_spec.lua
Normal file
40
lua/cmp/utils/async_spec.lua
Normal 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
57
lua/cmp/utils/cache.lua
Normal 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
|
||||
|
||||
0
lua/cmp/utils/cache_spec.lua
Normal file
0
lua/cmp/utils/cache_spec.lua
Normal file
113
lua/cmp/utils/char.lua
Normal file
113
lua/cmp/utils/char.lua
Normal 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
20
lua/cmp/utils/debug.lua
Normal 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
134
lua/cmp/utils/keymap.lua
Normal 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
|
||||
|
||||
13
lua/cmp/utils/keymap_spec.lua
Normal file
13
lua/cmp/utils/keymap_spec.lua
Normal 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
112
lua/cmp/utils/misc.lua
Normal 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
32
lua/cmp/utils/patch.lua
Normal 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
21
lua/cmp/utils/pattern.lua
Normal 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
42
lua/cmp/utils/spec.lua
Normal 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
150
lua/cmp/utils/str.lua
Normal 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
|
||||
|
||||
|
||||
26
lua/cmp/utils/str_spec.lua
Normal file
26
lua/cmp/utils/str_spec.lua
Normal 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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user