diff --git a/README.md b/README.md index 61f1b72..92e4c26 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ Before running any test command, test-samurai runs `:wall` to save all buffers. - After `TSamNearest`, `TSamFile`, `TSamAll`, `TSamFailedOnly`, etc., the UI opens in listing mode (only **Test-Listing-Float** visible). - Press `` on a `[ FAIL ] ...` line in the listing to open/update the **Detail-Float** as a 20/80 split (left 20% listing, right 80% detail). - ANSI color translation is only applied in the **Detail-Float**; the **Test-Listing-Float** shows raw text without ANSI translation. -- `` hides the floating window; `TSamShowOutput` reopens it. +- `` hides the floating window and restores the cursor position; `TSamShowOutput` reopens it. - If no output is captured for a test, the **Detail-Float** shows `No output captured`. - Summary lines (`TOTAL`/`DURATION`) are appended in the listing output, including `TSamLast`. diff --git a/doc/test-samurai.txt b/doc/test-samurai.txt index d85ae83..59e24f6 100644 --- a/doc/test-samurai.txt +++ b/doc/test-samurai.txt @@ -64,7 +64,7 @@ Additional keymaps: z Toggle Detail-Float full width Focus Detail-Float (press l again for full) Focus Test-Listing-Float - Close Testing-Float + Close Testing-Float and restore cursor Close Detail-Float (when focused) Notes: diff --git a/lua/test-samurai/core.lua b/lua/test-samurai/core.lua index 383ff8e..25f9aeb 100644 --- a/lua/test-samurai/core.lua +++ b/lua/test-samurai/core.lua @@ -23,6 +23,9 @@ local state = { detail_win = nil, detail_opening = false, detail_full = false, + trigger_win = nil, + trigger_buf = nil, + trigger_cursor = nil, listing_unfiltered_lines = nil, listing_filtered_kind = nil, hardtime_refcount = 0, @@ -71,7 +74,7 @@ local function help_lines() "", "Testing-Float (Listing):", " Open Detail-Float for selected test", - " Close Testing-Float", + " Close Testing-Float and restore cursor", " Focus Detail-Float (press l again for full)", " Focus Test-Listing-Float", " z Toggle Detail-Float full width", @@ -81,7 +84,7 @@ local function help_lines() " ? Show this help", "", "Testing-Float (Detail):", - " Close Testing-Float", + " Close Testing-Float and restore cursor", " Focus Test-Listing-Float", " h Focus Test-Listing-Float", " Focus Detail-Float", @@ -337,6 +340,54 @@ local function find_normal_window() return nil end +local function capture_trigger_location() + local win = vim.api.nvim_get_current_win() + local cfg = vim.api.nvim_win_get_config(win) + if cfg.relative ~= "" then + win = find_normal_window() + end + if not (win and vim.api.nvim_win_is_valid(win)) then + return + end + local buf = vim.api.nvim_win_get_buf(win) + if not (buf and vim.api.nvim_buf_is_valid(buf)) then + return + end + local cursor = vim.api.nvim_win_get_cursor(win) + state.trigger_win = win + state.trigger_buf = buf + state.trigger_cursor = { cursor[1], cursor[2] } +end + +local function restore_trigger_location() + local buf = state.trigger_buf + local cursor = state.trigger_cursor + if not (buf and vim.api.nvim_buf_is_valid(buf)) then + return + end + local target_win = nil + if state.trigger_win and vim.api.nvim_win_is_valid(state.trigger_win) then + local cfg = vim.api.nvim_win_get_config(state.trigger_win) + if cfg.relative == "" then + target_win = state.trigger_win + end + end + if not target_win then + target_win = find_normal_window() + end + if not (target_win and vim.api.nvim_win_is_valid(target_win)) then + return + end + vim.api.nvim_set_current_win(target_win) + vim.api.nvim_win_set_buf(target_win, buf) + if cursor then + pcall(vim.api.nvim_win_set_cursor, target_win, cursor) + end + state.trigger_win = nil + state.trigger_buf = nil + state.trigger_cursor = nil +end + local function jump_to_listing_test() local cursor = vim.api.nvim_win_get_cursor(0) local line = cursor[1] @@ -643,6 +694,11 @@ close_container = function() end end +local function close_container_and_restore() + close_container() + restore_trigger_location() +end + jump_to_first_quickfix = function() close_container() local info = vim.fn.getqflist({ size = 0 }) @@ -728,6 +784,7 @@ local function apply_split_layout(left_ratio) end local function create_output_win(initial_lines) + capture_trigger_location() if state.detail_win and vim.api.nvim_win_is_valid(state.detail_win) then pcall(vim.api.nvim_win_close, state.detail_win, true) state.detail_win = nil @@ -762,7 +819,7 @@ local function create_output_win(initial_lines) hardtime_disable() vim.keymap.set("n", "", function() - close_container() + close_container_and_restore() end, { buffer = buf, nowait = true, silent = true }) vim.keymap.set("n", "", function() M.open_test_output_at_cursor() @@ -832,6 +889,7 @@ local function reopen_output_win() state.detail_win = nil end + capture_trigger_location() local width, height, row, col = float_geometry() state.last_float = { width = width, height = height, row = row, col = col } @@ -847,7 +905,7 @@ local function reopen_output_win() hardtime_disable() vim.keymap.set("n", "", function() - close_container() + close_container_and_restore() end, { buffer = state.last_buf, nowait = true, silent = true }) vim.keymap.set("n", "", function() M.open_test_output_at_cursor() @@ -1189,7 +1247,7 @@ local function ensure_detail_buf(lines) vim.api.nvim_buf_set_option(buf, "filetype", "test-samurai-output") state.detail_buf = buf vim.keymap.set("n", "", function() - close_container() + close_container_and_restore() end, { buffer = buf, nowait = true, silent = true }) vim.keymap.set("n", "h", function() M.focus_listing() @@ -2393,6 +2451,10 @@ function M.run_failed_only() }) end +function M.close_output_and_restore() + close_container_and_restore() +end + function M.show_output() if not (state.last_buf and vim.api.nvim_buf_is_valid(state.last_buf)) then vim.notify("[test-samurai] No previous output", vim.log.levels.WARN) diff --git a/tests/test_samurai_core_spec.lua b/tests/test_samurai_core_spec.lua index f3507f7..7c4e468 100644 --- a/tests/test_samurai_core_spec.lua +++ b/tests/test_samurai_core_spec.lua @@ -254,6 +254,102 @@ describe("test-samurai core (no bundled runners)", function() vim.fn.jobstart = orig_jobstart end) + it("restores cursor location after closing output with ", function() + local runner = { + name = "test-runner-restore", + } + + function runner.is_test_file(_bufnr) + return true + end + + function runner.find_nearest(bufnr, _row, _col) + return { file = vim.api.nvim_buf_get_name(bufnr), cwd = vim.loop.cwd(), test_name = "TestA" } + end + + function runner.build_command(spec) + return { cmd = { "echo", "nearest" }, cwd = spec.cwd } + end + + function runner.build_file_command(_bufnr) + return { cmd = { "echo", "file" } } + end + + function runner.build_all_command(_bufnr) + return { cmd = { "echo", "all" } } + end + + function runner.build_failed_command(last_command, _failures, _scope_kind) + return { cmd = { "echo", "failed" }, cwd = last_command and last_command.cwd or nil } + end + + function runner.parse_results(_output) + return { passes = {}, failures = {}, skips = {} } + end + + function runner.output_parser() + return { + on_line = function(_line, _state) + return nil + end, + on_complete = function(output, _state) + return runner.parse_results(output) + end, + } + end + + function runner.parse_test_output(_output) + return {} + end + + function runner.collect_failed_locations(_failures, _command, _scope_kind) + return {} + end + + package.loaded["test-samurai-restore-runner"] = runner + test_samurai.setup({ runner_modules = { "test-samurai-restore-runner" } }) + + local normal_win = nil + for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do + local cfg = vim.api.nvim_win_get_config(win) + if cfg.relative == "" then + normal_win = win + break + end + end + if normal_win then + vim.api.nvim_set_current_win(normal_win) + end + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, "/tmp/test_samurai_restore.go") + vim.bo[bufnr].filetype = "go" + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { "line1", "line2", "line3" }) + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_win_set_cursor(0, { 2, 1 }) + + local orig_jobstart = vim.fn.jobstart + vim.fn.jobstart = function(_cmd, opts) + if opts.on_exit then + opts.on_exit(nil, 0, nil) + end + return 1 + end + + core.run_nearest() + + core.close_output_and_restore() + + local cur_buf = vim.api.nvim_get_current_buf() + local cur_cursor = vim.api.nvim_win_get_cursor(0) + + vim.fn.jobstart = orig_jobstart + + assert.equals(bufnr, cur_buf) + assert.equals(2, cur_cursor[1]) + assert.equals(1, cur_cursor[2]) + end) + it("applies listing break/join substitutions", function() local buf = vim.api.nvim_create_buf(false, true) vim.api.nvim_set_current_buf(buf)