local unpack = unpack or table.unpack
local M = {}
local validation_rules = {
['editor'] = { 'table' },
['editor.client'] = { 'string' },
['editor.tooltip'] = { 'string' },
['editor.icon'] = { 'string' },
['display'] = { 'table' },
['display.theme'] = { 'string' },
['display.swap_fields'] = { 'boolean' },
['display.swap_icons'] = { 'boolean' },
['timestamp'] = { 'table' },
['timestamp.enabled'] = { 'boolean' },
['timestamp.reset_on_idle'] = { 'boolean' },
['timestamp.reset_on_change'] = { 'boolean' },
['idle'] = { 'table' },
['idle.enabled'] = { 'boolean' },
['idle.timeout'] = { 'number' },
['idle.show_status'] = { 'boolean' },
['idle.ignore_focus'] = { 'boolean' },
['idle.unidle_on_focus'] = { 'boolean' },
['idle.smart_idle'] = { 'boolean' },
['idle.details'] = { 'string', 'function' },
['idle.state'] = { 'string', 'function' },
['idle.tooltip'] = { 'string', 'function' },
['idle.icon'] = { 'string', 'function' },
['text'] = { 'table' },
['text.workspace'] = { 'string', 'boolean', 'function' },
['text.viewing'] = { 'string', 'boolean', 'function' },
['text.editing'] = { 'string', 'boolean', 'function' },
['text.file_browser'] = { 'string', 'boolean', 'function' },
['text.plugin_manager'] = { 'string', 'boolean', 'function' },
['text.lsp'] = { 'string', 'boolean', 'function' },
['text.docs'] = { 'string', 'boolean', 'function' },
['text.vcs'] = { 'string', 'boolean', 'function' },
['text.notes'] = { 'string', 'boolean', 'function' },
['text.debug'] = { 'string', 'boolean', 'function' },
['text.test'] = { 'string', 'boolean', 'function' },
['text.games'] = { 'string', 'boolean', 'function' },
['text.diagnostics'] = { 'string', 'boolean', 'function' },
['text.terminal'] = { 'string', 'boolean', 'function' },
['text.dashboard'] = { 'string', 'boolean', 'function' },
['buttons'] = { 'table' },
['buttons.*.label'] = { 'string', 'function' },
['buttons.*.url'] = { 'string', 'function' },
['assets'] = { 'table' },
['variables'] = { 'boolean', 'table' },
['plugins'] = { 'table' },
['hooks'] = { 'table' },
['hooks.ready'] = { 'function', 'table' },
['hooks.shutdown'] = { 'function', 'table' },
['hooks.pre_activity'] = { 'function', 'table' },
['hooks.post_activity'] = { 'function', 'table' },
['hooks.idle_enter'] = { 'function', 'table' },
['hooks.idle_leave'] = { 'function', 'table' },
['hooks.workspace_change'] = { 'function', 'table' },
['advanced'] = { 'table' },
['advanced.plugin'] = { 'table' },
['advanced.plugin.autocmds'] = { 'boolean' },
['advanced.plugin.log_level'] = { 'number' },
['advanced.plugin.cursor_update'] = { 'string' },
['advanced.plugin.match_in_mappings'] = { 'boolean' },
['advanced.server'] = { 'table' },
['advanced.server.update'] = { 'string' },
['advanced.server.pipe_path'] = { 'string' },
['advanced.server.executable_path'] = { 'string' },
['advanced.server.timeout'] = { 'number' },
['advanced.discord'] = { 'table' },
['advanced.discord.reconnect'] = { 'table' },
['advanced.discord.reconnect.enabled'] = { 'boolean' },
['advanced.discord.reconnect.interval'] = { 'number' },
['advanced.discord.reconnect.initial'] = { 'boolean', 'table' },
}
local array_paths = {
['buttons'] = true,
['plugins'] = true,
}
local skip_subtrees = {
['plugins'] = true,
}
local dict_paths = {
['assets'] = { 'string', 'table' },
}
local function validate_type(value, allowed_types)
for _, t in ipairs(allowed_types) do
local ty = type(value)
if t == 'table' and ty == 'table' then
return true
elseif t == 'string' and ty == 'string' then
return true
elseif t == 'number' and ty == 'number' then
return true
elseif t == 'boolean' and ty == 'boolean' then
return true
elseif t == 'function' and ty == 'function' then
return true
end
end
return false
end
local function is_valid_path(path)
if validation_rules[path] then return true end
local parts = vim.split(path, '.', { plain = true })
if #parts >= 2 then
local parent = parts[1]
if dict_paths[parent] then return true end
local wildcard_path = table.concat(
vim.tbl_flatten {
{ parts[1] },
{ '*' },
{ unpack(parts, 3) },
},
'.'
)
return validation_rules[wildcard_path] ~= nil
end
return false
end
local function get_nested_value(config, path)
local parts = vim.split(path, '.', { plain = true })
local current = config
for _, part in ipairs(parts) do
if type(current) ~= 'table' then return nil end
current = current[part]
end
return current
end
M.validate = function(user_config)
local errors = {}
local warnings = {}
local function check_unknown_entries(config, prefix)
prefix = prefix or ''
for k, v in pairs(config) do
local full_path = prefix == '' and k or (prefix .. '.' .. k)
local base_path = vim.split(full_path, '.', { plain = true })[1]
local is_plugin_config = base_path == 'plugins' and type(k) == 'number'
if
not (
(array_paths[base_path] and type(k) == 'number')
or (dict_paths[base_path] and type(k) == 'string')
or is_plugin_config
) and not is_valid_path(full_path)
then
table.insert(warnings, string.format('Unknown configuration entry: `%s`', full_path))
end
if dict_paths[base_path] and type(k) == 'string' then
if not validate_type(v, dict_paths[base_path]) then
table.insert(errors, {
msg = string.format('Invalid type \'%s\' for `%s`', type(v), full_path),
hint = string.format(
'Allowed types: \'%s\'',
table.concat(dict_paths[base_path], '\', \'')
),
})
end
end
if type(v) == 'table' and not (skip_subtrees[base_path] and type(k) == 'number') then
check_unknown_entries(v, full_path)
end
end
end
check_unknown_entries(user_config)
for path, allowed_types in pairs(validation_rules) do
local value = get_nested_value(user_config, path)
if value ~= nil and not validate_type(value, allowed_types) then
table.insert(errors, {
msg = string.format('Invalid type \'%s\' for `%s`', type(value), path),
hint = string.format('Allowed types: \'%s\'', table.concat(allowed_types, '\', \'')),
})
end
end
return {
is_valid = #errors == 0 and #warnings == 0,
errors = errors,
warnings = warnings,
}
end
M.check = function()
local health = vim.health or require 'cord.api.config'
local start = health.start or health.report_start
local ok = health.ok or health.report_ok
local warn = health.warn or health.report_warn
local err = health.error or health.report_error
start 'cord.nvim'
local results = M.validate(require('cord.plugin.config.util').user_config)
if results.is_valid then
ok 'Health check passed'
else
for _, error in ipairs(results.errors) do
err(error.msg, error.hint)
end
for _, warning in ipairs(results.warnings) do
warn(warning)
end
end
end
return M