diff --git a/lua/quicktest/colored_printer.lua b/lua/quicktest/colored_printer.lua index 65776ce..07026d3 100644 --- a/lua/quicktest/colored_printer.lua +++ b/lua/quicktest/colored_printer.lua @@ -233,9 +233,7 @@ local function get_highlight_def(group) return hl_string end -function ColoredPrinter:set_next_lines(lines, buf, shift) - local line_count = vim.api.nvim_buf_line_count(buf) - +function ColoredPrinter:set_next_lines(lines, buf, lines_count) local parsed_lines = {} local parsed_highlights = {} @@ -245,13 +243,13 @@ function ColoredPrinter:set_next_lines(lines, buf, shift) table.insert(parsed_highlights, highlights) end - api.nvim_buf_set_lines(buf, line_count - shift, -1, false, parsed_lines) + api.nvim_buf_set_lines(buf, lines_count, -1, false, parsed_lines) for i, hl in ipairs(parsed_highlights) do 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, line_count - shift - 1 + i, h.start, h.end_) + api.nvim_buf_add_highlight(buf, -1, h.group, lines_count - 1 + i, h.start, h.end_) end end diff --git a/lua/quicktest/module.lua b/lua/quicktest/module.lua index 8ab30f6..5415f83 100644 --- a/lua/quicktest/module.lua +++ b/lua/quicktest/module.lua @@ -35,7 +35,9 @@ local M = {} ---@field default_win_mode WinModeWithoutAuto ---@field use_builtin_colorizer boolean ---- @type {id: number, started_at: number, pid: number?} | nil +---@alias JobStatus 'running' | 'finished' | 'canceled' +---@alias CmdJob {id: number, started_at: number, finished_at?: number, pid: number?, status: JobStatus, exit_code?: number} +---@type CmdJob | nil local current_job = nil --- @type {[string]: {type: string, adapter_name: string, bufname: string, cursor_pos: integer[]}} | nil local previous_run = nil @@ -120,6 +122,9 @@ local function get_adapter_by_name(adapters, name) return adapter end +local status_ns = vim.api.nvim_create_namespace("quicktest_status") +local stderr_ns = vim.api.nvim_create_namespace("quicktest_stderr") + --- @param adapter QuicktestAdapter --- @param params any --- @param config QuicktestConfig @@ -149,42 +154,61 @@ function M.run(adapter, params, config, opts) local printer = colorized_printer.new() - --- @type {id: number, started_at: number, pid: number?, exit_code: number?} - local job = { id = math.random(10000000000000000), started_at = vim.uv.now() } + --- @type CmdJob + local job = { id = math.random(10000000000000000), started_at = vim.uv.now(), status = "running" } current_job = job local is_running = function() - return current_job and job.id == current_job.id + return current_job and job.id == current_job.id and current_job.status == "running" end - local print_status = function() - for _, buf in ipairs(ui.get_buffers()) do - local line_count = vim.api.nvim_buf_line_count(buf) + local print_buf_status = function(buf, line_count) + local passedTime = vim.loop.now() - job.started_at + if job.finished_at then + passedTime = job.finished_at - job.started_at + end - local passedTime = vim.loop.now() - job.started_at - local time_display = string.format("%.2f", passedTime / 1000) .. "s" + local time_display = string.format("%.2f", passedTime / 1000) .. "s" - if job.exit_code == nil then - vim.api.nvim_buf_set_lines(buf, line_count - 1, line_count, false, { - "Running " .. time_display, - }) - vim.api.nvim_buf_add_highlight(buf, -1, "DiagnosticInfo", line_count - 1, 0, -1) + vim.api.nvim_buf_clear_namespace(buf, status_ns, 0, -1) + + local hl_group = "" + local line = "" + if job.exit_code == nil and job.status == "running" then + line = "Running " .. time_display + hl_group = "DiagnosticInfo" + else + if job.status == "canceled" then + line = "Canceled " .. time_display + hl_group = "DiagnosticWarn" + elseif job.exit_code ~= 0 then + line = "Failed " .. time_display + hl_group = "DiagnosticError" else - if job.exit_code ~= 0 then - vim.api.nvim_buf_set_lines(buf, line_count - 1, line_count, false, { "Failed " .. time_display }) + line = "Passed " .. time_display + hl_group = "DiagnosticOk" + end + end - vim.api.nvim_buf_add_highlight(buf, -1, "DiagnosticError", line_count - 1, 0, -1) - else - vim.api.nvim_buf_set_lines(buf, line_count - 1, line_count, false, { "Passed " .. time_display }) + vim.api.nvim_buf_set_lines(buf, line_count - 1, line_count, false, { + line, + }) + vim.api.nvim_buf_set_extmark(buf, status_ns, line_count - 1, 0, { + end_col = line:len(), + hl_group = hl_group, + }) + end + local print_status = function() + for _, buf in ipairs(ui.get_buffers()) do + local line_count = vim.api.nvim_buf_line_count(buf) - vim.api.nvim_buf_add_highlight(buf, -1, "DiagnosticOk", line_count - 1, 0, -1) - end - end + print_buf_status(buf, line_count) end end for _, buf in ipairs(ui.get_buffers()) do vim.api.nvim_buf_set_lines(buf, 0, -1, false, {}) + vim.api.nvim_buf_clear_namespace(buf, -1, 0, -1) ui.scroll_down(buf) end @@ -217,87 +241,113 @@ function M.run(adapter, params, config, opts) end end - ---@diagnostic disable-next-line: missing-parameter - a.run(function() + local results = {} + local lines_buffer = {} + local errored_lines = {} + + local run_printer = function() + local print_lines = function() + local new_lines_count = #lines_buffer + + if new_lines_count > 0 then + table.insert(lines_buffer, "") + table.insert(lines_buffer, "") + + for _, buf in ipairs(ui.get_buffers()) do + local lines_count = vim.api.nvim_buf_line_count(buf) + local should_scroll = ui.should_continue_scroll(buf, lines_count - 2) + + if config.use_builtin_colorizer then + printer:set_next_lines(lines_buffer, buf, lines_count - 2) + else + set_ansi_lines(buf, lines_count - 2, -1, false, lines_buffer) + 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 } + ) + end + + print_buf_status(buf, lines_count + new_lines_count) + if should_scroll then + ui.scroll_down(buf) + end + end + + lines_buffer = {} + errored_lines = {} + end + end + while is_running() do + u.scheduler() + + print_lines() print_status() + + vim.cmd("redraw") u.sleep(100) end - end) - local last_update_time = 0 - local update_interval = 100 -- ms + -- In case new job is started and we don't want to print the previous one + if job.id == current_job.id then + u.scheduler() + print_lines() + print_status() + end + end + + ---@diagnostic disable-next-line: missing-parameter + a.run(function() + xpcall(run_printer, function(err) + print("Error in async job:", err) + print("Stack trace:", debug.traceback()) + + notify.error("Test run failed: " .. err) + end) + end) - local results = {} while is_running() do local result = receiver.recv() table.insert(results, result) - u.scheduler() - if not is_running() then return end if result.type == "exit" then job.exit_code = result.code + job.finished_at = vim.uv.now() + job.status = "finished" - current_job = nil if adapter.after_run then + u.scheduler() adapter.after_run(params, results) end end - for _, buf in ipairs(ui.get_buffers()) do - local should_scroll = ui.should_continue_scroll(buf) - - if result.type == "stdout" then - if result.output then - local lines = vim.split(result.output, "\n") - - table.insert(lines, "") - table.insert(lines, "") - - if config.use_builtin_colorizer then - printer:set_next_lines(lines, buf, 2) - else - local line_count = vim.api.nvim_buf_line_count(buf) - set_ansi_lines(buf, line_count - 2, -1, false, lines) - end - end - end - - if result.type == "stderr" then - if result.output then - local line_count = vim.api.nvim_buf_line_count(buf) - local lines = vim.split(result.output, "\n") + if result.type == "stdout" or result.type == "stderr" then + if result.output then + local lines = vim.split(result.output, "\n") + local new_lines_count = #lines - table.insert(lines, "") - table.insert(lines, "") - if #lines > 0 then - set_ansi_lines(buf, line_count - 2, -1, false, lines) + if new_lines_count > 0 then + local buffer_size = #lines_buffer + for i, line in ipairs(lines) do + table.insert(lines_buffer, line) - for i = 0, #lines - 1 do - vim.api.nvim_buf_add_highlight(buf, -1, "DiagnosticError", line_count - 2 + i, 0, -1) + if result.type == "stderr" then + errored_lines[buffer_size + i] = true end end end end - - local current_time = vim.loop.now() - if (current_time - last_update_time) > update_interval then - u.scheduler(function() - vim.cmd("redraw") - end) - last_update_time = current_time - end - - if should_scroll then - ui.scroll_down(buf) - end end - - print_status() end end @@ -391,15 +441,24 @@ function M.run_previous(config, mode) end local bufnr = get_buf_by_name(current_run.bufname) - if bufnr == nil then + local is_loaded = false + if bufnr ~= nil then + is_loaded = vim.api.nvim_buf_is_loaded(bufnr) + end + + if not is_loaded then -- If the buffer doesn't exist, try to open the file bufnr = vim.fn.bufadd(current_run.bufname) if bufnr == 0 then return notify.warn("Failed to open previous run file: " .. current_run.bufname) end - -- Ensure the buffer is loaded + -- Ensure the buffer is listed vim.bo[bufnr].buflisted = true + + vim.api.nvim_buf_call(bufnr, function() + vim.cmd("silent! e!") -- Try to edit the buffer in place to force load it + end) end if vim.bo[bufnr].filetype == "" then @@ -425,17 +484,9 @@ function M.kill_current_run() if current_job then local job = current_job vim.system({ "kill", tostring(current_job.pid) }):wait() - current_job = nil - for _, buf in ipairs(ui.get_buffers()) do - local line_count = vim.api.nvim_buf_line_count(buf) - - local passedTime = vim.loop.now() - job.started_at - local time_display = string.format("%.2f", passedTime / 1000) .. "s" - - vim.api.nvim_buf_set_lines(buf, line_count - 1, line_count, false, { "Cancelled after " .. time_display }) - vim.api.nvim_buf_add_highlight(buf, -1, "DiagnosticWarn", line_count - 1, 0, -1) - end + job.status = "canceled" + job.finished_at = vim.uv.now() end end diff --git a/lua/quicktest/ui.lua b/lua/quicktest/ui.lua index 023e924..61f8a3f 100644 --- a/lua/quicktest/ui.lua +++ b/lua/quicktest/ui.lua @@ -80,6 +80,7 @@ end M.get_buffers = function() return { get_split_buf(), get_popup_buf() } end + M.is_split_opened = function() return is_buf_visible(get_split_buf()) end @@ -160,27 +161,22 @@ function M.scroll_down(buf) for _, win in ipairs(windows) do local win_bufnr = vim.api.nvim_win_get_buf(win) if win_bufnr == buf then - local line_count = vim.api.nvim_buf_line_count(buf) - - if line_count < 3 then - return - end - - vim.api.nvim_win_set_cursor(win, { line_count - 2, 0 }) + vim.api.nvim_win_call(win, function() + vim.cmd("normal! G2k") + end) end end end ---@param buf number -function M.should_continue_scroll(buf) +function M.should_continue_scroll(buf, line_count) local windows = vim.api.nvim_list_wins() for _, win in ipairs(windows) do local win_bufnr = vim.api.nvim_win_get_buf(win) if win_bufnr == buf then local current_pos = vim.api.nvim_win_get_cursor(win) - local line_count = vim.api.nvim_buf_line_count(buf) - return current_pos[1] >= line_count - 2 + return current_pos[1] >= line_count end end end diff --git a/tests/support/gotest/abc/abc_test.go b/tests/support/gotest/abc/abc_test.go index 01454c9..460cd30 100644 --- a/tests/support/gotest/abc/abc_test.go +++ b/tests/support/gotest/abc/abc_test.go @@ -42,18 +42,25 @@ func TestGradient(t *testing.T) { width := 80 height := 24 - for y := 0; y < height; y++ { - for x := 0; x < width; x++ { - r := uint8(255 * float64(x) / float64(width-1)) - g := uint8(255 * float64(y) / float64(height-1)) - b := uint8(255 * math.Abs(math.Sin(float64(x*y)/100))) + for range 100 { + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + r := uint8(255 * float64(x) / float64(width-1)) + g := uint8(255 * float64(y) / float64(height-1)) + b := uint8(255 * math.Abs(math.Sin(float64(x*y)/100))) - fmt.Printf("\x1b[48;2;%d;%d;%dm ", r, g, b) + fmt.Printf("\x1b[48;2;%d;%d;%dm ", r, g, b) + } + fmt.Print("\x1b[0m\n") } - fmt.Print("\x1b[0m\n") } } +func TestStdERR(t *testing.T) { + fmt.Fprint(os.Stderr, "number of foo\n") + fmt.Fprint(os.Stderr, "number of foo\n") +} + func TestSum2(t *testing.T) { fmt.Println("TestSum2 hey!") @@ -74,5 +81,6 @@ func TestSum2(t *testing.T) { func TestIntensiveOutput(t *testing.T) { for i := range 10000 { fmt.Println("hi!hi!hi!hi!hi!hi!hi!hi!hi!hi!hi!hi!hi!hi!hi!hi!hi!hi!hi!hi!hi!hi!" + strconv.Itoa(i)) + // time.Sleep(time.Millisecond * 500) } }