diff --git a/lua/quicktest.lua b/lua/quicktest.lua index ede3d8c..ed1f574 100644 --- a/lua/quicktest.lua +++ b/lua/quicktest.lua @@ -1,6 +1,7 @@ -- main module file local module = require("quicktest.module") +---@type QuicktestConfig local config = { adapters = {}, default_win_mode = "split", @@ -19,70 +20,84 @@ local config = { }, } ----@class MyModule +---@class Quicktest local M = {} --- @type QuicktestConfig M.config = config ----@param args QuicktestConfig? -M.setup = function(args) - M.config = vim.tbl_deep_extend("force", M.config, args or {}) +---@param args? QuicktestConfig +function M.setup(args) + args = args or {} + M.config = vim.tbl_deep_extend("force", M.config, args) end -M.current_win_mode = function() +function M.current_win_mode() return module.current_win_mode(M.config.default_win_mode) end --- @param mode WinModeWithoutAuto -M.open_win = function(mode) +function M.open_win(mode) return module.try_open_win(mode) end --- @param mode WinModeWithoutAuto -M.close_win = function(mode) +function M.close_win(mode) return module.try_close_win(mode) end --- @param mode WinModeWithoutAuto -M.toggle_win = function(mode) +function M.toggle_win(mode) return module.toggle_win(mode) end ---- @param mode WinMode? -M.run_previous = function(mode) - return module.run_previous(M.config, mode or "auto") +--- @param mode? WinMode +function M.run_previous(mode) + mode = mode or "auto" + return module.run_previous(M.config, mode) end ---- @param mode WinMode? ---- @param adapter Adapter? ---- @param opts AdapterRunOpts? -M.run_line = function(mode, adapter, opts) - return module.prepare_and_run(M.config, "line", mode or "auto", adapter or "auto", opts or {}) +--- @param mode? WinMode +--- @param adapter? Adapter +--- @param opts? AdapterRunOpts +function M.run_line(mode, adapter, opts) + mode = mode or "auto" + adapter = adapter or "auto" + opts = opts or {} + return module.prepare_and_run(M.config, "line", mode, adapter, opts) end ---- @param mode WinMode? ---- @param adapter Adapter? ---- @param opts AdapterRunOpts? -M.run_file = function(mode, adapter, opts) - return module.prepare_and_run(M.config, "file", mode or "auto", adapter or "auto", opts or {}) +--- @param mode? WinMode +--- @param adapter? Adapter +--- @param opts? AdapterRunOpts +function M.run_file(mode, adapter, opts) + mode = mode or "auto" + adapter = adapter or "auto" + opts = opts or {} + return module.prepare_and_run(M.config, "file", mode, adapter, opts) end ---- @param mode WinMode? ---- @param adapter Adapter? ---- @param opts AdapterRunOpts? -M.run_dir = function(mode, adapter, opts) - return module.prepare_and_run(M.config, "dir", mode or "auto", adapter or "auto", opts or {}) +--- @param mode? WinMode +--- @param adapter? Adapter +--- @param opts? AdapterRunOpts +function M.run_dir(mode, adapter, opts) + mode = mode or "auto" + adapter = adapter or "auto" + opts = opts or {} + return module.prepare_and_run(M.config, "dir", mode, adapter, opts) end ---- @param mode WinMode? ---- @param adapter Adapter? ---- @param opts AdapterRunOpts? -M.run_all = function(mode, adapter, opts) - return module.prepare_and_run(M.config, "all", mode or "auto", adapter or "auto", opts or {}) +--- @param mode? WinMode +--- @param adapter? Adapter +--- @param opts? AdapterRunOpts +function M.run_all(mode, adapter, opts) + mode = mode or "auto" + adapter = adapter or "auto" + opts = opts or {} + return module.prepare_and_run(M.config, "all", mode, adapter, opts) end -M.cancel_current_run = function() +function M.cancel_current_run() module.kill_current_run() end diff --git a/lua/quicktest/colored_printer.lua b/lua/quicktest/colored_printer.lua index e252e26..0f86a5b 100644 --- a/lua/quicktest/colored_printer.lua +++ b/lua/quicktest/colored_printer.lua @@ -1,5 +1,11 @@ local api = vim.api +---@class ColoredPrinter +---@field color_groups? table +---@field current_fg? string +---@field current_bg? string +---@field current_styles? string[] +---@field bright_color_bold? boolean local ColoredPrinter = {} ColoredPrinter.__index = ColoredPrinter @@ -14,6 +20,7 @@ function ColoredPrinter.new() return self end +---@param index integer function ColoredPrinter:get_256_color(index) -- Standard 16 colors (0-15) if index <= 15 then @@ -52,13 +59,13 @@ function ColoredPrinter:get_256_color(index) return 55 + c * 40 end - return string.format("#%02x%02x%02x", color_value(r), color_value(g), color_value(b)) + return ("#%02x%02x%02x"):format(color_value(r), color_value(g), color_value(b)) end -- Grayscale (232-255) if index <= 255 then local gray = 8 + (index - 232) * 10 - return string.format("#%02x%02x%02x", gray, gray, gray) + return ("#%02x%02x%02x"):format(gray, gray, gray) end return "#ffffff" -- fallback @@ -86,7 +93,8 @@ function ColoredPrinter:setup_highlight_groups() for code, color in pairs(basic_colors) do local group_name = "QuicktestAnsiColor_" .. code - vim.cmd(string.format("highlight %s ctermfg=%s guifg=%s", group_name, color:lower(), color)) + api.nvim_set_hl(0, group_name, { ctermfg = color:lower(), fg = color }) + -- vim.cmd(("highlight %s ctermfg=%s guifg=%s"):format(group_name, color:lower(), color)) self.color_groups[code] = group_name end @@ -113,46 +121,56 @@ function ColoredPrinter:setup_highlight_groups() for code, color in pairs(bg_colors) do local group_name = "QuicktestAnsiBgColor_" .. code - vim.cmd(string.format("highlight %s ctermbg=%s guibg=%s", group_name, color:lower(), color)) + api.nvim_set_hl(0, group_name, { ctermbg = color:lower(), bg = color }) + -- vim.cmd(("highlight %s ctermbg=%s guibg=%s"):format(group_name, color:lower(), color)) self.color_groups[code] = group_name end -- Use Normal highlight group directly to ensure proper default colors - vim.cmd("highlight default QuicktestAnsiColorDefault guifg=NONE guibg=NONE") - vim.cmd("highlight default link QuicktestAnsiColorDefault Normal") + api.nvim_set_hl(0, "QuicktestAnsiColorDefault", { default = true, fg = "NONE", bg = "NONE" }) + api.nvim_set_hl(0, "QuicktestAnsiColorDefault", { default = true, link = "Normal" }) + -- vim.cmd("highlight default QuicktestAnsiColorDefault guifg=NONE guibg=NONE") + -- vim.cmd("highlight default link QuicktestAnsiColorDefault Normal") self.color_groups["default"] = "QuicktestAnsiColorDefault" end +---@param fg string +---@param bg string +---@param styles string[] +---@return string function ColoredPrinter:get_or_create_color_group(fg, bg, styles) -- If no colors or styles are set, use the default group - if not fg and not bg and (#styles == 0) then + if not (fg or bg) and vim.tbl_isempty(styles) then return self.color_groups["default"] end + ---@param str string local function sanitize(str) - if str then - -- Replace # with "hex" and any non-alphanumeric characters with their hex code - return str:gsub("#", ""):gsub("[^%w]", function(c) - return string.format("%02x", string.byte(c)) - end) + if not str then + return "" end - return "" + + -- Replace # with "hex" and any non-alphanumeric characters with their hex code + return str:gsub("#", ""):gsub("[^%w]", function(c) ---@param c string + return ("%02x"):format(c:byte()) + end) end + ---@type table local unique_styles = {} for _, style in ipairs(styles) do unique_styles[style] = true end - unique_styles = vim.tbl_keys(unique_styles) + -- sort and uniqize the styles - styles = vim.tbl_filter(function(s) + styles = vim.tbl_filter(function(s) ---@param s string return s ~= "" - end, unique_styles) + end, vim.tbl_keys(unique_styles)) table.sort(styles) local color_key = table.concat( - vim.tbl_filter(function(s) + vim.tbl_filter(function(s) ---@param s string return s ~= "" end, { sanitize(fg), sanitize(bg), table.concat(styles, "_") }), "_" @@ -161,7 +179,7 @@ function ColoredPrinter:get_or_create_color_group(fg, bg, styles) if not self.color_groups[color_key] then local group_name = "QuicktestAnsiColor" - if #color_key > 0 then + if color_key:len() > 0 then group_name = group_name .. "_" .. color_key end @@ -170,33 +188,33 @@ function ColoredPrinter:get_or_create_color_group(fg, bg, styles) if fg then if fg:match("^#") then - cmd = cmd .. string.format(" guifg=%s", fg) + cmd = ("%s guifg=%s"):format(cmd, fg) elseif self.color_groups[fg] then local fg_color = vim.fn.synIDattr(vim.fn.synIDtrans(vim.fn.hlID(self.color_groups[fg])), "fg#") if fg_color and fg_color ~= "" then - cmd = cmd .. " guifg=" .. fg_color + cmd = ("%s guifg=%s"):format(cmd, fg_color) end end end if bg then if bg:match("^#") then - cmd = cmd .. string.format(" guibg=%s", bg) + cmd = ("%s guibg=%s"):format(cmd, bg) elseif self.color_groups[bg] then local bg_color = vim.fn.synIDattr(vim.fn.synIDtrans(vim.fn.hlID(self.color_groups[bg])), "bg#") if bg_color and bg_color ~= "" then - cmd = cmd .. " guibg=" .. bg_color + cmd = ("%s guibg=%s"):format(cmd, bg_color) end end end - if #styles > 0 then + if not vim.tbl_isempty(styles) then cmd = cmd .. " gui=" .. table.concat(styles, ",") end if start_cmd ~= cmd then ---@diagnostic disable-next-line: param-type-mismatch - local success, err = pcall(vim.cmd, cmd) + local success = pcall(vim.cmd, cmd) if not success then -- Fallback to basic highlight if command fails vim.cmd("highlight " .. group_name) @@ -208,6 +226,7 @@ function ColoredPrinter:get_or_create_color_group(fg, bg, styles) return self.color_groups[color_key] end +---@param line string function ColoredPrinter:parse_colors(line) local result = {} local highlights = {} @@ -222,7 +241,7 @@ function ColoredPrinter:parse_colors(line) color_start = #result end - while i <= #line do + while i <= line:len() do if line:sub(i, i) == "\27" and line:sub(i + 1, i + 1) == "[" then local j = line:find("m", i + 2) if j then @@ -321,17 +340,18 @@ function ColoredPrinter:parse_colors(line) local g = tonumber(codes[index + 3]) local b = tonumber(codes[index + 4]) if r and g and b then + local hex = ("#%02x%02x%02x"):format(r, g, b) if code == 38 then - self.current_fg = string.format("#%02x%02x%02x", r, g, b) + self.current_fg = hex else - self.current_bg = string.format("#%02x%02x%02x", r, g, b) + self.current_bg = hex end index = index + 4 end end elseif codes[index + 1] == "5" then if #codes >= index + 2 then - local color_index = tonumber(codes[index + 2]) + local color_index = tonumber(codes[index + 2]) ---@cast color_index integer if color_index and color_index >= 0 and color_index <= 255 then local color_hex = self:get_256_color(color_index) if code == 38 then @@ -369,19 +389,19 @@ local function get_highlight_def(group) return "highlight AnsiColor" end - local hl = vim.api.nvim_get_hl_by_name(group, true) - local hl_string = string.format("highlight %s", group) + local hl = api.nvim_get_hl(0, { name = group }) + local hl_string = ("highlight %s"):format(group) - if hl.foreground then - hl_string = hl_string .. string.format(" guifg=#%06x", hl.foreground) + if hl.fg then + hl_string = ("%s guifg=#%06x"):format(hl_string, hl.fg) end - if hl.background then - hl_string = hl_string .. string.format(" guibg=#%06x", hl.background) + if hl.bg then + hl_string = ("%s guibg=#%06x"):format(hl_string, hl.bg) end - if hl.special then - hl_string = hl_string .. string.format(" guisp=#%06x", hl.special) + if hl.sp then + hl_string = ("%s guisp=#%06x"):format(hl_string, hl.sp) end local gui_attrs = {} @@ -391,13 +411,14 @@ local function get_highlight_def(group) end end - if #gui_attrs > 0 then - hl_string = hl_string .. " gui=" .. table.concat(gui_attrs, ",") + if not vim.tbl_isempty(gui_attrs) then + hl_string = ("%s gui=%s"):format(hl_string, table.concat(gui_attrs, ",")) end return hl_string end +---@param buf integer function ColoredPrinter:set_next_lines(lines, buf, lines_count) local parsed_lines = {} local parsed_highlights = {} @@ -414,7 +435,8 @@ function ColoredPrinter:set_next_lines(lines, buf, lines_count) for _, h in ipairs(hl) do -- print("[" .. h.start .. "," .. h.end_ .. ")", get_highlight_def(h.group)) - api.nvim_buf_add_highlight(buf, -1, h.group, lines_count - 1 + i, h.start, h.end_) + vim.hl.range(buf, -1, h.group, { lines_count - 1 + i, h.start }, { lines_count - 1 + i, h.end_ }) + -- api.nvim_buf_add_highlight(buf, -1, h.group, lines_count - 1 + i, h.start, h.end_) end end @@ -428,7 +450,7 @@ function ColoredPrinter:debug_print_colors(line) print("Original line:", line) print("Parsed line:", plain_line) for _, hl in ipairs(highlights) do - print("[" .. hl.start .. "," .. hl.end_ .. ")", get_highlight_def(hl.group)) + print(("[%s,%s)"):format(hl.start, hl.end_), get_highlight_def(hl.group)) end end diff --git a/lua/quicktest/fs_utils.lua b/lua/quicktest/fs_utils.lua index 2fe32ab..66d40d8 100644 --- a/lua/quicktest/fs_utils.lua +++ b/lua/quicktest/fs_utils.lua @@ -28,6 +28,7 @@ M.path = (function() return path end + ---@return string|false local function exists(filename) local stat = uv.fs_stat(filename) return stat and stat.type or false diff --git a/lua/quicktest/module.lua b/lua/quicktest/module.lua index 5415f83..86497a7 100644 --- a/lua/quicktest/module.lua +++ b/lua/quicktest/module.lua @@ -1,4 +1,5 @@ local api = vim.api +local uv = vim.uv or vim.loop local notify = require("quicktest.notify") local a = require("plenary.async") local u = require("plenary.async.util") @@ -9,15 +10,20 @@ local p = require("plenary.path") local M = {} ---@alias Adapter string | "auto" ----@alias WinMode 'popup' | 'split' | 'auto' ----@alias WinModeWithoutAuto 'popup' | 'split +---@alias WinMode "popup" | "split" | "auto" +---@alias WinModeWithoutAuto "popup" | "split ---@class AdapterRunOpts ----@field additional_args string[]? +---@field additional_args? string[] ----@alias CmdData {type: 'stdout', raw: string, output: string?, decoded: any} | {type: 'stderr', raw: string, output: string?, decoded: any} | {type: 'exit', code: number} +---@class CmdData +---@field type "stdout" | "stderr" | "exit" +---@field raw string +---@field output? string +---@field decoded any +---@field code? number ----@alias RunType 'line' | 'file' | 'dir' | 'all' +---@alias RunType "line" | "file" | "dir" | "all" ---@class QuicktestAdapter ---@field name string @@ -26,7 +32,7 @@ local M = {} ---@field build_dir_run_params fun(bufnr: integer, cursor_pos: integer[], opts: AdapterRunOpts): any ---@field build_all_run_params fun(bufnr: integer, cursor_pos: integer[], opts: AdapterRunOpts): any ---@field run fun(params: any, send: fun(data: CmdData)): number ----@field after_run fun(params: any, results: CmdData)? +---@field after_run nil|fun(params: any, results: CmdData) ---@field title fun(params: any): string ---@field is_enabled fun(bufnr: number, type: RunType): boolean @@ -35,13 +41,31 @@ local M = {} ---@field default_win_mode WinModeWithoutAuto ---@field use_builtin_colorizer boolean ----@alias JobStatus 'running' | 'finished' | 'canceled' ----@alias CmdJob {id: number, started_at: number, finished_at?: number, pid: number?, status: JobStatus, exit_code?: number} +---@alias JobStatus "running" | "finished" | "canceled" + +---@class CmdJob +---@field id number +---@field started_at integer +---@field finished_at? integer +---@field pid? number +---@field status JobStatus +---@field exit_code? integer + +---@class PreviousRun +---@field type string +---@field adapter_name string +---@field bufname string +---@field cursor_pos integer[] + +---@alias PreviousRuns table + ---@type CmdJob | nil local current_job = nil ---- @type {[string]: {type: string, adapter_name: string, bufname: string, cursor_pos: integer[]}} | nil + +---@type nil | PreviousRuns local previous_run = nil +---@return PreviousRuns local function load_previous_run() local config_path = p:new(vim.fn.stdpath("data"), "quicktest_previous_runs.json") @@ -59,7 +83,7 @@ end local function save_previous_run() if previous_run then - ---@diagnostic disable-next-line: missing-parameter + ---@diagnostic disable-next-line:missing-parameter a.run(function() local config_path = p:new(vim.fn.stdpath("data"), "quicktest_previous_runs.json") local current_data = load_previous_run() @@ -131,23 +155,22 @@ local stderr_ns = vim.api.nvim_create_namespace("quicktest_stderr") --- @param opts AdapterRunOpts function M.run(adapter, params, config, opts) if current_job then - if current_job.pid then - vim.system({ "kill", tostring(current_job.pid) }):wait() - current_job = nil - else + if not current_job.pid then return notify.warn("Already running") end + vim.system({ "kill", tostring(current_job.pid) }):wait() + current_job = nil end --- @param buf integer --- @param start integer - --- @param finish number + --- @param finish integer --- @param strict_indexing boolean --- @param replacements string[] local set_ansi_lines = function(buf, start, finish, strict_indexing, replacements) local new_lines = {} for i, line in ipairs(replacements) do - new_lines[i] = string.gsub(line, "[\27\155][][()#;?%d]*[A-PRZcf-ntqry=><~]", "") + new_lines[i] = line:gsub("[\27\155][][()#;?%d]*[A-PRZcf-ntqry=><~]", "") end vim.api.nvim_buf_set_lines(buf, start, finish, strict_indexing, new_lines) end @@ -155,7 +178,7 @@ function M.run(adapter, params, config, opts) local printer = colorized_printer.new() --- @type CmdJob - local job = { id = math.random(10000000000000000), started_at = vim.uv.now(), status = "running" } + local job = { id = math.random(10000000000000000), started_at = uv.now(), status = "running" } current_job = job local is_running = function() @@ -163,12 +186,12 @@ function M.run(adapter, params, config, opts) end local print_buf_status = function(buf, line_count) - local passedTime = vim.loop.now() - job.started_at + local passedTime = uv.now() - job.started_at if job.finished_at then passedTime = job.finished_at - job.started_at end - local time_display = string.format("%.2f", passedTime / 1000) .. "s" + local time_display = ("%.2f"):format(passedTime / 1000) .. "s" vim.api.nvim_buf_clear_namespace(buf, status_ns, 0, -1) @@ -179,15 +202,17 @@ function M.run(adapter, params, config, opts) hl_group = "DiagnosticInfo" else if job.status == "canceled" then - line = "Canceled " .. time_display + line = "Canceled " hl_group = "DiagnosticWarn" elseif job.exit_code ~= 0 then - line = "Failed " .. time_display + line = "Failed " hl_group = "DiagnosticError" else - line = "Passed " .. time_display + line = "Passed " hl_group = "DiagnosticOk" end + + line = line .. time_display end vim.api.nvim_buf_set_lines(buf, line_count - 1, line_count, false, { @@ -264,13 +289,7 @@ function M.run(adapter, params, config, opts) end for i, _ in ipairs(errored_lines) do - vim.highlight.range( - buf, - stderr_ns, - "DiagnosticError", - { i + lines_count - 2, 0 }, - { i + lines_count - 2, -1 } - ) + vim.hl.range(buf, stderr_ns, "DiagnosticError", { i + lines_count - 2, 0 }, { i + lines_count - 2, -1 }) end print_buf_status(buf, lines_count + new_lines_count) @@ -322,7 +341,7 @@ function M.run(adapter, params, config, opts) if result.type == "exit" then job.exit_code = result.code - job.finished_at = vim.uv.now() + job.finished_at = uv.now() job.status = "finished" if adapter.after_run then @@ -386,7 +405,7 @@ local function get_adapter_and_params(config, type, adapter_name, current_buffer end --- @param config QuicktestConfig ---- @param type 'line' | 'file' | 'dir' | 'all' +--- @param type "line" | "file" | "dir" | "all" --- @param mode WinMode --- @param adapter_name Adapter --- @param opts AdapterRunOpts @@ -486,7 +505,7 @@ function M.kill_current_run() vim.system({ "kill", tostring(current_job.pid) }):wait() job.status = "canceled" - job.finished_at = vim.uv.now() + job.finished_at = uv.now() end end diff --git a/lua/quicktest/notify.lua b/lua/quicktest/notify.lua index 323269c..1c1a3c6 100644 --- a/lua/quicktest/notify.lua +++ b/lua/quicktest/notify.lua @@ -1,10 +1,10 @@ local M = {} ---@param msg string ----@param level number +---@param level vim.log.levels local function notify(msg, level) vim.schedule(function() - vim.api.nvim_notify(msg, level, { title = "Quicktest" }) + vim.notify(msg, level, { title = "Quicktest" }) end) end