From 58d22d16f5c5fd419729ee71098eb64e2b89867f Mon Sep 17 00:00:00 2001 From: johnseth97 <17620345+johnseth97@users.noreply.github.com> Date: Sun, 27 Apr 2025 19:51:54 -0400 Subject: [PATCH 1/3] fixing behavior of quit --- README.md | 1 - lua/gh_dash/init.lua | 128 ++++++++++++++++++++++++------------------- 2 files changed, 71 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 0c9b6fb..bfcdfb0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # gh-dash Neovim Plugin image - ## A Neovim plugin integrating the open-source gh-dash TUI for the `gh` CLI ([gh-dash](https://github.com/dlvhdr/gh-dash/)) > Latest version: ![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/johnseth97/gh-dash.nvim?sort=semver) diff --git a/lua/gh_dash/init.lua b/lua/gh_dash/init.lua index 7f439b9..b6c240b 100644 --- a/lua/gh_dash/init.lua +++ b/lua/gh_dash/init.lua @@ -1,10 +1,18 @@ -local vim = vim - local M = {} local config = { keymaps = {}, border = 'single', + custom_border = { + {}, -- Top left corner + {}, -- Top side + {}, -- Top right corner + {}, -- Right side + {}, -- Bottom right corner + {}, -- Bottom side + {}, -- Bottom left corner + {}, -- Left side + }, width = 0.8, height = 0.8, cmd = { 'gh', 'dash' }, @@ -33,6 +41,41 @@ function M.setup(user_config) end end +local styles = { + single = { + { '╭', 'FloatBorder' }, + { '─', 'FloatBorder' }, + { '╮', 'FloatBorder' }, + { '│', 'FloatBorder' }, + { '╯', 'FloatBorder' }, + { '─', 'FloatBorder' }, + { '╰', 'FloatBorder' }, + { '│', 'FloatBorder' }, + }, + double = { + { '╔', 'FloatBorder' }, + { '═', 'FloatBorder' }, + { '╗', 'FloatBorder' }, + { '║', 'FloatBorder' }, + { '╝', 'FloatBorder' }, + { '═', 'FloatBorder' }, + { '╚', 'FloatBorder' }, + { '║', 'FloatBorder' }, + }, + square = { + { '┌', 'FloatBorder' }, + { '─', 'FloatBorder' }, + { '┐', 'FloatBorder' }, + { '│', 'FloatBorder' }, + { '┘', 'FloatBorder' }, + { '─', 'FloatBorder' }, + { '└', 'FloatBorder' }, + { '│', 'FloatBorder' }, + }, + custom = config.custom_border, + none = nil, +} + -- Create a floating window displaying the gh_dash buffer local function open_window() -- compute dimensions and position @@ -43,40 +86,11 @@ local function open_window() -- resolve border style (string or table) local border = config.border if type(border) == 'string' then - local styles = { - single = { - { '╭', 'FloatBorder' }, - { '─', 'FloatBorder' }, - { '╮', 'FloatBorder' }, - { '│', 'FloatBorder' }, - { '╯', 'FloatBorder' }, - { '─', 'FloatBorder' }, - { '╰', 'FloatBorder' }, - { '│', 'FloatBorder' }, - }, - double = { - { '╔', 'FloatBorder' }, - { '═', 'FloatBorder' }, - { '╗', 'FloatBorder' }, - { '║', 'FloatBorder' }, - { '╝', 'FloatBorder' }, - { '═', 'FloatBorder' }, - { '╚', 'FloatBorder' }, - { '║', 'FloatBorder' }, - }, - square = { - { '┌', 'FloatBorder' }, - { '─', 'FloatBorder' }, - { '┐', 'FloatBorder' }, - { '│', 'FloatBorder' }, - { '┘', 'FloatBorder' }, - { '─', 'FloatBorder' }, - { '└', 'FloatBorder' }, - { '│', 'FloatBorder' }, - }, - none = nil, - } - border = styles[border] or styles.single + if border == 'none' then + border = 'none' + else + border = styles[border] or styles.single + end end -- open floating window state.win = vim.api.nvim_open_win(state.buf, true, { @@ -95,16 +109,13 @@ function M.open() vim.api.nvim_set_current_win(state.win) return end - if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then + if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) or vim.api.nvim_buf_get_option(state.buf, 'modified') then -- create an unlisted scratch buffer for the terminal state.buf = vim.api.nvim_create_buf(false, false) -- buffer options vim.api.nvim_buf_set_option(state.buf, 'bufhidden', 'hide') vim.api.nvim_buf_set_option(state.buf, 'swapfile', false) vim.api.nvim_buf_set_option(state.buf, 'filetype', 'gh_dash') - -- map in terminal and normal modes to close the gh_dash window - vim.api.nvim_buf_set_keymap(state.buf, 't', '', [[lua require('gh_dash').close()]], { noremap = true, silent = true }) - vim.api.nvim_buf_set_keymap(state.buf, 'n', '', [[lua require('gh_dash').close()]], { noremap = true, silent = true }) end open_window() -- determine if config.cmd is a simple executable name (no args) for checking @@ -120,40 +131,34 @@ function M.open() -- if simple command and not found, handle auto-install or notify if check_cmd and vim.fn.executable(check_cmd) == 0 then if config.autoinstall then - if vim.fn.executable 'npm' == 1 then + if vim.fn.executable 'gh' == 1 then -- install via npm in the floating terminal to show output do local shell_cmd = vim.o.shell or 'sh' local cmd = { shell_cmd, '-c', - "echo 'Autoinstalling OpenAI gh_dash via npm...'; npm install -g @openai/gh_dash", + "echo 'Autoinstalling gh_dash via gh CLI extensions...'; gh extension install dlvhdr/gh-dash", } state.job = vim.fn.termopen(cmd, { cwd = vim.loop.cwd(), - on_exit = function(_, exit_code) + on_exit = function(_, exit_code, _) + state.job = nil if exit_code == 0 then - vim.notify('[gh_dash.nvim] gh_dash CLI installed successfully', vim.log.levels.INFO) - -- automatically re-launch gh_dash CLI now that it's installed vim.schedule(function() M.close() - state.buf = nil - M.open() end) - else - vim.notify('[gh_dash.nvim] failed to install gh_dash CLI', vim.log.levels.ERROR) end - state.job = nil end, }) end else -- show installation instructions in the gh_dash popup local msg = { - 'npm not found; cannot auto-install gh_dash CLI.', + 'gh CLI not found; cannot auto-install gh_dash extension.', '', - 'Please install via your system package manager, or manually run:', - ' npm install -g @openai/gh_dash', + 'Please install the gh CLI via your system package manager', + 'i.e. `brew install gh`', } vim.api.nvim_buf_set_lines(state.buf, 0, -1, false, msg) end @@ -163,7 +168,7 @@ function M.open() 'gh_dash CLI not found.', '', 'Install with:', - ' npm install -g @openai/gh_dash', + 'gh extension install dlvhdr/gh-dash', '', 'Or enable autoinstall in your plugin setup:', ' require("gh_dash").setup{ autoinstall = true }', @@ -175,9 +180,14 @@ function M.open() -- spawn the gh_dash CLI in the floating terminal buffer if not state.job then state.job = vim.fn.termopen(config.cmd, { - cwd = vim.loop.cwd(), - on_exit = function() + wd = vim.loop.cwd(), + on_exit = function(_, exit_code, _) state.job = nil + if exit_code == 0 then + vim.schedule(function() + M.close() + end) + end end, }) end @@ -188,6 +198,10 @@ function M.close() vim.api.nvim_win_close(state.win, true) state.win = nil end + if state.buf and vim.api.nvim_buf_is_valid(state.buf) then + vim.api.nvim_buf_delete(state.buf, { force = true }) + state.buf = nil + end end function M.toggle() @@ -218,7 +232,7 @@ function M.status() return M.statusline() ~= '' end, -- gear icon - icon = '', + icon = '', -- default color (blue) color = { fg = '#51afef' }, } From bc33c8fd5e8d4b9f65eda296420784854b7bb08b Mon Sep 17 00:00:00 2001 From: johnseth97 <17620345+johnseth97@users.noreply.github.com> Date: Sun, 27 Apr 2025 20:05:42 -0400 Subject: [PATCH 2/3] updated behavior to quit cleanly w/ q, background w/ --- lua/gh_dash/init.lua | 17 ++++++++++++++++- tests/gh_dash_spec.lua | 35 ++++++++++++++++++++++------------- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/lua/gh_dash/init.lua b/lua/gh_dash/init.lua index b6c240b..09d2900 100644 --- a/lua/gh_dash/init.lua +++ b/lua/gh_dash/init.lua @@ -116,6 +116,15 @@ function M.open() vim.api.nvim_buf_set_option(state.buf, 'bufhidden', 'hide') vim.api.nvim_buf_set_option(state.buf, 'swapfile', false) vim.api.nvim_buf_set_option(state.buf, 'filetype', 'gh_dash') + -- Escape backgrounds the window cleanly + -- Map in terminal mode to hide the popup + vim.api.nvim_buf_set_keymap( + state.buf, + 't', + '', + [[lua vim.defer_fn(function() require('gh_dash').toggle() end, 10)]], + { noremap = true, silent = true } + ) end open_window() -- determine if config.cmd is a simple executable name (no args) for checking @@ -206,8 +215,14 @@ end function M.toggle() if state.win and vim.api.nvim_win_is_valid(state.win) then - M.close() + -- HIDE the window (don't kill the job) + vim.api.nvim_win_close(state.win, true) + state.win = nil + elseif state.buf and vim.api.nvim_buf_is_valid(state.buf) then + -- Reopen window into existing buffer + open_window() else + -- Full open if everything is gone M.open() end end diff --git a/tests/gh_dash_spec.lua b/tests/gh_dash_spec.lua index 531fd5b..5f437ad 100644 --- a/tests/gh_dash_spec.lua +++ b/tests/gh_dash_spec.lua @@ -1,13 +1,22 @@ -- tests/gh_dash_spec.lua -- luacheck: globals describe it assert eq -- luacheck: ignore a -- “a” is imported but unused +-- tests/gh_dash_spec.lua +-- luacheck: globals describe it assert eq local a = require 'plenary.async.tests' local eq = assert.equals describe('gh_dash.nvim', function() before_each(function() - vim.cmd 'set noswapfile' -- prevent side effects - vim.cmd 'silent! bwipeout!' -- close any open gh_dash windows + vim.cmd 'set noswapfile' + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + if vim.api.nvim_buf_is_valid(buf) then + local ft = vim.api.nvim_buf_get_option(buf, 'filetype') + if ft == 'gh_dash' then + vim.api.nvim_buf_delete(buf, { force = true }) + end + end + end end) it('loads the module', function() @@ -20,7 +29,6 @@ describe('gh_dash.nvim', function() it('creates gh_dash commands', function() require('gh_dash').setup { keymaps = {} } - local cmds = vim.api.nvim_get_commands {} assert(cmds['GHdash'], 'GHdash command not found') assert(cmds['GHdashToggle'], 'GHdashToggle command not found') @@ -32,15 +40,14 @@ describe('gh_dash.nvim', function() local win = vim.api.nvim_get_current_win() local buf = vim.api.nvim_win_get_buf(win) - local ft = vim.api.nvim_buf_get_option(buf, 'filetype') - eq(ft, 'gh_dash') + assert(vim.api.nvim_buf_is_valid(buf), 'buffer should exist') + eq(vim.api.nvim_buf_get_option(buf, 'filetype'), 'gh_dash') require('gh_dash').close() end) it('toggles the window', function() require('gh_dash').setup { cmd = "echo 'test'" } - require('gh_dash').toggle() local win1 = vim.api.nvim_get_current_win() assert(vim.api.nvim_win_is_valid(win1), 'gh_dash window should be open') @@ -50,14 +57,16 @@ describe('gh_dash.nvim', function() assert(not still_valid, 'gh_dash window should be closed') end) - it('shows statusline only when job is active but window is not', function() - require('gh_dash').setup { cmd = 'sleep 1000' } - require('gh_dash').open() - - vim.defer_fn(function() + it( + 'shows statusline only when job is active but window is not', + a.wrap(function() + require('gh_dash').setup { cmd = 'sleep 1' } + require('gh_dash').open() require('gh_dash').close() + vim.wait(100) + local status = require('gh_dash').statusline() eq(status, '[gh_dash]') - end, 100) - end) + end, 3000) + ) end) From 59d79982082817729ff444147118228b860d454a Mon Sep 17 00:00:00 2001 From: johnseth97 <17620345+johnseth97@users.noreply.github.com> Date: Sun, 27 Apr 2025 20:41:55 -0400 Subject: [PATCH 3/3] updated tests structure to use makefile. --- .github/workflows/ci.yml | 33 +++++++-------------------------- .gitignore | 2 ++ .luacheckrc | 9 ++++----- makefile | 25 +++++++++++++++++++++++++ tests/gh_dash_spec.lua | 39 +++++++++++++++++++++++---------------- tests/minimal_init.lua | 1 + 6 files changed, 62 insertions(+), 47 deletions(-) create mode 100644 .gitignore create mode 100644 makefile diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a29dfb7..066d8fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,25 +21,18 @@ jobs: - name: Install Neovim ${{ matrix.neovim }} run: | set -euo pipefail - VERSION=${{ matrix.neovim }} - - # All historic 0.x tags use nvim-linux64.tar.gz, everything newer uses -x86_64 if [[ "$VERSION" =~ ^v0\.[0-9]+\. ]]; then FILENAME=nvim-linux64.tar.gz else FILENAME=nvim-linux-x86_64.tar.gz fi - URL="https://github.com/neovim/neovim/releases/download/${VERSION}/${FILENAME}" echo "Downloading $URL" - - curl -fL -o nvim.tar.gz "$URL" # -f = fail on 4xx/5xx, -L = follow redirects - + curl -fL -o nvim.tar.gz "$URL" mkdir nvim-extract tar -xzf nvim.tar.gz -C nvim-extract - - DIR="$(ls nvim-extract | head -n1)" # should be nvim-linux64 or nvim-linux-x86_64 + DIR="$(ls nvim-extract | head -n1)" sudo mv "nvim-extract/$DIR" /opt/nvim echo "/opt/nvim/bin" >> "$GITHUB_PATH" @@ -55,32 +48,20 @@ jobs: run: | set -euo pipefail sudo apt-get update - sudo apt-get install -y lua5.1 liblua5.1-0-dev luarocks + sudo apt-get install -y lua5.1 liblua5.1-0-dev luarocks make luarocks --lua-version=5.1 --local install luacheck luarocks --lua-version=5.1 --local install luacov luarocks --lua-version=5.1 --local install luacov-reporter-lcov - echo "$HOME/.luarocks/bin" >> $GITHUB_PATH + echo "$HOME/.luarocks/bin" >> "$GITHUB_PATH" - - name: Run tests with coverage + - name: Run tests and generate coverage run: | eval "$(luarocks --lua-version=5.1 path)" - luarocks --lua-version=5.1 list # debug - nvim --headless -u tests/minimal_init.lua -c "luafile tests/run_cov.lua" - - luacov -r lcov > lcov.info - sed -i 's|SF:.*/codex.nvim/codex.nvim/|SF:|g' lcov.info - head -n 10 lcov.info # debug - - # Debug output - echo "=== first 20 lines of lcov.info ===" - head -n 20 lcov.info + make coverage - echo "=== all source-file entries ===" - grep '^SF:' lcov.info | sed -e 's/^SF://g' | sort | uniq | head -n 10 - - name: Upload code coverage uses: codecov/codecov-action@v4 with: - files: lcov.info # <-- new file + files: lcov.info disable_search: true token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..109b360 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +luacov.report.out +luacov.stats.out diff --git a/.luacheckrc b/.luacheckrc index f43584d..e6be2e2 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1,9 +1,8 @@ -- .luacheckrc -std = "luajit" -globals = { "vim" } +std = 'luajit' +globals = { 'vim', 'describe', 'it', 'before_each', 'after_each', 'pending', 'assert', 'eq' } ignore = { - "plugin/*", -- plugin loader shim + 'plugin/*', -- plugin loader shim } -max_line_length = false -- or turn it off completely - +max_line_length = false -- or turn it off completely diff --git a/makefile b/makefile new file mode 100644 index 0000000..dd186a5 --- /dev/null +++ b/makefile @@ -0,0 +1,25 @@ +# Makefile for gh_dash.nvim testing and coverage +# Usage: +# make test - run unit tests +# make coverage - run tests + generate coverage (luacov + lcov.info) + +# Force correct Lua version for Neovim (Lua 5.1) +LUAROCKS_ENV = eval "$(luarocks --lua-version=5.1 path)" + +# Headless Neovim test runner +NVIM_TEST_CMD = nvim --headless -u tests/minimal_init.lua -c "PlenaryBustedDirectory tests/" + +.PHONY: test coverage clean + +test: + $(LUAROCKS_ENV) && $(NVIM_TEST_CMD) + +coverage: + $(LUAROCKS_ENV) && nvim --headless -u tests/minimal_init.lua -c "luafile tests/run_cov.lua" + ls -lh luacov.stats.out + $(LUAROCKS_ENV) && luacov -t LcovReporter + @echo "Generated coverage report: lcov.info" + +clean: + rm -f luacov.stats.out lcov.info + @echo "Cleaned coverage artifacts" diff --git a/tests/gh_dash_spec.lua b/tests/gh_dash_spec.lua index 5f437ad..7445939 100644 --- a/tests/gh_dash_spec.lua +++ b/tests/gh_dash_spec.lua @@ -1,9 +1,4 @@ -- tests/gh_dash_spec.lua --- luacheck: globals describe it assert eq --- luacheck: ignore a -- “a” is imported but unused --- tests/gh_dash_spec.lua --- luacheck: globals describe it assert eq -local a = require 'plenary.async.tests' local eq = assert.equals describe('gh_dash.nvim', function() @@ -57,16 +52,28 @@ describe('gh_dash.nvim', function() assert(not still_valid, 'gh_dash window should be closed') end) - it( - 'shows statusline only when job is active but window is not', - a.wrap(function() - require('gh_dash').setup { cmd = 'sleep 1' } - require('gh_dash').open() - require('gh_dash').close() - vim.wait(100) + pending('hides the window on but keeps job alive', function() + -- this test is not reliable in headless mode, skipping for now + -- + -- require('gh_dash').setup { cmd = 'echo "test"' } + -- require('gh_dash').open() + -- + -- local win = vim.api.nvim_get_current_win() + -- vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('', true, false, true), 't', true) + -- + -- local win_still_valid = pcall(vim.api.nvim_win_get_buf, win) + -- assert(not win_still_valid, ' should hide the window') + -- + -- local status = require('gh_dash').statusline() + -- eq(status, '[gh_dash]', 'job should still be running after hiding window') + end) + + it('shows statusline only when job is active but window is not', function() + require('gh_dash').setup { cmd = 'echo "test"' } + require('gh_dash').open() + require('gh_dash').close() - local status = require('gh_dash').statusline() - eq(status, '[gh_dash]') - end, 3000) - ) + local status = require('gh_dash').statusline() + eq(status, '[gh_dash]') + end) end) diff --git a/tests/minimal_init.lua b/tests/minimal_init.lua index a050c77..ee16f20 100644 --- a/tests/minimal_init.lua +++ b/tests/minimal_init.lua @@ -1,3 +1,4 @@ vim.cmd 'set rtp+=.' vim.cmd 'set rtp+=./plenary.nvim' -- if using as a submodule or symlinked require 'plugin.gh_dash' -- triggers plugin/gh_dash.lua +vim.opt.runtimepath:append '~/.local/share/nvim/lazy/plenary.nvim/'