Compare commits

..
24 changed files with 373 additions and 823 deletions

View file

@ -29,7 +29,7 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
- uses: wimpysworld/nothing-but-nix@main
if: runner.os == 'Linux'
with:
@ -37,7 +37,7 @@ jobs:
- uses: cachix/install-nix-action@v31
with:
github_access_token: ${{ secrets.GH_TOKEN }}
- uses: cachix/cachix-action@v17
- uses: cachix/cachix-action@v14
with:
name: rde
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'

View file

@ -10,7 +10,7 @@ jobs:
id-token: "write"
contents: "read"
steps:
- uses: "actions/checkout@v7"
- uses: "actions/checkout@v5"
with:
persist-credentials: false
- uses: "DeterminateSystems/determinate-nix-action@v3"

View file

@ -10,13 +10,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v4
- uses: wimpysworld/nothing-but-nix@main
with:
hatchet-protocol: 'carve'
- name: Install Determinate Nix
uses: DeterminateSystems/determinate-nix-action@v3
- uses: cachix/cachix-action@v17
- uses: cachix/cachix-action@v14
with:
name: rde
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'

View file

@ -1,17 +0,0 @@
{
"lastAgent": "agent-local-096f2785-09c8-4487-b71b-f8ac2b81aaaf",
"sessionsByServer": {
"api.letta.com": {
"agentId": "agent-21b21fd6-49e4-4d5c-a1b7-1c6dcd8dacc9",
"conversationId": "default"
},
"local:/Users/daniel/.letta/lc-local-backend": {
"agentId": "agent-local-096f2785-09c8-4487-b71b-f8ac2b81aaaf",
"conversationId": "default"
}
},
"lastSession": {
"agentId": "agent-local-096f2785-09c8-4487-b71b-f8ac2b81aaaf",
"conversationId": "default"
}
}

View file

@ -1,185 +0,0 @@
// omp custom tool: read this machine's running Neovim buffers over its RPC
// socket. Read-only and on-demand — the model calls it when it needs buffer
// content; nothing is injected into prompts automatically.
//
// Install options
// 1. Project scope (this project): keep the file in .omp/tools/ — omp picks it
// up when a session's cwd is inside this repository.
// 2. All projects: mkdir -p ~/.omp/agent/tools
// ln -s "$PWD/.omp/tools/nvim_buffers.mjs" ~/.omp/agent/tools/
// then restart `omp` (custom tools are loaded at session bootstrap).
//
// Socket contract (must match lua/nvim_omp/init.lua):
// path = $NVIM_OMP_SOCKET or <XDG_STATE_HOME|~/.local/state>/nvim/omp.sock
// The nvim instance running this config starts the listener at startup.
// Only one instance can own the socket; a second instance never replaces a
// live owner. Remove a stale socket manually only after confirming no nvim
// process owns it.
import os from "node:os";
import path from "node:path";
// Safe default cap so a large buffer cannot flood the conversation.
const DEFAULT_MAX_LINES = 2000;
export default function (pi) {
// Resolve socket path with the same rule as lua/nvim_omp/init.lua.
const socketPath = () => {
const env = process.env.NVIM_OMP_SOCKET;
if (env) return env;
const state =
process.env.XDG_STATE_HOME ||
path.join(os.homedir(), ".local", "state");
return path.join(state, "nvim", "omp.sock");
};
// Evaluate expr in the remote nvim via `nvim --server`; returns parsed JSON.
const rpc = async (expr, signal) => {
const bin = process.env.NVIM_BIN || "nvim";
const { code, stdout, stderr, killed } = await pi.exec(
bin,
["--server", socketPath(), "--remote-expr", expr],
{ signal },
);
if (killed) throw new Error("cancelled");
if (code !== 0) {
const why = String(stderr || "").trim();
throw new Error(
why.includes("E247")
? `No Neovim RPC socket at ${socketPath()}. ` +
"Start nvim (or vv) first — the 31_nvim_omp.lua plugin binds the socket at startup."
: `nvim RPC failed (${code}): ${why}`,
);
}
const text = String(stdout ?? "").trim();
if (!text) throw new Error("empty response from nvim RPC");
return JSON.parse(text);
};
// Resolve a buffer argument (number, string, or partial path) to a buffer
// number. Returns null when nothing matches.
const resolveBuf = async (buffer, signal) => {
if (buffer === undefined || buffer === null || buffer === "") {
return rpc("json_encode(nvim_get_current_buf())", signal);
}
if (typeof buffer === "number") return buffer;
const bufnum = Number(buffer);
if (Number.isInteger(bufnum) && bufnum > 0) return bufnum;
const list = await rpc(
"json_encode(map(nvim_list_bufs(), {i, v -> " +
"{'bufnr': v, 'name': nvim_buf_get_name(v)}}))",
signal,
);
const q = String(buffer);
const hit =
list.find(
(b) => b.name === q || b.name.endsWith("/" + q) || b.name.endsWith(q),
) ||
list.find((b) => b.name.includes(q));
return hit ? hit.bufnr : null;
};
const tools = [];
// List open buffers -----------------------------------------------
tools.push({
name: "nvim_buffers",
label: "Neovim Buffers",
description:
"List the buffers currently open in the user's running Neovim " +
"(number, name, current/loaded state, modified). Use before " +
"nvim_buffer to pick a buffer.",
parameters: pi.arktype({}),
async execute(_id, _params, _onUpdate, _ctx, _signal) {
const list = await rpc(
"json_encode(map(nvim_list_bufs(), {i, v -> {'nr': v, " +
"'name': nvim_buf_get_name(v), " +
"'current': v == nvim_get_current_buf(), " +
"'loaded': nvim_buf_is_loaded(v), " +
"'modified': getbufvar(v, '&modified')}}))",
);
if (!list.length) {
return { content: [{ type: "text", text: "No buffers open." }] };
}
const lines = list.map(
(b) =>
`${b.nr}\t${b.name || "[no name]"}\t` +
`${b.current ? "current " : ""}` +
`${b.loaded ? "" : "unloaded "}` +
`${b.modified ? "modified" : ""}`.trim(),
);
return {
content: [
{
type: "text",
text: `Open buffers (${list.length}):\n` + lines.join("\n"),
},
],
};
},
});
// Read a single buffer, line-capped -------------------------------
tools.push({
name: "nvim_buffer",
label: "Neovim Buffer",
description:
"Read lines of a Neovim buffer. `buffer` accepts the current buffer " +
"(default), a buffer number from nvim_buffers, or a file name/path " +
"open in Neovim. `maxLines` caps the returned lines (default 2000); " +
"pass a larger value explicitly to read more of a long buffer.",
parameters: pi.arktype({
buffer: "string? | number?",
maxLines: "number?",
}),
async execute(_id, params, _onUpdate, _ctx, signal) {
const buf = await resolveBuf(params.buffer, signal);
if (buf === null) {
return {
content: [
{
type: "text",
text: `No buffer matches '${params.buffer}'. List open buffers with nvim_buffers.`,
},
],
};
}
const maxLines =
Number.isInteger(params.maxLines) && params.maxLines > 0
? Math.min(params.maxLines, 100000)
: DEFAULT_MAX_LINES;
const expr =
`json_encode({'name': nvim_buf_get_name(${buf}), ` +
`'total': nvim_buf_line_count(${buf}), ` +
`'lines': nvim_buf_get_lines(${buf}, 0, ${maxLines}, 0)})`;
const { name, total, lines } = await rpc(expr, signal);
if (!total) {
return {
content: [
{ type: "text", text: `Buffer ${buf} (${name}) is empty.` },
],
};
}
const truncated =
total > lines.length
? `\n... ${total - lines.length} more lines ` +
`(raise maxLines to read them)`
: "";
const numbered = lines.map((l, i) => `${i + 1}: ${l}`).join("\n");
return {
content: [
{
type: "text",
text:
`Buffer ${buf} (${name}), ${total} lines:\n` +
numbered +
truncated,
},
],
};
},
});
return tools;
}

