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:
Stevan Freeborn
2026-07-06 19:51:37 -05:00
parent 398d3dd585
commit 1c46c3fcd9
5 changed files with 417 additions and 22 deletions
+59 -7
View File
@@ -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 = {}
M.config = {
greeting = "Hello from my plugin",
}
---Merge user-provided options into the config and resolve the watchexec binary.
---Call this in your init.lua: `require("watchexec").setup({...})`.
---@param opts? watchexec.Config
function M.setup(opts)
M.config = vim.tbl_deep_extend("force", M.config, opts or {})
config.setup(opts)
end
function M.say_hello()
print(M.config.greeting)
---Start a watchexec process for the given command.
---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
return M
+57 -2
View File
@@ -1,9 +1,64 @@
---@type boolean|nil
if vim.g.loaded_watchexec then
return
end
vim.g.loaded_watchexec = 1
vim.api.nvim_create_user_command("SayHello", function()
require("watchexec").say_hello()
vim.keymap.set("n", "<Leader>wxt", function()
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, {})
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,
})
+269
View File
@@ -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)
+7 -2
View File
@@ -2,8 +2,11 @@ local health = require("watchexec.health")
local stub = require("luassert.stub")
describe("watchexec healthcheck", function()
local start_stub, ok_stub, error_stub
local has_stub, exec_stub
local start_stub
local ok_stub
local error_stub
local has_stub
local exec_stub
before_each(function()
start_stub = stub(vim.health, "start")
@@ -29,6 +32,7 @@ describe("watchexec healthcheck", function()
has_stub = stub(vim.fn, "has", function()
return 1
end)
exec_stub = stub(vim.fn, "executable", function()
return 1
end)
@@ -44,6 +48,7 @@ describe("watchexec healthcheck", function()
has_stub = stub(vim.fn, "has", function()
return 1
end)
exec_stub = stub(vim.fn, "executable", function()
return 0
end)
+25 -11
View File
@@ -1,17 +1,31 @@
local plugin = require("watchexec")
describe("watchexec logic", function()
before_each(function()
plugin.setup({
greeting = "Hello Test!",
})
end)
local watchexec = require("watchexec")
describe("watchexec module", function()
it("can be required without errors", function()
assert.not_nil(plugin)
assert.not_nil(watchexec)
end)
it("correctly applies user configuration", function()
assert.equals("Hello Test!", plugin.config.greeting)
it("exposes setup function", function()
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)