Skip to content

Commit c1ce4e9

Browse files
authored
Merge pull request #33 from mhiro2/fix/health-and-config-validation
feat(health,config): Expand :checkhealth and fill config validation gaps
2 parents 1b1f405 + 2177769 commit c1ce4e9

6 files changed

Lines changed: 527 additions & 39 deletions

File tree

lua/peekstack/config/validate/rules/persist.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ local PERSIST_AUTO_RULES = {
2121
{ key = "restore", validate = shared.field_type("boolean") },
2222
{ key = "save", validate = shared.field_type("boolean") },
2323
{ key = "restore_if_empty", validate = shared.field_type("boolean") },
24-
{ key = "debounce_ms", validate = shared.field_type("number") },
24+
{ key = "debounce_ms", validate = shared.field_number_range({ min = 0, max = 600000 }) },
2525
{ key = "save_on_leave", validate = shared.field_type("boolean") },
2626
}
2727

lua/peekstack/config/validate/rules/picker.lua

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,26 @@ local PICKER_RULES = {
1010
{ key = "backend", validate = shared.field_enum(KNOWN_BACKENDS), require_truthy = true },
1111
}
1212

13+
---@type PeekstackConfigFieldRule[]
14+
local PICKER_BUILTIN_RULES = {
15+
{ key = "preview_lines", validate = shared.field_number_range({ min = 0 }) },
16+
}
17+
1318
---@param cfg table
1419
---@param defaults PeekstackConfig
1520
function M.validate(cfg, defaults)
1621
local picker = shared.as_table(cfg.picker)
17-
if picker then
18-
shared.apply_rules(picker, "picker", defaults.picker, PICKER_RULES)
22+
if not picker then
23+
return
24+
end
25+
26+
shared.apply_rules(picker, "picker", defaults.picker, PICKER_RULES)
27+
28+
if picker.builtin ~= nil then
29+
local builtin = shared.ensure_table_field(picker, "builtin", "picker.builtin", defaults.picker.builtin)
30+
if builtin then
31+
shared.apply_rules(builtin, "picker.builtin", defaults.picker.builtin, PICKER_BUILTIN_RULES)
32+
end
1933
end
2034
end
2135

