mirror of
https://github.com/dwinkler1/nvimConfig.git
synced 2026-08-22 17:43:13 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93b88ee019 | ||
| b630932348 | |||
| c7543c978a |
5 changed files with 282 additions and 18 deletions
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;
|
||||||
|
}
|
||||||
24
flake.lock
generated
24
flake.lock
generated
|
|
@ -22,11 +22,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1785967620,
|
"lastModified": 1786106723,
|
||||||
"narHash": "sha256-IItrdb7Puk05RqOBWZYFC5X6Wl1sJmCfh5MWVHw5iMM=",
|
"narHash": "sha256-zDSUbpoeo/9ZmD2+wXnzxoo1+uhL8vxc0b8yuYMKYq0=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "b7c2ada94fe99c15b0dbcf4d11fd7850b957a436",
|
"rev": "f13ff45afd1bb73e640eaa08a7066dbed07e3238",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -39,11 +39,11 @@
|
||||||
"plugins-bloocky": {
|
"plugins-bloocky": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1786042339,
|
"lastModified": 1786129261,
|
||||||
"narHash": "sha256-8QcoC1bS8pRcwBkW3mqD2vgP9Mu0aCdIy6umZO6mUGM=",
|
"narHash": "sha256-u/s7wCZBZYIax5V/xKEqfs4/y3uCGnaLWY6CkUQRyTk=",
|
||||||
"owner": "atiladefreitas",
|
"owner": "atiladefreitas",
|
||||||
"repo": "bloocky",
|
"repo": "bloocky",
|
||||||
"rev": "a052c3b1a8126e04b194bb3db3c4c6ca641c0e4e",
|
"rev": "2f493d637bd9f385b812d49beb6a93564051bb77",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -71,11 +71,11 @@
|
||||||
"plugins-dooing": {
|
"plugins-dooing": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1785971574,
|
"lastModified": 1786125118,
|
||||||
"narHash": "sha256-GtEC5kWcH0DmiYXkzxzNjjfYLfyEb7zpTLQWdi7n3zs=",
|
"narHash": "sha256-Iil8eCCz61T1Hz1y2cij71Hti+QGXbjH49eGCPMqTbA=",
|
||||||
"owner": "atiladefreitas",
|
"owner": "atiladefreitas",
|
||||||
"repo": "dooing",
|
"repo": "dooing",
|
||||||
"rev": "6748316bc6b4601797fb952a83694cc0a5ef6da2",
|
"rev": "2871aaf400c49187aa4216e7cf7abfa37cd32840",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -108,11 +108,11 @@
|
||||||
},
|
},
|
||||||
"rixpkgs": {
|
"rixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1785776890,
|
"lastModified": 1786421894,
|
||||||
"narHash": "sha256-CzzgB1teVbroR/QhGAnsQToUYCDXEZZtFTcISAooASM=",
|
"narHash": "sha256-BQLZ0kJYdZ3FmC2Vq7PJFmx4wnyifoCdz+5CIkdbDxI=",
|
||||||
"owner": "dwinkler1",
|
"owner": "dwinkler1",
|
||||||
"repo": "rixpkgs",
|
"repo": "rixpkgs",
|
||||||
"rev": "a54080a58b57d5785dc85901850b817bef2aaf83",
|
"rev": "9518d16a41413cf08eecde557b337b87f1422e91",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
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
|
||||||
|
|
@ -13,16 +13,32 @@ end
|
||||||
|
|
||||||
local nix = require('config.nix')
|
local nix = require('config.nix')
|
||||||
local later = MiniDeps.later
|
local later = MiniDeps.later
|
||||||
|
local now = MiniDeps.now
|
||||||
later(function()
|
later(function()
|
||||||
if not nix.get_cat('general', false) then
|
if not nix.get_cat('general', false) then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
require('dooing').setup({
|
local dooing = require('dooing')
|
||||||
|
require("dooing").setup({
|
||||||
keymaps = {
|
keymaps = {
|
||||||
toggle_window = '<leader>cd',
|
toggle_window = "<leader>cd",
|
||||||
open_project_todo = '<leader>cD',
|
open_project_todo = "<leader>cD",
|
||||||
show_due_notification = '<leader>cN',
|
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)
|
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
|
||||||
Loading…
Reference in a new issue