1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-01-23 04:00:03 +08:00

feat(cmp-dictionary): add cmp-dictionary

This commit is contained in:
wsdjeg 2023-06-11 21:41:39 +08:00
parent b981945a13
commit 4cd290b71e
41 changed files with 6982 additions and 9 deletions

View File

@ -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,

View 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
View File

@ -0,0 +1 @@
/doc/tags

2
bundle/cmp-dictionary/.luacheckrc vendored Normal file
View File

@ -0,0 +1,2 @@
globals = { 'vim', 'describe', 'it', 'assert', 'before_each' }
max_line_length = false

21
bundle/cmp-dictionary/LICENSE vendored Normal file
View 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
View 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
View 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)

View 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()

View 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 dont 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:

View 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

View 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

View 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

View 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

View 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

View 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)

View 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

View 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

View 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

View 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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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)

View 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

View 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

View 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
View File

@ -0,0 +1,5 @@
column_width = 100
line_endings = "Unix"
indent_type = "Spaces"
indent_width = 2
quote_style = "AutoPreferDouble"

View File

@ -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",
-- },
-- })