add shout.vim Neovim plugin
This commit is contained in:
parent
00b844cafc
commit
ddd69e375f
44
vim/README.md
Normal file
44
vim/README.md
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# shout.vim
|
||||||
|
|
||||||
|
Neovim plugin for `.shout` shell integration test files.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Syntax highlighting** for commands, expected output, directives, wildcards, exit codes, and comments
|
||||||
|
- **`:ShoutRun`** — run the current `.shout` file, results in quickfix
|
||||||
|
- **`:ShoutUpdate`** — run with `--update` to capture actual output into the file
|
||||||
|
- **`:ShoutRunAll`** — run all test files
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
### lazy.nvim
|
||||||
|
|
||||||
|
```lua
|
||||||
|
{
|
||||||
|
dir = "~/path/to/shout/vim",
|
||||||
|
ft = "shout",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual / symlink
|
||||||
|
|
||||||
|
Symlink or copy the `vim/` directory into your nvim runtime path:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir -p ~/.local/share/nvim/site/pack/shout/start
|
||||||
|
ln -s /path/to/shout/vim ~/.local/share/nvim/site/pack/shout/start/shout
|
||||||
|
```
|
||||||
|
|
||||||
|
## Syntax elements
|
||||||
|
|
||||||
|
| Pattern | Highlight |
|
||||||
|
|---|---|
|
||||||
|
| `$ command` | Statement (prompt as Special) |
|
||||||
|
| `$# comment` | Comment |
|
||||||
|
| `@env KEY=VALUE` | PreProc / Identifier / String |
|
||||||
|
| `@setup`, `@teardown` | PreProc |
|
||||||
|
| Expected output | String |
|
||||||
|
| `...` (wildcard) | WarningMsg |
|
||||||
|
| `[N]`, `[*]` (exit code) | Constant |
|
||||||
|
| `\$ ...` (escaped dollar) | SpecialChar + String |
|
||||||
|
| `# inline comment` | Comment |
|
||||||
5
vim/ftdetect/shout.lua
Normal file
5
vim/ftdetect/shout.lua
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
vim.filetype.add({
|
||||||
|
extension = {
|
||||||
|
shout = "shout",
|
||||||
|
},
|
||||||
|
})
|
||||||
18
vim/ftplugin/shout.lua
Normal file
18
vim/ftplugin/shout.lua
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
-- Buffer-local settings for .shout files
|
||||||
|
vim.bo.commentstring = "$# %s"
|
||||||
|
vim.bo.shiftwidth = 0
|
||||||
|
vim.bo.tabstop = 2
|
||||||
|
vim.bo.expandtab = true
|
||||||
|
|
||||||
|
-- Commands
|
||||||
|
vim.api.nvim_buf_create_user_command(0, "ShoutRun", function()
|
||||||
|
require("shout").run()
|
||||||
|
end, { desc = "Run current .shout file" })
|
||||||
|
|
||||||
|
vim.api.nvim_buf_create_user_command(0, "ShoutUpdate", function()
|
||||||
|
require("shout").update()
|
||||||
|
end, { desc = "Run with --update to capture actual output" })
|
||||||
|
|
||||||
|
vim.api.nvim_buf_create_user_command(0, "ShoutRunAll", function()
|
||||||
|
require("shout").run_all()
|
||||||
|
end, { desc = "Run all .shout files in test/" })
|
||||||
118
vim/lua/shout/init.lua
Normal file
118
vim/lua/shout/init.lua
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
-- Find the shout binary: prefer local node_modules, then PATH
|
||||||
|
local function shout_cmd()
|
||||||
|
local local_bin = vim.fn.findfile("node_modules/.bin/shout", vim.fn.getcwd() .. ";")
|
||||||
|
if local_bin ~= "" then
|
||||||
|
return vim.fn.fnamemodify(local_bin, ":p")
|
||||||
|
end
|
||||||
|
-- Try bun run
|
||||||
|
if vim.fn.executable("bun") == 1 then
|
||||||
|
return "bun run shout"
|
||||||
|
end
|
||||||
|
return "shout"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Parse shout test output for quickfix entries
|
||||||
|
local function parse_output(lines, file)
|
||||||
|
local items = {}
|
||||||
|
for _, line in ipairs(lines) do
|
||||||
|
-- Match "FAIL path" lines
|
||||||
|
local fail_path = line:match("^FAIL%s+(.+)$")
|
||||||
|
if fail_path then
|
||||||
|
table.insert(items, {
|
||||||
|
filename = fail_path,
|
||||||
|
lnum = 1,
|
||||||
|
text = "FAIL",
|
||||||
|
type = "E",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
-- Match " $ command" lines after a FAIL (command that failed)
|
||||||
|
local cmd = line:match("^%s+%$%s+(.+)$")
|
||||||
|
if cmd then
|
||||||
|
table.insert(items, {
|
||||||
|
filename = file or "",
|
||||||
|
lnum = 0,
|
||||||
|
text = "$ " .. cmd,
|
||||||
|
type = "I",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return items
|
||||||
|
end
|
||||||
|
|
||||||
|
local function run_shout(args, on_exit)
|
||||||
|
local cmd = shout_cmd() .. " test " .. args
|
||||||
|
local output_lines = {}
|
||||||
|
|
||||||
|
vim.fn.jobstart(cmd, {
|
||||||
|
stdout_buffered = true,
|
||||||
|
stderr_buffered = true,
|
||||||
|
on_stdout = function(_, data)
|
||||||
|
vim.list_extend(output_lines, data)
|
||||||
|
end,
|
||||||
|
on_stderr = function(_, data)
|
||||||
|
vim.list_extend(output_lines, data)
|
||||||
|
end,
|
||||||
|
on_exit = function(_, code)
|
||||||
|
vim.schedule(function()
|
||||||
|
if on_exit then
|
||||||
|
on_exit(code, output_lines)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
local ns = vim.api.nvim_create_namespace("shout")
|
||||||
|
|
||||||
|
local function show_results(code, lines, file)
|
||||||
|
-- Clear previous diagnostics
|
||||||
|
if file then
|
||||||
|
local bufnr = vim.fn.bufnr(file)
|
||||||
|
if bufnr ~= -1 then
|
||||||
|
vim.diagnostic.reset(ns, bufnr)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Show in quickfix
|
||||||
|
local items = parse_output(lines, file)
|
||||||
|
vim.fn.setqflist(items, "r")
|
||||||
|
|
||||||
|
if code == 0 then
|
||||||
|
vim.notify("shout: all tests passed", vim.log.levels.INFO)
|
||||||
|
else
|
||||||
|
vim.notify("shout: tests failed (see :copen)", vim.log.levels.ERROR)
|
||||||
|
vim.cmd("copen")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.run()
|
||||||
|
local file = vim.fn.expand("%:p")
|
||||||
|
vim.notify("shout: running " .. vim.fn.expand("%:t") .. "...", vim.log.levels.INFO)
|
||||||
|
run_shout(vim.fn.shellescape(file), function(code, lines)
|
||||||
|
show_results(code, lines, file)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.update()
|
||||||
|
local file = vim.fn.expand("%:p")
|
||||||
|
vim.notify("shout: updating " .. vim.fn.expand("%:t") .. "...", vim.log.levels.INFO)
|
||||||
|
run_shout("--update " .. vim.fn.shellescape(file), function(code, lines)
|
||||||
|
if code == 0 then
|
||||||
|
vim.cmd("edit") -- reload the buffer
|
||||||
|
vim.notify("shout: updated " .. vim.fn.expand("%:t"), vim.log.levels.INFO)
|
||||||
|
else
|
||||||
|
show_results(code, lines, file)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.run_all()
|
||||||
|
vim.notify("shout: running all tests...", vim.log.levels.INFO)
|
||||||
|
run_shout("", function(code, lines)
|
||||||
|
show_results(code, lines, nil)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
69
vim/syntax/shout.vim
Normal file
69
vim/syntax/shout.vim
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
" Vim syntax file for .shout files
|
||||||
|
" Language: shout (shell integration tests)
|
||||||
|
|
||||||
|
if exists("b:current_syntax")
|
||||||
|
finish
|
||||||
|
endif
|
||||||
|
|
||||||
|
" Directives — must appear before first command
|
||||||
|
syn match shoutDirectiveKey /^@env\s/ contained
|
||||||
|
syn match shoutDirectiveKey /^@setup\s/ contained
|
||||||
|
syn match shoutDirectiveKey /^@teardown\s/ contained
|
||||||
|
syn match shoutEnvDirective /^@env\s\+\S\+=.*$/ contains=shoutDirectiveKey,shoutEnvName,shoutEnvValue
|
||||||
|
syn match shoutEnvName /\S\+\ze=/ contained nextgroup=shoutEnvEquals
|
||||||
|
syn match shoutEnvEquals /=/ contained nextgroup=shoutEnvValue
|
||||||
|
syn match shoutEnvValue /.*$/ contained
|
||||||
|
syn match shoutSetupDirective /^@setup\s\+.*$/ contains=shoutDirectiveKey
|
||||||
|
syn match shoutTeardownDirective /^@teardown\s\+.*$/ contains=shoutDirectiveKey
|
||||||
|
|
||||||
|
" Comment commands: $# ... or $ # ...
|
||||||
|
syn match shoutCommentCommand /^\$#.*$/
|
||||||
|
syn match shoutCommentCommand /^\$\s\+#.*$/
|
||||||
|
|
||||||
|
" Command lines: $ command
|
||||||
|
syn match shoutPrompt /^\$\s/ contained
|
||||||
|
syn match shoutCommand /^\$\s.\+/ contains=shoutPrompt,shoutInlineComment
|
||||||
|
syn match shoutInlineComment /\s\+#[^"']*$/ contained
|
||||||
|
|
||||||
|
" Escaped dollar in expected output
|
||||||
|
syn match shoutEscapedDollar /^\\\$/ contained
|
||||||
|
syn match shoutEscapedLine /^\\\$.*$/ contains=shoutEscapedDollar
|
||||||
|
|
||||||
|
" Wildcards
|
||||||
|
syn match shoutWildcardLine /^\.\.\.$/
|
||||||
|
syn match shoutWildcardInline /\.\.\./ contained
|
||||||
|
|
||||||
|
" Exit code assertions
|
||||||
|
syn match shoutExitCode /^\[\d\+\]$/
|
||||||
|
syn match shoutExitCodeWild /^\[\*\]$/
|
||||||
|
|
||||||
|
" Expected output (anything not matched above)
|
||||||
|
syn match shoutExpectedOutput /^[^$@\[\\].*$/ contains=shoutWildcardInline
|
||||||
|
|
||||||
|
" Highlighting
|
||||||
|
hi def link shoutDirectiveKey Keyword
|
||||||
|
hi def link shoutEnvDirective PreProc
|
||||||
|
hi def link shoutEnvName Identifier
|
||||||
|
hi def link shoutEnvEquals Operator
|
||||||
|
hi def link shoutEnvValue String
|
||||||
|
hi def link shoutSetupDirective PreProc
|
||||||
|
hi def link shoutTeardownDirective PreProc
|
||||||
|
|
||||||
|
hi def link shoutCommentCommand Comment
|
||||||
|
|
||||||
|
hi def link shoutPrompt Special
|
||||||
|
hi def link shoutCommand Statement
|
||||||
|
hi def link shoutInlineComment Comment
|
||||||
|
|
||||||
|
hi def link shoutEscapedDollar SpecialChar
|
||||||
|
hi def link shoutEscapedLine String
|
||||||
|
|
||||||
|
hi def link shoutWildcardLine WarningMsg
|
||||||
|
hi def link shoutWildcardInline WarningMsg
|
||||||
|
|
||||||
|
hi def link shoutExitCode Constant
|
||||||
|
hi def link shoutExitCodeWild Constant
|
||||||
|
|
||||||
|
hi def link shoutExpectedOutput String
|
||||||
|
|
||||||
|
let b:current_syntax = "shout"
|
||||||
Loading…
Reference in New Issue
Block a user