From fc7060e0edc4888fe7f82f3e0b89c8a23875eb2d Mon Sep 17 00:00:00 2001 From: Oscar Wallberg Date: Mon, 13 Apr 2026 01:20:52 +0200 Subject: [PATCH] refactor: correctness fixes, API modernization, and cleanup --- lua/core/mappings.lua | 2 +- lua/core/options.lua | 1 + lua/dap/hover.lua | 6 +- lua/dap/hover/content.lua | 7 ++- lua/dap/hover/node.lua | 2 + lua/dap/hover/window.lua | 111 ++++++++++++++++------------------ lua/dap/item.lua | 4 +- lua/linter.lua | 117 +++++++++++++++--------------------- lua/pack.lua | 23 ++++--- lua/plugins/gitsigns.lua | 22 +++---- lua/plugins/nvim-notify.lua | 8 ++- lua/util.lua | 86 +++++++------------------- 12 files changed, 166 insertions(+), 223 deletions(-) diff --git a/lua/core/mappings.lua b/lua/core/mappings.lua index 40d9b62..fed0b40 100644 --- a/lua/core/mappings.lua +++ b/lua/core/mappings.lua @@ -3,7 +3,7 @@ vim.keymap.set("n", "tn", vim.cmd.tabnew) vim.keymap.set("n", "tq", vim.cmd.tabclose) -- Clipboard -if vim.env.TMUX and vim.fn.executable("tmux") then +if vim.env.TMUX ~= nil and vim.fn.executable("tmux") == 1 then vim.keymap.set( { "n", "x" }, "y", diff --git a/lua/core/options.lua b/lua/core/options.lua index 13d1e6d..66843f6 100644 --- a/lua/core/options.lua +++ b/lua/core/options.lua @@ -26,6 +26,7 @@ vim.opt.foldlevel = 99 vim.opt.foldlevelstart = 99 vim.opt.foldmethod = "indent" vim.opt.foldignore = "" +---@diagnostic disable-next-line: undefined-field vim.opt.completeopt:append({ "menu", "menuone", diff --git a/lua/dap/hover.lua b/lua/dap/hover.lua index 9a01a86..4b47982 100644 --- a/lua/dap/hover.lua +++ b/lua/dap/hover.lua @@ -11,9 +11,7 @@ local function hover_async() return end - local win = Window.get_instance(session) - if win.winid and vim.api.nvim_win_is_valid(win.winid) then - vim.api.nvim_set_current_win(win.winid) + if Window.focus_if_shown() then return end @@ -115,7 +113,7 @@ local function hover_async() local item = Item.new(expr, resp.type, resp.result, resp.variablesReference) local root = Node.new(item, nil, session.filetype) root:load_children(session) - win:show(root) + Window.show(session, root) end local function hover() diff --git a/lua/dap/hover/content.lua b/lua/dap/hover/content.lua index e60f868..4214c19 100644 --- a/lua/dap/hover/content.lua +++ b/lua/dap/hover/content.lua @@ -70,7 +70,12 @@ function Content:add_with_treesitter(text, lang) return end - local tree = parser:parse()[1] + local trees = parser:parse() + if not trees then + return + end + + local tree = trees[1] if not tree then return end diff --git a/lua/dap/hover/node.lua b/lua/dap/hover/node.lua index 73e5821..991c03b 100644 --- a/lua/dap/hover/node.lua +++ b/lua/dap/hover/node.lua @@ -93,6 +93,7 @@ function Node:index_of(target) return search(self) end +---@async ---@param session dap.Session function Node:expand_all(session) if not self:is_expandable() then @@ -139,6 +140,7 @@ end function Node:is_c_pointer() return self:is_c_lang() and self:is_container() + and self.item.type ~= nil and self.item.type:match( "%*%s*[const%s]*[volatile%s]*[restrict%s]*$" ) diff --git a/lua/dap/hover/window.lua b/lua/dap/hover/window.lua index b9de58f..e1fda5f 100644 --- a/lua/dap/hover/window.lua +++ b/lua/dap/hover/window.lua @@ -6,9 +6,9 @@ local log = require("log") ---@field NS_ID integer ---@field max_width? integer ---@field max_height? integer ----@field winid? integer ----@field bufnr? integer ----@field augroup? integer +---@field winid integer +---@field bufnr integer +---@field augroup integer ---@field session dap.Session ---@field root ow.dap.hover.Node local Window = {} @@ -32,36 +32,25 @@ vim.api.nvim_create_autocmd("ColorScheme", { callback = setup_highlights, }) +---@type ow.dap.hover.Window? local instance = nil ----@param session dap.Session ----@return ow.dap.hover.Window -function Window.get_instance(session) - if instance then - return instance +---Focus the hover window if one is currently shown. +---@return boolean focused true if a window was shown and is now focused +function Window.focus_if_shown() + if instance and vim.api.nvim_win_is_valid(instance.winid) then + vim.api.nvim_set_current_win(instance.winid) + return true end - - instance = setmetatable({ - max_width = nil, - max_height = nil, - winid = nil, - bufnr = nil, - augroup = nil, - session = session, - root = nil, - }, Window) - - return instance + return false end function Window:destroy() - if self.winid and vim.api.nvim_win_is_valid(self.winid) then + if vim.api.nvim_win_is_valid(self.winid) then vim.api.nvim_win_close(self.winid, true) end - if self.augroup then - vim.api.nvim_del_augroup_by_id(self.augroup) - end + vim.api.nvim_del_augroup_by_id(self.augroup) instance = nil end @@ -237,13 +226,17 @@ function Window:yank_value() end ---@async +---@param session dap.Session ---@param root ow.dap.hover.Node -function Window:show(root) - self.root = root +function Window.show(session, root) + local win = instance or setmetatable({}, Window) + instance = win + win.session = session + win.root = root local prev_buf = vim.api.nvim_get_current_buf() - self.bufnr = vim.api.nvim_create_buf(false, true) - self.winid = vim.api.nvim_open_win(self.bufnr, false, { + win.bufnr = vim.api.nvim_create_buf(false, true) + win.winid = vim.api.nvim_open_win(win.bufnr, false, { relative = "cursor", width = 1, height = 1, @@ -252,85 +245,85 @@ function Window:show(root) border = "rounded", style = "minimal", }) - vim.wo[self.winid].wrap = false + vim.wo[win.winid].wrap = false - self:refresh_tree(self.root, 1, 1) + win:refresh_tree(win.root, 1, 1) - self.augroup = + win.augroup = vim.api.nvim_create_augroup(Window.NAMESPACE, { clear = true }) vim.api.nvim_create_autocmd({ "CursorMoved", "InsertEnter" }, { - group = self.augroup, + group = win.augroup, buffer = prev_buf, once = true, callback = function() - self:destroy() + win:destroy() end, }) vim.api.nvim_create_autocmd("BufEnter", { - group = self.augroup, + group = win.augroup, callback = function(arg) - if arg.buf ~= self.bufnr then - self:destroy() + if arg.buf ~= win.bufnr then + win:destroy() return true end end, }) vim.api.nvim_create_autocmd("WinClosed", { - group = self.augroup, + group = win.augroup, once = true, - pattern = tostring(self.winid), + pattern = tostring(win.winid), callback = function() - self:destroy() + win:destroy() end, }) -- Toggle expand/collapse vim.keymap.set("n", "", function() - self:toggle_node() - end, { buffer = self.bufnr, nowait = true }) + win:toggle_node() + end, { buffer = win.bufnr, nowait = true }) vim.keymap.set("n", "", function() - self:toggle_node() - end, { buffer = self.bufnr, nowait = true }) + win:toggle_node() + end, { buffer = win.bufnr, nowait = true }) -- Collapse vim.keymap.set("n", "", function() - self:collapse_parent() - end, { buffer = self.bufnr, nowait = true }) + win:collapse_parent() + end, { buffer = win.bufnr, nowait = true }) vim.keymap.set("n", "", function() - self:collapse_parent() - end, { buffer = self.bufnr, nowait = true }) + win:collapse_parent() + end, { buffer = win.bufnr, nowait = true }) -- Tree operations vim.keymap.set("n", "E", function() - self:expand_all_at_cursor() - end, { buffer = self.bufnr, nowait = true }) + win:expand_all_at_cursor() + end, { buffer = win.bufnr, nowait = true }) vim.keymap.set("n", "C", function() - self:collapse_all_at_cursor() - end, { buffer = self.bufnr, nowait = true }) + win:collapse_all_at_cursor() + end, { buffer = win.bufnr, nowait = true }) -- Navigation vim.keymap.set("n", "p", function() - self:goto_parent() - end, { buffer = self.bufnr, nowait = true }) + win:goto_parent() + end, { buffer = win.bufnr, nowait = true }) -- Quick actions vim.keymap.set("n", "q", function() - self:destroy() - end, { buffer = self.bufnr, nowait = true }) + win:destroy() + end, { buffer = win.bufnr, nowait = true }) vim.keymap.set("n", "", function() - self:destroy() - end, { buffer = self.bufnr, nowait = true }) + win:destroy() + end, { buffer = win.bufnr, nowait = true }) -- Yank operations vim.keymap.set("n", "y", function() - self:yank_value() - end, { buffer = self.bufnr, nowait = true }) + win:yank_value() + end, { buffer = win.bufnr, nowait = true }) end return Window diff --git a/lua/dap/item.lua b/lua/dap/item.lua index 09b044d..d7811c0 100644 --- a/lua/dap/item.lua +++ b/lua/dap/item.lua @@ -1,13 +1,13 @@ ---@class ow.dap.Item ---@field name string ----@field type string +---@field type? string ---@field value string ---@field variablesReference? number local Item = {} Item.__index = Item ---@param name string ----@param type string +---@param type? string ---@param value string ---@param variablesReference? number ---@return ow.dap.Item diff --git a/lua/linter.lua b/lua/linter.lua index 5dc3e89..db23fa9 100644 --- a/lua/linter.lua +++ b/lua/linter.lua @@ -33,9 +33,9 @@ local util = require("util") --- * %filename% - name of the current file ---@field cmd string[] --- Events that trigger the linter (default: TextChanged, TextChangedI) ----@field events? string[] +---@field events? vim.api.keyset.events[] --- Events that clear diagnostics ----@field clear_events? string[] +---@field clear_events? vim.api.keyset.events[] --- Pass buffer content via stdin (default: false) ---@field stdin? boolean --- Read diagnostics from stdout (default: false) @@ -51,7 +51,7 @@ local util = require("util") --- Source name for diagnostics (default: command name) ---@field source? string --- Debounce delay in ms (default: 100) ----@field debounce? number +---@field debounce? integer --- Configuration for JSON output parsing ---@field json? ow.lsp.linter.JsonConfig --- Map diagnostic codes to tags @@ -68,9 +68,9 @@ local util = require("util") ---@field hook? fun(self: ow.lsp.Linter, diagnostics: vim.Diagnostic[]) ---@class ow.lsp.Linter ----@field namespace number ----@field augroup number ----@field bufnr number +---@field namespace integer +---@field augroup integer +---@field bufnr integer ---@field config ow.lsp.linter.Config Linter = {} Linter.__index = Linter @@ -161,21 +161,21 @@ end ---@param diag vim.Diagnostic function Linter:fix_indexing(diag) if not self.config.zero_idx_lnum then - if diag.lnum then + if diag.lnum and diag.lnum > 0 then diag.lnum = diag.lnum - 1 end - if diag.end_lnum then + if diag.end_lnum and diag.end_lnum > 0 then diag.end_lnum = diag.end_lnum - 1 end end if not self.config.zero_idx_col then - if diag.col then + if diag.col and diag.col > 0 then diag.col = diag.col - 1 end - if diag.end_col then + if diag.end_col and diag.end_col > 0 then diag.end_col = diag.end_col - 1 end end @@ -236,68 +236,45 @@ end ---@param config ow.lsp.linter.Config ---@return boolean function Linter.validate(config) - local ok, resp = pcall(vim.validate, { - config = { config, "table" }, - }) - - if ok then - ok, resp = pcall(vim.validate, { - cmd = { - config.cmd, - function(t) - return util.is_list(t, "string") - end, - "list of strings", - }, - events = { - config.events, - function(t) - return util.is_list(t, "string") - end, - true, - "list of strings", - }, - clear_events = { - config.clear_events, - function(t) - return util.is_list(t, "string") - end, - true, - "list of strings", - }, - stdin = { config.stdin, "boolean", true }, - stdout = { config.stdout, "boolean", true }, - stderr = { config.stderr, "boolean", true }, - pattern = { config.pattern, "string", true }, - groups = { - config.groups, - function(t) - return util.is_list(t, "string") - end, - true, - "list of strings", - }, - severity_map = { - config.severity_map, - function(t) - return util.is_map(t, "string", "number") - end, - true, - "map of string and number", - }, - debounce = { config.debounce, "number", true }, - source = { config.source, "string", true }, - json = { config.json, "table", true }, - tags = { config.tags, "table", true }, - zero_idx_lnum = { config.zero_idx_lnum, "boolean", true }, - zero_idx_col = { config.zero_idx_col, "boolean", true }, - ignore_stderr = { config.ignore_stderr, "boolean", true }, - ignore_exit = { config.ignore_exit, "boolean", true }, - }) + local function list_of(ty) + return function(t) + return util.is_list(t, ty) + end end + local ok, err = pcall(function() + vim.validate("config", config, "table") + vim.validate("cmd", config.cmd, list_of("string"), "list of strings") + vim.validate( + "events", config.events, list_of("string"), true, "list of strings" + ) + vim.validate( + "clear_events", config.clear_events, list_of("string"), true, + "list of strings" + ) + vim.validate("stdin", config.stdin, "boolean", true) + vim.validate("stdout", config.stdout, "boolean", true) + vim.validate("stderr", config.stderr, "boolean", true) + vim.validate("pattern", config.pattern, "string", true) + vim.validate( + "groups", config.groups, list_of("string"), true, "list of strings" + ) + vim.validate("severity_map", config.severity_map, function(t) + return util.is_map(t, "string", "number") + end, true, "map of string to number" + ) + vim.validate("debounce", config.debounce, "number", true) + vim.validate("source", config.source, "string", true) + vim.validate("json", config.json, "table", true) + vim.validate("tags", config.tags, "table", true) + vim.validate("zero_idx_lnum", config.zero_idx_lnum, "boolean", true) + vim.validate("zero_idx_col", config.zero_idx_col, "boolean", true) + vim.validate("ignore_stderr", config.ignore_stderr, "boolean", true) + vim.validate("ignore_exit", config.ignore_exit, "boolean", true) + end) + if not ok then - log.error("Invalid config for linter: %s", resp) + log.error("Invalid config for linter: %s", err) return false end @@ -452,7 +429,7 @@ function Linter.add(bufnr, config) buffer = linter.bufnr, callback = util.debounce(function() linter:run() - end, linter.config.debounce), + end, config.debounce), group = linter.augroup, }) diff --git a/lua/pack.lua b/lua/pack.lua index 4969d50..9c43749 100644 --- a/lua/pack.lua +++ b/lua/pack.lua @@ -32,7 +32,12 @@ end ---@param plugin ow.Pack.Plugin local function load(plugin) - local name = plugin.name:match("[^.]+"):lower() + local name = plugin.name:match("[^.]+") + if not name then + log.error("Invalid plugin name: %s", plugin.name) + return + end + name = name:lower() local path = string.format("%s/lua/plugins/%s.lua", config_dir, name) if vim.uv.fs_stat(path) then @@ -121,7 +126,7 @@ end ---@type uv.uv_fs_event_t? local watcher = nil ----@type table +---@type table local timers = {} ---@class ow.Pack @@ -183,18 +188,20 @@ function M.watch() return end - if timers[filename] then - timers[filename]:stop() + local timer = timers[filename] + if timer ~= nil then + timer:stop() else - timers[filename] = vim.uv.new_timer() + timer = vim.uv.new_timer() + timers[filename] = timer end - timers[filename]:start( + timer:start( 100, 0, vim.schedule_wrap(function() - timers[filename]:stop() - timers[filename]:close() + timer:stop() + timer:close() timers[filename] = nil local path = plugins_dir .. "/" .. filename diff --git a/lua/plugins/gitsigns.lua b/lua/plugins/gitsigns.lua index 6564e44..5a83d0b 100644 --- a/lua/plugins/gitsigns.lua +++ b/lua/plugins/gitsigns.lua @@ -3,7 +3,7 @@ require("gitsigns").setup({ border = "single", }, on_attach = function(bufnr) - local gs = package.loaded.gitsigns + local gs = require("gitsigns") vim.keymap.set( "n", "gv", @@ -14,12 +14,6 @@ require("gitsigns").setup({ vim.keymap.set("x", "gs", function() gs.stage_hunk({ vim.fn.line("."), vim.fn.line("v") }) end, { buffer = bufnr }) - vim.keymap.set( - { "n", "x" }, - "gu", - gs.undo_stage_hunk, - { buffer = bufnr } - ) vim.keymap.set("n", "gr", gs.reset_hunk, { buffer = bufnr }) vim.keymap.set( "x", @@ -37,19 +31,25 @@ require("gitsigns").setup({ gs.blame_line({ full = true, ignore_whitespace = true }) end, { buffer = bufnr }) vim.keymap.set({ "n", "x" }, "]g", function() - gs.next_hunk({ + gs.nav_hunk("next", { wrap = true, - navigation_message = true, foldopen = true, + navigation_message = true, + greedy = true, preview = true, + count = 1, + target = "all", }) end) vim.keymap.set({ "n", "x" }, "[g", function() - gs.prev_hunk({ + gs.nav_hunk("prev", { wrap = true, - navigation_message = true, foldopen = true, + navigation_message = true, + greedy = true, preview = true, + count = 1, + target = "all", }) end) end, diff --git a/lua/plugins/nvim-notify.lua b/lua/plugins/nvim-notify.lua index 4e42f46..2918b65 100644 --- a/lua/plugins/nvim-notify.lua +++ b/lua/plugins/nvim-notify.lua @@ -1,6 +1,8 @@ -vim.notify = require("notify") ----@diagnostic disable-next-line: missing-fields -vim.notify.setup({ +local notify = require("notify") +notify.setup({ render = "default", stages = "static", + merge_duplicates = true, }) + +vim.notify = notify diff --git a/lua/util.lua b/lua/util.lua index 89dabf7..5854612 100644 --- a/lua/util.lua +++ b/lua/util.lua @@ -4,10 +4,7 @@ local Util = {} Util.os_name = vim.uv.os_uname().sysname ----@alias OutputStream ----| '"stdout"' ----| '"stderr"' ----| '"in_place"' +---@alias OutputStream "stdout" | "stderr" | "in_place" ---@class ow.FormatOptions ---@field buf? integer Buffer to apply formatting to @@ -26,37 +23,20 @@ Util.os_name = vim.uv.os_uname().sysname ---@field ignore_ret? boolean Ignore non-zero return codes ---@field ignore_stderr? boolean Ignore stderr output when not using stderr for output ---@field env? table Map of environment variables -local FormatOptions = {} -FormatOptions.__index = FormatOptions - -function FormatOptions.from_opts(opts) - return setmetatable({ - buf = opts.buf or vim.api.nvim_get_current_buf(), - cmd = opts.cmd, - output = opts.output or "stdout", - auto_indent = opts.auto_indent or false, - only_selection = opts.only_selection or false, - ignore_ret = opts.ignore_ret or false, - ignore_stderr = opts.ignore_stderr or false, - env = opts.env, - }, FormatOptions) -end --- Format buffer ---@param opts ow.FormatOptions function Util.format(opts) - opts = FormatOptions.from_opts(opts) - - if - opts.output ~= "stdout" - and opts.output ~= "stderr" - and opts.output ~= "in_place" - then - log.error( - "`output` must be set to either `stdout`, `stderr` or `in_place`." - ) - return - end + opts = { + buf = opts.buf or vim.api.nvim_get_current_buf(), + cmd = opts.cmd, + output = opts.output or "stdout", + auto_indent = opts.auto_indent, + only_selection = opts.only_selection, + ignore_ret = opts.ignore_ret, + ignore_stderr = opts.ignore_stderr, + env = opts.env, + } local file = vim.api.nvim_buf_get_name(opts.buf) local filename = vim.fn.fnamemodify(file, ":t") @@ -143,29 +123,12 @@ function Util.format(opts) opts.cmd[i] = arg end - local stdout, stderr, err local resp = vim.system(opts.cmd, { stdin = input, - stdout = opts.output == "stdout" and function(e, data) - if data then - stdout = stdout and stdout .. data or data - end - - if e then - err = err and err .. e or e - end - end, - stderr = not opts.ignore_stderr and function(e, data) - if e then - err = err and err .. e or e - end - - if data then - stderr = stderr and stderr .. data or data - end - end, env = opts.env, }):wait() + local stdout = resp.stdout or "" + local stderr = resp.stderr or "" local tmp_out if tmp then @@ -173,17 +136,12 @@ function Util.format(opts) os.remove(tmp) end - if err then - log.error("Error during formatting:\n%s", err) - return - end - if - not opts.ignore_ret and resp.code ~= 0 - or (opts.output ~= "stderr" and stderr) + (not opts.ignore_ret and resp.code ~= 0) + or (opts.output ~= "stderr" and not opts.ignore_stderr and stderr ~= "") then local msg = "" - if stderr then + if stderr ~= "" then msg = ":\n" .. stderr end @@ -191,13 +149,13 @@ function Util.format(opts) return end - local output + local output = "" if opts.output == "stdout" then output = stdout elseif opts.output == "stderr" then output = stderr elseif opts.output == "in_place" then - output = tmp_out + output = tmp_out or "" end output = output:gsub("%s+$", "") @@ -258,7 +216,7 @@ function Util.format(opts) local view = vim.fn.winsaveview() - vim.lsp.util.apply_text_edits(text_edits, opts.buf, vim.o.encoding) + vim.lsp.util.apply_text_edits(text_edits, opts.buf, "utf-16") if opts.auto_indent then vim.api.nvim_cmd({ @@ -335,10 +293,10 @@ end --- Creates a debounced function that delays execution of `fn` until after `delay` milliseconds have --- elapsed since the last time it was invoked. ---@param fn fun(...) Function to be debounced ----@param delay number Debounce delay in milliseconds +---@param delay integer Debounce delay in milliseconds ---@return fun(...) function Debounced function function Util.debounce(fn, delay) - ---@type uv_timer_t? + ---@type uv.uv_timer_t? local timer = nil return function(...) @@ -358,7 +316,7 @@ end --- Creates a debounced function that delays execution of `fn` until after `delay` milliseconds have --- elapsed since the last time it was invoked with the same unique identifier. ---@param fn fun(...) Function to be debounced ----@param delay number Debounce delay in milliseconds +---@param delay integer Debounce delay in milliseconds ---@return fun(id: any, ...) function Debounced function, where `id` is a unique identifier function Util.debounce_with_id(fn, delay) local map = {}