mirror of
https://github.com/SpaceVim/SpaceVim.git
synced 2025-01-23 07:10:06 +08:00
feat(cmp-dictionary): add cmp-dictionary
This commit is contained in:
parent
b981945a13
commit
4cd290b71e
@ -107,6 +107,10 @@ function! SpaceVim#layers#autocomplete#plugins() abort
|
||||
\ 'merged' : 0,
|
||||
\ 'loadconf' : 1,
|
||||
\ }])
|
||||
call add(plugins, [g:_spacevim_root_dir . 'bundle/cmp-dictionary', {
|
||||
\ 'merged' : 0,
|
||||
\ 'loadconf' : 1,
|
||||
\ }])
|
||||
if g:spacevim_snippet_engine ==# 'neosnippet'
|
||||
call add(plugins, [g:_spacevim_root_dir . 'bundle/cmp-neosnippet', {
|
||||
\ 'merged' : 0,
|
||||
|
40
bundle/cmp-dictionary/.github/workflows/test.yml
vendored
Normal file
40
bundle/cmp-dictionary/.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
name: test
|
||||
|
||||
on:
|
||||
push:
|
||||
branchs:
|
||||
- main
|
||||
paths:
|
||||
- '**.lua'
|
||||
|
||||
jobs:
|
||||
integration:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup neovim
|
||||
uses: rhysd/action-setup-vim@v1
|
||||
with:
|
||||
version: nightly
|
||||
neovim: true
|
||||
|
||||
- name: Setup lua
|
||||
uses: leafo/gh-actions-lua@v8
|
||||
with:
|
||||
luaVersion: luajit-2.1.0-beta3
|
||||
|
||||
- name: Setup luarocks
|
||||
uses: leafo/gh-actions-luarocks@v4
|
||||
|
||||
- name: Setup tools
|
||||
shell: bash
|
||||
run: |
|
||||
luarocks --lua-version=5.1 install luacheck
|
||||
luarocks --lua-version=5.1 install vusted
|
||||
|
||||
- name: Run test
|
||||
shell: bash
|
||||
run: make
|
1
bundle/cmp-dictionary/.gitignore
vendored
Normal file
1
bundle/cmp-dictionary/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/doc/tags
|
2
bundle/cmp-dictionary/.luacheckrc
vendored
Normal file
2
bundle/cmp-dictionary/.luacheckrc
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
globals = { 'vim', 'describe', 'it', 'assert', 'before_each' }
|
||||
max_line_length = false
|
21
bundle/cmp-dictionary/LICENSE
vendored
Normal file
21
bundle/cmp-dictionary/LICENSE
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 uga-rosa
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
12
bundle/cmp-dictionary/Makefile
vendored
Normal file
12
bundle/cmp-dictionary/Makefile
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
.PHONY: test vusted luacheck format
|
||||
|
||||
test: luacheck vusted
|
||||
|
||||
vusted:
|
||||
vusted lua/
|
||||
|
||||
luacheck:
|
||||
luacheck lua/
|
||||
|
||||
format:
|
||||
stylua ./lua -g '!**/kit/**'
|
64
bundle/cmp-dictionary/README.md
vendored
Normal file
64
bundle/cmp-dictionary/README.md
vendored
Normal file
@ -0,0 +1,64 @@
|
||||
# cmp-dictionary
|
||||
|
||||
A dictionary completion source for [nvim-cmp](https://github.com/hrsh7th/nvim-cmp).
|
||||
|
||||
This plugin provides one of the easiest way to add desired completion candidates to nvim-cmp.
|
||||
|
||||
![image](https://user-images.githubusercontent.com/82267684/145278036-afa56b20-a365-4165-822f-98db5d7f11b1.png)
|
||||
|
||||
# Requirements
|
||||
|
||||
- neovim >= 0.7
|
||||
- nvim-cmp
|
||||
- [plenary.nvim](https://github.com/nvim-lua/plenary.nvim) (only document feature)
|
||||
- [sqlite.lua](https://github.com/kkharji/sqlite.lua) (only if sqlite option is enabled)
|
||||
|
||||
# Setting
|
||||
|
||||
```lua
|
||||
require("cmp").setup({
|
||||
-- other settings
|
||||
sources = {
|
||||
-- other sources
|
||||
{
|
||||
name = "dictionary",
|
||||
keyword_length = 2,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
local dict = require("cmp_dictionary")
|
||||
|
||||
dict.setup({
|
||||
-- The following are default values.
|
||||
exact = 2,
|
||||
first_case_insensitive = false,
|
||||
document = false,
|
||||
document_command = "wn %s -over",
|
||||
async = false,
|
||||
sqlite = false,
|
||||
max_items = -1,
|
||||
capacity = 5,
|
||||
debug = false,
|
||||
})
|
||||
|
||||
dict.switcher({
|
||||
filetype = {
|
||||
lua = "/path/to/lua.dict",
|
||||
javascript = { "/path/to/js.dict", "/path/to/js2.dict" },
|
||||
},
|
||||
filepath = {
|
||||
[".*xmake.lua"] = { "/path/to/xmake.dict", "/path/to/lua.dict" },
|
||||
["%.tmux.*%.conf"] = { "/path/to/js.dict", "/path/to/js2.dict" },
|
||||
},
|
||||
spelllang = {
|
||||
en = "/path/to/english.dict",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
See help for details.
|
||||
|
||||
# Examples of usage
|
||||
|
||||
See [wiki](https://github.com/uga-rosa/cmp-dictionary/wiki/Examples-of-usage)
|
18
bundle/cmp-dictionary/after/plugin/cmp_dictionary.lua
vendored
Normal file
18
bundle/cmp-dictionary/after/plugin/cmp_dictionary.lua
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
if vim.g.loaded_cmp_dictionary then
|
||||
return
|
||||
end
|
||||
vim.g.loaded_cmp_dictionary = true
|
||||
|
||||
require("cmp").register_source("dictionary", require("cmp_dictionary.source").new())
|
||||
|
||||
local update = require("cmp_dictionary").update
|
||||
|
||||
vim.api.nvim_create_user_command("CmpDictionaryUpdate", update, {})
|
||||
|
||||
vim.api.nvim_create_autocmd("OptionSet", {
|
||||
group = vim.api.nvim_create_augroup("cmp_dictionary_auto_update", {}),
|
||||
pattern = "dictionary",
|
||||
callback = update,
|
||||
})
|
||||
|
||||
update()
|
276
bundle/cmp-dictionary/doc/cmp-dictionary.txt
vendored
Normal file
276
bundle/cmp-dictionary/doc/cmp-dictionary.txt
vendored
Normal file
@ -0,0 +1,276 @@
|
||||
*cmp-dictionary.txt* Dictionary completion source for nvim-cmp
|
||||
|
||||
==============================================================================
|
||||
Contents *cmp-dictionary-contents*
|
||||
|
||||
Introduction |cmp-dictionary-introduction|
|
||||
Commands |cmp-dictionary-commands|
|
||||
Setting |cmp-dictionary-setting|
|
||||
Option |cmp-dictionary-option|
|
||||
Find dictionaries |cmp-dictionary-find-dictionaries|
|
||||
Create dictionaries |cmp-dictionary-create-dictionaries|
|
||||
Lazy loading |cmp-dictionary-lazy-loading|
|
||||
|
||||
==============================================================================
|
||||
Introduction *cmp-dictionary-introduction*
|
||||
|
||||
*cmp-dictionary*
|
||||
cmp-dictionary ~
|
||||
|
||||
A dictionary completion source for nvim-cmp.
|
||||
<https://github.com/hrsh7th/nvim-cmp>
|
||||
|
||||
This plugins refers to the value of |'dictionary'| to load dictionaries and
|
||||
provide words in them as a completion candidates to nvim-cmp. The
|
||||
|'dictionary'| has global and buffer local values, but this plugin uses both.
|
||||
It is recommended to register basic dictionaries that you always want to use
|
||||
globally, and do dictionaries that are only used in special cases locally.
|
||||
See also |cmp-dictionary-switcher|.
|
||||
|
||||
|
||||
Requirements
|
||||
- neovim >= 0.7
|
||||
- nvim-cmp
|
||||
- https://github.com/hrsh7th/nvim-cmp
|
||||
- plenary.nvim (only document feature)
|
||||
- https://github.com/nvim-lua/plenary.nvim
|
||||
|
||||
|
||||
==============================================================================
|
||||
Commands *cmp-dictionary-commands*
|
||||
|
||||
*CmpDictionaryUpdate*
|
||||
:CmpDictionaryUpdate ~
|
||||
In lua, `require("cmp_dictionary").update()`
|
||||
|
||||
Updates the dictionary. It is basically not necessary for the user to
|
||||
use it directly, as it is executed automatically by hooking into the
|
||||
updating of the |'dictionary'|.
|
||||
|
||||
|
||||
==============================================================================
|
||||
Setting *cmp-dictionary-setting*
|
||||
|
||||
Example setting.
|
||||
If you use the default settings, this plugin will work without calling setup.
|
||||
|
||||
>
|
||||
require("cmp").setup({
|
||||
-- other settings
|
||||
sources = {
|
||||
-- other sources
|
||||
{
|
||||
name = "dictionary",
|
||||
keyword_length = 2,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
require("cmp_dictionary").setup({
|
||||
-- Default settings
|
||||
exact = 2,
|
||||
first_case_insensitive = false,
|
||||
document = false,
|
||||
document_command = "wn %s -over",
|
||||
async = false,
|
||||
sqlite = false,
|
||||
max_items = 1000,
|
||||
capacity = 5,
|
||||
debug = false,
|
||||
})
|
||||
<
|
||||
|
||||
|
||||
==============================================================================
|
||||
Option *cmp-dictionary-option*
|
||||
|
||||
*cmp-dictionary-iskeyword*
|
||||
iskeyword ~
|
||||
|
||||
This plugin looks at |iskeyword| in vim. If you use a dictionary that
|
||||
contains special characters, please configure it appropriately. For
|
||||
example, if you want to complete the word `\word`, you would need to
|
||||
add `set iskeyword+=\` to your configuration file.
|
||||
|
||||
|
||||
*cmp-dictionary-exact*
|
||||
exact ~
|
||||
integer (default: 2)
|
||||
|
||||
It decides how many characters at the beginning are used as the exact
|
||||
match. If -1, only candidates with an exact prefix match will be
|
||||
returns.
|
||||
Candidate refinement by this source is only prefix match using this
|
||||
value (Fuzzy matching is left to the nvim-cmp body).
|
||||
|
||||
|
||||
*cmp-dictionary-first_case_insensitive*
|
||||
first_case_insensitive ~
|
||||
boolean (default: false)
|
||||
|
||||
If true, it will ignore the case of the first character. For example,
|
||||
if you have "Example" and "excuse" in your dictionary, typing "Ex"
|
||||
will bring up "Example" and "Excuse" as candidates, while typing "ex"
|
||||
will bring up "example" and "excuse".
|
||||
|
||||
|
||||
*cmp-dictionary-document*
|
||||
document ~
|
||||
boolean (default: false)
|
||||
|
||||
plenary.nvim (https://github.com/nvim-lua/plenary.nvim) is required.
|
||||
If true, activate document using external command. See
|
||||
|cmp-dictionary-document-command|
|
||||
|
||||
|
||||
*cmp-dictionary-document_command*
|
||||
document_command ~
|
||||
string or list-like table (default: 'wn %s -over')
|
||||
|
||||
This command is used above document feature. The '%s' will contain the
|
||||
candidate word. The default `wn` command is wordnet.
|
||||
<https://wordnet.princeton.edu/>
|
||||
|
||||
If a string, the arguments are recognized by separating it with a
|
||||
space character. If you don’t want that, use a table.
|
||||
|
||||
If a table, the first element is the command and the second and
|
||||
subsequent are the arguments. For example, the default setting would
|
||||
be '{"wn", "%s", "-over"}'.
|
||||
|
||||
|
||||
*cmp-dictionary-sqlite*
|
||||
sqlite ~
|
||||
boolean (default: false)
|
||||
|
||||
If true, use sqlite3 database to manage items. Basically, false is
|
||||
faster. If you have a huge dictionary and it takes a long time to
|
||||
initialize, you may want to try it. You will need the following.
|
||||
|
||||
- kkharji/sqlite.lua (https://github.com/kkharji/sqlite.lua)
|
||||
- sqlite (https://sqlite.org/index.html)
|
||||
|
||||
The database path is `stdpath('data') . '/cmp-dictionary.sqlite3'`
|
||||
|
||||
|
||||
*cmp-dictionary-max_items*
|
||||
max_items ~
|
||||
integer (default: -1)
|
||||
|
||||
This is the maximum number of candidates that this source will return
|
||||
to the nvim-cmp body. -1 means no limit. Using a very large dictionary
|
||||
and returning tens of thousands of candidates, completion becomes very
|
||||
laggy. This is an option to avoid that.
|
||||
If you experience lag, setting this option and `exact` appropriately
|
||||
may help.
|
||||
|
||||
|
||||
*cmp-dictionary-capacity*
|
||||
capacity ~
|
||||
integer (default: 5)
|
||||
|
||||
Determines the maximum number of dictionaries to be cached. This will
|
||||
prevent duplicate reads when you switch dictionaries with the settings
|
||||
described above.
|
||||
|
||||
|
||||
*cmp-dictionary-debug*
|
||||
debug ~
|
||||
boolean (default: false)
|
||||
|
||||
If true, debug messages are output.
|
||||
|
||||
|
||||
==============================================================================
|
||||
Utilities *cmp-dictionary-utilities*
|
||||
|
||||
*cmp-dictionary-utilities-switcher*
|
||||
switcher({opts}) ~
|
||||
{opts}: table<string, table<string, string>>
|
||||
Automatically set locally a option |'dictionary'|, and loads
|
||||
dictionaries.
|
||||
- The `filetype` of {opts} has keys are compared to |'filetype'|.
|
||||
- The `filepath` of {opts} has keys of Lua patterns, which are
|
||||
compared to `expand("%:p")`.
|
||||
- The `spelllang` of {opts} has keys are compared to |'spelllang'|.
|
||||
|
||||
Usage example:
|
||||
>
|
||||
local dict = require("cmp_dictionary")
|
||||
dict.switcher({
|
||||
filetype = {
|
||||
lua = "/path/to/lua.dict",
|
||||
javascript = { "/path/to/js.dict", "/path/to/js2.dict" },
|
||||
},
|
||||
filepath = {
|
||||
[".*xmake.lua"] = { "/path/to/xmake.dict", "/path/to/lua.dict" },
|
||||
["%.tmux.*%.conf"] = { "/path/to/js.dict", "/path/to/js2.dict" },
|
||||
},
|
||||
spelllang = {
|
||||
en = "/path/to/english.dict",
|
||||
},
|
||||
})
|
||||
<
|
||||
|
||||
==============================================================================
|
||||
Find dictionaries *cmp-dictionary-find-dictionaries*
|
||||
|
||||
You can download dic from aspell.net or installing by package manager, xbps
|
||||
extract to
|
||||
<https://ftp.gnu.org/gnu/aspell/dict/0index.html>
|
||||
|
||||
>
|
||||
$ ls /usr/share/dict/
|
||||
american-english british-english words
|
||||
<
|
||||
|
||||
After installing aspell and dictionary you want, run following command to get
|
||||
dic for this plugin (plain text).
|
||||
|
||||
>
|
||||
aspell -d <lang> dump master | aspell -l <lang> expand > my.dict
|
||||
<
|
||||
|
||||
|
||||
==============================================================================
|
||||
Create dictionaries *cmp-dictionary-create-dictionaries*
|
||||
|
||||
The dictionary is recognized as a list delimited by '%s'. '%s' is a space,
|
||||
','',', or '. For example, if you use the following file as a dictionary, the
|
||||
source to be added is'{"hello", "world", "!"}’.
|
||||
|
||||
>
|
||||
hello
|
||||
world !
|
||||
<
|
||||
|
||||
|
||||
==============================================================================
|
||||
Lazy loading *cmp-dictionary-lazy-loading*
|
||||
|
||||
By default, reading dictionaries are fired by `BufEnter`. So if this plugin
|
||||
loading is set to `InsertEnter` or something, the dictionary will not load and
|
||||
no candidates will appear. The workaround is to fire this update yourself when
|
||||
the plugin is loaded (after setup).
|
||||
|
||||
For example, if you use packer.nvim, you can use
|
||||
|
||||
>
|
||||
use({
|
||||
"hrsh7th/nvim-cmp",
|
||||
event = "InsertEnter",
|
||||
-- other setting
|
||||
})
|
||||
use({
|
||||
"uga-rosa/cmp-dictionary",
|
||||
after = "nvim-cmp",
|
||||
config = function()
|
||||
require("cmp_dictionary").update() -- THIS
|
||||
-- OR
|
||||
-- vim.cmd("CmpDictionaryUpdate")
|
||||
end
|
||||
})
|
||||
<
|
||||
|
||||
|
||||
vim:tw=78:ts=8:noet:ft=help:norl:
|
147
bundle/cmp-dictionary/lua/cmp_dictionary/caches.lua
vendored
Normal file
147
bundle/cmp-dictionary/lua/cmp_dictionary/caches.lua
vendored
Normal file
@ -0,0 +1,147 @@
|
||||
local util = require("cmp_dictionary.util")
|
||||
local lfu = require("cmp_dictionary.lfu")
|
||||
local config = require("cmp_dictionary.config")
|
||||
local utf8 = require("cmp_dictionary.lib.utf8")
|
||||
local Async = require("cmp_dictionary.kit.Async")
|
||||
local Worker = require("cmp_dictionary.kit.Thread.Worker")
|
||||
|
||||
---@class DictionaryData
|
||||
---@field items lsp.CompletionItem[]
|
||||
---@field mtime integer
|
||||
---@field path string
|
||||
|
||||
local Caches = {
|
||||
---@type DictionaryData[]
|
||||
valid = {},
|
||||
}
|
||||
|
||||
local just_updated = false
|
||||
local dictCache = lfu.init(config.get("capacity"))
|
||||
|
||||
---Filter to keep only dictionaries that have been updated or have not yet been cached.
|
||||
---@return {path: string, mtime: integer}[]
|
||||
local function need_to_load()
|
||||
local dictionaries = util.get_dictionaries()
|
||||
local updated_or_new = {}
|
||||
for _, dict in ipairs(dictionaries) do
|
||||
local path = vim.fn.expand(dict)
|
||||
if util.bool_fn.filereadable(path) then
|
||||
local mtime = vim.fn.getftime(path)
|
||||
local cache = dictCache:get(path)
|
||||
if cache and cache.mtime == mtime then
|
||||
table.insert(Caches.valid, cache)
|
||||
else
|
||||
table.insert(updated_or_new, { path = path, mtime = mtime })
|
||||
end
|
||||
end
|
||||
end
|
||||
return updated_or_new
|
||||
end
|
||||
|
||||
---Create dictionary data from buffers
|
||||
---@param path string
|
||||
---@param name string
|
||||
---@return lsp.CompletionItem[] items
|
||||
local read_items = Worker.new(function(path, name)
|
||||
local buffer = require("cmp_dictionary.util").read_file_sync(path)
|
||||
|
||||
local items = {}
|
||||
local detail = ("belong to `%s`"):format(name)
|
||||
for w in vim.gsplit(buffer, "%s+") do
|
||||
if w ~= "" then
|
||||
table.insert(items, { label = w, detail = detail })
|
||||
end
|
||||
end
|
||||
table.sort(items, function(item1, item2)
|
||||
return item1.label < item2.label
|
||||
end)
|
||||
|
||||
return items
|
||||
end)
|
||||
|
||||
---@param path string
|
||||
---@param mtime integer
|
||||
---@return cmp_dictionary.kit.Async.AsyncTask
|
||||
local function cache_update(path, mtime)
|
||||
local name = vim.fn.fnamemodify(path, ":t")
|
||||
return read_items(path, name):next(function(items)
|
||||
local cache = {
|
||||
items = items,
|
||||
mtime = mtime,
|
||||
path = path,
|
||||
}
|
||||
dictCache:set(path, cache)
|
||||
table.insert(Caches.valid, cache)
|
||||
end)
|
||||
end
|
||||
|
||||
local function update()
|
||||
local buftype = vim.api.nvim_buf_get_option(0, "buftype")
|
||||
if buftype ~= "" then
|
||||
return
|
||||
end
|
||||
|
||||
Caches.valid = {}
|
||||
|
||||
Async.all(vim.tbl_map(function(n)
|
||||
return cache_update(n.path, n.mtime)
|
||||
end, need_to_load())):next(function()
|
||||
just_updated = true
|
||||
end)
|
||||
end
|
||||
|
||||
function Caches.update()
|
||||
util.debounce("update", update, 100)
|
||||
end
|
||||
|
||||
---@param req string
|
||||
---@param isIncomplete boolean
|
||||
---@return lsp.CompletionItem[] items
|
||||
---@return boolean isIncomplete
|
||||
function Caches.request(req, isIncomplete)
|
||||
local items = {}
|
||||
isIncomplete = isIncomplete or false
|
||||
|
||||
local ok, offset, codepoint
|
||||
ok, offset = pcall(utf8.offset, req, -1)
|
||||
if not ok then
|
||||
return items, isIncomplete
|
||||
end
|
||||
ok, codepoint = pcall(utf8.codepoint, req, offset)
|
||||
if not ok then
|
||||
return items, isIncomplete
|
||||
end
|
||||
|
||||
local req_next = req:sub(1, offset - 1) .. utf8.char(codepoint + 1)
|
||||
|
||||
local max_items = config.get("max_items")
|
||||
for _, cache in pairs(Caches.valid) do
|
||||
local start = util.binary_search(cache.items, req, function(vector, index, key)
|
||||
return vector[index].label >= key
|
||||
end)
|
||||
local last = util.binary_search(cache.items, req_next, function(vector, index, key)
|
||||
return vector[index].label >= key
|
||||
end) - 1
|
||||
if start > 0 and last > 0 and start <= last then
|
||||
if max_items > 0 and last >= start + max_items then
|
||||
last = start + max_items
|
||||
isIncomplete = true
|
||||
end
|
||||
for i = start, last do
|
||||
local item = cache.items[i]
|
||||
table.insert(items, item)
|
||||
end
|
||||
end
|
||||
end
|
||||
return items, isIncomplete
|
||||
end
|
||||
|
||||
function Caches.is_just_updated()
|
||||
if just_updated then
|
||||
just_updated = false
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
return Caches
|
38
bundle/cmp-dictionary/lua/cmp_dictionary/config.lua
vendored
Normal file
38
bundle/cmp-dictionary/lua/cmp_dictionary/config.lua
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
local M = {}
|
||||
|
||||
M.config = {
|
||||
exact = 2,
|
||||
first_case_insensitive = false,
|
||||
document = false,
|
||||
document_command = "wn %s -over",
|
||||
sqlite = false,
|
||||
max_items = -1,
|
||||
capacity = 5,
|
||||
debug = false,
|
||||
}
|
||||
|
||||
---@param opt table
|
||||
function M.setup(opt)
|
||||
vim.validate({ opt = { opt, "table" } })
|
||||
|
||||
M.config = vim.tbl_extend("keep", opt, M.config)
|
||||
|
||||
local c = assert(M.config)
|
||||
vim.validate({
|
||||
exact = { c.exact, "n" },
|
||||
first_case_insensitive = { c.first_case_insensitive, "b" },
|
||||
document = { c.document, "b" },
|
||||
document_command = { c.document_command, { "s", "t" } },
|
||||
max_items = { c.max_items, "n" },
|
||||
capacity = { c.capacity, "n" },
|
||||
debug = { c.debug, "b" },
|
||||
})
|
||||
end
|
||||
|
||||
---@param name string
|
||||
---@return unknown
|
||||
function M.get(name)
|
||||
return M.config[name]
|
||||
end
|
||||
|
||||
return M
|
194
bundle/cmp-dictionary/lua/cmp_dictionary/db.lua
vendored
Normal file
194
bundle/cmp-dictionary/lua/cmp_dictionary/db.lua
vendored
Normal file
@ -0,0 +1,194 @@
|
||||
local util = require("cmp_dictionary.util")
|
||||
local config = require("cmp_dictionary.config")
|
||||
local Async = require("cmp_dictionary.kit.Async")
|
||||
local Worker = require("cmp_dictionary.kit.Thread.Worker")
|
||||
|
||||
local SQLite = {}
|
||||
|
||||
local just_updated = false
|
||||
|
||||
---@return table db
|
||||
function SQLite:open()
|
||||
if self.db then
|
||||
return self.db
|
||||
end
|
||||
|
||||
local ok, sqlite = pcall(require, "sqlite")
|
||||
if not ok or sqlite == nil then
|
||||
error("[cmp-dictionary] sqlite.lua is not installed!")
|
||||
end
|
||||
|
||||
local db_path = vim.fn.stdpath("data") .. "/cmp-dictionary.sqlite3"
|
||||
self.db = sqlite:open(db_path)
|
||||
if not self.db then
|
||||
error("[cmp-dictionary] Error in opening DB")
|
||||
end
|
||||
|
||||
if not self.db:exists("dictionary") then
|
||||
self.db:create("dictionary", {
|
||||
filepath = { "text", primary = true },
|
||||
mtime = { "integer", required = true },
|
||||
valid = { "integer", default = 1 },
|
||||
})
|
||||
end
|
||||
|
||||
if not self.db:exists("items") then
|
||||
self.db:create("items", {
|
||||
label = { "text", required = true },
|
||||
detail = { "text", required = true },
|
||||
filepath = { "text", required = true },
|
||||
documentation = "text",
|
||||
})
|
||||
end
|
||||
|
||||
vim.api.nvim_create_autocmd("VimLeave", {
|
||||
group = vim.api.nvim_create_augroup("cmp-dictionary-database", {}),
|
||||
callback = function()
|
||||
self.db:close()
|
||||
end,
|
||||
})
|
||||
|
||||
return self.db
|
||||
end
|
||||
|
||||
function SQLite:exists_index(name)
|
||||
self:open()
|
||||
-- Can't use db:select() for sqlite_master.
|
||||
local result = self.db:eval("SELECT * FROM sqlite_master WHERE type = 'index' AND name = ?", name)
|
||||
return type(result) == "table" and #result == 1
|
||||
end
|
||||
|
||||
function SQLite:index(tbl_name, column)
|
||||
local name = column .. "index"
|
||||
if SQLite:exists_index(name) then
|
||||
self.db:execute("DROP INDEX " .. name)
|
||||
end
|
||||
self.db:execute(("CREATE INDEX %s ON %s(%s)"):format(name, tbl_name, column))
|
||||
end
|
||||
|
||||
local function need_to_load(db)
|
||||
local dictionaries = util.get_dictionaries()
|
||||
local updated_or_new = {}
|
||||
for _, dictionary in ipairs(dictionaries) do
|
||||
local path = vim.fn.expand(dictionary)
|
||||
if util.bool_fn.filereadable(path) then
|
||||
local mtime = vim.fn.getftime(path)
|
||||
local mtime_cache = db:select("dictionary", { select = "mtime", where = { filepath = path } })
|
||||
if mtime_cache[1] and mtime_cache[1].mtime == mtime then
|
||||
db:update("dictionary", {
|
||||
set = { valid = 1 },
|
||||
where = { filepath = path },
|
||||
})
|
||||
else
|
||||
table.insert(updated_or_new, { path = path, mtime = mtime })
|
||||
end
|
||||
end
|
||||
end
|
||||
return updated_or_new
|
||||
end
|
||||
|
||||
local read_items = Worker.new(function(path, name)
|
||||
local buffer = require("cmp_dictionary.util").read_file_sync(path)
|
||||
|
||||
local detail = string.format("belong to `%s`", name)
|
||||
local items = {}
|
||||
for w in vim.gsplit(buffer, "%s+") do
|
||||
if w ~= "" then
|
||||
table.insert(items, { label = w, detail = detail, filepath = path })
|
||||
end
|
||||
end
|
||||
return items
|
||||
end)
|
||||
|
||||
local function update(db)
|
||||
local buftype = vim.api.nvim_buf_get_option(0, "buftype")
|
||||
if buftype ~= "" then
|
||||
return
|
||||
end
|
||||
|
||||
db:update("dictionary", { set = { valid = 0 } })
|
||||
|
||||
Async.all(vim.tbl_map(function(n)
|
||||
local path, mtime = n.path, n.mtime
|
||||
local name = vim.fn.fnamemodify(path, ":t")
|
||||
return read_items(path, name):next(function(items)
|
||||
db:delete("items", { where = { filepath = path } })
|
||||
db:insert("items", items)
|
||||
|
||||
-- Index for fast search
|
||||
SQLite:index("items", "label")
|
||||
SQLite:index("items", "filepath")
|
||||
|
||||
-- If there is no data matching where, it automatically switches to insert.
|
||||
db:update("dictionary", {
|
||||
set = { mtime = mtime, valid = 1 },
|
||||
where = { filepath = path },
|
||||
})
|
||||
end)
|
||||
end, need_to_load(db))):next(function()
|
||||
just_updated = true
|
||||
end)
|
||||
end
|
||||
|
||||
local DB = {}
|
||||
|
||||
function DB.update()
|
||||
local db = SQLite:open()
|
||||
util.debounce("update_db", function()
|
||||
update(db)
|
||||
end, 100)
|
||||
end
|
||||
|
||||
---@param req string
|
||||
---@return lsp.CompletionItem[] items
|
||||
---@return boolean isIncomplete
|
||||
function DB.request(req, _)
|
||||
local db = SQLite:open()
|
||||
local max_items = config.get("max_items")
|
||||
local items = db:eval(
|
||||
[[
|
||||
SELECT label, detail, documentation FROM items
|
||||
WHERE filepath IN (SELECT filepath FROM dictionary WHERE valid = 1)
|
||||
AND label GLOB :a
|
||||
LIMIT :b
|
||||
]],
|
||||
{ a = req .. "*", b = max_items }
|
||||
)
|
||||
if type(items) == "table" then
|
||||
return items, #items == max_items
|
||||
else
|
||||
return {}, false
|
||||
end
|
||||
end
|
||||
|
||||
function DB.is_just_updated()
|
||||
if just_updated then
|
||||
just_updated = false
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---@param completion_item lsp.CompletionItem
|
||||
---@param callback fun(completion_item: lsp.CompletionItem|nil)
|
||||
function DB.document(completion_item, callback)
|
||||
if completion_item.documentation then
|
||||
callback(completion_item)
|
||||
return
|
||||
end
|
||||
|
||||
local db = SQLite:open()
|
||||
local label = completion_item.label
|
||||
require("cmp_dictionary.document")(completion_item, function(completion_item_)
|
||||
if completion_item_ and completion_item_.documentation then
|
||||
-- By first_case_insensitive, the case of the label is ambiguous.
|
||||
db:eval(
|
||||
"UPDATE items SET documentation = :a WHERE label like :b",
|
||||
{ a = completion_item_.documentation, b = label }
|
||||
)
|
||||
end
|
||||
callback(completion_item_)
|
||||
end)
|
||||
end
|
||||
|
||||
return DB
|
76
bundle/cmp-dictionary/lua/cmp_dictionary/document.lua
vendored
Normal file
76
bundle/cmp-dictionary/lua/cmp_dictionary/document.lua
vendored
Normal file
@ -0,0 +1,76 @@
|
||||
local config = require("cmp_dictionary.config")
|
||||
|
||||
local document_cache = require("cmp_dictionary.lfu").init(100)
|
||||
|
||||
---@param word string
|
||||
---@return string
|
||||
---@return string[]
|
||||
local function get_command(word)
|
||||
local command = config.get("document_command")
|
||||
|
||||
local args
|
||||
if type(command) == "table" then
|
||||
-- copy
|
||||
args = {}
|
||||
for i, v in ipairs(command) do
|
||||
args[i] = v
|
||||
end
|
||||
elseif type(command) == "string" then
|
||||
args = vim.split(command, " ")
|
||||
end
|
||||
|
||||
local cmd = table.remove(args, 1)
|
||||
for i, arg in ipairs(args) do
|
||||
if arg:find("%s", 1, true) then
|
||||
args[i] = arg:format(word)
|
||||
end
|
||||
end
|
||||
|
||||
return cmd, args
|
||||
end
|
||||
|
||||
---@param completion_item lsp.CompletionItem
|
||||
---@param callback fun(completion_item: lsp.CompletionItem|nil)
|
||||
local function get_document(completion_item, callback)
|
||||
local ok, Job = pcall(require, "plenary.job")
|
||||
if not ok then
|
||||
vim.notify("[cmp-dictionary] document feature requires plenary.nvim")
|
||||
return
|
||||
end
|
||||
|
||||
local word = completion_item.label
|
||||
local command, args = get_command(word)
|
||||
if not command then
|
||||
callback(completion_item)
|
||||
return
|
||||
end
|
||||
|
||||
Job:new({
|
||||
command = command,
|
||||
args = args,
|
||||
on_exit = vim.schedule_wrap(function(j)
|
||||
local result = table.concat(j:result(), "\n")
|
||||
document_cache:set(word, result)
|
||||
completion_item.documentation = result
|
||||
callback(completion_item)
|
||||
end),
|
||||
}):start()
|
||||
end
|
||||
|
||||
---@param completion_item lsp.CompletionItem
|
||||
---@param callback fun(completion_item: lsp.CompletionItem|nil)
|
||||
local function resolve(completion_item, callback)
|
||||
if config.get("document") then
|
||||
local cached = document_cache:get(completion_item.label)
|
||||
if cached then
|
||||
completion_item.documentation = cached
|
||||
callback(completion_item)
|
||||
else
|
||||
get_document(completion_item, callback)
|
||||
end
|
||||
else
|
||||
callback(completion_item)
|
||||
end
|
||||
end
|
||||
|
||||
return resolve
|
87
bundle/cmp-dictionary/lua/cmp_dictionary/init.lua
vendored
Normal file
87
bundle/cmp-dictionary/lua/cmp_dictionary/init.lua
vendored
Normal file
@ -0,0 +1,87 @@
|
||||
local config = require("cmp_dictionary.config")
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.setup(opt)
|
||||
require("cmp_dictionary.config").setup(opt)
|
||||
end
|
||||
|
||||
function M.update()
|
||||
if config.get("sqlite") then
|
||||
require("cmp_dictionary.db").update()
|
||||
else
|
||||
require("cmp_dictionary.caches").update()
|
||||
end
|
||||
end
|
||||
|
||||
---@alias dictionaries table<string, string | string[]>
|
||||
---#key is a pattern, value is a value of option 'dictionary'.
|
||||
|
||||
---@param opt { filetype: dictionaries, filepath: dictionaries, spelllang: dictionaries }
|
||||
--- Usage:
|
||||
--- require("cmp_dictionary").switcher({
|
||||
--- filetype = {
|
||||
--- lua = "/path/to/lua.dict",
|
||||
--- javascript = { "/path/to/js.dict", "/path/to/js2.dict" },
|
||||
--- },
|
||||
--- filepath = {
|
||||
--- ["*xmake.lua"] = { "/path/to/xmake.dict", "/path/to/lua.dict" }
|
||||
--- [".tmux*.conf"] = { "/path/to/js.dict", "/path/to/js2.dict" },
|
||||
--- },
|
||||
--- spelllang = {
|
||||
--- en = "/path/to/english.dict",
|
||||
--- },
|
||||
-- })
|
||||
function M.switcher(opt)
|
||||
vim.validate({ opt = { opt, "table" } })
|
||||
|
||||
local id = vim.api.nvim_create_augroup("cmp_dictionary", {})
|
||||
|
||||
local function callback()
|
||||
vim.opt_local.dictionary = {}
|
||||
if opt.filetype then
|
||||
vim.opt_local.dictionary:append(opt.filetype[vim.bo.filetype] or "")
|
||||
end
|
||||
if opt.filepath then
|
||||
local fullpath = vim.fn.expand("%:p")
|
||||
for path, dict in pairs(opt.filepath) do
|
||||
if fullpath:find(path) then
|
||||
vim.opt_local.dictionary:append(dict)
|
||||
end
|
||||
end
|
||||
end
|
||||
if opt.spelllang then
|
||||
for _, sl in ipairs(vim.opt.spelllang:get()) do
|
||||
vim.opt_local.dictionary:append(opt.spelllang[sl] or "")
|
||||
end
|
||||
end
|
||||
M.update()
|
||||
end
|
||||
|
||||
if opt.filetype then
|
||||
vim.api.nvim_create_autocmd("FileType", {
|
||||
group = id,
|
||||
pattern = vim.tbl_keys(opt.filetype),
|
||||
callback = callback,
|
||||
})
|
||||
end
|
||||
|
||||
if opt.filepath then
|
||||
vim.api.nvim_create_autocmd("BufEnter", {
|
||||
group = id,
|
||||
callback = callback,
|
||||
})
|
||||
end
|
||||
|
||||
if opt.spelllang then
|
||||
vim.api.nvim_create_autocmd("OptionSet", {
|
||||
group = id,
|
||||
pattern = "spelllang",
|
||||
callback = callback,
|
||||
})
|
||||
end
|
||||
|
||||
callback()
|
||||
end
|
||||
|
||||
return M
|
34
bundle/cmp-dictionary/lua/cmp_dictionary/init_spec.lua
vendored
Normal file
34
bundle/cmp-dictionary/lua/cmp_dictionary/init_spec.lua
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
local main = require("cmp_dictionary")
|
||||
|
||||
local function dictionary()
|
||||
return vim.opt_local.dictionary:get()
|
||||
end
|
||||
|
||||
describe("Test for init.lua", function()
|
||||
before_each(function()
|
||||
vim.opt_local.dictionary = {}
|
||||
end)
|
||||
|
||||
describe("switcher", function()
|
||||
describe("filetype", function()
|
||||
it("single dictionary", function()
|
||||
main.switcher({
|
||||
filetype = {
|
||||
lua = "/path/to/lua.dict",
|
||||
},
|
||||
})
|
||||
vim.opt.filetype = "lua"
|
||||
assert.are.same({ "/path/to/lua.dict" }, dictionary())
|
||||
end)
|
||||
it("multi dictionaries", function()
|
||||
main.switcher({
|
||||
filetype = {
|
||||
javascript = { "/path/to/js.dict", "/path/to/js2.dict" },
|
||||
},
|
||||
})
|
||||
vim.opt.filetype = "javascript"
|
||||
assert.are.same({ "/path/to/js.dict", "/path/to/js2.dict" }, dictionary())
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end)
|
70
bundle/cmp-dictionary/lua/cmp_dictionary/kit/App/Cache.lua
vendored
Normal file
70
bundle/cmp-dictionary/lua/cmp_dictionary/kit/App/Cache.lua
vendored
Normal file
@ -0,0 +1,70 @@
|
||||
---Create cache key.
|
||||
---@private
|
||||
---@param key string[]|string
|
||||
---@return string
|
||||
local function _key(key)
|
||||
if type(key) == 'table' then
|
||||
return table.concat(key, ':')
|
||||
end
|
||||
return key
|
||||
end
|
||||
|
||||
---@class cmp_dictionary.kit.App.Cache
|
||||
---@field private keys table<string, boolean>
|
||||
---@field private entries table<string, any>
|
||||
local Cache = {}
|
||||
Cache.__index = Cache
|
||||
|
||||
---Create new cache instance.
|
||||
function Cache.new()
|
||||
local self = setmetatable({}, Cache)
|
||||
self.keys = {}
|
||||
self.entries = {}
|
||||
return self
|
||||
end
|
||||
|
||||
---Get cache entry.
|
||||
---@param key string[]|string
|
||||
---@return any
|
||||
function Cache:get(key)
|
||||
return self.entries[_key(key)]
|
||||
end
|
||||
|
||||
---Set cache entry.
|
||||
---@param key string[]|string
|
||||
---@param val any
|
||||
function Cache:set(key, val)
|
||||
key = _key(key)
|
||||
self.keys[key] = true
|
||||
self.entries[key] = val
|
||||
end
|
||||
|
||||
---Delete cache entry.
|
||||
---@param key string[]|string
|
||||
function Cache:del(key)
|
||||
key = _key(key)
|
||||
self.keys[key] = nil
|
||||
self.entries[key] = nil
|
||||
end
|
||||
|
||||
---Return this cache has the key entry or not.
|
||||
---@param key string[]|string
|
||||
---@return boolean
|
||||
function Cache:has(key)
|
||||
key = _key(key)
|
||||
return not not self.keys[key]
|
||||
end
|
||||
|
||||
---Ensure cache entry.
|
||||
---@generic T
|
||||
---@param key string[]|string
|
||||
---@param callback function(): T
|
||||
---@return T
|
||||
function Cache:ensure(key, callback)
|
||||
if not self:has(key) then
|
||||
self:set(key, callback())
|
||||
end
|
||||
return self:get(key)
|
||||
end
|
||||
|
||||
return Cache
|
58
bundle/cmp-dictionary/lua/cmp_dictionary/kit/App/Character.lua
vendored
Normal file
58
bundle/cmp-dictionary/lua/cmp_dictionary/kit/App/Character.lua
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
---@diagnostic disable: discard-returns
|
||||
|
||||
local Character = {}
|
||||
|
||||
---@type table<integer, string>
|
||||
Character.alpha = {}
|
||||
string.gsub('abcdefghijklmnopqrstuvwxyz', '.', function(char)
|
||||
Character.alpha[string.byte(char)] = char
|
||||
end)
|
||||
|
||||
---@type table<integer, string>
|
||||
Character.digit = {}
|
||||
string.gsub('1234567890', '.', function(char)
|
||||
Character.digit[string.byte(char)] = char
|
||||
end)
|
||||
|
||||
---@type table<integer, string>
|
||||
Character.white = {}
|
||||
string.gsub(' \t\n', '.', function(char)
|
||||
Character.white[string.byte(char)] = char
|
||||
end)
|
||||
|
||||
---Return specified byte is alpha or not.
|
||||
---@param byte integer
|
||||
---@return boolean
|
||||
function Character.is_alpha(byte)
|
||||
return Character.alpha[byte] ~= nil or Character.alpha[byte + 32] ~= nil
|
||||
end
|
||||
|
||||
---Return specified byte is digit or not.
|
||||
---@param byte integer
|
||||
---@return boolean
|
||||
function Character.is_digit(byte)
|
||||
return Character.digit[byte] ~= nil
|
||||
end
|
||||
|
||||
---Return specified byte is alpha or not.
|
||||
---@param byte integer
|
||||
---@return boolean
|
||||
function Character.is_alnum(byte)
|
||||
return Character.is_alpha(byte) or Character.is_digit(byte)
|
||||
end
|
||||
|
||||
---Return specified byte is white or not.
|
||||
---@param byte integer
|
||||
---@return boolean
|
||||
function Character.is_white(byte)
|
||||
return Character.white[byte] ~= nil
|
||||
end
|
||||
|
||||
---Return specified byte is symbol or not.
|
||||
---@param byte integer
|
||||
---@return boolean
|
||||
function Character.is_symbol(byte)
|
||||
return not Character.is_alnum(byte) and not Character.is_white(byte)
|
||||
end
|
||||
|
||||
return Character
|
99
bundle/cmp-dictionary/lua/cmp_dictionary/kit/App/Config.lua
vendored
Normal file
99
bundle/cmp-dictionary/lua/cmp_dictionary/kit/App/Config.lua
vendored
Normal file
@ -0,0 +1,99 @@
|
||||
local kit = require('cmp_dictionary.kit')
|
||||
local Cache = require('cmp_dictionary.kit.App.Cache')
|
||||
|
||||
---@class cmp_dictionary.kit.App.Config.Schema
|
||||
|
||||
---@alias cmp_dictionary.kit.App.Config.SchemaInternal cmp_dictionary.kit.App.Config.Schema|{ revision: integer }
|
||||
|
||||
---@class cmp_dictionary.kit.App.Config
|
||||
---@field private _cache cmp_dictionary.kit.App.Cache
|
||||
---@field private _default cmp_dictionary.kit.App.Config.SchemaInternal
|
||||
---@field private _global cmp_dictionary.kit.App.Config.SchemaInternal
|
||||
---@field private _filetype table<string, cmp_dictionary.kit.App.Config.SchemaInternal>
|
||||
---@field private _buffer table<integer, cmp_dictionary.kit.App.Config.SchemaInternal>
|
||||
local Config = {}
|
||||
Config.__index = Config
|
||||
|
||||
---Create new config instance.
|
||||
---@param default cmp_dictionary.kit.App.Config.Schema
|
||||
function Config.new(default)
|
||||
local self = setmetatable({}, Config)
|
||||
self._cache = Cache.new()
|
||||
self._default = default
|
||||
self._global = {}
|
||||
self._filetype = {}
|
||||
self._buffer = {}
|
||||
return self
|
||||
end
|
||||
|
||||
---Update global config.
|
||||
---@param config cmp_dictionary.kit.App.Config.Schema
|
||||
function Config:global(config)
|
||||
local revision = (self._global.revision or 1) + 1
|
||||
self._global = config or {}
|
||||
self._global.revision = revision
|
||||
end
|
||||
|
||||
---Update filetype config.
|
||||
---@param filetypes string|string[]
|
||||
---@param config cmp_dictionary.kit.App.Config.Schema
|
||||
function Config:filetype(filetypes, config)
|
||||
for _, filetype in ipairs(kit.to_array(filetypes)) do
|
||||
local revision = ((self._filetype[filetype] or {}).revision or 1) + 1
|
||||
self._filetype[filetype] = config or {}
|
||||
self._filetype[filetype].revision = revision
|
||||
end
|
||||
end
|
||||
|
||||
---Update filetype config.
|
||||
---@param bufnr integer
|
||||
---@param config cmp_dictionary.kit.App.Config.Schema
|
||||
function Config:buffer(bufnr, config)
|
||||
bufnr = bufnr == 0 and vim.api.nvim_get_current_buf() or bufnr
|
||||
local revision = ((self._buffer[bufnr] or {}).revision or 1) + 1
|
||||
self._buffer[bufnr] = config or {}
|
||||
self._buffer[bufnr].revision = revision
|
||||
end
|
||||
|
||||
---Get current configuration.
|
||||
---@return cmp_dictionary.kit.App.Config.Schema
|
||||
function Config:get()
|
||||
local filetype = vim.api.nvim_buf_get_option(0, 'filetype')
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
return self._cache:ensure({
|
||||
tostring(self._global.revision or 0),
|
||||
tostring((self._buffer[bufnr] or {}).revision or 0),
|
||||
tostring((self._filetype[filetype] or {}).revision or 0),
|
||||
}, function()
|
||||
local config = self._default
|
||||
config = kit.merge(self._global, config)
|
||||
config = kit.merge(self._filetype[filetype] or {}, config)
|
||||
config = kit.merge(self._buffer[bufnr] or {}, config)
|
||||
config.revision = nil
|
||||
return config
|
||||
end)
|
||||
end
|
||||
|
||||
---Create setup interface.
|
||||
---@return fun(config: cmp_dictionary.kit.App.Config.Schema)|{ filetype: fun(filetypes: string|string[], config: cmp_dictionary.kit.App.Config.Schema), buffer: fun(bufnr: integer, config: cmp_dictionary.kit.App.Config.Schema) }
|
||||
function Config:create_setup_interface()
|
||||
return setmetatable({
|
||||
---@param filetypes string|string[]
|
||||
---@param config cmp_dictionary.kit.App.Config.Schema
|
||||
filetype = function(filetypes, config)
|
||||
self:filetype(filetypes, config)
|
||||
end,
|
||||
---@param bufnr integer
|
||||
---@param config cmp_dictionary.kit.App.Config.Schema
|
||||
buffer = function(bufnr, config)
|
||||
self:buffer(bufnr, config)
|
||||
end,
|
||||
}, {
|
||||
---@param config cmp_dictionary.kit.App.Config.Schema
|
||||
__call = function(_, config)
|
||||
self:global(config)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
return Config
|
77
bundle/cmp-dictionary/lua/cmp_dictionary/kit/App/Event.lua
vendored
Normal file
77
bundle/cmp-dictionary/lua/cmp_dictionary/kit/App/Event.lua
vendored
Normal file
@ -0,0 +1,77 @@
|
||||
---@class cmp_dictionary.kit.App.Event
|
||||
---@field private _events table<string, table>
|
||||
local Event = {}
|
||||
Event.__index = Event
|
||||
|
||||
---Create new Event.
|
||||
function Event.new()
|
||||
local self = setmetatable({}, Event)
|
||||
self._events = {}
|
||||
return self
|
||||
end
|
||||
|
||||
---Register listener.
|
||||
---@param name string
|
||||
---@param listener function
|
||||
---@return function
|
||||
function Event:on(name, listener)
|
||||
self._events[name] = self._events[name] or {}
|
||||
table.insert(self._events[name], listener)
|
||||
return function()
|
||||
self:off(name, listener)
|
||||
end
|
||||
end
|
||||
|
||||
---Register once listener.
|
||||
---@param name string
|
||||
---@param listener function
|
||||
function Event:once(name, listener)
|
||||
local off
|
||||
off = self:on(name, function(...)
|
||||
listener(...)
|
||||
off()
|
||||
end)
|
||||
end
|
||||
|
||||
---Off specified listener from event.
|
||||
---@param name string
|
||||
---@param listener function
|
||||
function Event:off(name, listener)
|
||||
self._events[name] = self._events[name] or {}
|
||||
if not listener then
|
||||
self._events[name] = nil
|
||||
else
|
||||
for i = #self._events[name], 1, -1 do
|
||||
if self._events[name][i] == listener then
|
||||
table.remove(self._events[name], i)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Return if the listener is registered.
|
||||
---@param name string
|
||||
---@param listener? function
|
||||
---@return boolean
|
||||
function Event:has(name, listener)
|
||||
self._events[name] = self._events[name] or {}
|
||||
for _, v in ipairs(self._events[name]) do
|
||||
if v == listener then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---Emit event.
|
||||
---@param name string
|
||||
---@vararg any
|
||||
function Event:emit(name, ...)
|
||||
self._events[name] = self._events[name] or {}
|
||||
for _, v in ipairs(self._events[name]) do
|
||||
v(...)
|
||||
end
|
||||
end
|
||||
|
||||
return Event
|
241
bundle/cmp-dictionary/lua/cmp_dictionary/kit/Async/AsyncTask.lua
vendored
Normal file
241
bundle/cmp-dictionary/lua/cmp_dictionary/kit/Async/AsyncTask.lua
vendored
Normal file
@ -0,0 +1,241 @@
|
||||
---@diagnostic disable: invisible
|
||||
local uv = require('luv')
|
||||
local kit = require('cmp_dictionary.kit')
|
||||
|
||||
local is_thread = vim.is_thread()
|
||||
|
||||
---@class cmp_dictionary.kit.Async.AsyncTask
|
||||
---@field private value any
|
||||
---@field private status cmp_dictionary.kit.Async.AsyncTask.Status
|
||||
---@field private synced boolean
|
||||
---@field private chained boolean
|
||||
---@field private children (fun(): any)[]
|
||||
local AsyncTask = {}
|
||||
AsyncTask.__index = AsyncTask
|
||||
|
||||
---Settle the specified task.
|
||||
---@param task cmp_dictionary.kit.Async.AsyncTask
|
||||
---@param status cmp_dictionary.kit.Async.AsyncTask.Status
|
||||
---@param value any
|
||||
local function settle(task, status, value)
|
||||
task.status = status
|
||||
task.value = value
|
||||
for _, c in ipairs(task.children) do
|
||||
c()
|
||||
end
|
||||
|
||||
if status == AsyncTask.Status.Rejected then
|
||||
if not task.chained and not task.synced then
|
||||
local timer = uv.new_timer()
|
||||
timer:start(
|
||||
0,
|
||||
0,
|
||||
kit.safe_schedule_wrap(function()
|
||||
timer:stop()
|
||||
timer:close()
|
||||
if not task.chained and not task.synced then
|
||||
AsyncTask.on_unhandled_rejection(value)
|
||||
end
|
||||
end)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@enum cmp_dictionary.kit.Async.AsyncTask.Status
|
||||
AsyncTask.Status = {
|
||||
Pending = 0,
|
||||
Fulfilled = 1,
|
||||
Rejected = 2,
|
||||
}
|
||||
|
||||
---Handle unhandled rejection.
|
||||
---@param err any
|
||||
function AsyncTask.on_unhandled_rejection(err)
|
||||
error('AsyncTask.on_unhandled_rejection: ' .. tostring(err))
|
||||
end
|
||||
|
||||
---Return the value is AsyncTask or not.
|
||||
---@param value any
|
||||
---@return boolean
|
||||
function AsyncTask.is(value)
|
||||
return getmetatable(value) == AsyncTask
|
||||
end
|
||||
|
||||
---Resolve all tasks.
|
||||
---@param tasks any[]
|
||||
---@return cmp_dictionary.kit.Async.AsyncTask
|
||||
function AsyncTask.all(tasks)
|
||||
return AsyncTask.new(function(resolve, reject)
|
||||
local values = {}
|
||||
local count = 0
|
||||
for i, task in ipairs(tasks) do
|
||||
task:dispatch(function(value)
|
||||
values[i] = value
|
||||
count = count + 1
|
||||
if #tasks == count then
|
||||
resolve(values)
|
||||
end
|
||||
end, reject)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---Resolve first resolved task.
|
||||
---@param tasks any[]
|
||||
---@return cmp_dictionary.kit.Async.AsyncTask
|
||||
function AsyncTask.race(tasks)
|
||||
return AsyncTask.new(function(resolve, reject)
|
||||
for _, task in ipairs(tasks) do
|
||||
task:dispatch(resolve, reject)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---Create resolved AsyncTask.
|
||||
---@param v any
|
||||
---@return cmp_dictionary.kit.Async.AsyncTask
|
||||
function AsyncTask.resolve(v)
|
||||
if AsyncTask.is(v) then
|
||||
return v
|
||||
end
|
||||
return AsyncTask.new(function(resolve)
|
||||
resolve(v)
|
||||
end)
|
||||
end
|
||||
|
||||
---Create new AsyncTask.
|
||||
---@NOET: The AsyncTask has similar interface to JavaScript Promise but the AsyncTask can be worked as synchronous.
|
||||
---@param v any
|
||||
---@return cmp_dictionary.kit.Async.AsyncTask
|
||||
function AsyncTask.reject(v)
|
||||
if AsyncTask.is(v) then
|
||||
return v
|
||||
end
|
||||
return AsyncTask.new(function(_, reject)
|
||||
reject(v)
|
||||
end)
|
||||
end
|
||||
|
||||
---Create new async task object.
|
||||
---@param runner fun(resolve?: fun(value: any?), reject?: fun(err: any?))
|
||||
function AsyncTask.new(runner)
|
||||
local self = setmetatable({}, AsyncTask)
|
||||
|
||||
self.value = nil
|
||||
self.status = AsyncTask.Status.Pending
|
||||
self.synced = false
|
||||
self.chained = false
|
||||
self.children = {}
|
||||
local ok, err = pcall(runner, function(res)
|
||||
if self.status == AsyncTask.Status.Pending then
|
||||
settle(self, AsyncTask.Status.Fulfilled, res)
|
||||
end
|
||||
end, function(err)
|
||||
if self.status == AsyncTask.Status.Pending then
|
||||
settle(self, AsyncTask.Status.Rejected, err)
|
||||
end
|
||||
end)
|
||||
if not ok then
|
||||
settle(self, AsyncTask.Status.Rejected, err)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
---Sync async task.
|
||||
---@NOTE: This method uses `vim.wait` so that this can't wait the typeahead to be empty.
|
||||
---@param timeout? number
|
||||
---@return any
|
||||
function AsyncTask:sync(timeout)
|
||||
self.synced = true
|
||||
|
||||
if is_thread then
|
||||
while true do
|
||||
if self.status ~= AsyncTask.Status.Pending then
|
||||
break
|
||||
end
|
||||
uv.run('once')
|
||||
end
|
||||
else
|
||||
vim.wait(timeout or 24 * 60 * 60 * 1000, function()
|
||||
return self.status ~= AsyncTask.Status.Pending
|
||||
end, 1, false)
|
||||
end
|
||||
if self.status == AsyncTask.Status.Rejected then
|
||||
error(self.value, 2)
|
||||
end
|
||||
if self.status ~= AsyncTask.Status.Fulfilled then
|
||||
error('AsyncTask:sync is timeout.', 2)
|
||||
end
|
||||
return self.value
|
||||
end
|
||||
|
||||
---Await async task.
|
||||
---@param schedule? boolean
|
||||
---@return any
|
||||
function AsyncTask:await(schedule)
|
||||
local Async = require('cmp_dictionary.kit.Async')
|
||||
local ok, res = pcall(Async.await, self)
|
||||
if not ok then
|
||||
error(res, 2)
|
||||
end
|
||||
if schedule then
|
||||
Async.await(Async.schedule())
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
---Return current state of task.
|
||||
---@return { status: cmp_dictionary.kit.Async.AsyncTask.Status, value: any }
|
||||
function AsyncTask:state()
|
||||
return {
|
||||
status = self.status,
|
||||
value = self.value,
|
||||
}
|
||||
end
|
||||
|
||||
---Register next step.
|
||||
---@param on_fulfilled fun(value: any): any
|
||||
function AsyncTask:next(on_fulfilled)
|
||||
return self:dispatch(on_fulfilled, function(err)
|
||||
error(err, 2)
|
||||
end)
|
||||
end
|
||||
|
||||
---Register catch step.
|
||||
---@param on_rejected fun(value: any): any
|
||||
---@return cmp_dictionary.kit.Async.AsyncTask
|
||||
function AsyncTask:catch(on_rejected)
|
||||
return self:dispatch(function(value)
|
||||
return value
|
||||
end, on_rejected)
|
||||
end
|
||||
|
||||
---Dispatch task state.
|
||||
---@param on_fulfilled fun(value: any): any
|
||||
---@param on_rejected fun(err: any): any
|
||||
---@return cmp_dictionary.kit.Async.AsyncTask
|
||||
function AsyncTask:dispatch(on_fulfilled, on_rejected)
|
||||
self.chained = true
|
||||
|
||||
local function dispatch(resolve, reject)
|
||||
local on_next = self.status == AsyncTask.Status.Fulfilled and on_fulfilled or on_rejected
|
||||
local res = on_next(self.value)
|
||||
if AsyncTask.is(res) then
|
||||
res:dispatch(resolve, reject)
|
||||
else
|
||||
resolve(res)
|
||||
end
|
||||
end
|
||||
|
||||
if self.status == AsyncTask.Status.Pending then
|
||||
return AsyncTask.new(function(resolve, reject)
|
||||
table.insert(self.children, function()
|
||||
dispatch(resolve, reject)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
return AsyncTask.new(dispatch)
|
||||
end
|
||||
|
||||
return AsyncTask
|
161
bundle/cmp-dictionary/lua/cmp_dictionary/kit/Async/init.lua
vendored
Normal file
161
bundle/cmp-dictionary/lua/cmp_dictionary/kit/Async/init.lua
vendored
Normal file
@ -0,0 +1,161 @@
|
||||
local AsyncTask = require('cmp_dictionary.kit.Async.AsyncTask')
|
||||
|
||||
local Async = {}
|
||||
|
||||
---@type table<thread, integer>
|
||||
Async.___threads___ = {}
|
||||
|
||||
---Alias of AsyncTask.all.
|
||||
---@param tasks cmp_dictionary.kit.Async.AsyncTask[]
|
||||
---@return cmp_dictionary.kit.Async.AsyncTask
|
||||
function Async.all(tasks)
|
||||
return AsyncTask.all(tasks)
|
||||
end
|
||||
|
||||
---Alias of AsyncTask.race.
|
||||
---@param tasks cmp_dictionary.kit.Async.AsyncTask[]
|
||||
---@return cmp_dictionary.kit.Async.AsyncTask
|
||||
function Async.race(tasks)
|
||||
return AsyncTask.race(tasks)
|
||||
end
|
||||
|
||||
---Alias of AsyncTask.resolve(v).
|
||||
---@param v any
|
||||
---@return cmp_dictionary.kit.Async.AsyncTask
|
||||
function Async.resolve(v)
|
||||
return AsyncTask.resolve(v)
|
||||
end
|
||||
|
||||
---Alias of AsyncTask.reject(v).
|
||||
---@param v any
|
||||
---@return cmp_dictionary.kit.Async.AsyncTask
|
||||
function Async.reject(v)
|
||||
return AsyncTask.reject(v)
|
||||
end
|
||||
|
||||
---Alias of AsyncTask.new(...).
|
||||
---@param runner fun(resolve: fun(value: any), reject: fun(err: any))
|
||||
---@return cmp_dictionary.kit.Async.AsyncTask
|
||||
function Async.new(runner)
|
||||
return AsyncTask.new(runner)
|
||||
end
|
||||
|
||||
---Run async function immediately.
|
||||
---@generic T: fun(): cmp_dictionary.kit.Async.AsyncTask
|
||||
---@param runner T
|
||||
---@return cmp_dictionary.kit.Async.AsyncTask
|
||||
function Async.run(runner)
|
||||
return Async.async(runner)()
|
||||
end
|
||||
|
||||
---Return current context is async coroutine or not.
|
||||
---@return boolean
|
||||
function Async.in_context()
|
||||
return Async.___threads___[coroutine.running()] ~= nil
|
||||
end
|
||||
|
||||
---Create async function.
|
||||
---@generic T: fun(...): cmp_dictionary.kit.Async.AsyncTask
|
||||
---@param runner T
|
||||
---@return T
|
||||
function Async.async(runner)
|
||||
return function(...)
|
||||
local args = { ... }
|
||||
|
||||
local thread = coroutine.create(runner)
|
||||
return AsyncTask.new(function(resolve, reject)
|
||||
Async.___threads___[thread] = 1
|
||||
|
||||
local function next_step(ok, v)
|
||||
if coroutine.status(thread) == 'dead' then
|
||||
Async.___threads___[thread] = nil
|
||||
if AsyncTask.is(v) then
|
||||
v:dispatch(resolve, reject)
|
||||
else
|
||||
if ok then
|
||||
resolve(v)
|
||||
else
|
||||
reject(v)
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
v:dispatch(function(...)
|
||||
next_step(coroutine.resume(thread, true, ...))
|
||||
end, function(...)
|
||||
next_step(coroutine.resume(thread, false, ...))
|
||||
end)
|
||||
end
|
||||
|
||||
next_step(coroutine.resume(thread, unpack(args)))
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
---Await async task.
|
||||
---@param task cmp_dictionary.kit.Async.AsyncTask
|
||||
---@return any
|
||||
function Async.await(task)
|
||||
if not Async.___threads___[coroutine.running()] then
|
||||
error('`Async.await` must be called in async context.')
|
||||
end
|
||||
if not AsyncTask.is(task) then
|
||||
error('`Async.await` must be called with AsyncTask.')
|
||||
end
|
||||
|
||||
local ok, res = coroutine.yield(task)
|
||||
if not ok then
|
||||
error(res, 2)
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
---Create vim.schedule task.
|
||||
---@return cmp_dictionary.kit.Async.AsyncTask
|
||||
function Async.schedule()
|
||||
return AsyncTask.new(function(resolve)
|
||||
vim.schedule(resolve)
|
||||
end)
|
||||
end
|
||||
|
||||
---Create vim.defer_fn task.
|
||||
---@param timeout integer
|
||||
---@return cmp_dictionary.kit.Async.AsyncTask
|
||||
function Async.timeout(timeout)
|
||||
return AsyncTask.new(function(resolve)
|
||||
vim.defer_fn(resolve, timeout)
|
||||
end)
|
||||
end
|
||||
|
||||
---Create async function from callback function.
|
||||
---@generic T: ...
|
||||
---@param runner fun(...: T)
|
||||
---@param option? { schedule?: boolean, callback?: integer }
|
||||
---@return fun(...: T): cmp_dictionary.kit.Async.AsyncTask
|
||||
function Async.promisify(runner, option)
|
||||
option = option or {}
|
||||
option.schedule = not vim.is_thread() and (option.schedule or false)
|
||||
option.callback = option.callback or nil
|
||||
return function(...)
|
||||
local args = { ... }
|
||||
return AsyncTask.new(function(resolve, reject)
|
||||
local max = #args + 1
|
||||
local pos = math.min(option.callback or max, max)
|
||||
table.insert(args, pos, function(err, ...)
|
||||
if option.schedule and vim.in_fast_event() then
|
||||
resolve = vim.schedule_wrap(resolve)
|
||||
reject = vim.schedule_wrap(reject)
|
||||
end
|
||||
if err then
|
||||
reject(err)
|
||||
else
|
||||
resolve(...)
|
||||
end
|
||||
end)
|
||||
runner(unpack(args))
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
return Async
|
448
bundle/cmp-dictionary/lua/cmp_dictionary/kit/IO/init.lua
vendored
Normal file
448
bundle/cmp-dictionary/lua/cmp_dictionary/kit/IO/init.lua
vendored
Normal file
@ -0,0 +1,448 @@
|
||||
local uv = require('luv')
|
||||
local Async = require('cmp_dictionary.kit.Async')
|
||||
|
||||
local is_windows = uv.os_uname().sysname:lower() == 'windows'
|
||||
|
||||
---@see https://github.com/luvit/luvit/blob/master/deps/fs.lua
|
||||
local IO = {}
|
||||
|
||||
---@class cmp_dictionary.kit.IO.UV.Stat
|
||||
---@field public dev integer
|
||||
---@field public mode integer
|
||||
---@field public nlink integer
|
||||
---@field public uid integer
|
||||
---@field public gid integer
|
||||
---@field public rdev integer
|
||||
---@field public ino integer
|
||||
---@field public size integer
|
||||
---@field public blksize integer
|
||||
---@field public blocks integer
|
||||
---@field public flags integer
|
||||
---@field public gen integer
|
||||
---@field public atime { sec: integer, nsec: integer }
|
||||
---@field public mtime { sec: integer, nsec: integer }
|
||||
---@field public ctime { sec: integer, nsec: integer }
|
||||
---@field public birthtime { sec: integer, nsec: integer }
|
||||
---@field public type string
|
||||
|
||||
---@enum cmp_dictionary.kit.IO.UV.AccessMode
|
||||
IO.AccessMode = {
|
||||
r = 'r',
|
||||
rs = 'rs',
|
||||
sr = 'sr',
|
||||
['r+'] = 'r+',
|
||||
['rs+'] = 'rs+',
|
||||
['sr+'] = 'sr+',
|
||||
w = 'w',
|
||||
wx = 'wx',
|
||||
xw = 'xw',
|
||||
['w+'] = 'w+',
|
||||
['wx+'] = 'wx+',
|
||||
['xw+'] = 'xw+',
|
||||
a = 'a',
|
||||
ax = 'ax',
|
||||
xa = 'xa',
|
||||
['a+'] = 'a+',
|
||||
['ax+'] = 'ax+',
|
||||
['xa+'] = 'xa+',
|
||||
}
|
||||
|
||||
---@enum cmp_dictionary.kit.IO.WalkStatus
|
||||
IO.WalkStatus = {
|
||||
SkipDir = 1,
|
||||
Break = 2,
|
||||
}
|
||||
|
||||
---@type fun(path: string): cmp_dictionary.kit.Async.AsyncTask
|
||||
IO.fs_stat = Async.promisify(uv.fs_stat)
|
||||
|
||||
---@type fun(path: string): cmp_dictionary.kit.Async.AsyncTask
|
||||
IO.fs_unlink = Async.promisify(uv.fs_unlink)
|
||||
|
||||
---@type fun(path: string): cmp_dictionary.kit.Async.AsyncTask
|
||||
IO.fs_rmdir = Async.promisify(uv.fs_rmdir)
|
||||
|
||||
---@type fun(path: string, mode: integer): cmp_dictionary.kit.Async.AsyncTask
|
||||
IO.fs_mkdir = Async.promisify(uv.fs_mkdir)
|
||||
|
||||
---@type fun(from: string, to: string, option?: { excl?: boolean, ficlone?: boolean, ficlone_force?: boolean }): cmp_dictionary.kit.Async.AsyncTask
|
||||
IO.fs_copyfile = Async.promisify(uv.fs_copyfile)
|
||||
|
||||
---@type fun(path: string, flags: cmp_dictionary.kit.IO.UV.AccessMode, mode: integer): cmp_dictionary.kit.Async.AsyncTask
|
||||
IO.fs_open = Async.promisify(uv.fs_open)
|
||||
|
||||
---@type fun(fd: userdata): cmp_dictionary.kit.Async.AsyncTask
|
||||
IO.fs_close = Async.promisify(uv.fs_close)
|
||||
|
||||
---@type fun(fd: userdata, chunk_size: integer, offset?: integer): cmp_dictionary.kit.Async.AsyncTask
|
||||
IO.fs_read = Async.promisify(uv.fs_read)
|
||||
|
||||
---@type fun(fd: userdata, content: string, offset?: integer): cmp_dictionary.kit.Async.AsyncTask
|
||||
IO.fs_write = Async.promisify(uv.fs_write)
|
||||
|
||||
---@type fun(fd: userdata, offset: integer): cmp_dictionary.kit.Async.AsyncTask
|
||||
IO.fs_ftruncate = Async.promisify(uv.fs_ftruncate)
|
||||
|
||||
---@type fun(path: string, chunk_size?: integer): cmp_dictionary.kit.Async.AsyncTask
|
||||
IO.fs_opendir = Async.promisify(uv.fs_opendir, { callback = 2 })
|
||||
|
||||
---@type fun(fd: userdata): cmp_dictionary.kit.Async.AsyncTask
|
||||
IO.fs_closedir = Async.promisify(uv.fs_closedir)
|
||||
|
||||
---@type fun(fd: userdata): cmp_dictionary.kit.Async.AsyncTask
|
||||
IO.fs_readdir = Async.promisify(uv.fs_readdir)
|
||||
|
||||
---@type fun(path: string): cmp_dictionary.kit.Async.AsyncTask
|
||||
IO.fs_scandir = Async.promisify(uv.fs_scandir)
|
||||
|
||||
---@type fun(path: string): cmp_dictionary.kit.Async.AsyncTask
|
||||
IO.fs_realpath = Async.promisify(uv.fs_realpath)
|
||||
|
||||
---Return if the path is directory.
|
||||
---@param path string
|
||||
---@return cmp_dictionary.kit.Async.AsyncTask
|
||||
function IO.is_directory(path)
|
||||
path = IO.normalize(path)
|
||||
return Async.run(function()
|
||||
return IO.fs_stat(path):catch(function()
|
||||
return {}
|
||||
end):await().type == 'directory'
|
||||
end)
|
||||
end
|
||||
|
||||
---Read file.
|
||||
---@param path string
|
||||
---@param chunk_size? integer
|
||||
---@return cmp_dictionary.kit.Async.AsyncTask
|
||||
function IO.read_file(path, chunk_size)
|
||||
chunk_size = chunk_size or 1024
|
||||
return Async.run(function()
|
||||
local stat = IO.fs_stat(path):await()
|
||||
local fd = IO.fs_open(path, IO.AccessMode.r, tonumber('755', 8)):await()
|
||||
local ok, res = pcall(function()
|
||||
local chunks = {}
|
||||
local offset = 0
|
||||
while offset < stat.size do
|
||||
local chunk = IO.fs_read(fd, math.min(chunk_size, stat.size - offset), offset):await()
|
||||
if not chunk then
|
||||
break
|
||||
end
|
||||
table.insert(chunks, chunk)
|
||||
offset = offset + #chunk
|
||||
end
|
||||
return table.concat(chunks, ''):sub(1, stat.size - 1) -- remove EOF.
|
||||
end)
|
||||
IO.fs_close(fd):await()
|
||||
if not ok then
|
||||
error(res)
|
||||
end
|
||||
return res
|
||||
end)
|
||||
end
|
||||
|
||||
---Write file.
|
||||
---@param path string
|
||||
---@param content string
|
||||
---@param chunk_size? integer
|
||||
function IO.write_file(path, content, chunk_size)
|
||||
chunk_size = chunk_size or 1024
|
||||
content = content .. '\n' -- add EOF.
|
||||
return Async.run(function()
|
||||
local fd = IO.fs_open(path, IO.AccessMode.w, tonumber('755', 8)):await()
|
||||
local ok, err = pcall(function()
|
||||
local offset = 0
|
||||
while offset < #content do
|
||||
local chunk = content:sub(offset + 1, offset + chunk_size)
|
||||
offset = offset + IO.fs_write(fd, chunk, offset):await()
|
||||
end
|
||||
IO.fs_ftruncate(fd, offset):await()
|
||||
end)
|
||||
IO.fs_close(fd):await()
|
||||
if not ok then
|
||||
error(err)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---Create directory.
|
||||
---@param path string
|
||||
---@param mode integer
|
||||
---@param option? { recursive?: boolean }
|
||||
function IO.mkdir(path, mode, option)
|
||||
path = IO.normalize(path)
|
||||
option = option or {}
|
||||
option.recursive = option.recursive or false
|
||||
return Async.run(function()
|
||||
if not option.recursive then
|
||||
IO.fs_mkdir(path, mode):await()
|
||||
else
|
||||
local not_exists = {}
|
||||
local current = path
|
||||
while current ~= '/' do
|
||||
local stat = IO.fs_stat(current):catch(function() end):await()
|
||||
if stat then
|
||||
break
|
||||
end
|
||||
table.insert(not_exists, 1, current)
|
||||
current = IO.dirname(current)
|
||||
end
|
||||
for _, dir in ipairs(not_exists) do
|
||||
IO.fs_mkdir(dir, mode):await()
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---Remove file or directory.
|
||||
---@param start_path string
|
||||
---@param option? { recursive?: boolean }
|
||||
function IO.rm(start_path, option)
|
||||
start_path = IO.normalize(start_path)
|
||||
option = option or {}
|
||||
option.recursive = option.recursive or false
|
||||
return Async.run(function()
|
||||
local stat = IO.fs_stat(start_path):await()
|
||||
if stat.type == 'directory' then
|
||||
local children = IO.scandir(start_path):await()
|
||||
if not option.recursive and #children > 0 then
|
||||
error(('IO.rm: `%s` is a directory and not empty.'):format(start_path))
|
||||
end
|
||||
IO.walk(start_path, function(err, entry)
|
||||
if err then
|
||||
error('IO.rm: ' .. tostring(err))
|
||||
end
|
||||
if entry.type == 'directory' then
|
||||
IO.fs_rmdir(entry.path):await()
|
||||
else
|
||||
IO.fs_unlink(entry.path):await()
|
||||
end
|
||||
end, { postorder = true }):await()
|
||||
else
|
||||
IO.fs_unlink(start_path):await()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---Copy file or directory.
|
||||
---@param from any
|
||||
---@param to any
|
||||
---@param option? { recursive?: boolean }
|
||||
---@return cmp_dictionary.kit.Async.AsyncTask
|
||||
function IO.cp(from, to, option)
|
||||
from = IO.normalize(from)
|
||||
to = IO.normalize(to)
|
||||
option = option or {}
|
||||
option.recursive = option.recursive or false
|
||||
return Async.run(function()
|
||||
local stat = IO.fs_stat(from):await()
|
||||
if stat.type == 'directory' then
|
||||
if not option.recursive then
|
||||
error(('IO.cp: `%s` is a directory.'):format(from))
|
||||
end
|
||||
IO.walk(from, function(err, entry)
|
||||
if err then
|
||||
error('IO.cp: ' .. tostring(err))
|
||||
end
|
||||
local new_path = entry.path:gsub(vim.pesc(from), to)
|
||||
if entry.type == 'directory' then
|
||||
IO.mkdir(new_path, tonumber(stat.mode, 10), { recursive = true }):await()
|
||||
else
|
||||
IO.fs_copyfile(entry.path, new_path):await()
|
||||
end
|
||||
end):await()
|
||||
else
|
||||
IO.fs_copyfile(from, to):await()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---Walk directory entries recursively.
|
||||
---@param start_path string
|
||||
---@param callback fun(err: string|nil, entry: { path: string, type: string }): cmp_dictionary.kit.IO.WalkStatus?
|
||||
---@param option? { postorder?: boolean }
|
||||
function IO.walk(start_path, callback, option)
|
||||
start_path = IO.normalize(start_path)
|
||||
option = option or {}
|
||||
option.postorder = option.postorder or false
|
||||
return Async.run(function()
|
||||
local function walk_pre(dir)
|
||||
local ok, iter_entries = pcall(function()
|
||||
return IO.iter_scandir(dir.path):await()
|
||||
end)
|
||||
if not ok then
|
||||
return callback(iter_entries, dir)
|
||||
end
|
||||
local status = callback(nil, dir)
|
||||
if status == IO.WalkStatus.SkipDir then
|
||||
return
|
||||
elseif status == IO.WalkStatus.Break then
|
||||
return status
|
||||
end
|
||||
for entry in iter_entries do
|
||||
if entry.type == 'directory' then
|
||||
if walk_pre(entry) == IO.WalkStatus.Break then
|
||||
return IO.WalkStatus.Break
|
||||
end
|
||||
else
|
||||
if callback(nil, entry) == IO.WalkStatus.Break then
|
||||
return IO.WalkStatus.Break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function walk_post(dir)
|
||||
local ok, iter_entries = pcall(function()
|
||||
return IO.iter_scandir(dir.path):await()
|
||||
end)
|
||||
if not ok then
|
||||
return callback(iter_entries, dir)
|
||||
end
|
||||
for entry in iter_entries do
|
||||
if entry.type == 'directory' then
|
||||
if walk_post(entry) == IO.WalkStatus.Break then
|
||||
return IO.WalkStatus.Break
|
||||
end
|
||||
else
|
||||
if callback(nil, entry) == IO.WalkStatus.Break then
|
||||
return IO.WalkStatus.Break
|
||||
end
|
||||
end
|
||||
end
|
||||
return callback(nil, dir)
|
||||
end
|
||||
|
||||
if not IO.is_directory(start_path) then
|
||||
error(('IO.walk: `%s` is not a directory.'):format(start_path))
|
||||
end
|
||||
if option.postorder then
|
||||
walk_post({ path = start_path, type = 'directory' })
|
||||
else
|
||||
walk_pre({ path = start_path, type = 'directory' })
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---Scan directory entries.
|
||||
---@param path string
|
||||
---@return cmp_dictionary.kit.Async.AsyncTask
|
||||
function IO.scandir(path)
|
||||
path = IO.normalize(path)
|
||||
return Async.run(function()
|
||||
local fd = IO.fs_scandir(path):await()
|
||||
local entries = {}
|
||||
while true do
|
||||
local name, type = uv.fs_scandir_next(fd)
|
||||
if not name then
|
||||
break
|
||||
end
|
||||
table.insert(entries, {
|
||||
type = type,
|
||||
path = IO.join(path, name),
|
||||
})
|
||||
end
|
||||
return entries
|
||||
end)
|
||||
end
|
||||
|
||||
---Scan directory entries.
|
||||
---@param path any
|
||||
---@return cmp_dictionary.kit.Async.AsyncTask
|
||||
function IO.iter_scandir(path)
|
||||
path = IO.normalize(path)
|
||||
return Async.run(function()
|
||||
local fd = IO.fs_scandir(path):await()
|
||||
return function()
|
||||
local name, type = uv.fs_scandir_next(fd)
|
||||
if name then
|
||||
return {
|
||||
type = type,
|
||||
path = IO.join(path, name),
|
||||
}
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---Return normalized path.
|
||||
---@param path string
|
||||
---@return string
|
||||
function IO.normalize(path)
|
||||
if is_windows then
|
||||
path = path:gsub('\\', '/')
|
||||
end
|
||||
|
||||
-- remove trailing slash.
|
||||
if path:sub(-1) == '/' then
|
||||
path = path:sub(1, -2)
|
||||
end
|
||||
|
||||
-- skip if the path already absolute.
|
||||
if IO.is_absolute(path) then
|
||||
return path
|
||||
end
|
||||
|
||||
-- homedir.
|
||||
if path:sub(1, 1) == '~' then
|
||||
path = IO.join(uv.os_homedir(), path:sub(2))
|
||||
end
|
||||
|
||||
-- absolute.
|
||||
if path:sub(1, 1) == '/' then
|
||||
return path:sub(-1) == '/' and path:sub(1, -2) or path
|
||||
end
|
||||
|
||||
-- resolve relative path.
|
||||
local up = uv.cwd()
|
||||
up = up:sub(-1) == '/' and up:sub(1, -2) or up
|
||||
while true do
|
||||
if path:sub(1, 3) == '../' then
|
||||
path = path:sub(4)
|
||||
up = IO.dirname(up)
|
||||
elseif path:sub(1, 2) == './' then
|
||||
path = path:sub(3)
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
return IO.join(up, path)
|
||||
end
|
||||
|
||||
---Join the paths.
|
||||
---@param base string
|
||||
---@param path string
|
||||
---@return string
|
||||
function IO.join(base, path)
|
||||
if base:sub(-1) == '/' then
|
||||
base = base:sub(1, -2)
|
||||
end
|
||||
return base .. '/' .. path
|
||||
end
|
||||
|
||||
---Return the path of the current working directory.
|
||||
---@param path string
|
||||
---@return string
|
||||
function IO.dirname(path)
|
||||
if path:sub(-1) == '/' then
|
||||
path = path:sub(1, -2)
|
||||
end
|
||||
return (path:gsub('/[^/]+$', ''))
|
||||
end
|
||||
|
||||
if is_windows then
|
||||
---Return the path is absolute or not.
|
||||
---@param path string
|
||||
---@return boolean
|
||||
function IO.is_absolute(path)
|
||||
return path:sub(1, 1) == '/' or path:match('^%a://')
|
||||
end
|
||||
else
|
||||
---Return the path is absolute or not.
|
||||
---@param path string
|
||||
---@return boolean
|
||||
function IO.is_absolute(path)
|
||||
return path:sub(1, 1) == '/'
|
||||
end
|
||||
end
|
||||
|
||||
return IO
|
1300
bundle/cmp-dictionary/lua/cmp_dictionary/kit/LSP/Client.lua
vendored
Normal file
1300
bundle/cmp-dictionary/lua/cmp_dictionary/kit/LSP/Client.lua
vendored
Normal file
File diff suppressed because it is too large
Load Diff
103
bundle/cmp-dictionary/lua/cmp_dictionary/kit/LSP/Position.lua
vendored
Normal file
103
bundle/cmp-dictionary/lua/cmp_dictionary/kit/LSP/Position.lua
vendored
Normal file
@ -0,0 +1,103 @@
|
||||
local LSP = require('cmp_dictionary.kit.LSP')
|
||||
|
||||
local Position = {}
|
||||
|
||||
---Return the value is position or not.
|
||||
---@param v any
|
||||
---@return boolean
|
||||
function Position.is(v)
|
||||
local is = true
|
||||
is = is and (type(v) == 'table' and type(v.line) == 'number' and type(v.character) == 'number')
|
||||
return is
|
||||
end
|
||||
|
||||
---Create a cursor position.
|
||||
---@param encoding? cmp_dictionary.kit.LSP.PositionEncodingKind
|
||||
function Position.cursor(encoding)
|
||||
local r, c = unpack(vim.api.nvim_win_get_cursor(0))
|
||||
local utf8 = { line = r - 1, character = c }
|
||||
if encoding == LSP.PositionEncodingKind.UTF8 then
|
||||
return utf8
|
||||
else
|
||||
local text = vim.api.nvim_get_current_line()
|
||||
if encoding == LSP.PositionEncodingKind.UTF32 then
|
||||
return Position.to(text, utf8, LSP.PositionEncodingKind.UTF8, LSP.PositionEncodingKind.UTF32)
|
||||
end
|
||||
return Position.to(text, utf8, LSP.PositionEncodingKind.UTF8, LSP.PositionEncodingKind.UTF16)
|
||||
end
|
||||
end
|
||||
|
||||
---Convert position to specified encoding from specified encoding.
|
||||
---@param text string
|
||||
---@param position cmp_dictionary.kit.LSP.Position
|
||||
---@param from_encoding cmp_dictionary.kit.LSP.PositionEncodingKind
|
||||
---@param to_encoding cmp_dictionary.kit.LSP.PositionEncodingKind
|
||||
function Position.to(text, position, from_encoding, to_encoding)
|
||||
if to_encoding == LSP.PositionEncodingKind.UTF8 then
|
||||
return Position.to_utf8(text, position, from_encoding)
|
||||
elseif to_encoding == LSP.PositionEncodingKind.UTF16 then
|
||||
return Position.to_utf16(text, position, from_encoding)
|
||||
elseif to_encoding == LSP.PositionEncodingKind.UTF32 then
|
||||
return Position.to_utf32(text, position, from_encoding)
|
||||
end
|
||||
error('LSP.Position: Unsupported encoding: ' .. to_encoding)
|
||||
end
|
||||
|
||||
---Convert position to utf8 from specified encoding.
|
||||
---@param text string
|
||||
---@param position cmp_dictionary.kit.LSP.Position
|
||||
---@param from_encoding? cmp_dictionary.kit.LSP.PositionEncodingKind
|
||||
---@return cmp_dictionary.kit.LSP.Position
|
||||
function Position.to_utf8(text, position, from_encoding)
|
||||
from_encoding = from_encoding or LSP.PositionEncodingKind.UTF16
|
||||
if from_encoding == LSP.PositionEncodingKind.UTF8 then
|
||||
return position
|
||||
end
|
||||
local ok, byteindex = pcall(function()
|
||||
return vim.str_byteindex(text, position.character, from_encoding == LSP.PositionEncodingKind.UTF16)
|
||||
end)
|
||||
if ok then
|
||||
position = { line = position.line, character = byteindex }
|
||||
end
|
||||
return position
|
||||
end
|
||||
|
||||
---Convert position to utf16 from specified encoding.
|
||||
---@param text string
|
||||
---@param position cmp_dictionary.kit.LSP.Position
|
||||
---@param from_encoding? cmp_dictionary.kit.LSP.PositionEncodingKind
|
||||
---@return cmp_dictionary.kit.LSP.Position
|
||||
function Position.to_utf16(text, position, from_encoding)
|
||||
local utf8 = Position.to_utf8(text, position, from_encoding)
|
||||
for index = utf8.character, 0, -1 do
|
||||
local ok, utf16index = pcall(function()
|
||||
return select(2, vim.str_utfindex(text, index))
|
||||
end)
|
||||
if ok then
|
||||
position = { line = utf8.line, character = utf16index }
|
||||
break
|
||||
end
|
||||
end
|
||||
return position
|
||||
end
|
||||
|
||||
---Convert position to utf32 from specified encoding.
|
||||
---@param text string
|
||||
---@param position cmp_dictionary.kit.LSP.Position
|
||||
---@param from_encoding? cmp_dictionary.kit.LSP.PositionEncodingKind
|
||||
---@return cmp_dictionary.kit.LSP.Position
|
||||
function Position.to_utf32(text, position, from_encoding)
|
||||
local utf8 = Position.to_utf8(text, position, from_encoding)
|
||||
for index = utf8.character, 0, -1 do
|
||||
local ok, utf32index = pcall(function()
|
||||
return select(1, vim.str_utfindex(text, index))
|
||||
end)
|
||||
if ok then
|
||||
position = { line = utf8.line, character = utf32index }
|
||||
break
|
||||
end
|
||||
end
|
||||
return position
|
||||
end
|
||||
|
||||
return Position
|
55
bundle/cmp-dictionary/lua/cmp_dictionary/kit/LSP/Range.lua
vendored
Normal file
55
bundle/cmp-dictionary/lua/cmp_dictionary/kit/LSP/Range.lua
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
local Position = require('cmp_dictionary.kit.LSP.Position')
|
||||
|
||||
local Range = {}
|
||||
|
||||
---Return the value is range or not.
|
||||
---@param v any
|
||||
---@return boolean
|
||||
function Range.is(v)
|
||||
return type(v) == 'table' and Position.is(v.start) and Position.is(v['end'])
|
||||
end
|
||||
|
||||
---Return the range is empty or not.
|
||||
---@param range cmp_dictionary.kit.LSP.Range
|
||||
---@return boolean
|
||||
function Range.empty(range)
|
||||
return range.start.line == range['end'].line and range.start.character == range['end'].character
|
||||
end
|
||||
|
||||
---Convert range to utf8 from specified encoding.
|
||||
---@param text_start string
|
||||
---@param range cmp_dictionary.kit.LSP.Range
|
||||
---@param from_encoding? cmp_dictionary.kit.LSP.PositionEncodingKind
|
||||
---@return cmp_dictionary.kit.LSP.Range
|
||||
function Range.to_utf8(text_start, text_end, range, from_encoding)
|
||||
return {
|
||||
start = Position.to_utf8(text_start, range.start, from_encoding),
|
||||
['end'] = Position.to_utf8(text_end, range['end'], from_encoding),
|
||||
}
|
||||
end
|
||||
|
||||
---Convert range to utf16 from specified encoding.
|
||||
---@param text_start string
|
||||
---@param range cmp_dictionary.kit.LSP.Range
|
||||
---@param from_encoding? cmp_dictionary.kit.LSP.PositionEncodingKind
|
||||
---@return cmp_dictionary.kit.LSP.Range
|
||||
function Range.to_utf16(text_start, text_end, range, from_encoding)
|
||||
return {
|
||||
start = Position.to_utf16(text_start, range.start, from_encoding),
|
||||
['end'] = Position.to_utf16(text_end, range['end'], from_encoding),
|
||||
}
|
||||
end
|
||||
|
||||
---Convert range to utf32 from specified encoding.
|
||||
---@param text_start string
|
||||
---@param range cmp_dictionary.kit.LSP.Range
|
||||
---@param from_encoding? cmp_dictionary.kit.LSP.PositionEncodingKind
|
||||
---@return cmp_dictionary.kit.LSP.Range
|
||||
function Range.to_utf32(text_start, text_end, range, from_encoding)
|
||||
return {
|
||||
start = Position.to_utf32(text_start, range.start, from_encoding),
|
||||
['end'] = Position.to_utf32(text_end, range['end'], from_encoding),
|
||||
}
|
||||
end
|
||||
|
||||
return Range
|
1911
bundle/cmp-dictionary/lua/cmp_dictionary/kit/LSP/init.lua
vendored
Normal file
1911
bundle/cmp-dictionary/lua/cmp_dictionary/kit/LSP/init.lua
vendored
Normal file
File diff suppressed because it is too large
Load Diff
117
bundle/cmp-dictionary/lua/cmp_dictionary/kit/Thread/Server/Session.lua
vendored
Normal file
117
bundle/cmp-dictionary/lua/cmp_dictionary/kit/Thread/Server/Session.lua
vendored
Normal file
@ -0,0 +1,117 @@
|
||||
---@diagnostic disable: invisible
|
||||
local mpack = require('mpack')
|
||||
local Async = require('cmp_dictionary.kit.Async')
|
||||
|
||||
---Encode data to msgpack.
|
||||
---@param v any
|
||||
---@return string
|
||||
local function encode(v)
|
||||
if v == nil then
|
||||
return mpack.encode(mpack.NIL)
|
||||
end
|
||||
return mpack.encode(v)
|
||||
end
|
||||
|
||||
---@class cmp_dictionary.kit.Thread.Server.Session
|
||||
---@field private mpack_session any
|
||||
---@field private reader uv.uv_pipe_t
|
||||
---@field private writer uv.uv_pipe_t
|
||||
---@field private _on_request table<string, fun(params: table): any>
|
||||
---@field private _on_notification table<string, fun(params: table): nil>
|
||||
local Session = {}
|
||||
Session.__index = Session
|
||||
|
||||
---Create new session.
|
||||
---@return cmp_dictionary.kit.Thread.Server.Session
|
||||
function Session.new()
|
||||
local self = setmetatable({}, Session)
|
||||
self.mpack_session = mpack.Session({ unpack = mpack.Unpacker() })
|
||||
self.reader = nil
|
||||
self.writer = nil
|
||||
self._on_request = {}
|
||||
self._on_notification = {}
|
||||
return self
|
||||
end
|
||||
|
||||
---Connect reader/writer.
|
||||
---@param reader uv.uv_pipe_t
|
||||
---@param writer uv.uv_pipe_t
|
||||
function Session:connect(reader, writer)
|
||||
self.reader = reader
|
||||
self.writer = writer
|
||||
|
||||
self.reader:read_start(function(err, data)
|
||||
if err then
|
||||
error(err)
|
||||
end
|
||||
|
||||
local offset = 1
|
||||
local length = #data
|
||||
while offset <= length do
|
||||
local type, id_or_cb, method_or_error, params_or_result, new_offset = self.mpack_session:receive(data, offset)
|
||||
if type == 'request' then
|
||||
local request_id, method, params = id_or_cb, method_or_error, params_or_result
|
||||
Async.resolve():next(function()
|
||||
return Async.run(function()
|
||||
return self._on_request[method](params)
|
||||
end)
|
||||
end):next(function(res)
|
||||
self.writer:write(self.mpack_session:reply(request_id) .. encode(mpack.NIL) .. encode(res))
|
||||
end):catch(function(err_)
|
||||
self.writer:write(self.mpack_session:reply(request_id) .. encode(err_) .. encode(mpack.NIL))
|
||||
end)
|
||||
elseif type == 'notification' then
|
||||
local method, params = method_or_error, params_or_result
|
||||
self._on_notification[method](params)
|
||||
elseif type == 'response' then
|
||||
local callback, err_, res = id_or_cb, method_or_error, params_or_result
|
||||
if err_ == mpack.NIL then
|
||||
callback(nil, res)
|
||||
else
|
||||
callback(err_, nil)
|
||||
end
|
||||
end
|
||||
offset = new_offset
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---Add request handler.
|
||||
---@param method string
|
||||
---@param callback fun(params: table): any
|
||||
function Session:on_request(method, callback)
|
||||
self._on_request[method] = callback
|
||||
end
|
||||
|
||||
---Add notification handler.
|
||||
---@param method string
|
||||
---@param callback fun(params: table)
|
||||
function Session:on_notification(method, callback)
|
||||
self._on_notification[method] = callback
|
||||
end
|
||||
|
||||
---Send request to the peer.
|
||||
---@param method string
|
||||
---@param params table
|
||||
---@return cmp_dictionary.kit.Async.AsyncTask
|
||||
function Session:request(method, params)
|
||||
return Async.new(function(resolve, reject)
|
||||
local request = self.mpack_session:request(function(err, res)
|
||||
if err then
|
||||
reject(err)
|
||||
else
|
||||
resolve(res)
|
||||
end
|
||||
end)
|
||||
self.writer:write(request .. encode(method) .. encode(params))
|
||||
end)
|
||||
end
|
||||
|
||||
---Send notification to the peer.
|
||||
---@param method string
|
||||
---@param params table
|
||||
function Session:notify(method, params)
|
||||
self.writer:write(self.mpack_session:notify() .. encode(method) .. encode(params))
|
||||
end
|
||||
|
||||
return Session
|
20
bundle/cmp-dictionary/lua/cmp_dictionary/kit/Thread/Server/_bootstrap.lua
vendored
Normal file
20
bundle/cmp-dictionary/lua/cmp_dictionary/kit/Thread/Server/_bootstrap.lua
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
vim.o.runtimepath = _G.arg[1]
|
||||
|
||||
local uv = require('luv')
|
||||
local Session = require('cmp_dictionary.kit.Thread.Server.Session')
|
||||
|
||||
local stdin = uv.new_pipe()
|
||||
stdin:open(0)
|
||||
local stdout = uv.new_pipe()
|
||||
stdout:open(1)
|
||||
|
||||
local session = Session.new()
|
||||
session:connect(stdin, stdout)
|
||||
|
||||
session:on_request('connect', function(params)
|
||||
loadstring(params.dispatcher)(session)
|
||||
end)
|
||||
|
||||
while true do
|
||||
uv.run('once')
|
||||
end
|
111
bundle/cmp-dictionary/lua/cmp_dictionary/kit/Thread/Server/init.lua
vendored
Normal file
111
bundle/cmp-dictionary/lua/cmp_dictionary/kit/Thread/Server/init.lua
vendored
Normal file
@ -0,0 +1,111 @@
|
||||
local uv = require('luv')
|
||||
local Async = require('cmp_dictionary.kit.Async')
|
||||
local Session = require('cmp_dictionary.kit.Thread.Server.Session')
|
||||
|
||||
---Return current executing file directory.
|
||||
---@return string
|
||||
local function dirname()
|
||||
return debug.getinfo(2, "S").source:sub(2):match("(.*)/")
|
||||
end
|
||||
|
||||
---@class cmp_dictionary.kit.Thread.Server
|
||||
---@field private stdin uv.uv_pipe_t
|
||||
---@field private stdout uv.uv_pipe_t
|
||||
---@field private stderr uv.uv_pipe_t
|
||||
---@field private dispatcher fun(session: cmp_dictionary.kit.Thread.Server.Session): nil
|
||||
---@field private process? uv.uv_process_t
|
||||
---@field private session? cmp_dictionary.kit.Thread.Server.Session
|
||||
local Server = {}
|
||||
Server.__index = Server
|
||||
|
||||
---Create new server instance.
|
||||
---@param dispatcher fun(session: cmp_dictionary.kit.Thread.Server.Session): nil
|
||||
---@return cmp_dictionary.kit.Thread.Server
|
||||
function Server.new(dispatcher)
|
||||
local self = setmetatable({}, Server)
|
||||
self.dispatcher = dispatcher
|
||||
self.session = Session.new()
|
||||
self.process = nil
|
||||
return self
|
||||
end
|
||||
|
||||
---Connect to server.
|
||||
---@return cmp_dictionary.kit.Async.AsyncTask
|
||||
function Server:connect()
|
||||
return Async.run(function()
|
||||
Async.schedule():await()
|
||||
local stdin = uv.new_pipe()
|
||||
local stdout = uv.new_pipe()
|
||||
local stderr = uv.new_pipe()
|
||||
self.process = uv.spawn('nvim', {
|
||||
cwd = uv.cwd(),
|
||||
args = {
|
||||
'--headless',
|
||||
'--noplugin',
|
||||
'-l',
|
||||
('%s/_bootstrap.lua'):format(dirname()),
|
||||
vim.o.runtimepath
|
||||
},
|
||||
stdio = { stdin, stdout, stderr }
|
||||
})
|
||||
|
||||
stderr:read_start(function(err, data)
|
||||
if err then
|
||||
error(err)
|
||||
end
|
||||
print(data)
|
||||
end)
|
||||
|
||||
self.session:connect(stdout, stdin)
|
||||
return self.session:request('connect', {
|
||||
dispatcher = string.dump(self.dispatcher)
|
||||
}):await()
|
||||
end)
|
||||
end
|
||||
|
||||
---Add request handler.
|
||||
---@param method string
|
||||
---@param callback fun(params: table): any
|
||||
function Server:on_request(method, callback)
|
||||
self.session:on_request(method, callback)
|
||||
end
|
||||
|
||||
---Add notification handler.
|
||||
---@param method string
|
||||
---@param callback fun(params: table)
|
||||
function Server:on_notification(method, callback)
|
||||
self.session:on_notification(method, callback)
|
||||
end
|
||||
|
||||
--- Send request.
|
||||
---@param method string
|
||||
---@param params table
|
||||
function Server:request(method, params)
|
||||
if not self.process then
|
||||
error('Server is not connected.')
|
||||
end
|
||||
return self.session:request(method, params)
|
||||
end
|
||||
|
||||
---Send notification.
|
||||
---@param method string
|
||||
---@param params table
|
||||
function Server:notify(method, params)
|
||||
if not self.process then
|
||||
error('Server is not connected.')
|
||||
end
|
||||
self.session:notify(method, params)
|
||||
end
|
||||
|
||||
---Kill server process.
|
||||
function Server:kill()
|
||||
if self.process then
|
||||
local ok, err = self.process:kill('SIGINT')
|
||||
if not ok then
|
||||
error(err)
|
||||
end
|
||||
self.process = nil
|
||||
end
|
||||
end
|
||||
|
||||
return Server
|
62
bundle/cmp-dictionary/lua/cmp_dictionary/kit/Thread/Worker.lua
vendored
Normal file
62
bundle/cmp-dictionary/lua/cmp_dictionary/kit/Thread/Worker.lua
vendored
Normal file
@ -0,0 +1,62 @@
|
||||
local uv = require('luv')
|
||||
local AsyncTask = require('cmp_dictionary.kit.Async.AsyncTask')
|
||||
|
||||
---@class cmp_dictionary.kit.Thread.WorkerOption
|
||||
---@field public runtimepath string[]
|
||||
|
||||
local Worker = {}
|
||||
Worker.__index = Worker
|
||||
|
||||
---Create a new thread.
|
||||
---@param runner function
|
||||
function Worker.new(runner)
|
||||
local self = setmetatable({}, Worker)
|
||||
self.runner = string.dump(runner)
|
||||
return self
|
||||
end
|
||||
|
||||
---Call worker function.
|
||||
---@return cmp_dictionary.kit.Async.AsyncTask
|
||||
function Worker:__call(...)
|
||||
local args_ = { ... }
|
||||
return AsyncTask.new(function(resolve, reject)
|
||||
uv.new_work(function(runner, args, option)
|
||||
args = vim.mpack.decode(args)
|
||||
option = vim.mpack.decode(option)
|
||||
|
||||
--Initialize cwd.
|
||||
require('luv').chdir(option.cwd)
|
||||
|
||||
--Initialize package.loaders.
|
||||
table.insert(package.loaders, 2, vim._load_package)
|
||||
|
||||
--Run runner function.
|
||||
local ok, res = pcall(function()
|
||||
return require('cmp_dictionary.kit.Async.AsyncTask').resolve(assert(loadstring(runner))(unpack(args))):sync()
|
||||
end)
|
||||
|
||||
res = vim.mpack.encode({ res })
|
||||
|
||||
--Return error or result.
|
||||
if not ok then
|
||||
return res, nil
|
||||
else
|
||||
return nil, res
|
||||
end
|
||||
end, function(err, res)
|
||||
if err then
|
||||
reject(vim.mpack.decode(err)[1])
|
||||
else
|
||||
resolve(vim.mpack.decode(res)[1])
|
||||
end
|
||||
end):queue(
|
||||
self.runner,
|
||||
vim.mpack.encode(args_),
|
||||
vim.mpack.encode({
|
||||
cwd = uv.cwd(),
|
||||
})
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
return Worker
|
88
bundle/cmp-dictionary/lua/cmp_dictionary/kit/Vim/Keymap.lua
vendored
Normal file
88
bundle/cmp-dictionary/lua/cmp_dictionary/kit/Vim/Keymap.lua
vendored
Normal file
@ -0,0 +1,88 @@
|
||||
local kit = require('cmp_dictionary.kit')
|
||||
local Async = require('cmp_dictionary.kit.Async')
|
||||
|
||||
---@alias cmp_dictionary.kit.Vim.Keymap.Keys { keys: string, remap: boolean }
|
||||
---@alias cmp_dictionary.kit.Vim.Keymap.KeysSpecifier string|{ keys: string, remap: boolean }
|
||||
|
||||
---@param keys cmp_dictionary.kit.Vim.Keymap.KeysSpecifier
|
||||
---@return cmp_dictionary.kit.Vim.Keymap.Keys
|
||||
local function to_keys(keys)
|
||||
if type(keys) == 'table' then
|
||||
return keys
|
||||
end
|
||||
return { keys = keys, remap = false }
|
||||
end
|
||||
|
||||
local Keymap = {}
|
||||
|
||||
Keymap._callbacks = {}
|
||||
|
||||
---Replace termcodes.
|
||||
---@param keys string
|
||||
---@return string
|
||||
function Keymap.termcodes(keys)
|
||||
return vim.api.nvim_replace_termcodes(keys, true, true, true)
|
||||
end
|
||||
|
||||
---Set callback for consuming next typeahead.
|
||||
---@param callback fun()
|
||||
---@return cmp_dictionary.kit.Async.AsyncTask
|
||||
function Keymap.next(callback)
|
||||
return Keymap.send(''):next(callback)
|
||||
end
|
||||
|
||||
---Send keys.
|
||||
---@param keys cmp_dictionary.kit.Vim.Keymap.KeysSpecifier|cmp_dictionary.kit.Vim.Keymap.KeysSpecifier[]
|
||||
---@param no_insert? boolean
|
||||
---@return cmp_dictionary.kit.Async.AsyncTask
|
||||
function Keymap.send(keys, no_insert)
|
||||
local unique_id = kit.unique_id()
|
||||
return Async.new(function(resolve, _)
|
||||
Keymap._callbacks[unique_id] = resolve
|
||||
|
||||
local callback = Keymap.termcodes(('<Cmd>lua require("cmp_dictionary.kit.Vim.Keymap")._resolve(%s)<CR>'):format(unique_id))
|
||||
if no_insert then
|
||||
for _, keys_ in ipairs(kit.to_array(keys)) do
|
||||
keys_ = to_keys(keys_)
|
||||
vim.api.nvim_feedkeys(keys_.keys, keys_.remap and 'm' or 'n', true)
|
||||
end
|
||||
vim.api.nvim_feedkeys(callback, 'n', true)
|
||||
else
|
||||
vim.api.nvim_feedkeys(callback, 'in', true)
|
||||
for _, keys_ in ipairs(kit.reverse(kit.to_array(keys))) do
|
||||
keys_ = to_keys(keys_)
|
||||
vim.api.nvim_feedkeys(keys_.keys, 'i' .. (keys_.remap and 'm' or 'n'), true)
|
||||
end
|
||||
end
|
||||
end):catch(function()
|
||||
Keymap._callbacks[unique_id] = nil
|
||||
end)
|
||||
end
|
||||
|
||||
---Return sendabke keys with callback function.
|
||||
---@param callback fun(...: any): any
|
||||
---@return string
|
||||
function Keymap.to_sendable(callback)
|
||||
local unique_id = kit.unique_id()
|
||||
Keymap._callbacks[unique_id] = Async.async(callback)
|
||||
return Keymap.termcodes(('<Cmd>lua require("cmp_dictionary.kit.Vim.Keymap")._resolve(%s)<CR>'):format(unique_id))
|
||||
end
|
||||
|
||||
---Test spec helper.
|
||||
---@param spec fun(): any
|
||||
function Keymap.spec(spec)
|
||||
local task = Async.resolve():next(Async.async(spec))
|
||||
vim.api.nvim_feedkeys('', 'x', true)
|
||||
task:sync()
|
||||
collectgarbage('collect')
|
||||
vim.wait(200)
|
||||
end
|
||||
|
||||
---Resolve running keys.
|
||||
---@param unique_id integer
|
||||
function Keymap._resolve(unique_id)
|
||||
Keymap._callbacks[unique_id]()
|
||||
Keymap._callbacks[unique_id] = nil
|
||||
end
|
||||
|
||||
return Keymap
|
42
bundle/cmp-dictionary/lua/cmp_dictionary/kit/Vim/RegExp.lua
vendored
Normal file
42
bundle/cmp-dictionary/lua/cmp_dictionary/kit/Vim/RegExp.lua
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
local RegExp = {}
|
||||
|
||||
---@type table<string, { match_str: fun(self, text: string) }>
|
||||
RegExp._cache = {}
|
||||
|
||||
---Create a RegExp object.
|
||||
---@param pattern string
|
||||
---@return { match_str: fun(self, text: string) }
|
||||
function RegExp.get(pattern)
|
||||
if not RegExp._cache[pattern] then
|
||||
RegExp._cache[pattern] = vim.regex(pattern)
|
||||
end
|
||||
return RegExp._cache[pattern]
|
||||
end
|
||||
|
||||
---Grep and substitute text.
|
||||
---@param text string
|
||||
---@param pattern string
|
||||
---@param replacement string
|
||||
---@return string
|
||||
function RegExp.gsub(text, pattern, replacement)
|
||||
return vim.fn.substitute(text, pattern, replacement, 'g')
|
||||
end
|
||||
|
||||
---Match pattern in text for specified position.
|
||||
---@param text string
|
||||
---@param pattern string
|
||||
---@param pos number 1-origin index
|
||||
---@return string?, integer?, integer? 1-origin-index
|
||||
function RegExp.extract_at(text, pattern, pos)
|
||||
local before_text = text:sub(1, pos - 1)
|
||||
local after_text = text:sub(pos)
|
||||
local b_s, _ = RegExp.get(pattern .. '$'):match_str(before_text)
|
||||
local _, a_e = RegExp.get('^' .. pattern):match_str(after_text)
|
||||
if b_s or a_e then
|
||||
b_s = b_s or #before_text
|
||||
a_e = #before_text + (a_e or 0)
|
||||
return text:sub(b_s + 1, a_e), b_s + 1, a_e + 1
|
||||
end
|
||||
end
|
||||
|
||||
return RegExp
|
61
bundle/cmp-dictionary/lua/cmp_dictionary/kit/Vim/Syntax.lua
vendored
Normal file
61
bundle/cmp-dictionary/lua/cmp_dictionary/kit/Vim/Syntax.lua
vendored
Normal file
@ -0,0 +1,61 @@
|
||||
local kit = require('cmp_dictionary.kit')
|
||||
|
||||
local Syntax = {}
|
||||
|
||||
---Return the specified position is in the specified syntax.
|
||||
---@param cursor { [1]: integer, [2]: integer }
|
||||
---@param groups string[]
|
||||
function Syntax.within(cursor, groups)
|
||||
for _, group in ipairs(Syntax.get_syntax_groups(cursor)) do
|
||||
if vim.tbl_contains(groups, group) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---Get all syntax groups for specified position.
|
||||
---NOTE: This function accepts 0-origin cursor position.
|
||||
---@param cursor { [1]: integer, [2]: integer }
|
||||
---@return string[]
|
||||
function Syntax.get_syntax_groups(cursor)
|
||||
return kit.concat(Syntax.get_vim_syntax_groups(cursor), Syntax.get_treesitter_syntax_groups(cursor))
|
||||
end
|
||||
|
||||
---Get vim's syntax groups for specified position.
|
||||
---NOTE: This function accepts 0-origin cursor position.
|
||||
---@param cursor { [1]: integer, [2]: integer }
|
||||
---@return string[]
|
||||
function Syntax.get_vim_syntax_groups(cursor)
|
||||
local unique = {}
|
||||
local groups = {}
|
||||
for _, syntax_id in ipairs(vim.fn.synstack(cursor[1] + 1, cursor[2] + 1)) do
|
||||
local name = vim.fn.synIDattr(vim.fn.synIDtrans(syntax_id), 'name')
|
||||
if not unique[name] then
|
||||
unique[name] = true
|
||||
table.insert(groups, name)
|
||||
end
|
||||
end
|
||||
for _, syntax_id in ipairs(vim.fn.synstack(cursor[1] + 1, cursor[2] + 1)) do
|
||||
local name = vim.fn.synIDattr(syntax_id, 'name')
|
||||
if not unique[name] then
|
||||
unique[name] = true
|
||||
table.insert(groups, name)
|
||||
end
|
||||
end
|
||||
return groups
|
||||
end
|
||||
|
||||
---Get tree-sitter's syntax groups for specified position.
|
||||
---NOTE: This function accepts 0-origin cursor position.
|
||||
---@param cursor { [1]: integer, [2]: integer }
|
||||
---@return string[]
|
||||
function Syntax.get_treesitter_syntax_groups(cursor)
|
||||
local groups = {}
|
||||
for _, capture in ipairs(vim.treesitter.get_captures_at_pos(0, cursor[1], cursor[2])) do
|
||||
table.insert(groups, ('@%s'):format(capture.capture))
|
||||
end
|
||||
return groups
|
||||
end
|
||||
|
||||
return Syntax
|
213
bundle/cmp-dictionary/lua/cmp_dictionary/kit/init.lua
vendored
Normal file
213
bundle/cmp-dictionary/lua/cmp_dictionary/kit/init.lua
vendored
Normal file
@ -0,0 +1,213 @@
|
||||
local kit = {}
|
||||
|
||||
local is_thread = vim.is_thread()
|
||||
|
||||
---Create gabage collection detector.
|
||||
---@param callback fun(...: any): any
|
||||
---@return userdata
|
||||
function kit.gc(callback)
|
||||
local gc = newproxy(true)
|
||||
if vim.is_thread() or os.getenv('NODE_ENV') == 'test' then
|
||||
getmetatable(gc).__gc = callback
|
||||
else
|
||||
getmetatable(gc).__gc = vim.schedule_wrap(callback)
|
||||
end
|
||||
return gc
|
||||
end
|
||||
|
||||
---Bind arguments for function.
|
||||
---@param fn fun(...: any): any
|
||||
---@vararg any
|
||||
---@return fun(...: any): any
|
||||
function kit.bind(fn, ...)
|
||||
local args = { ... }
|
||||
return function(...)
|
||||
return fn(unpack(args), ...)
|
||||
end
|
||||
end
|
||||
|
||||
---Safe version of vim.schedule.
|
||||
---@param fn fun(...: any): any
|
||||
function kit.safe_schedule(fn)
|
||||
if is_thread then
|
||||
fn()
|
||||
else
|
||||
vim.schedule(fn)
|
||||
end
|
||||
end
|
||||
|
||||
---Safe version of vim.schedule_wrap.
|
||||
---@param fn fun(...: any): any
|
||||
function kit.safe_schedule_wrap(fn)
|
||||
if is_thread then
|
||||
return fn
|
||||
else
|
||||
return vim.schedule_wrap(fn)
|
||||
end
|
||||
end
|
||||
|
||||
---Create unique id.
|
||||
---@return integer
|
||||
kit.unique_id = setmetatable({
|
||||
unique_id = 0,
|
||||
}, {
|
||||
__call = function(self)
|
||||
self.unique_id = self.unique_id + 1
|
||||
return self.unique_id
|
||||
end,
|
||||
})
|
||||
|
||||
---Merge two tables.
|
||||
---@generic T
|
||||
---NOTE: This doesn't merge array-like table.
|
||||
---@param tbl1 T
|
||||
---@param tbl2 T
|
||||
---@return T
|
||||
function kit.merge(tbl1, tbl2)
|
||||
local is_dict1 = kit.is_dict(tbl1)
|
||||
local is_dict2 = kit.is_dict(tbl2)
|
||||
if is_dict1 and is_dict2 then
|
||||
local new_tbl = {}
|
||||
for k, v in pairs(tbl2) do
|
||||
if tbl1[k] ~= vim.NIL then
|
||||
new_tbl[k] = kit.merge(tbl1[k], v)
|
||||
end
|
||||
end
|
||||
for k, v in pairs(tbl1) do
|
||||
if tbl2[k] == nil then
|
||||
if v ~= vim.NIL then
|
||||
new_tbl[k] = v
|
||||
else
|
||||
new_tbl[k] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
return new_tbl
|
||||
elseif is_dict1 and not is_dict2 then
|
||||
return kit.merge(tbl1, {})
|
||||
elseif not is_dict1 and is_dict2 then
|
||||
return kit.merge(tbl2, {})
|
||||
end
|
||||
|
||||
if tbl1 == vim.NIL then
|
||||
return nil
|
||||
elseif tbl1 == nil then
|
||||
return tbl2
|
||||
else
|
||||
return tbl1
|
||||
end
|
||||
end
|
||||
|
||||
---Recursive convert value via callback function.
|
||||
---@param tbl table
|
||||
---@param callback fun(value: any): any
|
||||
---@return table
|
||||
function kit.convert(tbl, callback)
|
||||
if kit.is_dict(tbl) then
|
||||
local new_tbl = {}
|
||||
for k, v in pairs(tbl) do
|
||||
new_tbl[k] = kit.convert(v, callback)
|
||||
end
|
||||
return new_tbl
|
||||
end
|
||||
return callback(tbl)
|
||||
end
|
||||
|
||||
---Map array.
|
||||
---@param array table
|
||||
---@param fn fun(item: unknown, index: integer): unknown
|
||||
---@return unknown[]
|
||||
function kit.map(array, fn)
|
||||
local new_array = {}
|
||||
for i, item in ipairs(array) do
|
||||
table.insert(new_array, fn(item, i))
|
||||
end
|
||||
return new_array
|
||||
end
|
||||
|
||||
---Concatenate two tables.
|
||||
---NOTE: This doesn't concatenate dict-like table.
|
||||
---@param tbl1 table
|
||||
---@param tbl2 table
|
||||
---@return table
|
||||
function kit.concat(tbl1, tbl2)
|
||||
local new_tbl = {}
|
||||
for _, item in ipairs(tbl1) do
|
||||
table.insert(new_tbl, item)
|
||||
end
|
||||
for _, item in ipairs(tbl2) do
|
||||
table.insert(new_tbl, item)
|
||||
end
|
||||
return new_tbl
|
||||
end
|
||||
|
||||
---The value to array.
|
||||
---@param value any
|
||||
---@return table
|
||||
function kit.to_array(value)
|
||||
if type(value) == 'table' then
|
||||
if vim.tbl_islist(value) or vim.tbl_isempty(value) then
|
||||
return value
|
||||
end
|
||||
end
|
||||
return { value }
|
||||
end
|
||||
|
||||
---Check the value is array.
|
||||
---@param value any
|
||||
---@return boolean
|
||||
function kit.is_array(value)
|
||||
return not not (type(value) == 'table' and (vim.tbl_islist(value) or vim.tbl_isempty(value)))
|
||||
end
|
||||
|
||||
---Check the value is dict.
|
||||
---@param value any
|
||||
---@return boolean
|
||||
function kit.is_dict(value)
|
||||
return type(value) == 'table' and (not vim.tbl_islist(value) or vim.tbl_isempty(value))
|
||||
end
|
||||
|
||||
---Reverse the array.
|
||||
---@param array table
|
||||
---@return table
|
||||
function kit.reverse(array)
|
||||
if not kit.is_array(array) then
|
||||
error('[kit] specified value is not an array.')
|
||||
end
|
||||
|
||||
local new_array = {}
|
||||
for i = #array, 1, -1 do
|
||||
table.insert(new_array, array[i])
|
||||
end
|
||||
return new_array
|
||||
end
|
||||
|
||||
---@generic T
|
||||
---@param value T?
|
||||
---@param default T
|
||||
function kit.default(value, default)
|
||||
if value == nil then
|
||||
return default
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
---Get object path with default value.
|
||||
---@generic T
|
||||
---@param value table
|
||||
---@param path integer|string|(string|integer)[]
|
||||
---@param default? T
|
||||
---@return T
|
||||
function kit.get(value, path, default)
|
||||
local result = value
|
||||
for _, key in ipairs(kit.to_array(path)) do
|
||||
if type(result) == 'table' then
|
||||
result = result[key]
|
||||
else
|
||||
return default
|
||||
end
|
||||
end
|
||||
return result or default
|
||||
end
|
||||
|
||||
return kit
|
132
bundle/cmp-dictionary/lua/cmp_dictionary/lfu.lua
vendored
Normal file
132
bundle/cmp-dictionary/lua/cmp_dictionary/lfu.lua
vendored
Normal file
@ -0,0 +1,132 @@
|
||||
---@class CacheNode
|
||||
---@field key integer
|
||||
---@field value integer
|
||||
---@field freq integer
|
||||
---@field prev CacheNode
|
||||
---@field next CacheNode
|
||||
local CacheNode = {}
|
||||
|
||||
---Initialize the cache node
|
||||
---@param key any
|
||||
---@param value any
|
||||
---@return CacheNode
|
||||
function CacheNode.init(key, value)
|
||||
return {
|
||||
key = key,
|
||||
value = value,
|
||||
freq = 1,
|
||||
prev = nil,
|
||||
next = nil,
|
||||
}
|
||||
end
|
||||
|
||||
---@class LinkedList
|
||||
---@field head CacheNode
|
||||
---@field tail CacheNode
|
||||
---@field length integer
|
||||
local LinkedList = {}
|
||||
|
||||
---Initialize the linked list
|
||||
---@return LinkedList
|
||||
function LinkedList.init()
|
||||
local self = {}
|
||||
self.head = CacheNode.init(0, 0) -- dummy
|
||||
self.tail = CacheNode.init(0, 0) -- dummy
|
||||
self.head.next = self.tail
|
||||
self.tail.prev = self.head
|
||||
self.length = 0
|
||||
return setmetatable(self, { __index = LinkedList })
|
||||
end
|
||||
|
||||
---Add node
|
||||
---@param node CacheNode
|
||||
function LinkedList:add(node)
|
||||
node.prev = self.head
|
||||
node.next = self.head.next
|
||||
self.head.next = node
|
||||
node.next.prev = node
|
||||
self.length = self.length + 1
|
||||
end
|
||||
|
||||
---Remove node
|
||||
---@param node CacheNode
|
||||
function LinkedList:remove(node)
|
||||
node.prev.next = node.next
|
||||
node.next.prev = node.prev
|
||||
self.length = self.length - 1
|
||||
end
|
||||
|
||||
---@class LfuCache
|
||||
---@field capacity integer
|
||||
---@field key2node table<any, CacheNode>
|
||||
---@field list_map table<integer, LinkedList>
|
||||
---@field total_size integer
|
||||
---@field min_freq integer
|
||||
local LfuCache = {}
|
||||
|
||||
---Initialize the cache
|
||||
---@param capacity integer
|
||||
---@return LfuCache
|
||||
function LfuCache.init(capacity)
|
||||
local self = {}
|
||||
self.capacity = capacity
|
||||
self.key2node = {}
|
||||
self.list_map = { LinkedList.init() }
|
||||
self.total_size = 0
|
||||
self.min_freq = 0
|
||||
return setmetatable(self, { __index = LfuCache })
|
||||
end
|
||||
|
||||
---Add a data to the cache
|
||||
---@param key any
|
||||
---@param value any
|
||||
function LfuCache:set(key, value)
|
||||
if self.key2node[key] then
|
||||
local node = self.key2node[key]
|
||||
node.value = value
|
||||
self:_update(node)
|
||||
else
|
||||
if self.total_size == self.capacity then
|
||||
local last_node = self.list_map[self.min_freq].tail.prev
|
||||
self.key2node[last_node.key] = nil
|
||||
self.list_map[self.min_freq]:remove(last_node)
|
||||
self.total_size = self.total_size - 1
|
||||
end
|
||||
|
||||
local new_node = CacheNode.init(key, value)
|
||||
self.key2node[key] = new_node
|
||||
self.list_map[1]:add(new_node)
|
||||
self.min_freq = 1
|
||||
self.total_size = self.total_size + 1
|
||||
end
|
||||
end
|
||||
|
||||
---Fetching a data from the cache
|
||||
---@param key any
|
||||
---@return any
|
||||
function LfuCache:get(key)
|
||||
if self.key2node[key] then
|
||||
local node = self.key2node[key]
|
||||
self:_update(node)
|
||||
return node.value
|
||||
end
|
||||
end
|
||||
|
||||
---Update the number of accesses to a node
|
||||
---@param node CacheNode
|
||||
function LfuCache:_update(node)
|
||||
local cur_freq = node.freq
|
||||
self.list_map[cur_freq]:remove(node)
|
||||
|
||||
node.freq = cur_freq + 1
|
||||
if not self.list_map[node.freq] then
|
||||
self.list_map[node.freq] = LinkedList.init()
|
||||
end
|
||||
self.list_map[node.freq]:add(node)
|
||||
|
||||
if self.list_map[self.min_freq].length == 0 then
|
||||
self.min_freq = cur_freq + 1
|
||||
end
|
||||
end
|
||||
|
||||
return LfuCache
|
28
bundle/cmp-dictionary/lua/cmp_dictionary/lfu_spec.lua
vendored
Normal file
28
bundle/cmp-dictionary/lua/cmp_dictionary/lfu_spec.lua
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
local lfu = require("cmp_dictionary.lfu")
|
||||
|
||||
local cache
|
||||
|
||||
describe("Test for lfu.lua", function()
|
||||
before_each(function()
|
||||
cache = lfu.init(3)
|
||||
end)
|
||||
|
||||
it("single cache", function()
|
||||
cache:set("a", 1)
|
||||
assert.are.equals(1, cache:get("a"))
|
||||
end)
|
||||
|
||||
it("remove the least frequent cache", function()
|
||||
cache:set("a", 1)
|
||||
cache:set("b", 2)
|
||||
cache:set("c", 3)
|
||||
assert.are.equals(1, cache:get("a")) -- freq = 2
|
||||
assert.are.equals(1, cache:get("a")) -- freq = 3
|
||||
assert.are.equals(2, cache:get("b")) -- freq = 2
|
||||
assert.are.equals(3, cache:get("c")) -- freq = 2
|
||||
|
||||
cache:set("d", 4)
|
||||
-- Removed the least frequent cache with the oldest accesses.
|
||||
assert.is_nil(cache:get("b"))
|
||||
end)
|
||||
end)
|
279
bundle/cmp-dictionary/lua/cmp_dictionary/lib/utf8.lua
vendored
Normal file
279
bundle/cmp-dictionary/lua/cmp_dictionary/lib/utf8.lua
vendored
Normal file
@ -0,0 +1,279 @@
|
||||
local utf8 = {}
|
||||
|
||||
local bit = require("bit") -- luajit
|
||||
|
||||
local band = bit.band
|
||||
local bor = bit.bor
|
||||
local rshift = bit.rshift
|
||||
local lshift = bit.lshift
|
||||
|
||||
---The pattern (a string, not a function) "[\0-\x7F\xC2-\xF4][\x80-\xBF]*",
|
||||
---which matches exactly one UTF-8 byte sequence, assuming that the subject is a valid UTF-8 string.
|
||||
utf8.charpattern = "[%z\x01-\x7F\xC2-\xF4][\x80-\xBF]*"
|
||||
|
||||
---@param idx integer
|
||||
---@param func_name string
|
||||
---@param range_name string
|
||||
---@return string @error message
|
||||
local function create_errmsg(idx, func_name, range_name)
|
||||
return string.format("bad argument #%s to '%s' (%s out of range)", idx, func_name, range_name)
|
||||
end
|
||||
|
||||
---Converts indexes of a string to positive numbers.
|
||||
---@param str string
|
||||
---@param idx integer
|
||||
---@param msg string
|
||||
---@return integer
|
||||
local function validate_range(str, idx, msg)
|
||||
idx = idx > 0 and idx or #str + idx + 1
|
||||
if idx < 0 or idx > #str then
|
||||
error(msg, 2)
|
||||
end
|
||||
return idx
|
||||
end
|
||||
|
||||
---Receives zero or more integers, converts each one to its corresponding UTF-8 byte sequence
|
||||
---and returns a string with the concatenation of all these sequences.
|
||||
---@vararg integer
|
||||
---@return string
|
||||
function utf8.char(...)
|
||||
local buffer = {}
|
||||
for i, v in ipairs({ ... }) do
|
||||
if v < 0 or v > 0x10FFFF then
|
||||
error(create_errmsg(i, "char", "value"), 2)
|
||||
elseif v < 0x80 then
|
||||
-- single-byte
|
||||
buffer[i] = string.char(v)
|
||||
elseif v < 0x800 then
|
||||
-- two-byte
|
||||
local b1 = bor(0xC0, band(rshift(v, 6), 0x1F)) -- 110x-xxxx
|
||||
local b2 = bor(0x80, band(v, 0x3F)) -- 10xx-xxxx
|
||||
buffer[i] = string.char(b1, b2)
|
||||
elseif v < 0x10000 then
|
||||
-- three-byte
|
||||
local b1 = bor(0xE0, band(rshift(v, 12), 0x0F)) -- 1110-xxxx
|
||||
local b2 = bor(0x80, band(rshift(v, 6), 0x3F)) -- 10xx-xxxx
|
||||
local b3 = bor(0x80, band(v, 0x3F)) -- 10xx-xxxx
|
||||
buffer[i] = string.char(b1, b2, b3)
|
||||
else
|
||||
-- four-byte
|
||||
local b1 = bor(0xF0, band(rshift(v, 18), 0x07)) -- 1111-0xxx
|
||||
local b2 = bor(0x80, band(rshift(v, 12), 0x3F)) -- 10xx-xxxx
|
||||
local b3 = bor(0x80, band(rshift(v, 6), 0x3F)) -- 10xx-xxxx
|
||||
local b4 = bor(0x80, band(v, 0x3F)) -- 10xx-xxxx
|
||||
buffer[i] = string.char(b1, b2, b3, b4)
|
||||
end
|
||||
end
|
||||
return table.concat(buffer, "")
|
||||
end
|
||||
|
||||
---Returns the next one character range.
|
||||
---@param s string
|
||||
---@param start_pos integer
|
||||
---@return integer? start_pos, integer? end_pos
|
||||
local function next_char(s, start_pos)
|
||||
local b1 = s:byte(start_pos)
|
||||
if not b1 then
|
||||
return -- for offset's #s+1
|
||||
end
|
||||
|
||||
local end_pos
|
||||
|
||||
if band(b1, 0x80) == 0x00 then -- single-byte (0xxx-xxxx)
|
||||
return start_pos, start_pos
|
||||
elseif 0xC2 <= b1 and b1 <= 0xDF then -- two-byte (range 0xC2 to 0xDF)
|
||||
end_pos = start_pos + 1
|
||||
elseif band(b1, 0xF0) == 0xE0 then -- three-byte (1110-xxxx)
|
||||
end_pos = start_pos + 2
|
||||
elseif 0xF0 <= b1 and b1 <= 0xF4 then -- four-byte (range 0xF0 to 0xF4)
|
||||
end_pos = start_pos + 3
|
||||
else -- invalid 1st byte
|
||||
return
|
||||
end
|
||||
|
||||
-- validate (end_pos)
|
||||
if end_pos > #s then
|
||||
return
|
||||
end
|
||||
-- validate (continuation)
|
||||
for _, bn in ipairs({ s:byte(start_pos + 1, end_pos) }) do
|
||||
if band(bn, 0xC0) ~= 0x80 then -- 10xx-xxxx?
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
return start_pos, end_pos
|
||||
end
|
||||
|
||||
---Returns values so that the construction
|
||||
---
|
||||
---for p, c in utf8.codes(s) do body end
|
||||
---
|
||||
---will iterate over all UTF-8 characters in string s, with p being the position (in bytes) and c the code point of each character.
|
||||
---It raises an error if it meets any invalid byte sequence.
|
||||
---@param s string
|
||||
---@return function iterator
|
||||
function utf8.codes(s)
|
||||
vim.validate({
|
||||
s = { s, "string" },
|
||||
})
|
||||
|
||||
local i = 1
|
||||
return function()
|
||||
if i > #s then
|
||||
return
|
||||
end
|
||||
|
||||
local start_pos, end_pos = next_char(s, i)
|
||||
if start_pos == nil then
|
||||
error("invalid UTF-8 code", 2)
|
||||
end
|
||||
|
||||
i = end_pos + 1
|
||||
return start_pos, s:sub(start_pos, end_pos)
|
||||
end
|
||||
end
|
||||
|
||||
---Returns the code points (as integers) from all characters in s that start between byte position i and j (both included).
|
||||
---The default for i is 1 and for j is i.
|
||||
---It raises an error if it meets any invalid byte sequence.
|
||||
---@param s string
|
||||
---@param i? integer start position. default=1
|
||||
---@param j? integer end position. default=i
|
||||
---@return integer @code point
|
||||
function utf8.codepoint(s, i, j)
|
||||
vim.validate({
|
||||
s = { s, "string" },
|
||||
i = { i, "number", true },
|
||||
j = { j, "number", true },
|
||||
})
|
||||
i = validate_range(s, i or 1, create_errmsg(2, "codepoint", "initial position"))
|
||||
j = validate_range(s, j or i, create_errmsg(3, "codepoint", "final position"))
|
||||
|
||||
local ret = {}
|
||||
repeat
|
||||
local char_start, char_end = next_char(s, i)
|
||||
if char_start == nil then
|
||||
error("invalid UTF-8 code", 2)
|
||||
end
|
||||
|
||||
i = char_end + 1
|
||||
|
||||
local len = char_end - char_start + 1
|
||||
if len == 1 then
|
||||
-- single-byte
|
||||
table.insert(ret, s:byte(char_start))
|
||||
else
|
||||
-- multi-byte
|
||||
local b1 = s:byte(char_start)
|
||||
b1 = band(lshift(b1, len + 1), 0xFF) -- e.g. 110x-xxxx -> xxxx-x000
|
||||
b1 = lshift(b1, len * 5 - 7) -- >> len+1 and << (len-1)*6
|
||||
|
||||
local cp = 0
|
||||
for k = char_start + 1, char_end do
|
||||
local bn = s:byte(k)
|
||||
cp = bor(lshift(cp, 6), band(bn, 0x3F))
|
||||
end
|
||||
|
||||
cp = bor(b1, cp)
|
||||
table.insert(ret, cp)
|
||||
end
|
||||
until char_end >= j
|
||||
|
||||
return unpack(ret)
|
||||
end
|
||||
|
||||
---Returns the number of UTF-8 characters in string s that start between positions i and j (both inclusive).
|
||||
---The default for i is 1 and for j is -1.
|
||||
---If it finds any invalid byte sequence, returns fail plus the position of the first invalid byte.
|
||||
---@param s string
|
||||
---@param i? integer start position. default=1
|
||||
---@param j? integer end position. default=-1
|
||||
---@return integer | nil
|
||||
---@return integer?
|
||||
function utf8.len(s, i, j)
|
||||
vim.validate({
|
||||
s = { s, "string" },
|
||||
i = { i, "number", true },
|
||||
j = { j, "number", true },
|
||||
})
|
||||
i = validate_range(s, i or 1, create_errmsg(2, "len", "initial position"))
|
||||
j = validate_range(s, j or -1, create_errmsg(3, "len", "final position"))
|
||||
|
||||
local len = 0
|
||||
|
||||
repeat
|
||||
local char_start, char_end = next_char(s, i)
|
||||
if char_start == nil then
|
||||
return nil, i
|
||||
end
|
||||
|
||||
i = char_end + 1
|
||||
len = len + 1
|
||||
until char_end >= j
|
||||
|
||||
return len
|
||||
end
|
||||
|
||||
---Returns the position (in bytes) where the encoding of the n-th character of s (counting from position i) starts.
|
||||
---A negative n gets characters before position i.
|
||||
---The default for i is 1 when n is non-negative and #s+1 otherwise, so that utf8.offset(s, -n) gets the offset of the n-th character from the end of the string.
|
||||
---If the specified character is neither in the subject nor right after its end, the function returns fail.
|
||||
---
|
||||
---As a special case, when n is 0 the function returns the start of the encoding of the character that contains the i-th byte of s.
|
||||
---@param s string
|
||||
---@param n integer
|
||||
---@param i? integer start position. if n >= 0, default=1, otherwise default=#s+1
|
||||
---@return integer?
|
||||
function utf8.offset(s, n, i)
|
||||
vim.validate({
|
||||
s = { s, "string" },
|
||||
n = { n, "number" },
|
||||
i = { i, "number", true },
|
||||
})
|
||||
|
||||
i = i or n >= 0 and 1 or #s + 1
|
||||
|
||||
if n >= 0 or i ~= #s + 1 then
|
||||
i = validate_range(s, i, create_errmsg(3, "offset", "position"))
|
||||
end
|
||||
|
||||
if n == 0 then
|
||||
for j = i, 1, -1 do
|
||||
local char_start = next_char(s, j)
|
||||
if char_start then
|
||||
return char_start
|
||||
end
|
||||
end
|
||||
elseif n > 0 then
|
||||
if not next_char(s, i) then
|
||||
error("initial position is a continuation byte", 2)
|
||||
end
|
||||
|
||||
for j = i, #s do
|
||||
local char_start = next_char(s, j)
|
||||
if char_start then
|
||||
n = n - 1
|
||||
if n == 0 then
|
||||
return char_start
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
if i ~= #s + 1 and not next_char(s, i) then
|
||||
error("initial position is a continuation byte", 2)
|
||||
end
|
||||
|
||||
for j = i, 1, -1 do
|
||||
local char_start = next_char(s, j)
|
||||
if char_start then
|
||||
n = n + 1
|
||||
if n == 0 then
|
||||
return char_start
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return utf8
|
125
bundle/cmp-dictionary/lua/cmp_dictionary/source.lua
vendored
Normal file
125
bundle/cmp-dictionary/lua/cmp_dictionary/source.lua
vendored
Normal file
@ -0,0 +1,125 @@
|
||||
local source = {}
|
||||
|
||||
local utf8 = require("cmp_dictionary.lib.utf8")
|
||||
local config = require("cmp_dictionary.config")
|
||||
local caches = require("cmp_dictionary.caches")
|
||||
local db = require("cmp_dictionary.db")
|
||||
|
||||
function source.new()
|
||||
return setmetatable({}, { __index = source })
|
||||
end
|
||||
|
||||
---@return string
|
||||
function source.get_keyword_pattern()
|
||||
return [[\k\+]]
|
||||
end
|
||||
|
||||
local candidate_cache = {
|
||||
req = "",
|
||||
items = {},
|
||||
}
|
||||
|
||||
---@param str string
|
||||
---@return boolean
|
||||
local function is_capital(str)
|
||||
return str:find("^%u") and true or false
|
||||
end
|
||||
|
||||
---@param str string
|
||||
---@return string
|
||||
local function to_lower_first(str)
|
||||
local l = str:gsub("^.", string.lower)
|
||||
return l
|
||||
end
|
||||
|
||||
---@param str string
|
||||
---@return string
|
||||
local function to_upper_first(str)
|
||||
local u = str:gsub("^.", string.upper)
|
||||
return u
|
||||
end
|
||||
|
||||
---@param req string
|
||||
---@param isIncomplete boolean
|
||||
---@return table
|
||||
function source.get_candidate(req, isIncomplete)
|
||||
if candidate_cache.req == req then
|
||||
return { items = candidate_cache.items, isIncomplete = isIncomplete }
|
||||
end
|
||||
|
||||
local items
|
||||
local request = config.get("sqlite") and db.request or caches.request
|
||||
items, isIncomplete = request(req, isIncomplete)
|
||||
|
||||
if config.get("first_case_insensitive") then
|
||||
local pre, post = to_upper_first, to_lower_first
|
||||
if is_capital(req) then
|
||||
pre, post = post, pre
|
||||
end
|
||||
for _, item in ipairs(request(pre(req), isIncomplete)) do
|
||||
table.insert(items, { label = post(item.label), detail = item.detail })
|
||||
end
|
||||
end
|
||||
|
||||
candidate_cache.req = req
|
||||
candidate_cache.items = items
|
||||
|
||||
return { items = items, isIncomplete = isIncomplete }
|
||||
end
|
||||
|
||||
---@param request cmp.SourceCompletionApiParams
|
||||
---@param callback fun(response: lsp.CompletionResponse|nil)
|
||||
function source.complete(_, request, callback)
|
||||
-- Clear the cache since the dictionary has been updated.
|
||||
if config.get("sqlite") then
|
||||
if db.is_just_updated() then
|
||||
candidate_cache = {}
|
||||
end
|
||||
else
|
||||
if caches.is_just_updated() then
|
||||
candidate_cache = {}
|
||||
end
|
||||
end
|
||||
|
||||
local exact = config.get("exact")
|
||||
|
||||
---@type string
|
||||
local line = request.context.cursor_before_line
|
||||
local offset = request.offset
|
||||
line = line:sub(offset)
|
||||
if line == "" then
|
||||
return
|
||||
end
|
||||
|
||||
local req, isIncomplete
|
||||
if exact > 0 then
|
||||
local line_len = utf8.len(line)
|
||||
if line_len <= exact then
|
||||
req = line
|
||||
isIncomplete = line_len < exact
|
||||
else
|
||||
local last = exact
|
||||
if line_len ~= #line then
|
||||
last = utf8.offset(line, exact + 1) - 1
|
||||
end
|
||||
req = line:sub(1, last)
|
||||
isIncomplete = false
|
||||
end
|
||||
else
|
||||
-- must be -1
|
||||
req = line
|
||||
isIncomplete = true
|
||||
end
|
||||
|
||||
callback(source.get_candidate(req, isIncomplete))
|
||||
end
|
||||
|
||||
function source.resolve(_, completion_item, callback)
|
||||
if config.get("sqlite") then
|
||||
db.document(completion_item, callback)
|
||||
else
|
||||
require("cmp_dictionary.document")(completion_item, callback)
|
||||
end
|
||||
end
|
||||
|
||||
return source
|
118
bundle/cmp-dictionary/lua/cmp_dictionary/util.lua
vendored
Normal file
118
bundle/cmp-dictionary/lua/cmp_dictionary/util.lua
vendored
Normal file
@ -0,0 +1,118 @@
|
||||
local uv = vim.uv or vim.loop
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param path string
|
||||
---@return string buffer
|
||||
function M.read_file_sync(path)
|
||||
-- 292 == 0x444
|
||||
local fd = assert(uv.fs_open(path, "r", 292))
|
||||
local stat = assert(uv.fs_fstat(fd))
|
||||
local buffer = assert(uv.fs_read(fd, stat.size, 0))
|
||||
uv.fs_close(fd)
|
||||
return buffer
|
||||
end
|
||||
|
||||
---@param list unknown[]
|
||||
---@return unknown[]
|
||||
local function deduplicate(list)
|
||||
local set = {}
|
||||
local new_list = {}
|
||||
for _, v in ipairs(list) do
|
||||
if not set[v] then
|
||||
table.insert(new_list, v)
|
||||
set[v] = true
|
||||
end
|
||||
end
|
||||
return new_list
|
||||
end
|
||||
|
||||
---@return string[]
|
||||
function M.get_dictionaries()
|
||||
-- Workaround. vim.opt_global returns now a local value.
|
||||
-- https://github.com/neovim/neovim/issues/21506
|
||||
---@type string[]
|
||||
local global = vim.split(vim.go.dictionary, ",")
|
||||
---@type string[]
|
||||
local local_ = vim.opt_local.dictionary:get()
|
||||
|
||||
local dict = {}
|
||||
for _, al in ipairs({ global, local_ }) do
|
||||
for _, d in ipairs(al) do
|
||||
if vim.fn.filereadable(vim.fn.expand(d)) == 1 then
|
||||
table.insert(dict, d)
|
||||
end
|
||||
end
|
||||
end
|
||||
dict = deduplicate(dict)
|
||||
return dict
|
||||
end
|
||||
|
||||
---@param vector string[]
|
||||
---@param index integer
|
||||
---@param key string
|
||||
---@return boolean
|
||||
local function ascending_order(vector, index, key)
|
||||
return vector[index] >= key
|
||||
end
|
||||
|
||||
---@param vector unknown[]
|
||||
---@param key string
|
||||
---@param cb fun(vec: unknown[], idx: integer, key: string): boolean
|
||||
---@return integer
|
||||
function M.binary_search(vector, key, cb)
|
||||
local left = 0
|
||||
local right = #vector
|
||||
local isOK = cb or ascending_order
|
||||
|
||||
-- (left, right]
|
||||
while right - left > 1 do
|
||||
local mid = math.floor((left + right) / 2)
|
||||
if isOK(vector, mid, key) then
|
||||
right = mid
|
||||
else
|
||||
left = mid
|
||||
end
|
||||
end
|
||||
|
||||
return right
|
||||
end
|
||||
|
||||
local timer = {}
|
||||
|
||||
local function stop(name)
|
||||
if timer[name] then
|
||||
timer[name]:stop()
|
||||
timer[name]:close()
|
||||
timer[name] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function M.debounce(name, callback, timeout)
|
||||
stop(name)
|
||||
timer[name] = uv.new_timer()
|
||||
timer[name]:start(
|
||||
timeout,
|
||||
0,
|
||||
vim.schedule_wrap(function()
|
||||
stop(name)
|
||||
callback()
|
||||
end)
|
||||
)
|
||||
end
|
||||
|
||||
M.bool_fn = setmetatable({}, {
|
||||
__index = function(_, key)
|
||||
return function(...)
|
||||
local v = vim.fn[key](...)
|
||||
if not v or v == 0 or v == "" then
|
||||
return false
|
||||
elseif type(v) == "table" and next(v) == nil then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
return M
|
5
bundle/cmp-dictionary/stylua.toml
vendored
Normal file
5
bundle/cmp-dictionary/stylua.toml
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
column_width = 100
|
||||
line_endings = "Unix"
|
||||
indent_type = "Spaces"
|
||||
indent_width = 2
|
||||
quote_style = "AutoPreferDouble"
|
@ -98,6 +98,10 @@ cmp.setup({
|
||||
},
|
||||
sources = cmp.config.sources({
|
||||
{ name = 'nvim_lsp' },
|
||||
{
|
||||
name = 'dictionary',
|
||||
keyword_length = 2,
|
||||
},
|
||||
{ name = 'path' },
|
||||
{ name = 'neosnippet' },
|
||||
}, {
|
||||
@ -106,20 +110,51 @@ cmp.setup({
|
||||
})
|
||||
-- `/` cmdline setup.
|
||||
-- cmp.setup.cmdline('/', {
|
||||
-- mapping = cmp.mapping.preset.cmdline(),
|
||||
-- sources = {
|
||||
-- { name = 'buffer' },
|
||||
-- },
|
||||
-- mapping = cmp.mapping.preset.cmdline(),
|
||||
-- sources = {
|
||||
-- { name = 'buffer' },
|
||||
-- },
|
||||
-- })
|
||||
-- `/` cmdline setup.
|
||||
-- cmp.setup.cmdline(':', {
|
||||
-- mapping = cmp.mapping.preset.cmdline(),
|
||||
-- sources = {
|
||||
-- { name = 'buffer' },
|
||||
-- { name = 'path' },
|
||||
-- },
|
||||
-- mapping = cmp.mapping.preset.cmdline(),
|
||||
-- sources = {
|
||||
-- { name = 'buffer' },
|
||||
-- { name = 'path' },
|
||||
-- },
|
||||
-- })
|
||||
-- Setup lspconfig.
|
||||
local capabilities =
|
||||
require('cmp_nvim_lsp').update_capabilities(vim.lsp.protocol.make_client_capabilities())
|
||||
-- Replace <YOUR_LSP_SERVER> with each lsp server you've enabled.
|
||||
|
||||
-- for cmp dictionary
|
||||
local dict = require("cmp_dictionary")
|
||||
|
||||
dict.setup({
|
||||
-- The following are default values.
|
||||
exact = 2,
|
||||
first_case_insensitive = false,
|
||||
document = false,
|
||||
-- document_command = "wn %s -over",
|
||||
async = true,
|
||||
sqlite = false,
|
||||
max_items = -1,
|
||||
capacity = 5,
|
||||
debug = false,
|
||||
})
|
||||
|
||||
|
||||
-- dict.switcher({
|
||||
-- filetype = {
|
||||
-- lua = "/path/to/lua.dict",
|
||||
-- javascript = { "/path/to/js.dict", "/path/to/js2.dict" },
|
||||
-- },
|
||||
-- filepath = {
|
||||
-- [".*xmake.lua"] = { "/path/to/xmake.dict", "/path/to/lua.dict" },
|
||||
-- ["%.tmux.*%.conf"] = { "/path/to/js.dict", "/path/to/js2.dict" },
|
||||
-- },
|
||||
-- spelllang = {
|
||||
-- en = "/path/to/english.dict",
|
||||
-- },
|
||||
-- })
|
||||
|
Loading…
Reference in New Issue
Block a user