local config = require("test-samurai.config") local M = {} local state = { runners = {}, last_win = nil, last_buf = nil, } local function load_runners() state.runners = {} local opts = config.get() local mods = opts.runner_modules or {} for _, mod in ipairs(mods) do local ok, runner = pcall(require, mod) if ok and type(runner) == "table" then table.insert(state.runners, runner) else vim.notify("[test-samurai] Failed to load runner " .. mod, vim.log.levels.WARN) end end end function M.setup() load_runners() end function M.reload_runners() load_runners() end function M.get_runner_for_buf(bufnr) for _, runner in ipairs(state.runners) do if type(runner.is_test_file) == "function" then local ok, is_test = pcall(runner.is_test_file, bufnr) if ok and is_test then return runner end end end return nil end local function open_float(lines) if state.last_win and vim.api.nvim_win_is_valid(state.last_win) then pcall(vim.api.nvim_win_close, state.last_win, true) end if state.last_buf and vim.api.nvim_buf_is_valid(state.last_buf) then pcall(vim.api.nvim_buf_delete, state.last_buf, { force = true }) end local width = math.floor(vim.o.columns * 0.8) local height = math.floor(vim.o.lines * 0.8) local row = math.floor((vim.o.lines - height) / 2) local col = math.floor((vim.o.columns - width) / 2) local buf = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_option(buf, "bufhidden", "wipe") vim.api.nvim_buf_set_option(buf, "filetype", "test-samurai-output") vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) local win = vim.api.nvim_open_win(buf, true, { relative = "editor", width = width, height = height, row = row, col = col, style = "minimal", border = "rounded", }) vim.keymap.set("n", "", function() if vim.api.nvim_win_is_valid(win) then vim.api.nvim_win_close(win, true) end end, { buffer = buf, nowait = true, silent = true }) state.last_win = win state.last_buf = buf end local function run_cmd(cmd, cwd, on_exit) if vim.system then vim.system(cmd, { cwd = cwd, text = true }, function(obj) local code = obj.code or -1 local stdout = obj.stdout or "" local stderr = obj.stderr or "" vim.schedule(function() on_exit(code, stdout, stderr) end) end) else local stdout_chunks = {} local stderr_chunks = {} vim.fn.jobstart(cmd, { cwd = cwd, stdout_buffered = true, stderr_buffered = true, on_stdout = function(_, data) if data then table.insert(stdout_chunks, table.concat(data, "\n")) end end, on_stderr = function(_, data) if data then table.insert(stderr_chunks, table.concat(data, "\n")) end end, on_exit = function(_, code) local stdout = table.concat(stdout_chunks, "\n") local stderr = table.concat(stderr_chunks, "\n") on_exit(code, stdout, stderr) end, }) end end function M.run_nearest() local bufnr = vim.api.nvim_get_current_buf() local pos = vim.api.nvim_win_get_cursor(0) local row = pos[1] - 1 local col = pos[2] local runner = M.get_runner_for_buf(bufnr) if not runner then vim.notify("[test-samurai] No runner for this file", vim.log.levels.WARN) return end if type(runner.find_nearest) ~= "function" or type(runner.build_command) ~= "function" then vim.notify("[test-samurai] Runner missing methods", vim.log.levels.ERROR) return end local ok, spec_or_err = pcall(runner.find_nearest, bufnr, row, col) if not ok or not spec_or_err then local msg = "[test-samurai] No test found" if type(spec_or_err) == "string" then msg = "[test-samurai] " .. spec_or_err end vim.notify(msg, vim.log.levels.WARN) return end local spec = spec_or_err local ok_cmd, command = pcall(runner.build_command, spec) if not ok_cmd or not command or type(command.cmd) ~= "table" or #command.cmd == 0 then vim.notify("[test-samurai] Runner failed to build command", vim.log.levels.ERROR) return end local cmd = command.cmd local cwd = command.cwd or vim.loop.cwd() run_cmd(cmd, cwd, function(code, stdout, stderr) local header = "$ " .. table.concat(cmd, " ") local lines = { header, "" } if stdout ~= "" then local out_lines = vim.split(stdout, "\n", { plain = true }) vim.list_extend(lines, out_lines) end if stderr ~= "" then table.insert(lines, "") table.insert(lines, "[stderr]") local err_lines = vim.split(stderr, "\n", { plain = true }) vim.list_extend(lines, err_lines) end table.insert(lines, "") table.insert(lines, "[exit code] " .. tostring(code)) open_float(lines) end) end return M