diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index e96a388..fcf1db9 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -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 }}' diff --git a/.github/workflows/flakehub-publish-rolling.yml b/.github/workflows/flakehub-publish-rolling.yml index 50ccb47..57250d0 100644 --- a/.github/workflows/flakehub-publish-rolling.yml +++ b/.github/workflows/flakehub-publish-rolling.yml @@ -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" diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml index 3bd7493..a65fbdc 100644 --- a/.github/workflows/update.yml +++ b/.github/workflows/update.yml @@ -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 }}' diff --git a/.letta/settings.local.json b/.letta/settings.local.json deleted file mode 100644 index 7656883..0000000 --- a/.letta/settings.local.json +++ /dev/null @@ -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" - } -} \ No newline at end of file diff --git a/.omp/tools/nvim_buffers.mjs b/.omp/tools/nvim_buffers.mjs deleted file mode 100644 index 82e9ce6..0000000 --- a/.omp/tools/nvim_buffers.mjs +++ /dev/null @@ -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 /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; -} \ No newline at end of file diff --git a/flake.lock b/flake.lock index 195ab40..17fe244 100644 --- a/flake.lock +++ b/flake.lock @@ -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" diff --git a/flake.nix b/flake.nix index 00ad477..db14f58 100644 --- a/flake.nix +++ b/flake.nix @@ -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 - ''; } ); diff --git a/lua/keymap/leader.lua b/lua/keymap/leader.lua index e741c81..795079f 100644 --- a/lua/keymap/leader.lua +++ b/lua/keymap/leader.lua @@ -11,39 +11,28 @@ nmap_leader('', 'bnext', 'Next buffer') nmap_leader('', 'bprev', '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", "CodeCompanion /agent", "Agent chat (@{agent} tools)") +nmap_leader("ac", "CodeCompanionChat Toggle", "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", "CodeCompanion /commit", "Generate commit message") +nmap_leader("ai", "CodeCompanionActions", "Chat Action") +nmap_leader("al", "CodeCompanion /lsp", "Explain LSP Diagnostics") +nmap_leader("an", "CodeCompanionChat Add", "Chat New") +nmap_leader("as", "CodeCompanion /suggest", "Suggest Improvements") +nmap_leader("aw", "CodeCompanion /tdd", "Workflow: plan, implement, test") +nmap_leader("ax", "CodeCompanion /fixer", "Code Fixer") +xmap_leader("aa", "CodeCompanion /agent", "Agent on selection") +xmap_leader("ae", "CodeCompanion /explain", "Explain Code") +xmap_leader("af", "CodeCompanion /fix", "Fix Code") +xmap_leader("ap", "CodeCompanion /expert", "Code Expert") +xmap_leader("as", "CodeCompanion /suggest", "Suggest Improvements") +nmap_leader("ak", "CodeCompanionChat adapter=codex", "Chat with Codex") -- b is for 'buffer' nmap_leader('bb', 'b#', 'Alternate') @@ -225,24 +214,6 @@ nmap_leader("wh", "h", "Go to Left Window", { remap = true }) nmap_leader("wj", "j", "Go to Lower Window", { remap = true }) nmap_leader("wk", "k", "Go to Upper Window", { remap = true }) nmap_leader("wl", "l", "Go to Right Window", { remap = true }) -nmap_leader("w>", "vertical resize +5", "Increase Window Width") -nmap_leader("w", "vertical resize -5", "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("_", "s", "Split Window Below", { remap = true }) nmap_leader("|", "v", "Split Window Right", { remap = true }) diff --git a/lua/nvim_omp/init.lua b/lua/nvim_omp/init.lua deleted file mode 100644 index 4400d49..0000000 --- a/lua/nvim_omp/init.lua +++ /dev/null @@ -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 /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 --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 diff --git a/modules/module/specs/plugins.nix b/modules/module/specs/plugins.nix index babb1ba..7630632 100644 --- a/modules/module/specs/plugins.nix +++ b/modules/module/specs/plugins.nix @@ -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; }; diff --git a/nvim.log b/nvim.log deleted file mode 100644 index 4eb4ddf..0000000 --- a/nvim.log +++ /dev/null @@ -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 diff --git a/overlays/plugins.nix b/overlays/plugins.nix index 17d7c5f..91c01aa 100644 --- a/overlays/plugins.nix +++ b/overlays/plugins.nix @@ -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" diff --git a/overlays/r.nix b/overlays/r.nix index f03d07e..e736b77 100644 --- a/overlays/r.nix +++ b/overlays/r.nix @@ -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 = [];}; } diff --git a/plugin/03_terminal.lua b/plugin/03_terminal.lua index 43a53a8..95d7a29 100644 --- a/plugin/03_terminal.lua +++ b/plugin/03_terminal.lua @@ -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 diff --git a/plugin/20_startup.lua b/plugin/20_startup.lua index f5cc9e8..36bab13 100644 --- a/plugin/20_startup.lua +++ b/plugin/20_startup.lua @@ -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 diff --git a/plugin/22_languages.lua b/plugin/22_languages.lua index 3a6282e..cd5ebed 100644 --- a/plugin/22_languages.lua +++ b/plugin/22_languages.lua @@ -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) diff --git a/plugin/23_editor.lua b/plugin/23_editor.lua index 362333d..4345e2d 100644 --- a/plugin/23_editor.lua +++ b/plugin/23_editor.lua @@ -37,12 +37,7 @@ later(function() end) later(function() - require("mini.align").setup({ - mappings = { - start = "gA", - start_with_preview = "g", - }, - }) + 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", diff --git a/plugin/24_completion.lua b/plugin/24_completion.lua index 9e70b27..c2291c7 100644 --- a/plugin/24_completion.lua +++ b/plugin/24_completion.lua @@ -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", [""] = { "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", diff --git a/plugin/25_lsp.lua b/plugin/25_lsp.lua index 6a719ca..5250cdd 100644 --- a/plugin/25_lsp.lua +++ b/plugin/25_lsp.lua @@ -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" }, diff --git a/plugin/28_latex.lua b/plugin/28_latex.lua index 3b24f9e..b77b201 100644 --- a/plugin/28_latex.lua +++ b/plugin/28_latex.lua @@ -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 group; + -- vimtex stashes its own 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", { diff --git a/plugin/29_bloocky.lua b/plugin/29_bloocky.lua deleted file mode 100644 index 5e6a3ea..0000000 --- a/plugin/29_bloocky.lua +++ /dev/null @@ -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 tb, which collides with the terminal map --- (toggle bracketed paste), so it is moved to the calendar/tasks group 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 = 'cb', - }, - }) -end) diff --git a/plugin/30_dooing.lua b/plugin/30_dooing.lua deleted file mode 100644 index f47ecc2..0000000 --- a/plugin/30_dooing.lua +++ /dev/null @@ -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 td / tN occupy the terminal group, so the --- todo toggles are moved to the calendar/tasks group 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 = "cd", - open_project_todo = "cD", - show_due_notification = "cN", - create_nested_task = "cn", -- Create nested subtask under current todo - toggle_priority = "a", - }, - calendar = { - week_start_day = "monday", - }, - }) - -- Remove Dooing's old defaults. - vim.keymap.del("n", "td") - vim.keymap.del("n", "tD") - vim.keymap.del("n", "tN") - - -- Restore mappings overwritten by Dooing. - local helpers = require('keymap.helpers') - local nmap_leader = helpers.nmap_leader - -- t is for 'terminal' - nmap_leader("td", 'lua Config.terminal.open_duckdb();Config.terminal.toggle_bracket()', 'Open DuckDB') -end) diff --git a/plugin/31_nvim_omp.lua b/plugin/31_nvim_omp.lua deleted file mode 100644 index 44076cf..0000000 --- a/plugin/31_nvim_omp.lua +++ /dev/null @@ -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 \ No newline at end of file diff --git a/tests/smoke.lua b/tests/smoke.lua deleted file mode 100644 index 2e4112e..0000000 --- a/tests/smoke.lua +++ /dev/null @@ -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) - -- is stored as the literal leader key, so test both the raw - -- symbolic form and the expanded form (e.g. "ff" and " ff"). - local expanded = lhs:gsub('^', 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('ff'), "leader ff -> pick files") - assert_true(has_normal_map('fg'), "leader fg -> live grep") - assert_true(has_normal_map('bb'), "leader bb -> alternate buffer") - assert_true(has_normal_map('ed'), "leader ed -> mini.files open") - assert_true(has_normal_map(''), "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