mirror of
https://github.com/dwinkler1/nvimConfig.git
synced 2026-08-22 17:43:13 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b630932348 | |||
| c7543c978a | |||
| 25a88599c1 | |||
| 92729cff56 | |||
| b8f72cb150 | |||
| 6b6f5b3e57 | |||
|
475a914ad2 |
|||
| d9a8c31bc6 | |||
|
5f6b1248dd |
|||
|
f60ba80756 |
|||
|
931dfc1691 |
|||
|
|
60b23e42d0 |
||
|
|
6db6d131be |
24 changed files with 823 additions and 373 deletions
4
.github/workflows/check.yml
vendored
4
.github/workflows/check.yml
vendored
|
|
@ -29,7 +29,7 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest]
|
os: [ubuntu-latest, macos-latest]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- uses: wimpysworld/nothing-but-nix@main
|
- uses: wimpysworld/nothing-but-nix@main
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
with:
|
with:
|
||||||
|
|
@ -37,7 +37,7 @@ jobs:
|
||||||
- uses: cachix/install-nix-action@v31
|
- uses: cachix/install-nix-action@v31
|
||||||
with:
|
with:
|
||||||
github_access_token: ${{ secrets.GH_TOKEN }}
|
github_access_token: ${{ secrets.GH_TOKEN }}
|
||||||
- uses: cachix/cachix-action@v14
|
- uses: cachix/cachix-action@v17
|
||||||
with:
|
with:
|
||||||
name: rde
|
name: rde
|
||||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ jobs:
|
||||||
id-token: "write"
|
id-token: "write"
|
||||||
contents: "read"
|
contents: "read"
|
||||||
steps:
|
steps:
|
||||||
- uses: "actions/checkout@v5"
|
- uses: "actions/checkout@v7"
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: "DeterminateSystems/determinate-nix-action@v3"
|
- uses: "DeterminateSystems/determinate-nix-action@v3"
|
||||||
|
|
|
||||||
4
.github/workflows/update.yml
vendored
4
.github/workflows/update.yml
vendored
|
|
@ -10,13 +10,13 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
- uses: wimpysworld/nothing-but-nix@main
|
- uses: wimpysworld/nothing-but-nix@main
|
||||||
with:
|
with:
|
||||||
hatchet-protocol: 'carve'
|
hatchet-protocol: 'carve'
|
||||||
- name: Install Determinate Nix
|
- name: Install Determinate Nix
|
||||||
uses: DeterminateSystems/determinate-nix-action@v3
|
uses: DeterminateSystems/determinate-nix-action@v3
|
||||||
- uses: cachix/cachix-action@v14
|
- uses: cachix/cachix-action@v17
|
||||||
with:
|
with:
|
||||||
name: rde
|
name: rde
|
||||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||||
|
|
|
||||||
17
.letta/settings.local.json
Normal file
17
.letta/settings.local.json
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
185
.omp/tools/nvim_buffers.mjs
Normal file
185
.omp/tools/nvim_buffers.mjs
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
// 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
46
flake.lock
generated
|
|
@ -22,11 +22,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1784796856,
|
"lastModified": 1785967620,
|
||||||
"narHash": "sha256-wWFrV5/Qbm+lyt5x20E/bSbfJiGKMo4RCxZV8cl/WZI=",
|
"narHash": "sha256-IItrdb7Puk05RqOBWZYFC5X6Wl1sJmCfh5MWVHw5iMM=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "e2587caef70cea85dd97d7daab492899902dbf5d",
|
"rev": "b7c2ada94fe99c15b0dbcf4d11fd7850b957a436",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -36,6 +36,22 @@
|
||||||
"type": "github"
|
"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": {
|
"plugins-cmp-pandoc-references": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
|
|
@ -52,6 +68,22 @@
|
||||||
"type": "github"
|
"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": {
|
"r-nvim-nix": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
|
|
@ -76,11 +108,11 @@
|
||||||
},
|
},
|
||||||
"rixpkgs": {
|
"rixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1782576256,
|
"lastModified": 1785776890,
|
||||||
"narHash": "sha256-KOvpL9DJJmShb64mX9QTjhxHaxE8MizQIqNUAniuJ2E=",
|
"narHash": "sha256-CzzgB1teVbroR/QhGAnsQToUYCDXEZZtFTcISAooASM=",
|
||||||
"owner": "dwinkler1",
|
"owner": "dwinkler1",
|
||||||
"repo": "rixpkgs",
|
"repo": "rixpkgs",
|
||||||
"rev": "815afc01bc0cc9a2eba80906645bd08f976a4401",
|
"rev": "a54080a58b57d5785dc85901850b817bef2aaf83",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -111,7 +143,9 @@
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"fran": "fran",
|
"fran": "fran",
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs",
|
||||||
|
"plugins-bloocky": "plugins-bloocky",
|
||||||
"plugins-cmp-pandoc-references": "plugins-cmp-pandoc-references",
|
"plugins-cmp-pandoc-references": "plugins-cmp-pandoc-references",
|
||||||
|
"plugins-dooing": "plugins-dooing",
|
||||||
"r-nvim-nix": "r-nvim-nix",
|
"r-nvim-nix": "r-nvim-nix",
|
||||||
"rixpkgs": "rixpkgs",
|
"rixpkgs": "rixpkgs",
|
||||||
"wrappers": "wrappers"
|
"wrappers": "wrappers"
|
||||||
|
|
|
||||||
22
flake.nix
22
flake.nix
|
|
@ -29,6 +29,16 @@
|
||||||
url = "github:jmbuhr/cmp-pandoc-references";
|
url = "github:jmbuhr/cmp-pandoc-references";
|
||||||
flake = false;
|
flake = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
"plugins-bloocky" = {
|
||||||
|
url = "github:atiladefreitas/bloocky";
|
||||||
|
flake = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
"plugins-dooing" = {
|
||||||
|
url = "github:atiladefreitas/dooing";
|
||||||
|
flake = false;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = {
|
outputs = {
|
||||||
|
|
@ -230,6 +240,18 @@
|
||||||
nvim --headless -u NONE -c "set runtimepath+=${./.}" -l ${./tests/init.lua}
|
nvim --headless -u NONE -c "set runtimepath+=${./.}" -l ${./tests/init.lua}
|
||||||
touch $out
|
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
|
||||||
|
'';
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,28 +11,39 @@ nmap_leader('<Tab>', '<Cmd>bnext<CR>', 'Next buffer')
|
||||||
nmap_leader('<S-Tab>', '<Cmd>bprev<CR>', 'Prev buffer')
|
nmap_leader('<S-Tab>', '<Cmd>bprev<CR>', 'Prev buffer')
|
||||||
|
|
||||||
-- a is for 'AI'
|
-- a is for 'AI'
|
||||||
nmap_leader("aa", "<cmd>CodeCompanion /agent<CR>", "Agent chat (@{agent} tools)")
|
nmap_leader("aa", function()
|
||||||
nmap_leader("ac", "<cmd>CodeCompanionChat Toggle<CR>", "Chat Toggle")
|
require("opencode").ask("@this: ")
|
||||||
nmap_leader("aC", function()
|
end, "Ask OpenCode about this")
|
||||||
local chat = require("codecompanion").last_chat()
|
nmap_leader("ab", function()
|
||||||
if not chat then
|
require("opencode").prompt("Analyse @buffer")
|
||||||
return vim.notify("No CodeCompanion chat to compact", vim.log.levels.WARN)
|
end, "Analyse buffer")
|
||||||
end
|
nmap_leader("aB", function()
|
||||||
require("codecompanion.interactions.chat.context_management.compaction").compact(chat, { min_token_savings = 0 })
|
require("opencode").prompt("Analyse @buffers")
|
||||||
end, "Compact chat")
|
end, "Analyse open buffers")
|
||||||
nmap_leader("ag", "<cmd>CodeCompanion /commit<CR>", "Generate commit message")
|
nmap_leader("ad", function()
|
||||||
nmap_leader("ai", "<cmd>CodeCompanionActions<CR>", "Chat Action")
|
require("opencode").prompt("Explain @diagnostics and fix the underlying issue")
|
||||||
nmap_leader("al", "<cmd>CodeCompanion /lsp<CR>", "Explain LSP Diagnostics")
|
end, "Explain/fix diagnostics")
|
||||||
nmap_leader("an", "<cmd>CodeCompanionChat Add<CR>", "Chat New")
|
nmap_leader("an", function()
|
||||||
nmap_leader("as", "<cmd>CodeCompanion /suggest<CR>", "Suggest Improvements")
|
require("opencode").command("session.new")
|
||||||
nmap_leader("aw", "<cmd>CodeCompanion /tdd<CR>", "Workflow: plan, implement, test")
|
end, "New OpenCode session")
|
||||||
nmap_leader("ax", "<cmd>CodeCompanion /fixer<CR>", "Code Fixer")
|
nmap_leader("ap", function()
|
||||||
xmap_leader("aa", "<cmd>CodeCompanion /agent<CR>", "Agent on selection")
|
require("opencode").ask("@")
|
||||||
xmap_leader("ae", "<cmd>CodeCompanion /explain<CR>", "Explain Code")
|
end, "Ask OpenCode with context")
|
||||||
xmap_leader("af", "<cmd>CodeCompanion /fix<CR>", "Fix Code")
|
nmap_leader("as", function()
|
||||||
xmap_leader("ap", "<cmd>CodeCompanion /expert<CR>", "Code Expert")
|
require("opencode").select()
|
||||||
xmap_leader("as", "<cmd>CodeCompanion /suggest<CR>", "Suggest Improvements")
|
end, "Select OpenCode action")
|
||||||
nmap_leader("ak", "<cmd>CodeCompanionChat adapter=codex<CR>", "Chat with Codex")
|
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")
|
||||||
|
|
||||||
-- b is for 'buffer'
|
-- b is for 'buffer'
|
||||||
nmap_leader('bb', '<Cmd>b#<CR>', 'Alternate')
|
nmap_leader('bb', '<Cmd>b#<CR>', 'Alternate')
|
||||||
|
|
@ -214,6 +225,24 @@ 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("wj", "<C-w>j", "Go to Lower Window", { remap = true })
|
||||||
nmap_leader("wk", "<C-w>k", "Go to Upper 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("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>s", "Split Window Below", { remap = true })
|
||||||
nmap_leader("|", "<C-W>v", "Split Window Right", { remap = true })
|
nmap_leader("|", "<C-W>v", "Split Window Right", { remap = true })
|
||||||
|
|
|
||||||
55
lua/nvim_omp/init.lua
Normal file
55
lua/nvim_omp/init.lua
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
-- 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
|
||||||
|
|
@ -78,7 +78,10 @@ in {
|
||||||
lze
|
lze
|
||||||
lzextras
|
lzextras
|
||||||
plenary-nvim
|
plenary-nvim
|
||||||
|
vimtex
|
||||||
neogit
|
neogit
|
||||||
|
config.nvim-lib.neovimPlugins.bloocky
|
||||||
|
config.nvim-lib.neovimPlugins.dooing
|
||||||
{
|
{
|
||||||
data = mini-nvim;
|
data = mini-nvim;
|
||||||
pname = "mini.nvim";
|
pname = "mini.nvim";
|
||||||
|
|
@ -140,7 +143,6 @@ in {
|
||||||
data = with pkgs.vimPlugins; [
|
data = with pkgs.vimPlugins; [
|
||||||
quarto-nvim
|
quarto-nvim
|
||||||
render-markdown-nvim
|
render-markdown-nvim
|
||||||
vimtex
|
|
||||||
{
|
{
|
||||||
data = otter-nvim;
|
data = otter-nvim;
|
||||||
pname = "otter";
|
pname = "otter";
|
||||||
|
|
@ -163,10 +165,8 @@ in {
|
||||||
pname = "nvim-treesitter";
|
pname = "nvim-treesitter";
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
data = pkgs.codecompanion-nvim.overrideAttrs (old: {
|
data = pkgs.vimPlugins.opencode-nvim;
|
||||||
doCheck = false;
|
pname = "opencode.nvim";
|
||||||
});
|
|
||||||
pname = "codecompanion";
|
|
||||||
}
|
}
|
||||||
] ++ builtins.attrValues pkgs.vimPlugins.nvim-treesitter.queries;
|
] ++ builtins.attrValues pkgs.vimPlugins.nvim-treesitter.queries;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
13
nvim.log
Normal file
13
nvim.log
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
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
|
||||||
|
|
@ -1,22 +1,6 @@
|
||||||
{ ... }:
|
{ ... }:
|
||||||
final: prev:
|
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 {
|
zk-nvim = prev.vimPlugins.zk-nvim.overrideAttrs {
|
||||||
nvimSkipModules = [
|
nvimSkipModules = [
|
||||||
"zk.pickers.fzf_lua"
|
"zk.pickers.fzf_lua"
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
};
|
};
|
||||||
in {
|
in {
|
||||||
inherit rpkgs;
|
inherit rpkgs;
|
||||||
baseRPackages = [rpkgs.nvimcom];
|
baseRPackages = [rpkgs.nvimcom rpkgs.rPackages.btw];
|
||||||
rWrapper = rpkgs.rWrapper.override {packages = [];};
|
rWrapper = rpkgs.rWrapper.override {packages = [];};
|
||||||
quarto = rpkgs.quarto.override {extraRPackages = [];};
|
quarto = rpkgs.quarto.override {extraRPackages = [];};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ local defaults = {
|
||||||
duckdb = "duckdb",
|
duckdb = "duckdb",
|
||||||
julia = "julia",
|
julia = "julia",
|
||||||
python = "ipython",
|
python = "ipython",
|
||||||
shell = "echo 'Hello " .. vim.env.USER .. "!'",
|
shell = "echo 'Hello " .. (vim.env.USER or "user") .. "!'",
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Registry of terminal commands
|
-- Registry of terminal commands
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ end)
|
||||||
-- Treesitter
|
-- Treesitter
|
||||||
|
|
||||||
now_if_args(function()
|
now_if_args(function()
|
||||||
vim.treesitter.language.register("markdown", { "markdown", "codecompanion", "rmd", "quarto" })
|
vim.treesitter.language.register("markdown", { "markdown", "rmd", "quarto" })
|
||||||
|
|
||||||
require 'treesitter-context'.setup {
|
require 'treesitter-context'.setup {
|
||||||
enable = true,
|
enable = true,
|
||||||
|
|
@ -216,7 +216,7 @@ now_if_args(function()
|
||||||
"julia", "rnoweb", "latex", "gitcommit", "gitignore",
|
"julia", "rnoweb", "latex", "gitcommit", "gitignore",
|
||||||
"git_config", "git_rebase", "diff", "dockerfile",
|
"git_config", "git_rebase", "diff", "dockerfile",
|
||||||
"make", "xml", "zig", "regex", "csv", "bash",
|
"make", "xml", "zig", "regex", "csv", "bash",
|
||||||
"markdown_inline", "quarto", "rmd", "codecompanion",
|
"markdown_inline", "quarto", "rmd",
|
||||||
}
|
}
|
||||||
local function start_treesitter(buf, filetype)
|
local function start_treesitter(buf, filetype)
|
||||||
local lang = vim.treesitter.language.get_lang(filetype) or filetype
|
local lang = vim.treesitter.language.get_lang(filetype) or filetype
|
||||||
|
|
|
||||||
|
|
@ -55,21 +55,24 @@ later(function()
|
||||||
ignore_exitcode = true,
|
ignore_exitcode = true,
|
||||||
parser = function(output, bufnr, linter_cwd)
|
parser = function(output, bufnr, linter_cwd)
|
||||||
local diagnostics = {}
|
local diagnostics = {}
|
||||||
-- Pattern: /path/file.R:10:5: style: Some message
|
-- 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,
|
||||||
|
}
|
||||||
for line in output:gmatch("[^\r\n]+") do
|
for line in output:gmatch("[^\r\n]+") do
|
||||||
local path, lnum, col, severity, message = line:match("^[^:]+:(%d+):(%d+):%s*(%w+):%s*(.+)$")
|
-- Capture the file path as well so lnum/col line up with the numbers.
|
||||||
if path then
|
local path, lnum, col, severity, message = line:match("^(.-):(%d+):(%d+):%s*(%w+):%s*(.+)$")
|
||||||
local severity_map = {
|
if path and lnum and col and severity then
|
||||||
style = vim.diagnostic.severity.INFO,
|
local line_num = tonumber(lnum)
|
||||||
warning = vim.diagnostic.severity.WARN,
|
local col_num = tonumber(col)
|
||||||
error = vim.diagnostic.severity.ERROR,
|
|
||||||
}
|
|
||||||
table.insert(diagnostics, {
|
table.insert(diagnostics, {
|
||||||
bufnr = bufnr,
|
bufnr = bufnr,
|
||||||
lnum = math.max(0, tonumber(lnum) - 1),
|
lnum = math.max(0, line_num - 1),
|
||||||
col = math.max(0, tonumber(col) - 1),
|
col = math.max(0, col_num - 1),
|
||||||
end_lnum = tonumber(lnum) - 1,
|
end_lnum = line_num - 1,
|
||||||
end_col = tonumber(col),
|
end_col = col_num,
|
||||||
severity = severity_map[severity:lower()] or vim.diagnostic.severity.WARN,
|
severity = severity_map[severity:lower()] or vim.diagnostic.severity.WARN,
|
||||||
message = message or "lintr issue",
|
message = message or "lintr issue",
|
||||||
source = "lintr",
|
source = "lintr",
|
||||||
|
|
@ -99,7 +102,7 @@ now_if_args(function()
|
||||||
add("render-markdown.nvim")
|
add("render-markdown.nvim")
|
||||||
require('render-markdown').setup({
|
require('render-markdown').setup({
|
||||||
-- completions = { blink = { enabled = true } },
|
-- completions = { blink = { enabled = true } },
|
||||||
file_types = { 'markdown', 'codecompanion', },
|
file_types = { 'markdown', },
|
||||||
link = {
|
link = {
|
||||||
wiki = {
|
wiki = {
|
||||||
body = function(ctx)
|
body = function(ctx)
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,12 @@ later(function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
later(function()
|
later(function()
|
||||||
require("mini.align").setup()
|
require("mini.align").setup({
|
||||||
|
mappings = {
|
||||||
|
start = "gA",
|
||||||
|
start_with_preview = "g<C-A>",
|
||||||
|
},
|
||||||
|
})
|
||||||
end)
|
end)
|
||||||
|
|
||||||
later(function()
|
later(function()
|
||||||
|
|
@ -213,6 +218,8 @@ later(function()
|
||||||
local minikeymap = require("mini.keymap")
|
local minikeymap = require("mini.keymap")
|
||||||
minikeymap.setup()
|
minikeymap.setup()
|
||||||
local map_multistep = minikeymap.map_multistep
|
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 = {
|
local tab_steps = {
|
||||||
"blink_next",
|
"blink_next",
|
||||||
"pmenu_next",
|
"pmenu_next",
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,6 @@ local later = MiniDeps.later
|
||||||
local now = MiniDeps.now
|
local now = MiniDeps.now
|
||||||
local now_if_args = Config.now_if_args
|
local now_if_args = Config.now_if_args
|
||||||
|
|
||||||
-- Constants
|
|
||||||
local BLINK_VERSION = "v1.10.2"
|
|
||||||
|
|
||||||
-- Plugin sources configuration
|
-- Plugin sources configuration
|
||||||
local PLUGIN_SOURCES = {
|
local PLUGIN_SOURCES = {
|
||||||
"hrsh7th/cmp-cmdline",
|
"hrsh7th/cmp-cmdline",
|
||||||
|
|
@ -14,7 +11,7 @@ local PLUGIN_SOURCES = {
|
||||||
"zbirenbaum/copilot.lua",
|
"zbirenbaum/copilot.lua",
|
||||||
"jmbuhr/cmp-pandoc-references",
|
"jmbuhr/cmp-pandoc-references",
|
||||||
"fang2hou/blink-copilot",
|
"fang2hou/blink-copilot",
|
||||||
"olimorris/codecompanion.nvim"
|
"nickjvandyke/opencode.nvim",
|
||||||
}
|
}
|
||||||
|
|
||||||
local PLUGIN_ADDS = {
|
local PLUGIN_ADDS = {
|
||||||
|
|
@ -24,34 +21,16 @@ local PLUGIN_ADDS = {
|
||||||
"cmp-pandoc-references",
|
"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 function get_mini_icons_highlight(ctx)
|
||||||
local _, hl, _ = require("mini.icons").get("lsp", ctx.kind)
|
local _, hl, _ = require("mini.icons").get("lsp", ctx.kind)
|
||||||
return hl
|
return hl
|
||||||
end
|
end
|
||||||
|
|
||||||
local function get_blink_fuzzy_setting()
|
local function get_blink_fuzzy_setting()
|
||||||
local setting = {
|
return {
|
||||||
sorts = { "exact", "score", "sort_text" }
|
sorts = { "exact", "score", "sort_text" },
|
||||||
|
use_proximity = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
if not Config.isNixCats then
|
|
||||||
setting.prebuilt_binaries = { force_version = BLINK_VERSION }
|
|
||||||
end
|
|
||||||
|
|
||||||
return setting
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Plugin loading
|
-- Plugin loading
|
||||||
|
|
@ -62,7 +41,6 @@ if not Config.isNixCats then
|
||||||
add({
|
add({
|
||||||
source = "saghen/blink.cmp",
|
source = "saghen/blink.cmp",
|
||||||
depends = { "rafamadriz/friendly-snippets" },
|
depends = { "rafamadriz/friendly-snippets" },
|
||||||
checkout = BLINK_VERSION,
|
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -73,241 +51,6 @@ if not Config.isNixCats then
|
||||||
end)
|
end)
|
||||||
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
|
-- Batch add simple plugins
|
||||||
later(function()
|
later(function()
|
||||||
for _, plugin in ipairs(PLUGIN_ADDS) do
|
for _, plugin in ipairs(PLUGIN_ADDS) do
|
||||||
|
|
@ -359,26 +102,33 @@ later(function()
|
||||||
})
|
})
|
||||||
end)
|
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()
|
later(function()
|
||||||
add("codecompanion.nvim")
|
vim.g.opencode_opts = {
|
||||||
|
events = {
|
||||||
-- now use function
|
reload = true,
|
||||||
require("codecompanion").setup(get_codecompanion_config())
|
permissions = {
|
||||||
vim.cmd([[cab cc CodeCompanion]])
|
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")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
now_if_args(function()
|
now_if_args(function()
|
||||||
add("blink.cmp")
|
add("blink.cmp")
|
||||||
|
|
||||||
require("blink.cmp").setup({
|
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 = {
|
keymap = {
|
||||||
preset = "default",
|
preset = "default",
|
||||||
["<C-space>"] = { "show", "select_next" },
|
["<C-space>"] = { "show", "select_next" },
|
||||||
|
|
@ -415,6 +165,7 @@ now_if_args(function()
|
||||||
},
|
},
|
||||||
completion = {
|
completion = {
|
||||||
menu = {
|
menu = {
|
||||||
|
border = "rounded",
|
||||||
draw = {
|
draw = {
|
||||||
treesitter = { "lsp" },
|
treesitter = { "lsp" },
|
||||||
components = {
|
components = {
|
||||||
|
|
@ -434,14 +185,12 @@ now_if_args(function()
|
||||||
list = {
|
list = {
|
||||||
selection = { preselect = false, auto_insert = true }
|
selection = { preselect = false, auto_insert = true }
|
||||||
},
|
},
|
||||||
documentation = { auto_show = true },
|
ghost_text = { enabled = true, show_with_menu = true },
|
||||||
|
documentation = { auto_show = true, window = { border = "rounded" } },
|
||||||
trigger = { show_in_snippet = false },
|
trigger = { show_in_snippet = false },
|
||||||
},
|
},
|
||||||
snippets = { preset = "mini_snippets" },
|
snippets = { preset = "mini_snippets" },
|
||||||
sources = {
|
sources = {
|
||||||
per_filetype = {
|
|
||||||
codecompanion = { "codecompanion" },
|
|
||||||
},
|
|
||||||
default = { "references", "lsp", "path", "snippets", "buffer", "omni", "copilot" },
|
default = { "references", "lsp", "path", "snippets", "buffer", "omni", "copilot" },
|
||||||
providers = {
|
providers = {
|
||||||
path = {
|
path = {
|
||||||
|
|
@ -467,12 +216,6 @@ now_if_args(function()
|
||||||
score_offset = 45,
|
score_offset = 45,
|
||||||
async = true,
|
async = true,
|
||||||
},
|
},
|
||||||
codecompanion = {
|
|
||||||
name = "CodeCompanion",
|
|
||||||
module = "codecompanion.providers.completion.blink",
|
|
||||||
score_offset = 45,
|
|
||||||
async = true,
|
|
||||||
},
|
|
||||||
references = {
|
references = {
|
||||||
name = "pandoc_references",
|
name = "pandoc_references",
|
||||||
module = "cmp-pandoc-references.blink",
|
module = "cmp-pandoc-references.blink",
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ now_if_args(function()
|
||||||
basedpyright = {},
|
basedpyright = {},
|
||||||
ruff = {},
|
ruff = {},
|
||||||
marksman = {
|
marksman = {
|
||||||
filetypes = { "markdown", "markdown_inline", "codecompanion" },
|
filetypes = { "markdown", "markdown_inline" },
|
||||||
},
|
},
|
||||||
harper_ls = {
|
harper_ls = {
|
||||||
cmd = { "harper-ls", "--stdio" },
|
cmd = { "harper-ls", "--stdio" },
|
||||||
|
|
|
||||||
|
|
@ -11,24 +11,19 @@ later(function()
|
||||||
return
|
return
|
||||||
end
|
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")
|
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
|
-- Filetype detection is normally on, but force it explicitly so .tex files
|
||||||
-- opened outside Quarto still pick up the LSP + viewer hooks.
|
-- opened outside Quarto still pick up the LSP + viewer hooks.
|
||||||
vim.api.nvim_create_autocmd("BufReadPost", {
|
vim.api.nvim_create_autocmd("BufReadPost", {
|
||||||
|
|
|
||||||
43
plugin/29_bloocky.lua
Normal file
43
plugin/29_bloocky.lua
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
-- 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)
|
||||||
44
plugin/30_dooing.lua
Normal file
44
plugin/30_dooing.lua
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
-- 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)
|
||||||
8
plugin/31_nvim_omp.lua
Normal file
8
plugin/31_nvim_omp.lua
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
-- 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
|
||||||
268
tests/smoke.lua
Normal file
268
tests/smoke.lua
Normal file
|
|
@ -0,0 +1,268 @@
|
||||||
|
-- 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
|
||||||
Loading…
Reference in a new issue