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