46
flake.lock generated
View file

@ -22,11 +22,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1785967620,
"narHash": "sha256-IItrdb7Puk05RqOBWZYFC5X6Wl1sJmCfh5MWVHw5iMM=",
"lastModified": 1784796856,
"narHash": "sha256-wWFrV5/Qbm+lyt5x20E/bSbfJiGKMo4RCxZV8cl/WZI=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "b7c2ada94fe99c15b0dbcf4d11fd7850b957a436",
"rev": "e2587caef70cea85dd97d7daab492899902dbf5d",
"type": "github"
},
"original": {
@ -36,22 +36,6 @@
"type": "github"
}
},
"plugins-bloocky": {
"flake": false,
"locked": {
"lastModified": 1786042339,
"narHash": "sha256-8QcoC1bS8pRcwBkW3mqD2vgP9Mu0aCdIy6umZO6mUGM=",
"owner": "atiladefreitas",
"repo": "bloocky",
"rev": "a052c3b1a8126e04b194bb3db3c4c6ca641c0e4e",
"type": "github"
},
"original": {
"owner": "atiladefreitas",
"repo": "bloocky",
"type": "github"
}
},
"plugins-cmp-pandoc-references": {
"flake": false,
"locked": {
@ -68,22 +52,6 @@
"type": "github"
}
},
"plugins-dooing": {
"flake": false,
"locked": {
"lastModified": 1785971574,
"narHash": "sha256-GtEC5kWcH0DmiYXkzxzNjjfYLfyEb7zpTLQWdi7n3zs=",
"owner": "atiladefreitas",
"repo": "dooing",
"rev": "6748316bc6b4601797fb952a83694cc0a5ef6da2",
"type": "github"
},
"original": {
"owner": "atiladefreitas",
"repo": "dooing",
"type": "github"
}
},
"r-nvim-nix": {
"inputs": {
"nixpkgs": [
@ -108,11 +76,11 @@
},
"rixpkgs": {
"locked": {
"lastModified": 1785776890,
"narHash": "sha256-CzzgB1teVbroR/QhGAnsQToUYCDXEZZtFTcISAooASM=",
"lastModified": 1782576256,
"narHash": "sha256-KOvpL9DJJmShb64mX9QTjhxHaxE8MizQIqNUAniuJ2E=",
"owner": "dwinkler1",
"repo": "rixpkgs",
"rev": "a54080a58b57d5785dc85901850b817bef2aaf83",
"rev": "815afc01bc0cc9a2eba80906645bd08f976a4401",
"type": "github"
},
"original": {
@ -143,9 +111,7 @@
"inputs": {
"fran": "fran",
"nixpkgs": "nixpkgs",
"plugins-bloocky": "plugins-bloocky",
"plugins-cmp-pandoc-references": "plugins-cmp-pandoc-references",
"plugins-dooing": "plugins-dooing",
"r-nvim-nix": "r-nvim-nix",
"rixpkgs": "rixpkgs",
"wrappers": "wrappers"

View file

@ -29,16 +29,6 @@
url = "github:jmbuhr/cmp-pandoc-references";
flake = false;
};
"plugins-bloocky" = {
url = "github:atiladefreitas/bloocky";
flake = false;
};
"plugins-dooing" = {
url = "github:atiladefreitas/dooing";
flake = false;
};
};
outputs = {
@ -240,18 +230,6 @@
nvim --headless -u NONE -c "set runtimepath+=${./.}" -l ${./tests/init.lua}
touch $out
'';
smoke-test = pkgs.runCommand "smoke-test" {} ''
# The Nix build sandbox has a read-only HOME; point XDG dirs at a
# writable location so vim.lsp/shaDa can write state headlessly.
export XDG_CONFIG_HOME=$TMPDIR/xdg-config
export XDG_STATE_HOME=$TMPDIR/xdg-state
export XDG_CACHE_HOME=$TMPDIR/xdg-cache
export XDG_DATA_HOME=$TMPDIR/xdg-data
BINARY_PATH="${defaultNvimPkg}/bin/vv"
"$BINARY_PATH" --headless -c "luafile ${./tests/smoke.lua}" -c "qa!"
touch $out
'';
}
);

View file

@ -11,39 +11,28 @@ nmap_leader('<Tab>', '<Cmd>bnext<CR>', 'Next buffer')
nmap_leader('<S-Tab>', '<Cmd>bprev<CR>', 'Prev buffer')
-- a is for 'AI'
nmap_leader("aa", function()
require("opencode").ask("@this: ")
end, "Ask OpenCode about this")
nmap_leader("ab", function()
require("opencode").prompt("Analyse @buffer")
end, "Analyse buffer")
nmap_leader("aB", function()
require("opencode").prompt("Analyse @buffers")
end, "Analyse open buffers")
nmap_leader("ad", function()
require("opencode").prompt("Explain @diagnostics and fix the underlying issue")
end, "Explain/fix diagnostics")
nmap_leader("an", function()
require("opencode").command("session.new")
end, "New OpenCode session")
nmap_leader("ap", function()
require("opencode").ask("@")
end, "Ask OpenCode with context")
nmap_leader("as", function()
require("opencode").select()
end, "Select OpenCode action")
xmap_leader("aa", function()
require("opencode").ask("@this: ")
end, "Ask OpenCode about selection")
xmap_leader("af", function()
require("opencode").prompt("Fix the selected code and preserve its surrounding API:\n@this")
end, "Fix selection")
xmap_leader("ap", function()
require("opencode").ask("@")
end, "Ask OpenCode with context")
xmap_leader("as", function()
require("opencode").select()
end, "Select OpenCode action")
nmap_leader("aa", "<cmd>CodeCompanion /agent<CR>", "Agent chat (@{agent} tools)")
nmap_leader("ac", "<cmd>CodeCompanionChat Toggle<CR>", "Chat Toggle")
nmap_leader("aC", function()
local chat = require("codecompanion").last_chat()
if not chat then
return vim.notify("No CodeCompanion chat to compact", vim.log.levels.WARN)
end
require("codecompanion.interactions.chat.context_management.compaction").compact(chat, { min_token_savings = 0 })
end, "Compact chat")
nmap_leader("ag", "<cmd>CodeCompanion /commit<CR>", "Generate commit message")
nmap_leader("ai", "<cmd>CodeCompanionActions<CR>", "Chat Action")
nmap_leader("al", "<cmd>CodeCompanion /lsp<CR>", "Explain LSP Diagnostics")
nmap_leader("an", "<cmd>CodeCompanionChat Add<CR>", "Chat New")
nmap_leader("as", "<cmd>CodeCompanion /suggest<CR>", "Suggest Improvements")
nmap_leader("aw", "<cmd>CodeCompanion /tdd<CR>", "Workflow: plan, implement, test")
nmap_leader("ax", "<cmd>CodeCompanion /fixer<CR>", "Code Fixer")
xmap_leader("aa", "<cmd>CodeCompanion /agent<CR>", "Agent on selection")
xmap_leader("ae", "<cmd>CodeCompanion /explain<CR>", "Explain Code")
xmap_leader("af", "<cmd>CodeCompanion /fix<CR>", "Fix Code")
xmap_leader("ap", "<cmd>CodeCompanion /expert<CR>", "Code Expert")
xmap_leader("as", "<cmd>CodeCompanion /suggest<CR>", "Suggest Improvements")
nmap_leader("ak", "<cmd>CodeCompanionChat adapter=codex<CR>", "Chat with Codex")
-- b is for 'buffer'
nmap_leader('bb', '<Cmd>b#<CR>', 'Alternate')
@ -225,24 +214,6 @@ nmap_leader("wh", "<C-w>h", "Go to Left Window", { remap = true })
nmap_leader("wj", "<C-w>j", "Go to Lower Window", { remap = true })
nmap_leader("wk", "<C-w>k", "Go to Upper Window", { remap = true })
nmap_leader("wl", "<C-w>l", "Go to Right Window", { remap = true })
nmap_leader("w>", "<Cmd>vertical resize +5<CR>", "Increase Window Width")
nmap_leader("w<lt>", "<Cmd>vertical resize -5<CR>", "Decrease Window Width")
local function set_window_width_percent(percent)
return function()
local target_width = math.max(1, math.floor(vim.o.columns * percent / 100 + 0.5))
vim.api.nvim_win_set_width(0, target_width)
end
end
for i = 1, 9 do
local percent = i * 10
nmap_leader(
"w" .. i,
set_window_width_percent(percent),
string.format("Set Window Width to %d%%", percent)
)
end
nmap_leader("_", "<C-W>s", "Split Window Below", { remap = true })
nmap_leader("|", "<C-W>v", "Split Window Right", { remap = true })

View file

@ -1,55 +0,0 @@
-- omp bridge: expose an RPC socket so the omp harness can read this Neovim
-- instance's buffers on demand. Read-only on the agent side; this module only
-- owns the socket lifecycle.
--
-- Contract
-- * Socket path: $NVIM_OMP_SOCKET if set, else <stdpath('state')>/omp.sock
-- (~/.local/state/nvim/omp.sock on macOS, ~/.local/state/nvim/omp.sock
-- elsewhere). The omp-side tool in .omp/tools/nvim_buffers.mjs resolves the
-- *same* path, so the two sides agree without configuration.
-- * The agent reads buffers by evaluating read-only nvim API expressions over
-- the socket with `nvim --server <path> --remote-expr 'json_encode(...)'`.
-- Nothing here writes buffers or executes model-supplied commands.
local M = {}
local SOCKET_NAME = "omp.sock"
-- Resolve the deterministic socket path. Copies the rule on the omp side; keep
-- the two files in sync when changing the fallback or env override.
function M.socket_path()
local env = vim.env.NVIM_OMP_SOCKET
if env and env ~= "" then
return env
end
return vim.fn.stdpath("state") .. "/" .. SOCKET_NAME
end
-- Start the RPC listener. Returns the live socket path, or nil.
-- A second instance cannot bind the same address. This function deliberately
-- never removes an existing socket file: a failed liveness probe must not
-- disconnect another live Neovim instance.
-- If a crash leaves a stale socket, remove it manually only after confirming
-- that no Neovim process owns the path, then restart vv.
function M.start()
if vim.v.headless == 1 then
-- Headless runs (tests, CI) get no socket; nothing should depend on one.
return nil
end
local path = M.socket_path()
local ok, res = pcall(vim.fn.serverstart, path)
if ok and type(res) == "string" and res ~= "" then
-- serverstart returns the bound address string (e.g. "/tmp/omp.sock").
vim.notify("nvim_omp: RPC socket ready at " .. path, vim.log.levels.INFO)
return path
end
vim.notify(
"nvim_omp: could not bind RPC socket at " .. path
.. "; another instance may own it or a stale socket needs manual cleanup.",
vim.log.levels.WARN
)
return nil
end
return M

View file

@ -78,10 +78,7 @@ in {
lze
lzextras
plenary-nvim
vimtex
neogit
config.nvim-lib.neovimPlugins.bloocky
config.nvim-lib.neovimPlugins.dooing
{
data = mini-nvim;
pname = "mini.nvim";
@ -143,6 +140,7 @@ in {
data = with pkgs.vimPlugins; [
quarto-nvim
render-markdown-nvim
vimtex
{
data = otter-nvim;
pname = "otter";
@ -165,8 +163,10 @@ in {
pname = "nvim-treesitter";
}
{
data = pkgs.vimPlugins.opencode-nvim;
pname = "opencode.nvim";
data = pkgs.codecompanion-nvim.overrideAttrs (old: {
doCheck = false;
});
pname = "codecompanion";
}
] ++ builtins.attrValues pkgs.vimPlugins.nvim-treesitter.queries;
};

View file

@ -1,13 +0,0 @@
WRN 2026-07-31T16:36:01.037 ?.50508 server_start:197: Failed to start server: operation not permitted: /tmp/nvim.daniel/TN25Kv/nvim.50508.0
WRN 2026-07-31T16:36:38.634 ?.50644 server_start:197: Failed to start server: operation not permitted: /tmp/nvim.daniel/6jT8As/nvim.50644.0
WRN 2026-07-31T16:46:12.752 ?.54395 server_start:197: Failed to start server: operation not permitted: /tmp/nvim.daniel/zY36Oi/nvim.54395.0
WRN 2026-07-31T17:01:43.138 ?.58656 server_start:197: Failed to start server: operation not permitted: /tmp/nvim.daniel/nDisRA/nvim.58656.0
WRN 2026-07-31T17:01:50.641 ?.58696 server_start:197: Failed to start server: operation not permitted: /tmp/nvim.daniel/okDyqb/nvim.58696.0
WRN 2026-07-31T17:02:04.773 ?.58741 server_start:197: Failed to start server: operation not permitted: /tmp/nvim.daniel/5wxfRu/nvim.58741.0
WRN 2026-07-31T17:02:29.644 ?.58854 server_start:197: Failed to start server: operation not permitted: /tmp/nvim.daniel/OMTfpB/nvim.58854.0
WRN 2026-07-31T17:02:29.644 ?.58852 server_start:197: Failed to start server: operation not permitted: /tmp/nvim.daniel/QZvLy2/nvim.58852.0
WRN 2026-07-31T17:14:31.289 ?.64329 server_start:197: Failed to start server: operation not permitted: /tmp/nvim.daniel/zh9tLm/nvim.64329.0
WRN 2026-07-31T17:14:40.017 ?.64399 server_start:197: Failed to start server: operation not permitted: /tmp/nvim.daniel/pGaDez/nvim.64399.0
WRN 2026-07-31T17:17:48.228 ?.64921 server_start:197: Failed to start server: operation not permitted: /tmp/nvim.daniel/ryMpX0/nvim.64921.0
WRN 2026-07-31T17:17:52.664 ?.64953 server_start:197: Failed to start server: operation not permitted: /tmp/nvim.daniel/X7RmXh/nvim.64953.0
WRN 2026-07-31T17:18:00.691 ?.64989 server_start:197: Failed to start server: operation not permitted: /tmp/nvim.daniel/C6njjq/nvim.64989.0

View file

@ -1,6 +1,22 @@
{ ... }:
final: prev:
{
codecompanion-nvim = prev.vimPlugins.codecompanion-nvim.overrideAttrs {
checkInputs = with prev.vimPlugins; [
blink-cmp
mini-nvim
];
dependencies = [ prev.vimPlugins.plenary-nvim ];
nvimSkipModules = [
"codecompanion.actions.static"
"codecompanion.actions.init"
"minimal"
"codecompanion.providers.actions.fzf_lua"
"codecompanion.providers.completion.cmp.setup"
"codecompanion.providers.actions.telescope"
"codecompanion.providers.actions.snacks"
];
};
zk-nvim = prev.vimPlugins.zk-nvim.overrideAttrs {
nvimSkipModules = [
"zk.pickers.fzf_lua"

View file

@ -5,7 +5,7 @@
};
in {
inherit rpkgs;
baseRPackages = [rpkgs.nvimcom rpkgs.rPackages.btw];
baseRPackages = [rpkgs.nvimcom];
rWrapper = rpkgs.rWrapper.override {packages = [];};
quarto = rpkgs.quarto.override {extraRPackages = [];};
}

View file

@ -18,7 +18,7 @@ local defaults = {
duckdb = "duckdb",
julia = "julia",
python = "ipython",
shell = "echo 'Hello " .. (vim.env.USER or "user") .. "!'",
shell = "echo 'Hello " .. vim.env.USER .. "!'",
}
-- Registry of terminal commands

View file

@ -191,7 +191,7 @@ end)
-- Treesitter
now_if_args(function()
vim.treesitter.language.register("markdown", { "markdown", "rmd", "quarto" })
vim.treesitter.language.register("markdown", { "markdown", "codecompanion", "rmd", "quarto" })
require 'treesitter-context'.setup {
enable = true,
@ -216,7 +216,7 @@ now_if_args(function()
"julia", "rnoweb", "latex", "gitcommit", "gitignore",
"git_config", "git_rebase", "diff", "dockerfile",
"make", "xml", "zig", "regex", "csv", "bash",
"markdown_inline", "quarto", "rmd",
"markdown_inline", "quarto", "rmd", "codecompanion",
}
local function start_treesitter(buf, filetype)
local lang = vim.treesitter.language.get_lang(filetype) or filetype

View file

@ -55,24 +55,21 @@ later(function()
ignore_exitcode = true,
parser = function(output, bufnr, linter_cwd)
local diagnostics = {}
-- Expected format: /path/file.R:10:5: style: Some message
local severity_map = {
style = vim.diagnostic.severity.INFO,
warning = vim.diagnostic.severity.WARN,
error = vim.diagnostic.severity.ERROR,
}
-- Pattern: /path/file.R:10:5: style: Some message
for line in output:gmatch("[^\r\n]+") do
-- Capture the file path as well so lnum/col line up with the numbers.
local path, lnum, col, severity, message = line:match("^(.-):(%d+):(%d+):%s*(%w+):%s*(.+)$")
if path and lnum and col and severity then
local line_num = tonumber(lnum)
local col_num = tonumber(col)
local path, lnum, col, severity, message = line:match("^[^:]+:(%d+):(%d+):%s*(%w+):%s*(.+)$")
if path then
local severity_map = {
style = vim.diagnostic.severity.INFO,
warning = vim.diagnostic.severity.WARN,
error = vim.diagnostic.severity.ERROR,
}
table.insert(diagnostics, {
bufnr = bufnr,
lnum = math.max(0, line_num - 1),
col = math.max(0, col_num - 1),
end_lnum = line_num - 1,
end_col = col_num,
lnum = math.max(0, tonumber(lnum) - 1),
col = math.max(0, tonumber(col) - 1),
end_lnum = tonumber(lnum) - 1,
end_col = tonumber(col),
severity = severity_map[severity:lower()] or vim.diagnostic.severity.WARN,
message = message or "lintr issue",
source = "lintr",
@ -102,7 +99,7 @@ now_if_args(function()
add("render-markdown.nvim")
require('render-markdown').setup({
-- completions = { blink = { enabled = true } },
file_types = { 'markdown', },
file_types = { 'markdown', 'codecompanion', },
link = {
wiki = {
body = function(ctx)

View file

@ -37,12 +37,7 @@ later(function()
end)
later(function()
require("mini.align").setup({
mappings = {
start = "gA",
start_with_preview = "g<C-A>",
},
})
require("mini.align").setup()
end)
later(function()
@ -218,8 +213,6 @@ later(function()
local minikeymap = require("mini.keymap")
minikeymap.setup()
local map_multistep = minikeymap.map_multistep
-- blink_next/blink_prev/blink_accept are blink.cmp actions.
-- Direct blink keymaps (C-space/C-l) are set in 24_completion.lua.
local tab_steps = {
"blink_next",
"pmenu_next",

View file

@ -4,6 +4,9 @@ local later = MiniDeps.later
local now = MiniDeps.now
local now_if_args = Config.now_if_args
-- Constants
local BLINK_VERSION = "v1.10.2"
-- Plugin sources configuration
local PLUGIN_SOURCES = {
"hrsh7th/cmp-cmdline",
@ -11,7 +14,7 @@ local PLUGIN_SOURCES = {
"zbirenbaum/copilot.lua",
"jmbuhr/cmp-pandoc-references",
"fang2hou/blink-copilot",
"nickjvandyke/opencode.nvim",
"olimorris/codecompanion.nvim"
}
local PLUGIN_ADDS = {
@ -21,16 +24,34 @@ local PLUGIN_ADDS = {
"cmp-pandoc-references",
}
-- Helper functions
local function create_system_prompt(role_description)
return function(context)
local lang = context.filetype or "programmer"
return "I want you to act as a senior " .. lang .. " developer. " .. role_description
end
end
local function get_code_block(context)
local text = require("codecompanion.helpers.code").get_code(context.start_line, context.end_line)
return "```" .. context.filetype .. "\n" .. text .. "\n```"
end
local function get_mini_icons_highlight(ctx)
local _, hl, _ = require("mini.icons").get("lsp", ctx.kind)
return hl
end
local function get_blink_fuzzy_setting()
return {
sorts = { "exact", "score", "sort_text" },
use_proximity = true,
local setting = {
sorts = { "exact", "score", "sort_text" }
}
if not Config.isNixCats then
setting.prebuilt_binaries = { force_version = BLINK_VERSION }
end
return setting
end
-- Plugin loading
@ -41,6 +62,7 @@ if not Config.isNixCats then
add({
source = "saghen/blink.cmp",
depends = { "rafamadriz/friendly-snippets" },
checkout = BLINK_VERSION,
})
end)
@ -51,6 +73,241 @@ if not Config.isNixCats then
end)
end
local function get_codecompanion_config()
return {
adapters = {
acp = {
-- Codex = heavy agent lane (ChatGPT Edu login via `codex login`; ~/.codex/auth.json).
-- Requires `codex-acp` on PATH (~/.nix-profile/bin). ACP-only slash commands in the
-- chat buffer: /resume (restore a past session, fresh chat only), /mode (switch agent
-- mode), /command, /acp_session_options (e.g. model per session); `\` triggers ACP
-- command completion (1-5s delay after chat open).
codex = function()
return require("codecompanion.adapters").extend("codex", {
defaults = {
auth_method = "chatgpt",
},
})
end,
},
},
interactions = {
chat = {
adapter = {
name = "copilot",
model = "claude-sonnet-5",
},
slash_commands = {
["share"] = {
opts = {
token = os.getenv("GITHUB_GIST_TOKEN"),
},
},
},
opts = {
completion_provider = "blink",
context_management = {
editing = {
trigger = 0.65,
keep_cycles = 3,
exclude_tools = { "memory" },
},
compaction = {
trigger = 0.85,
min_token_savings = 10000,
},
},
},
},
inline = {
adapter = {
name = "copilot",
model = "gpt-5-mini",
},
},
shared = {
keymaps = {
accept_change = {
modes = { n = "ga" },
description = "Accept the suggested change",
},
reject_change = {
modes = { n = "gr" },
opts = { nowait = true },
description = "Reject the suggested change",
},
},
},
background = {
adapter = {
name = "copilot",
model = "gpt-5-mini",
},
chat = {
callbacks = {
["on_ready"] = {
actions = { "interactions.background.builtin.chat_make_title" },
enabled = true,
},
},
opts = {
enabled = true,
},
},
},
},
display = {
chat = {
show_settings = false,
window = {
layout = "horizontal",
position = "bottom",
height = 0.33,
},
},
diff = {
enabled = true,
threshold_for_chat = 6,
},
},
rules = {
default = {
description = "Collection of common files for all projects",
files = {
".clinerules",
".cursorrules",
".rules",
".github/copilot-instructions.md",
"AGENT.md",
"AGENTS.md",
{ path = "CLAUDE.md", parser = "claude" },
{ path = "CLAUDE.local.md", parser = "claude" },
{ path = "~/.claude/CLAUDE.md", parser = "claude" },
},
},
opts = {
chat = {
autoload = "default",
enabled = true,
},
},
},
prompt_library = {
["expert"] = {
interaction = "chat",
description = "Get expert advice from an LLM",
opts = { alias = "expert" },
prompts = {
{
role = "system",
content = create_system_prompt(
"I will ask you specific questions and I want you to return concise explanations and codeblock examples."
),
},
{
role = "user",
content = function(context)
return "I have the following code:\n\n" .. get_code_block(context) .. "\n\n"
end,
opts = { contains_code = true },
},
},
},
["fixer"] = {
interaction = "chat",
description = "Fix code errors with expert guidance",
opts = { alias = "fixer" },
prompts = {
{
role = "system",
content = create_system_prompt(
"I have a block of code that is not working and will give you a hint about the error. I want you to return the corrected code and a concise explanation of the corrections."
),
},
{
role = "user",
content = function(context)
return "The following code has an error:\n\n" .. get_code_block(context) .. "\n\nThe error is:"
end,
opts = { contains_code = true },
},
},
},
["suggest"] = {
interaction = "chat",
description = "Suggest improvements to the buffer",
opts = { alias = "suggest" },
prompts = {
{
role = "system",
content = create_system_prompt(
"When asked to improve code, follow these steps:\n" ..
"1. Identify the programming language.\n" ..
"2. Think separately for each function or significant block of code and think about possible improvements (e.g., for better readability or speed) in the context of the language.\n" ..
"3. Think about the whole document and think about possible improvements.\n" ..
"4. Provide the improved code.\n" ..
"5. Provide a concise explanation of the improvements."
),
},
{
role = "user",
content = function(context)
return "Please improve the following code:\n\n" .. get_code_block(context)
end,
opts = { contains_code = true },
},
},
},
["agent"] = {
interaction = "chat",
description = "Agentic coding with the @{agent} tool group (read/edit/grep/run)",
opts = { alias = "agent" },
prompts = {
{
role = "user",
content = function(context)
return "@{agent} Work on the following code:\n\n" .. get_code_block(context) .. "\n\n"
end,
opts = { contains_code = true },
},
},
},
["tdd"] = {
interaction = "chat",
description = "Workflow: plan the buffer change, implement it, run the tests",
opts = { alias = "tdd", is_workflow = true },
prompts = {
{
{
role = "user",
content = function(context)
return "Let's work test-driven. First, study #buffer and the relevant parts of the codebase, then propose a concise implementation plan (no code yet).\n\nThe code under discussion:\n\n"
.. get_code_block(context)
.. "\n\nThe task: "
end,
opts = { contains_code = true },
},
},
{
{
role = "user",
content = "Implement the plan now, writing or updating tests alongside the code. @{agent}",
opts = { auto_submit = true },
},
},
{
{
role = "user",
content = "Run the project's test suite with @{run_command} and fix any failures until it passes.",
opts = { auto_submit = true },
},
},
},
},
},
}
end
-- Batch add simple plugins
later(function()
for _, plugin in ipairs(PLUGIN_ADDS) do
@ -102,33 +359,26 @@ later(function()
})
end)
-- CodeCompanion habit notes (chat buffer unless stated):
-- /compact compact history, keep summary /fork fork the conversation
-- /symbols insert symbols for a file /share export chat to a GitHub gist
-- gm toggle "btw" ephemeral message gty YOLO: approve all tool calls
-- gba/gbd buffer sync add/drop gd debug window (adapter/tools info)
-- Codex (ACP) lane: /resume (fresh chat only), /mode, /command, /acp_session_options,
-- `\` ACP command completion. Prompt library: /expert /fixer /suggest /agent /tdd (workflow).
later(function()
vim.g.opencode_opts = {
events = {
reload = true,
permissions = {
enabled = true,
edits = { enabled = true },
},
},
server = {
start = function()
vim.cmd("vsplit term://opencode --port")
vim.cmd("vertical resize " .. math.floor(vim.o.columns * 0.4))
vim.cmd("wincmd p")
end
},
}
add("opencode.nvim")
add("codecompanion.nvim")
-- now use function
require("codecompanion").setup(get_codecompanion_config())
vim.cmd([[cab cc CodeCompanion]])
end)
now_if_args(function()
add("blink.cmp")
require("blink.cmp").setup({
-- Direct blink keymaps (C-space/C-l).
-- Tab/Enter/Up/Down are handled via multistep chains in 23_editor.lua,
-- which chain blink_next/blink_prev/blink_accept with other editor actions.
keymap = {
preset = "default",
["<C-space>"] = { "show", "select_next" },
@ -165,7 +415,6 @@ now_if_args(function()
},
completion = {
menu = {
border = "rounded",
draw = {
treesitter = { "lsp" },
components = {
@ -185,12 +434,14 @@ now_if_args(function()
list = {
selection = { preselect = false, auto_insert = true }
},
ghost_text = { enabled = true, show_with_menu = true },
documentation = { auto_show = true, window = { border = "rounded" } },
documentation = { auto_show = true },
trigger = { show_in_snippet = false },
},
snippets = { preset = "mini_snippets" },
sources = {
per_filetype = {
codecompanion = { "codecompanion" },
},
default = { "references", "lsp", "path", "snippets", "buffer", "omni", "copilot" },
providers = {
path = {
@ -216,6 +467,12 @@ now_if_args(function()
score_offset = 45,
async = true,
},
codecompanion = {
name = "CodeCompanion",
module = "codecompanion.providers.completion.blink",
score_offset = 45,
async = true,
},
references = {
name = "pandoc_references",
module = "cmp-pandoc-references.blink",

View file

@ -15,7 +15,7 @@ now_if_args(function()
basedpyright = {},
ruff = {},
marksman = {
filetypes = { "markdown", "markdown_inline" },
filetypes = { "markdown", "markdown_inline", "codecompanion" },
},
harper_ls = {
cmd = { "harper-ls", "--stdio" },

View file

@ -11,19 +11,24 @@ later(function()
return
end
-- vimtex is a VimL plugin: it does not expose a Lua module. Configure it
-- via globals before loading so the plugin picks them up on startup.
vim.g.vimtex_compiler_method = "latexmk"
vim.g.vimtex_compiler_latexmk = {
-- Keep conservative defaults: no continuous background compilation and no
-- callback chatter. Manual :VimtexCompile still works on demand.
continuous = 0,
callback = 0,
}
-- Let vimtex choose the first available viewer (zathura, Skim, Evince, ...).
Config.add("vimtex")
local ok, vimtex = pcall(require, "vimtex")
if not ok then
vim.notify("vimtex not available", vim.log.levels.WARN)
return
end
-- Keep conservative defaults: latexmk continuous compilation off, single
-- viewer (zathura falls back to Evince on most setups).
vimtex.setup({
enabled = true,
compile_on_save = false,
compiler = "latexmk",
-- Avoid hooking spell/formatting into our global <leader> group;
-- vimtex stashes its own <localleader> mappings automatically.
})
-- Filetype detection is normally on, but force it explicitly so .tex files
-- opened outside Quarto still pick up the LSP + viewer hooks.
vim.api.nvim_create_autocmd("BufReadPost", {

View file

@ -1,43 +0,0 @@
-- Bloocky: timeblocking calendar (day/week/month views), persisted to JSON.
-- Loaded via the `general` cat spec (nix mode) or MiniDeps (non-nix).
-- Default global toggle is <leader>tb, which collides with the terminal map
-- (toggle bracketed paste), so it is moved to the calendar/tasks group <leader>c.
local Config = require('config')
if not Config.isNixCats then
local later = MiniDeps.later
later(function()
MiniDeps.add({ source = 'atiladefreitas/bloocky' })
end)
end
local nix = require('config.nix')
local later = MiniDeps.later
later(function()
if not nix.get_cat('general', false) then
return
end
require('bloocky').setup({
week_start = "monday",
window = {
-- Width per view: fraction of the editor width (or absolute columns if > 1).
-- A single number applies to every view.
width = {
month = 0.99,
week = 0.99,
day = 0.8,
},
border = "rounded",
},
integrations = {
dooing = {
enabled = true, -- show Dooing todos on their due date
show_done = false, -- also show completed todos
},
},
keymaps = {
toggle = '<leader>cb',
},
})
end)

View file

@ -1,44 +0,0 @@
-- Dooing: minimalist todo list manager with a floating window, persisted to JSON.
-- Loaded via the `general` cat spec (nix mode) or MiniDeps (non-nix).
-- Default globals <leader>td / <leader>tN occupy the terminal group, so the
-- todo toggles are moved to the calendar/tasks group <leader>c.
local Config = require('config')
if not Config.isNixCats then
local later = MiniDeps.later
later(function()
MiniDeps.add({ source = 'atiladefreitas/dooing' })
end)
end
local nix = require('config.nix')
local later = MiniDeps.later
local now = MiniDeps.now
later(function()
if not nix.get_cat('general', false) then
return
end
local dooing = require('dooing')
require("dooing").setup({
keymaps = {
toggle_window = "<leader>cd",
open_project_todo = "<leader>cD",
show_due_notification = "<leader>cN",
create_nested_task = "<leader>cn", -- Create nested subtask under current todo
toggle_priority = "a",
},
calendar = {
week_start_day = "monday",
},
})
-- Remove Dooing's old defaults.
vim.keymap.del("n", "<leader>td")
vim.keymap.del("n", "<leader>tD")
vim.keymap.del("n", "<leader>tN")
-- Restore mappings overwritten by Dooing.
local helpers = require('keymap.helpers')
local nmap_leader = helpers.nmap_leader
-- t is for 'terminal'
nmap_leader("td", '<Cmd>lua Config.terminal.open_duckdb();Config.terminal.toggle_bracket()<CR>', 'Open DuckDB')
end)

View file

@ -1,8 +0,0 @@
-- omp bridge entrypoint: start the RPC socket as early as possible so the omp
-- harness can read this instance's buffers on demand (see lua/nvim_omp/init.lua
-- and .omp/tools/nvim_buffers.mjs). Safe to fail silently — the socket is a
-- convenience, not a dependency of the editor.
local ok, nvim_omp = pcall(require, "nvim_omp")
if ok then
nvim_omp.start()
end

View file

@ -1,268 +0,0 @@
-- Comprehensive CI smoke tests for the Neovim configuration.
-- Run inside the wrapped Neovim binary, e.g.:
-- vv --headless -c "luafile tests/smoke.lua" -c "qa!"
--
-- The script loads the full config (plugin/*.lua files are sourced by Neovim on
-- startup), waits for mini.deps deferred work, then exercises key functionality
-- without user interaction.
local M = {}
-- ---------------------------------------------------------------------------
-- Tiny test harness
-- ---------------------------------------------------------------------------
local failures = {}
local passed = 0
local function fail(msg)
table.insert(failures, msg)
print(" FAIL: " .. msg)
end
local function pass(msg)
passed = passed + 1
print(" PASS: " .. msg)
end
local function assert_eq(a, b, msg)
if a == b then
pass(msg)
else
fail(string.format("%s (expected %s, got %s)", msg, vim.inspect(b), vim.inspect(a)))
end
end
local function assert_true(cond, msg)
if cond then
pass(msg)
else
fail(msg)
end
end
local function assert_loaded(mod, msg)
assert_true(package.loaded[mod] ~= nil, msg or ("module '" .. mod .. "' loaded"))
end
-- ---------------------------------------------------------------------------
-- Wait for deferred mini.deps/later work.
-- We patch MiniDeps.later to count pending callbacks and then drain the event
-- loop until the counter returns to zero (or we time out).
-- ---------------------------------------------------------------------------
local function wait_for_deferred(timeout_ms)
timeout_ms = timeout_ms or 10000
-- Fire VimEnter so startup autocmds run, then drain the event loop so
-- mini.deps' deferred setup closures have a chance to execute before we
-- assert anything.
-- The headless smoke run has no start screen to display. Disabling
-- mini.starter also prevents it from replacing the current buffer and
-- trying to create a swap file in the read-only Nix sandbox.
vim.g.ministarter_disable = true
vim.cmd('doautocmd VimEnter')
vim.wait(timeout_ms, function() return false end, 50)
end
-- ---------------------------------------------------------------------------
-- Helper: check whether a keymap is registered for a given lhs in normal mode.
-- ---------------------------------------------------------------------------
local function has_normal_map(lhs)
-- <leader> is stored as the literal leader key, so test both the raw
-- symbolic form and the expanded form (e.g. "<leader>ff" and " ff").
local expanded = lhs:gsub('^<leader>', vim.g.mapleader or '\\')
if vim.keymap and vim.keymap.get then
local maps = vim.keymap.get('n')
for _, map in ipairs(maps) do
if map.lhs == lhs or map.lhs == expanded then
return true
end
end
end
return vim.fn.maparg(lhs, 'n') ~= '' or vim.fn.maparg(expanded, 'n') ~= ''
end
-- ---------------------------------------------------------------------------
-- Helper: check whether a Treesitter parser is available.
-- ---------------------------------------------------------------------------
local function has_parser(lang)
return #vim.api.nvim_get_runtime_file("parser/" .. lang .. ".*", false) > 0
end
-- ---------------------------------------------------------------------------
-- Helper: check whether an LSP config was registered (Neovim 0.11+ native API).
-- ---------------------------------------------------------------------------
local function has_lsp_config(name)
if vim.lsp and vim.lsp.config then
-- Neovim 0.12 exposes configs through the callable `vim.lsp.config`
-- table; older versions expose a function-like API.
local ok, cfg = pcall(function() return vim.lsp.config[name] end)
if not ok or type(cfg) ~= 'table' or next(cfg) == nil then
ok, cfg = pcall(vim.lsp.config, name)
end
if ok and cfg and next(cfg) ~= nil then
return true
end
end
-- Fallback: inspect lspconfig internal table.
local ok, configs = pcall(require, 'lspconfig.configs')
if ok and configs and configs[name] then
return true
end
return false
end
-- ---------------------------------------------------------------------------
-- Test suites
-- ---------------------------------------------------------------------------
function M.test_core_config()
print("\n=== Core configuration ===")
assert_eq(vim.g.mapleader, ' ', "leader is space")
assert_eq(vim.g.maplocalleader, ',', "localleader is comma")
assert_true(vim.o.backup, "backup option enabled")
assert_true(vim.o.undofile, "undofile enabled")
assert_eq(vim.o.mouse, 'a', "mouse enabled")
end
function M.test_config_module()
print("\n=== Config module ===")
assert_true(_G.Config ~= nil, "global Config table exists")
assert_true(type(Config.edit) == 'function', "Config.edit helper exists")
assert_true(type(Config.terminal) == 'table', "Config.terminal namespace exists")
assert_true(type(Config.treesitter_helpers) == 'table', "Config.treesitter_helpers exists")
end
function M.test_mini_modules()
print("\n=== mini.nvim modules ===")
assert_loaded('mini.basics', 'mini.basics loaded')
assert_loaded('mini.statusline', 'mini.statusline loaded')
assert_loaded('mini.tabline', 'mini.tabline loaded')
assert_loaded('mini.clue', 'mini.clue loaded')
assert_loaded('mini.pick', 'mini.pick loaded')
assert_loaded('mini.notify', 'mini.notify loaded')
end
function M.test_keymaps()
print("\n=== Keymaps ===")
assert_true(has_normal_map('<leader>ff'), "leader ff -> pick files")
assert_true(has_normal_map('<leader>fg'), "leader fg -> live grep")
assert_true(has_normal_map('<leader>bb'), "leader bb -> alternate buffer")
assert_true(has_normal_map('<leader>ed'), "leader ed -> mini.files open")
assert_true(has_normal_map('<Esc>'), "Esc clears search highlight")
end
function M.test_treesitter()
print("\n=== Treesitter parsers ===")
local expected = { 'lua', 'python', 'nix', 'markdown', 'latex', 'r', 'julia' }
for _, lang in ipairs(expected) do
assert_true(has_parser(lang), "parser available: " .. lang)
end
end
function M.test_filetype_detection()
print("\n=== Filetype detection ===")
local test_buf = vim.api.nvim_create_buf(false, true)
local orig_buf = vim.api.nvim_get_current_buf()
vim.api.nvim_set_current_buf(test_buf)
-- Test by manually triggering filetype detection for a couple of languages.
vim.api.nvim_buf_set_name(test_buf, 'test.py')
vim.api.nvim_set_option_value('filetype', 'python', { buf = test_buf })
assert_eq(vim.bo.filetype, 'python', "python filetype set")
vim.api.nvim_buf_set_name(test_buf, 'test.lua')
vim.api.nvim_set_option_value('filetype', 'lua', { buf = test_buf })
assert_eq(vim.bo.filetype, 'lua', "lua filetype set")
vim.api.nvim_set_current_buf(orig_buf)
vim.api.nvim_buf_delete(test_buf, { force = true })
end
function M.test_lsp_config()
print("\n=== LSP server registration ===")
local servers = { 'lua_ls', 'basedpyright', 'ruff', 'nil_ls', 'texlab', 'marksman', 'harper_ls' }
for _, name in ipairs(servers) do
assert_true(has_lsp_config(name), "LSP config registered: " .. name)
end
end
function M.test_plugin_configs()
print("\n=== Plugin-specific configuration ===")
-- vimtex globals should be set when the markdown cat is on.
if vim.g.vimtex_compiler_method then
assert_eq(vim.g.vimtex_compiler_method, 'latexmk', "vimtex compiler is latexmk")
else
pass("vimtex not configured (markdown cat disabled)")
end
-- conform formatters should be registered.
local ok, conform = pcall(require, 'conform')
if ok and conform then
local formatters = require('conform').formatters
assert_true(formatters ~= nil, "conform formatters table exists")
else
pass("conform not loaded (expected if utils cat disabled)")
end
-- blink.cmp keymap preset should be available.
local blink_ok, blink = pcall(require, 'blink.cmp')
if blink_ok and blink then
pass("blink.cmp loaded")
else
pass("blink.cmp not loaded (expected if utils cat disabled)")
end
end
function M.test_nix_cats_helper()
print("\n=== nixCats helper ===")
local ok, nix = pcall(require, 'config.nix')
assert_true(ok and nix ~= nil, "config.nix can be required")
if ok and nix then
local cat = nix.get_cat('general', true)
assert_true(type(cat) == 'boolean', "get_cat returns boolean default")
end
end
-- ---------------------------------------------------------------------------
-- Entry point
-- ---------------------------------------------------------------------------
function M.run()
print("Neovim config CI smoke tests")
local v = vim.version()
print(string.format("Neovim version: %d.%d.%d", v.major, v.minor, v.patch))
-- Drain deferred plugin setup before asserting.
wait_for_deferred()
M.test_core_config()
M.test_config_module()
M.test_mini_modules()
M.test_keymaps()
M.test_treesitter()
M.test_filetype_detection()
M.test_lsp_config()
M.test_plugin_configs()
M.test_nix_cats_helper()
print("\n=== Summary ===")
print(string.format("Passed: %d", passed))
print(string.format("Failed: %d", #failures))
if #failures > 0 then
print("\nFailed tests:")
for _, f in ipairs(failures) do
print(" - " .. f)
end
vim.cmd('cquit 1')
else
print("\nAll smoke tests passed!")
vim.cmd('cquit 0')
end
end
local ok, err = pcall(M.run)
if not ok then
print("CRASH: " .. tostring(err))
vim.cmd('cquit 1')
end