lua/peekstack/config/validate/rules/ui.lua

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ local TITLE_ICON_RULES = {
100100
local LAYOUT_RULES = {
101101
{ key = "style", validate = shared.field_enum(KNOWN_LAYOUT_STYLES), require_truthy = true },
102102
{ key = "max_ratio", validate = shared.field_ratio() },
103+
{ key = "zindex_base", validate = shared.field_number_range({ min = 1 }) },
103104
}
104105

105106
---@type PeekstackConfigFieldRule[]
@@ -120,6 +121,74 @@ local LAYOUT_OFFSET_RULES = {
120121
{ key = "col", validate = shared.field_number_range({ min = 0 }) },
121122
}
122123

124+
---@param map table
125+
local function sanitize_icon_map(map)
126+
local invalid_keys = {}
127+
for key, value in pairs(map) do
128+
if type(key) ~= "string" or type(value) ~= "string" then
129+
invalid_keys[#invalid_keys + 1] = key
130+
end
131+
end
132+
for _, key in ipairs(invalid_keys) do
133+
notify.warn(
134+
string.format(
135+
"ui.title.icons.map[%s] must be a string mapping to a string. Dropping invalid entry",
136+
tostring(key)
137+
)
138+
)
139+
map[key] = nil
140+
end
141+
end
142+
143+
---@param node_types table
144+
local function sanitize_node_types(node_types)
145+
local invalid_keys = {}
146+
for filetype, list in pairs(node_types) do
147+
if type(filetype) ~= "string" then
148+
notify.warn(
149+
string.format("ui.title.context.node_types has non-string key %s. Dropping invalid entry", tostring(filetype))
150+
)
151+
invalid_keys[#invalid_keys + 1] = filetype
152+
elseif type(list) ~= "table" then
153+
notify.warn(
154+
string.format(
155+
"ui.title.context.node_types[%q] must be a list of strings, got %s. Dropping invalid entry",
156+
filetype,
157+
type(list)
158+
)
159+
)
160+
invalid_keys[#invalid_keys + 1] = filetype
161+
else
162+
local sanitized = {}
163+
local dropped = 0
164+
for _, node_type in ipairs(list) do
165+
if type(node_type) == "string" and node_type ~= "" then
166+
sanitized[#sanitized + 1] = node_type
167+
else
168+
dropped = dropped + 1
169+
end
170+
end
171+
if dropped > 0 then
172+
notify.warn(
173+
string.format(
174+
"ui.title.context.node_types[%q] contains %d invalid entries. Ignoring invalid values",
175+
filetype,
176+
dropped
177+
)
178+
)
179+
end
180+
if #sanitized == 0 then
181+
invalid_keys[#invalid_keys + 1] = filetype
182+
else
183+
node_types[filetype] = sanitized
184+
end
185+
end
186+
end
187+
for _, key in ipairs(invalid_keys) do
188+
node_types[key] = nil
189+
end
190+
end
191+
123192
---@param ui table
124193
---@param defaults PeekstackConfigUI
125194
local function validate_keys(ui, defaults)
@@ -250,13 +319,27 @@ local function validate_title(ui, defaults)
250319
if icons.map ~= nil and type(icons.map) ~= "table" then
251320
notify.warn("ui.title.icons.map must be a table, got " .. type(icons.map) .. ". Falling back to defaults")
252321
icons.map = vim.deepcopy(defaults.icons.map)
322+
elseif type(icons.map) == "table" then
323+
sanitize_icon_map(icons.map)
253324
end
254325
end
255326

256327
if title.context ~= nil then
257328
local context = shared.ensure_table_field(title, "context", "ui.title.context", defaults.context)
258329
if context then
259330
shared.apply_rules(context, "ui.title.context", defaults.context, TITLE_CONTEXT_RULES)
331+
if context.node_types ~= nil then
332+
if type(context.node_types) ~= "table" then
333+
notify.warn(
334+
"ui.title.context.node_types must be a table, got "
335+
.. type(context.node_types)
336+
.. ". Falling back to defaults"
337+
)
338+
context.node_types = vim.deepcopy(defaults.context.node_types)
339+
else
340+
sanitize_node_types(context.node_types)
341+
end
342+
end
260343
end
261344
end
262345
end

lua/peekstack/health.lua

Lines changed: 156 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@ local M = {}
22

33
---@type table<string, string>
44
local PICKER_MODULES = {
5-
telescope = "telescope",
5+
telescope = "telescope.pickers",
66
["fzf-lua"] = "fzf-lua",
7-
snacks = "snacks",
7+
snacks = "snacks.picker",
88
}
99

10-
function M.check()
11-
vim.health.start("peekstack")
12-
10+
local function report_environment()
1311
if vim.fn.has("nvim-0.12") == 1 then
1412
vim.health.ok("nvim >= 0.12")
1513
else
@@ -21,20 +19,52 @@ function M.check()
2119
else
2220
vim.health.warn("rg not found (grep.search will be unavailable)")
2321
end
22+
end
2423

25-
local ok, cfg_mod = pcall(require, "peekstack.config")
26-
if not ok then
27-
return
24+
---@param cfg PeekstackConfig
25+
local function report_providers(cfg)
26+
---@type { name: string, enabled: boolean, note: string? }[]
27+
local entries = {
28+
{ name = "lsp", enabled = cfg.providers.lsp.enable },
29+
{ name = "diagnostics", enabled = cfg.providers.diagnostics.enable },
30+
{ name = "file", enabled = cfg.providers.file.enable },
31+
{ name = "grep", enabled = true, note = "always registered" },
32+
{ name = "marks", enabled = cfg.providers.marks.enable },
33+
}
34+
35+
---@type string[]
36+
local enabled_names = {}
37+
for _, entry in ipairs(entries) do
38+
if entry.enabled then
39+
enabled_names[#enabled_names + 1] = entry.name
40+
end
2841
end
29-
local cfg = cfg_mod.get()
3042

31-
-- Picker backend
32-
local backend = cfg.picker and cfg.picker.backend
33-
if backend and backend ~= "builtin" then
43+
if #enabled_names == 0 then
44+
vim.health.warn("providers: none enabled")
45+
else
46+
vim.health.ok("providers enabled: " .. table.concat(enabled_names, ", "))
47+
end
48+
49+
for _, entry in ipairs(entries) do
50+
if not entry.enabled then
51+
vim.health.info(string.format("provider '%s' disabled", entry.name))
52+
elseif entry.note then
53+
vim.health.info(string.format("provider '%s' (%s)", entry.name, entry.note))
54+
end
55+
end
56+
end
57+
58+
---@param cfg PeekstackConfig
59+
local function report_picker(cfg)
60+
local backend = cfg.picker and cfg.picker.backend or "builtin"
61+
if backend == "builtin" then
62+
vim.health.ok("picker backend 'builtin'")
63+
else
3464
local plugin_name = PICKER_MODULES[backend]
3565
if plugin_name then
36-
local has = pcall(require, plugin_name)
37-
if has then
66+
local installed = pcall(require, plugin_name)
67+
if installed then
3868
vim.health.ok("picker backend '" .. backend .. "' available")
3969
else
4070
vim.health.warn("picker backend '" .. backend .. "' is configured but the plugin is not installed")
@@ -44,31 +74,124 @@ function M.check()
4474
end
4575
end
4676

47-
-- Persist
77+
if cfg.picker and cfg.picker.builtin and cfg.picker.builtin.preview_lines ~= nil then
78+
vim.health.info(string.format("picker.builtin.preview_lines = %d", cfg.picker.builtin.preview_lines))
79+
end
80+
end
81+
82+
---@param cfg PeekstackConfig
83+
local function report_persist(cfg)
4884
local persist = cfg.persist
49-
if persist and persist.enabled then
50-
local fs = require("peekstack.util.fs")
51-
local repo = fs.repo_root()
52-
if repo then
53-
vim.health.ok("persist enabled (repo: " .. repo .. ")")
54-
else
55-
vim.health.warn("persist enabled but not inside a git repository; sessions will use cwd-based storage")
85+
if not persist or not persist.enabled then
86+
vim.health.info("persist disabled")
87+
return
88+
end
89+
90+
vim.health.ok("persist enabled (max_items=" .. tostring(persist.max_items) .. ")")
91+
92+
local has_repo = true
93+
local ok_fs, fs = pcall(require, "peekstack.util.fs")
94+
if ok_fs then
95+
local ok_path, path = pcall(fs.scope_path, "repo")
96+
if ok_path and path then
97+
vim.health.info("storage path: " .. path)
5698
end
57-
if persist.auto and persist.auto.enabled then
58-
vim.health.ok("auto persist enabled (session: " .. (persist.auto.session_name or "auto") .. ")")
99+
has_repo = fs.repo_root() ~= nil
100+
if not has_repo then
101+
vim.health.warn("not inside a git repository; sessions fall back to cwd-based storage")
59102
end
60103
end
61104

62-
-- Tree-sitter context
63-
local title = cfg.ui and cfg.ui.title
64-
if title and title.context and title.context.enabled then
65-
local parser = vim.treesitter.get_parser(0)
66-
if parser then
67-
vim.health.ok("tree-sitter context enabled (parser available for current buffer)")
68-
else
69-
vim.health.info("tree-sitter context enabled but no parser for the current buffer filetype")
70-
end
105+
if type(persist.auto) ~= "table" then
106+
vim.health.info("auto persist disabled")
107+
return
108+
end
109+
110+
if not persist.auto.enabled then
111+
vim.health.info("auto persist disabled")
112+
return
113+
end
114+
115+
local message = string.format(
116+
"auto persist enabled (session=%q, debounce_ms=%d, save_on_leave=%s)",
117+
tostring(persist.auto.session_name),
118+
persist.auto.debounce_ms or 0,
119+
tostring(persist.auto.save_on_leave)
120+
)
121+
if has_repo then
122+
vim.health.ok(message)
123+
else
124+
vim.health.warn(message .. "; inactive outside git repository")
125+
end
126+
end
127+
128+
---@param events string[]?
129+
---@return string
130+
local function format_events(events)
131+
if not events or #events == 0 then
132+
return "(none)"
133+
end
134+
return table.concat(events, ", ")
135+
end
136+
137+
---@param cfg PeekstackConfig
138+
local function report_close_events(cfg)
139+
local quick = cfg.ui and cfg.ui.quick_peek and cfg.ui.quick_peek.close_events
140+
vim.health.info("quick_peek close_events: " .. format_events(quick))
141+
142+
local inline = cfg.ui and cfg.ui.inline_preview and cfg.ui.inline_preview.close_events
143+
if cfg.ui and cfg.ui.inline_preview and cfg.ui.inline_preview.enabled then
144+
vim.health.info("inline_preview close_events: " .. format_events(inline))
145+
else
146+
vim.health.info("inline_preview disabled")
147+
end
148+
end
149+
150+
---@param cfg PeekstackConfig
151+
local function report_treesitter(cfg)
152+
local context = cfg.ui and cfg.ui.title and cfg.ui.title.context
153+
if not context or not context.enabled then
154+
vim.health.info("title context disabled (ui.title.context.enabled = false)")
155+
return
156+
end
157+
158+
vim.health.ok(string.format("title context enabled (max_depth=%d)", context.max_depth or 0))
159+
160+
local bufnr = vim.api.nvim_get_current_buf()
161+
local filetype = vim.bo[bufnr].filetype
162+
local label = filetype ~= "" and string.format("filetype=%q", filetype) or "no filetype"
163+
164+
local ok_parser, parser = pcall(vim.treesitter.get_parser, bufnr)
165+
if ok_parser and parser then
166+
vim.health.ok("tree-sitter parser available for current buffer (" .. label .. ")")
167+
else
168+
vim.health.info("tree-sitter context enabled but no parser for the current buffer (" .. label .. ")")
169+
end
170+
end
171+
172+
function M.check()
173+
vim.health.start("peekstack: environment")
174+
report_environment()
175+
176+
local ok, cfg_mod = pcall(require, "peekstack.config")
177+
if not ok then
178+
vim.health.error("peekstack.config could not be loaded")
179+
return
71180
end
181+
local cfg = cfg_mod.get()
182+
183+
vim.health.start("peekstack: providers")
184+
report_providers(cfg)
185+
186+
vim.health.start("peekstack: picker")
187+
report_picker(cfg)
188+
189+
vim.health.start("peekstack: persist")
190+
report_persist(cfg)
191+
192+
vim.health.start("peekstack: ui")
193+
report_close_events(cfg)
194+
report_treesitter(cfg)
72195
end
73196

74197
return M

0 commit comments

Comments
 (0)