feat: implement public API and plugin commands
Rewrite init.lua to expose setup(), run(), stop(), and toggle() that
coordinate across config, runner, window, and indicator modules. Replace
the placeholder SayHello command in plugin/watchexec.lua with keymaps
(<Leader>wx{t,s,r}), :WatchexecRun/:WatchexecStop/:WatchexecToggle
commands, VimLeavePre cleanup, and VimResized auto-resize.
This commit is contained in:
+59
-7
@@ -1,15 +1,67 @@
|
|||||||
|
---@brief [[
|
||||||
|
--- watchexec.nvim integrates the watchexec CLI into Neovim, providing a
|
||||||
|
--- floating or split window to display file-watching command output.
|
||||||
|
---@brief ]]
|
||||||
|
|
||||||
|
local config = require("watchexec.config")
|
||||||
|
local runner = require("watchexec.runner")
|
||||||
|
local window = require("watchexec.window")
|
||||||
|
local indicator = require("watchexec.indicator")
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
M.config = {
|
---Merge user-provided options into the config and resolve the watchexec binary.
|
||||||
greeting = "Hello from my plugin",
|
---Call this in your init.lua: `require("watchexec").setup({...})`.
|
||||||
}
|
---@param opts? watchexec.Config
|
||||||
|
|
||||||
function M.setup(opts)
|
function M.setup(opts)
|
||||||
M.config = vim.tbl_deep_extend("force", M.config, opts or {})
|
config.setup(opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.say_hello()
|
---Start a watchexec process for the given command.
|
||||||
print(M.config.greeting)
|
---Stops any previously running process, opens the output window if hidden,
|
||||||
|
---then spawns the child process.
|
||||||
|
---@param command string Shell command to run under watchexec
|
||||||
|
function M.run(command)
|
||||||
|
if runner.is_running() then
|
||||||
|
runner.stop()
|
||||||
|
end
|
||||||
|
|
||||||
|
window.clear()
|
||||||
|
|
||||||
|
if not window.is_visible() then
|
||||||
|
window.open()
|
||||||
|
end
|
||||||
|
|
||||||
|
runner.start(command)
|
||||||
|
indicator.refresh()
|
||||||
|
end
|
||||||
|
|
||||||
|
---Stop the currently running watchexec process.
|
||||||
|
function M.stop()
|
||||||
|
runner.stop()
|
||||||
|
window.clear()
|
||||||
|
indicator.reset()
|
||||||
|
indicator.refresh()
|
||||||
|
end
|
||||||
|
|
||||||
|
---Toggle the watchexec output window.
|
||||||
|
---If no process is running and window is hidden, opens with a placeholder.
|
||||||
|
---If no process is running and window is visible, closes it.
|
||||||
|
---If running and visible, hides the window.
|
||||||
|
---If running but hidden, shows the window.
|
||||||
|
function M.toggle()
|
||||||
|
if not runner.is_running() then
|
||||||
|
if window.is_visible() then
|
||||||
|
window.close()
|
||||||
|
else
|
||||||
|
window.clear()
|
||||||
|
window.open()
|
||||||
|
end
|
||||||
|
elseif window.is_visible() then
|
||||||
|
window.toggle()
|
||||||
|
else
|
||||||
|
window.open()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
+57
-2
@@ -1,9 +1,64 @@
|
|||||||
|
---@type boolean|nil
|
||||||
if vim.g.loaded_watchexec then
|
if vim.g.loaded_watchexec then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
vim.g.loaded_watchexec = 1
|
vim.g.loaded_watchexec = 1
|
||||||
|
|
||||||
vim.api.nvim_create_user_command("SayHello", function()
|
vim.keymap.set("n", "<Leader>wxt", function()
|
||||||
require("watchexec").say_hello()
|
require("watchexec").toggle()
|
||||||
|
end, { desc = "Toggle watchexec window" })
|
||||||
|
|
||||||
|
vim.keymap.set("n", "<Leader>wxs", function()
|
||||||
|
require("watchexec").stop()
|
||||||
|
end, { desc = "Stop watchexec" })
|
||||||
|
|
||||||
|
vim.keymap.set("n", "<Leader>wxr", function()
|
||||||
|
local cmd = vim.fn.input("Watchexec: ")
|
||||||
|
|
||||||
|
if cmd and #cmd > 0 then
|
||||||
|
require("watchexec").run(cmd)
|
||||||
|
end
|
||||||
|
end, { desc = "Run watchexec with prompt" })
|
||||||
|
|
||||||
|
vim.api.nvim_create_user_command("WatchexecRun", function(opts)
|
||||||
|
require("watchexec").run(opts.args)
|
||||||
|
end, { nargs = 1, complete = "file" })
|
||||||
|
|
||||||
|
vim.api.nvim_create_user_command("WatchexecStop", function()
|
||||||
|
require("watchexec").stop()
|
||||||
end, {})
|
end, {})
|
||||||
|
|
||||||
|
vim.api.nvim_create_user_command("WatchexecToggle", function()
|
||||||
|
require("watchexec").toggle()
|
||||||
|
end, {})
|
||||||
|
|
||||||
|
vim.api.nvim_set_hl(0, "WatchexecSuccess", { bg = "#00ff00", default = true })
|
||||||
|
vim.api.nvim_set_hl(0, "WatchexecFailure", { bg = "#ff0000", default = true })
|
||||||
|
|
||||||
|
vim.api.nvim_create_augroup("watchexec_nvim", { clear = true })
|
||||||
|
|
||||||
|
vim.api.nvim_create_autocmd("VimLeavePre", {
|
||||||
|
group = "watchexec_nvim",
|
||||||
|
callback = function()
|
||||||
|
require("watchexec.runner").stop()
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
---@type table|nil
|
||||||
|
local resize_timer
|
||||||
|
|
||||||
|
vim.api.nvim_create_autocmd("VimResized", {
|
||||||
|
group = "watchexec_nvim",
|
||||||
|
callback = function()
|
||||||
|
if resize_timer then
|
||||||
|
resize_timer:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
resize_timer = vim.defer_fn(function()
|
||||||
|
resize_timer = nil
|
||||||
|
require("watchexec.window").resize_float()
|
||||||
|
require("watchexec.indicator").reposition()
|
||||||
|
end, 100)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|||||||
@@ -0,0 +1,269 @@
|
|||||||
|
local watchexec = require("watchexec")
|
||||||
|
local stub = require("luassert.stub")
|
||||||
|
|
||||||
|
describe("watchexec commands", function()
|
||||||
|
local runner
|
||||||
|
local window
|
||||||
|
local indicator
|
||||||
|
|
||||||
|
before_each(function()
|
||||||
|
runner = require("watchexec.runner")
|
||||||
|
window = require("watchexec.window")
|
||||||
|
indicator = require("watchexec.indicator")
|
||||||
|
|
||||||
|
stub(runner, "start")
|
||||||
|
stub(runner, "stop")
|
||||||
|
|
||||||
|
stub(runner, "is_running", function()
|
||||||
|
return false
|
||||||
|
end)
|
||||||
|
|
||||||
|
stub(window, "open")
|
||||||
|
stub(window, "close")
|
||||||
|
stub(window, "toggle")
|
||||||
|
|
||||||
|
stub(window, "resize_float")
|
||||||
|
stub(window, "clear")
|
||||||
|
|
||||||
|
stub(window, "is_visible", function()
|
||||||
|
return false
|
||||||
|
end)
|
||||||
|
|
||||||
|
stub(indicator, "refresh")
|
||||||
|
stub(indicator, "reset")
|
||||||
|
end)
|
||||||
|
|
||||||
|
after_each(function()
|
||||||
|
runner.start:revert()
|
||||||
|
runner.stop:revert()
|
||||||
|
runner.is_running:revert()
|
||||||
|
|
||||||
|
window.open:revert()
|
||||||
|
window.close:revert()
|
||||||
|
window.toggle:revert()
|
||||||
|
window.resize_float:revert()
|
||||||
|
window.clear:revert()
|
||||||
|
window.is_visible:revert()
|
||||||
|
|
||||||
|
indicator.refresh:revert()
|
||||||
|
indicator.reset:revert()
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("run()", function()
|
||||||
|
it("opens window and starts runner", function()
|
||||||
|
watchexec.run("echo hello")
|
||||||
|
|
||||||
|
assert.stub(runner.start).was_called_with("echo hello")
|
||||||
|
assert.stub(window.clear).was_called(1)
|
||||||
|
assert.stub(window.open).was_called(1)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("does not open window if already visible", function()
|
||||||
|
window.is_visible:revert()
|
||||||
|
stub(window, "is_visible", function()
|
||||||
|
return true
|
||||||
|
end)
|
||||||
|
|
||||||
|
watchexec.run("echo hello")
|
||||||
|
|
||||||
|
assert.stub(window.clear).was_called(1)
|
||||||
|
assert.stub(window.open).was_called(0)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("stops previous run before starting new one", function()
|
||||||
|
runner.is_running:revert()
|
||||||
|
stub(runner, "is_running", function()
|
||||||
|
return true
|
||||||
|
end)
|
||||||
|
|
||||||
|
watchexec.run("echo hello")
|
||||||
|
|
||||||
|
assert.stub(runner.stop).was_called(1)
|
||||||
|
assert.stub(window.clear).was_called(1)
|
||||||
|
assert.stub(runner.start).was_called_with("echo hello")
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("stop()", function()
|
||||||
|
it("stops the runner", function()
|
||||||
|
watchexec.stop()
|
||||||
|
|
||||||
|
assert.stub(runner.stop).was_called(1)
|
||||||
|
assert.stub(window.clear).was_called(1)
|
||||||
|
assert.stub(indicator.reset).was_called(1)
|
||||||
|
assert.stub(indicator.refresh).was_called(1)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("VimLeavePre autocmd", function()
|
||||||
|
it("stops the runner on exit", function()
|
||||||
|
dofile("plugin/watchexec.lua")
|
||||||
|
|
||||||
|
vim.api.nvim_exec_autocmds("VimLeavePre", { group = "watchexec_nvim" })
|
||||||
|
|
||||||
|
assert.stub(runner.stop).was_called(1)
|
||||||
|
assert.stub(window.clear).was_called(0)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("VimResized autocmd", function()
|
||||||
|
it("calls resize_float on terminal resize", function()
|
||||||
|
dofile("plugin/watchexec.lua")
|
||||||
|
|
||||||
|
vim.api.nvim_exec_autocmds("VimResized", { group = "watchexec_nvim" })
|
||||||
|
|
||||||
|
vim.wait(200, function()
|
||||||
|
return pcall(function()
|
||||||
|
assert.stub(window.resize_float).was_called(1)
|
||||||
|
return true
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert.stub(window.resize_float).was_called(1)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("keymaps", function()
|
||||||
|
before_each(function()
|
||||||
|
vim.g.loaded_watchexec = nil
|
||||||
|
dofile("plugin/watchexec.lua")
|
||||||
|
end)
|
||||||
|
|
||||||
|
local function leader()
|
||||||
|
return vim.g.mapleader or "\\"
|
||||||
|
end
|
||||||
|
|
||||||
|
it("defines <Leader>wxt toggle keymap", function()
|
||||||
|
local maps = vim.api.nvim_get_keymap("n")
|
||||||
|
local found = false
|
||||||
|
for _, m in ipairs(maps) do
|
||||||
|
if m.lhs == leader() .. "wxt" then
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_true(found)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("defines <Leader>wxs stop keymap", function()
|
||||||
|
local maps = vim.api.nvim_get_keymap("n")
|
||||||
|
local found = false
|
||||||
|
for _, m in ipairs(maps) do
|
||||||
|
if m.lhs == leader() .. "wxs" then
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_true(found)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("defines <Leader>wxr run keymap", function()
|
||||||
|
local maps = vim.api.nvim_get_keymap("n")
|
||||||
|
local found = false
|
||||||
|
for _, m in ipairs(maps) do
|
||||||
|
if m.lhs == leader() .. "wxr" then
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_true(found)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("invokes toggle via <Leader>wxt", function()
|
||||||
|
local ldr = leader()
|
||||||
|
vim.cmd("normal " .. ldr .. "wxt")
|
||||||
|
|
||||||
|
assert.stub(window.clear).was_called(1)
|
||||||
|
assert.stub(runner.start).was_called(0)
|
||||||
|
assert.stub(window.open).was_called(1)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("invokes stop via <Leader>wxs", function()
|
||||||
|
local ldr = leader()
|
||||||
|
vim.cmd("normal " .. ldr .. "wxs")
|
||||||
|
|
||||||
|
assert.stub(runner.stop).was_called(1)
|
||||||
|
assert.stub(window.clear).was_called(1)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("prompts and runs via <Leader>wxr", function()
|
||||||
|
local input_stub = stub(vim.fn, "input", function()
|
||||||
|
return "echo hello"
|
||||||
|
end)
|
||||||
|
|
||||||
|
local ldr = leader()
|
||||||
|
vim.cmd("normal " .. ldr .. "wxr")
|
||||||
|
|
||||||
|
assert.stub(window.clear).was_called(1)
|
||||||
|
assert.stub(runner.start).was_called_with("echo hello")
|
||||||
|
assert.stub(window.open).was_called(1)
|
||||||
|
|
||||||
|
input_stub:revert()
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("toggle()", function()
|
||||||
|
it("opens window with placeholder if nothing running", function()
|
||||||
|
watchexec.toggle()
|
||||||
|
|
||||||
|
assert.stub(window.clear).was_called(1)
|
||||||
|
assert.stub(runner.start).was_called(0)
|
||||||
|
assert.stub(window.open).was_called(1)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("hides window if runner is running and window is visible", function()
|
||||||
|
runner.is_running:revert()
|
||||||
|
|
||||||
|
stub(runner, "is_running", function()
|
||||||
|
return true
|
||||||
|
end)
|
||||||
|
|
||||||
|
window.is_visible:revert()
|
||||||
|
|
||||||
|
stub(window, "is_visible", function()
|
||||||
|
return true
|
||||||
|
end)
|
||||||
|
|
||||||
|
watchexec.toggle()
|
||||||
|
|
||||||
|
assert.stub(window.clear).was_called(0)
|
||||||
|
assert.stub(runner.start).was_called(0)
|
||||||
|
assert.stub(window.toggle).was_called(1)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("opens window if runner is running but not visible", function()
|
||||||
|
runner.is_running:revert()
|
||||||
|
|
||||||
|
stub(runner, "is_running", function()
|
||||||
|
return true
|
||||||
|
end)
|
||||||
|
|
||||||
|
window.is_visible:revert()
|
||||||
|
|
||||||
|
stub(window, "is_visible", function()
|
||||||
|
return false
|
||||||
|
end)
|
||||||
|
|
||||||
|
watchexec.toggle()
|
||||||
|
|
||||||
|
assert.stub(window.clear).was_called(0)
|
||||||
|
assert.stub(runner.start).was_called(0)
|
||||||
|
assert.stub(window.open).was_called(1)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("closes window if nothing running and window is visible", function()
|
||||||
|
window.is_visible:revert()
|
||||||
|
|
||||||
|
stub(window, "is_visible", function()
|
||||||
|
return true
|
||||||
|
end)
|
||||||
|
|
||||||
|
watchexec.toggle()
|
||||||
|
|
||||||
|
assert.stub(window.clear).was_called(0)
|
||||||
|
assert.stub(runner.start).was_called(0)
|
||||||
|
assert.stub(window.close).was_called(1)
|
||||||
|
assert.stub(window.open).was_called(0)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
@@ -2,8 +2,11 @@ local health = require("watchexec.health")
|
|||||||
local stub = require("luassert.stub")
|
local stub = require("luassert.stub")
|
||||||
|
|
||||||
describe("watchexec healthcheck", function()
|
describe("watchexec healthcheck", function()
|
||||||
local start_stub, ok_stub, error_stub
|
local start_stub
|
||||||
local has_stub, exec_stub
|
local ok_stub
|
||||||
|
local error_stub
|
||||||
|
local has_stub
|
||||||
|
local exec_stub
|
||||||
|
|
||||||
before_each(function()
|
before_each(function()
|
||||||
start_stub = stub(vim.health, "start")
|
start_stub = stub(vim.health, "start")
|
||||||
@@ -29,6 +32,7 @@ describe("watchexec healthcheck", function()
|
|||||||
has_stub = stub(vim.fn, "has", function()
|
has_stub = stub(vim.fn, "has", function()
|
||||||
return 1
|
return 1
|
||||||
end)
|
end)
|
||||||
|
|
||||||
exec_stub = stub(vim.fn, "executable", function()
|
exec_stub = stub(vim.fn, "executable", function()
|
||||||
return 1
|
return 1
|
||||||
end)
|
end)
|
||||||
@@ -44,6 +48,7 @@ describe("watchexec healthcheck", function()
|
|||||||
has_stub = stub(vim.fn, "has", function()
|
has_stub = stub(vim.fn, "has", function()
|
||||||
return 1
|
return 1
|
||||||
end)
|
end)
|
||||||
|
|
||||||
exec_stub = stub(vim.fn, "executable", function()
|
exec_stub = stub(vim.fn, "executable", function()
|
||||||
return 0
|
return 0
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -1,17 +1,31 @@
|
|||||||
local plugin = require("watchexec")
|
local watchexec = require("watchexec")
|
||||||
|
|
||||||
describe("watchexec logic", function()
|
|
||||||
before_each(function()
|
|
||||||
plugin.setup({
|
|
||||||
greeting = "Hello Test!",
|
|
||||||
})
|
|
||||||
end)
|
|
||||||
|
|
||||||
|
describe("watchexec module", function()
|
||||||
it("can be required without errors", function()
|
it("can be required without errors", function()
|
||||||
assert.not_nil(plugin)
|
assert.not_nil(watchexec)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("correctly applies user configuration", function()
|
it("exposes setup function", function()
|
||||||
assert.equals("Hello Test!", plugin.config.greeting)
|
assert.is_function(watchexec.setup)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("exposes run function", function()
|
||||||
|
assert.is_function(watchexec.run)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("exposes stop function", function()
|
||||||
|
assert.is_function(watchexec.stop)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("exposes toggle function", function()
|
||||||
|
assert.is_function(watchexec.toggle)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("setup delegates to config", function()
|
||||||
|
local config = require("watchexec.config")
|
||||||
|
|
||||||
|
watchexec.setup({ auto_scroll = false })
|
||||||
|
|
||||||
|
assert.equals(false, config.get().auto_scroll)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|||||||
Reference in New Issue
Block a user