Created
October 30, 2023 02:07
-
-
Save dharmx/9eae648c29a3bbb158173462b07ab4ce to your computer and use it in GitHub Desktop.
Get meanings, examples, antonyms and synonyms of a word using dictd in Neovim. This supports multiple sources like urban dictionary.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
---@diagnostic disable: cast-local-type, param-type-mismatch, assign-type-mismatch, need-check-nil, undefined-field | |
-- TODO: | |
-- + Add UI, HL and mapping options. | |
-- + Cleanup buffers. | |
-- + Use namespaces and add_highlights. | |
-- + Calculate floating window width. | |
-- + Add a telescope extension. | |
-- + Variable name suggestor. | |
-- + Write docs. | |
local M = {} | |
local _PROXY = {} | |
local Task = require("plenary.job") | |
local F = vim.fn | |
local A = vim.api | |
-- NOTE: | |
-- DICTD allows multiple results from different databases but currently | |
-- I have no idea on how to support that. How do I parse those multiple matches? | |
-- Internals {{{ | |
---The active database. | |
---@type string | |
M._database = nil | |
---List of minimal floating buffers. This is used for storing the IDs of | |
---opened minimal word-definition buffers. | |
---@type number[] | |
M._buffers = {} | |
---List of minimal floating windows. This is used for storing the IDs of | |
---opened minimal word-definition windows. | |
---@type integer[] | |
M._windows = {} | |
---@class EthosConfig | |
---@field database string Try using `dict -D` for all available values. | |
---The table of all supported settings for ethos.lua. This should be | |
---treated as the fallback or, the default config. | |
---@type EthosConfig | |
M._defaults = { database = "wn" } | |
---The current config. Values are extended with `M._defaults` and any | |
---invalid value will result in an error. | |
---@type EthosConfig | |
M._config = M._defaults | |
---Searching algorithms. Consult the manpage for more. | |
---@see https://www.mankier.com/8/dictd#Search_Algorithms | |
---@enum DictStrats | |
M._strategy = { | |
"exact", | |
"prefix", | |
"nprefix", | |
"substring", | |
"suffix", | |
"re", | |
"regexp", | |
"soundex", | |
"lev", | |
"word", | |
"first", | |
"last", | |
} | |
-- }}} | |
-- Validators {{{ | |
---Validating functions that can be used for checking the validity of a configuration option value. | |
---@type function[] | |
local validators = {} | |
---@param value? {timeout?: number, wait_interval?: number} | |
---@return boolean | |
---@see Job.sync | |
function validators.sync(value) | |
if value == nil then return true end | |
if type(value) == "boolean" then return true end | |
if type(value.timeout) == "number" and type(value.wait_interval) == "nil" then return true end | |
if type(value.timeout) == "nil" and type(value.wait_interval) == "number" then return true end | |
if type(value.timeout) == "number" and type(value.wait_interval) == "number" then return true end | |
return false | |
end | |
---@param value? string | |
---@return boolean | |
function validators.word(value) | |
if value == nil then return false end | |
return not not value:match("^[a-zA-Z]+$") -- only qualify string with alpha values as word | |
end | |
---@param value? string | |
---@return boolean | |
function validators.database(value) | |
if value == nil then return false end | |
return vim.tbl_contains(M._database, value) | |
end | |
---@param value DictStrats | |
---@return boolean | |
function validators.strategy(value) | |
if value == nil then return true end | |
return vim.tbl_contains(M._strategy, value) | |
end | |
-- }}} | |
---@class DictConfig | |
---@field database string | |
---@field word string | |
---@field formatted boolean | |
---@field strategy DictStrats | |
---@field on_stdout fun(error: string, data: string, self?: Job) | |
---@field on_stderr fun(error: string, data: string, self?: Job) | |
---@field on_exit fun(self?: Job, code: number, signal: number) | |
---@field sync {timeout?: number, wait_interval?: number}|boolean | |
---@async | |
---Get word definition. | |
---@param word string The word that needs to be queried from the dictd database(s). | |
---@param options? DictConfig Dictionary related configurations. | |
---@return table|Job|nil | |
function M.get(word, options) | |
options = vim.F.if_nil(options, {}) | |
-- This is just option validation. You may skip this. | |
vim.validate({ | |
database = { options.database, validators.database, "Valid options are: " .. table.concat(M._database, "|") }, | |
strategy = { options.strategy, validators.strategy, "Valid options are: " .. table.concat(M._strategy, "|") }, | |
formatted = { options.formatted, "boolean", true }, | |
on_stdout = { options.on_stdout, "function", true }, | |
on_exit = { options.on_exit, "function", true }, | |
on_stderr = { options.on_stderr, "function", true }, | |
sync = { options.sync, validators.sync, "Either provide a boolean value / {timeout=number,wait_interval=number}" }, | |
word = { word, validators.word, "Only alphabets are allowed!" }, | |
}) | |
word = word:lower() | |
local args = {} | |
if options.database then | |
table.insert(args, "--database") | |
table.insert(args, options.database) | |
end | |
if options.strategy then | |
table.insert(args, "--strategy") | |
table.insert(args, options.strategy) | |
end | |
if options.formatted then table.insert(args, "--formatted") end | |
if options.nocorrect then table.insert(args, "--nocorrect") end | |
table.insert(args, word) | |
-- maybe allow a way to configure this? I have not thought that far ahead | |
local task = Task:new({ | |
command = "dict", | |
enabled_recording = true, | |
skip_validation = true, | |
args = args, | |
on_exit = options.on_exit, | |
on_stderr = options.on_stderr, | |
on_stdout = options.on_stdout, | |
}) | |
if options.sync then -- this is blocking | |
if type(options.sync) == "table" then return { task:sync(options.timeout, options.wait_interval) } end | |
return { task:sync() } | |
end | |
if options.on_exit or options.on_stderr or options.on_stdout then -- this is async | |
task:start() | |
return | |
end | |
return task | |
end | |
---@class DictSpec | |
---@field count number | |
---@field source string | |
---@field word string | |
---@field content string[] | |
---The output format of `dict -d wn ethos` is like the following. | |
---```bash | |
---1 definition found | |
---From WordNet (r) 3.1 (2011) [wn]: | |
--- | |
---ethos | |
--- n 1: (anthropology) the distinctive spirit of a culture or an | |
--- era; "the Greek ethos" | |
---``` | |
---Note that, the main problem with this is that it wraps manually. | |
---This hinders parsing. | |
---@param raw_value string[] | |
---@return DictSpec | |
function M.parse(raw_value) | |
-- WARN: Needs more guards. | |
local parsed = {} | |
parsed.count = tonumber(table.remove(raw_value, 1):sub(1, 1)) | |
parsed.source = vim.split(table.remove(raw_value, 2), " ", { plain = true })[2] | |
parsed.word = vim.trim(table.remove(raw_value, 3)) | |
parsed.content = {} -- root of all headaches | |
for _, item in ipairs(raw_value) do | |
local trimmed = vim.trim(item) | |
if trimmed ~= "" then table.insert(parsed.content, item) end | |
end | |
return parsed | |
end | |
---Open a minimal floating window and populate it with the word definition(s). | |
---@param word? string The word which needs to searched for in the dictd database(s). | |
---@param options? DictConfig Optional customization options. | |
function M.open(word, options) | |
word = vim.F.if_nil(word, F.expand("<cword>")) -- get word under the cursor | |
if not validators.word(word) then return end | |
options = vim.F.if_nil(options, {}) | |
options.database = M._config.database -- maybe allow to change on the fly? | |
-- this will be passed onto plenary.job. | |
options.on_exit = vim.schedule_wrap(function(self, code, _) | |
if code == 0 then | |
local parsed = M.parse(self:result()) | |
-- TODO: Format and display. Do I need to write a parser? Is there really no other way? | |
local replacement = parsed.content | |
-- allow customizing these. | |
local buffer = A.nvim_create_buf(false, true) | |
local window = A.nvim_open_win(buffer, false, { | |
border = "solid", | |
external = false, | |
focusable = true, | |
height = #replacement, | |
relative = "cursor", | |
width = 80, | |
row = 3, | |
col = 3, | |
style = "minimal", | |
}) | |
A.nvim_buf_set_lines(buffer, 0, -1, false, replacement) | |
vim.keymap.set("n", "q", function() A.nvim_win_close(window, true) end, { | |
buffer = buffer, | |
desc = "Close dictd window.", | |
}) | |
-- record float buffer and window history. | |
table.insert(M._buffers, buffer) | |
table.insert(M._windows, window) | |
end | |
end) | |
-- basically get the word definition and run a callback on exit. | |
M.get(word, options) | |
end | |
---I did not need to write this. But anyway. | |
---Utility function for closing all floating word-definition windows. | |
function M.close() | |
for _, window in ipairs(M._windows) do | |
A.nvim_win_close(window, true) | |
end | |
M._windows = {} | |
end | |
---Initialize the plugin and customize its behaviour. | |
---@param options EthosConfig? Optional customization options. | |
function M.setup(options) | |
options = vim.F.if_nil(options, {}) | |
---@see vim.tbl_deep_extend | |
M._config = vim.tbl_deep_extend("keep", options, M._defaults) | |
-- WARN: High risk of UI blockage. Maybe use Job.sync with a timeout? | |
local db = F.system({ "dict", "--dbs" }) | |
db = vim.split(db, "\n", { plain = true }) | |
table.remove(db, 1) | |
table.remove(db, #db) | |
for index, item in ipairs(db) do | |
db[index] = vim.split(vim.trim(item), " ", { plain = true })[1] | |
if db[index] == "" then table.remove(db, index) end | |
end | |
if #db > 0 then | |
M._database = db | |
else | |
error("No databases found. See https://github.com/cheusov/dictd.") | |
end | |
-- TODO: Allow customizations. | |
vim.keymap.set("n", ";f", function() | |
if #M._windows > 0 then | |
M.close() | |
return | |
end | |
M.open() | |
end) | |
M._loaded = true -- you are only allowed to call setup once. | |
end | |
---A shorthand for `vim.api.nvim_notify(message, vim.log.levels.WARN, {...})`. | |
---@param message string | |
function M.warn(message) A.nvim_notify(message, vim.log.levels.WARN, { title = "ethos.lua", icon = " " }) end | |
-- NOTE: Forbid calling non-existent members and setup twice. | |
setmetatable(_PROXY, { | |
__index = function(_, item) | |
if not M._loaded then | |
if item == "setup" then return M.setup end | |
M.warn("You need to run setup() first!") | |
return function() return false end | |
end | |
if vim.tbl_contains(vim.tbl_keys(M), item) then return M[item] end | |
M.warn(item .. " item does not exist!") | |
return function() return false end -- should I just call error() instead? | |
end, | |
}) | |
return _PROXY |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment