339 lines
16 KiB
Lua
Executable File
339 lines
16 KiB
Lua
Executable File
-- .d8888b. 888888 888 888
|
|
-- d88P Y88b "88b 888 888
|
|
-- 888 888 888 888 888
|
|
-- 888 .d88b. 88888b. 88888b. .d88b. 888d888 888 .d88b. 88888b. 88888b. .d8888b 888888 .d88b. 88888b. .d88b.
|
|
-- 888 d88''88b 888 '88b 888 '88b d88''88b 888P" 888 d88''88b 888 '88b 888 '88b 88K 888 d88''88b 888 '88b d8P Y8b
|
|
-- 888 888 888 888 888 888 888 888 888 888 888 888 888 888 888 888 888 888 'Y8888b. 888 888 888 888 888 88888888
|
|
-- Y88b d88P Y88..88P 888 888 888 888 Y88..88P 888 88P Y88..88P 888 888 888 888 X88 Y88b. Y88..88P 888 888 Y8b.
|
|
-- 'Y8888P" 'Y88P' 888 888 888 888 'Y88P' 888 888 'Y88P' 888 888 888 888 88888P' 'Y888 'Y88P' 888 888 'Y8888
|
|
-- .d88P
|
|
-- .d88P"
|
|
-- 888P"
|
|
--
|
|
|
|
-- ------------------------------------------------------------------------------
|
|
-- General Settings
|
|
-- ------------------------------------------------------------------------------
|
|
|
|
vim.opt.number = true
|
|
vim.opt.relativenumber = true
|
|
vim.opt.tabstop = 4
|
|
vim.opt.softtabstop = 4
|
|
vim.opt.shiftwidth = 4
|
|
vim.opt.expandtab = true
|
|
vim.opt.splitbelow = true
|
|
vim.opt.splitright = true
|
|
vim.opt.scrolloff = 5
|
|
vim.opt.sidescrolloff = 5
|
|
-- vim.opt.cmdheight = 2
|
|
vim.opt.diffopt = "vertical,closeoff,context:6"
|
|
vim.opt.clipboard = "unnamedplus"
|
|
vim.opt.mouse = "nv"
|
|
vim.opt.ignorecase = true
|
|
vim.opt.smartcase = true
|
|
vim.opt.spell = false
|
|
vim.opt.list = true
|
|
vim.opt.joinspaces = false
|
|
vim.opt.breakindent = true
|
|
vim.opt.undofile = true
|
|
vim.opt.completeopt = "menuone,noselect"
|
|
vim.opt.termguicolors = true
|
|
vim.opt.guifont = "RobotoMono Nerd Font:h11"
|
|
vim.opt.signcolumn = "yes"
|
|
vim.opt.winborder = "rounded"
|
|
vim.g.mapleader = " "
|
|
vim.g.maplocalleader = ","
|
|
vim.g.leader = " "
|
|
|
|
-- ------------------------------------------------------------------------------
|
|
-- PlugIns
|
|
-- ------------------------------------------------------------------------------
|
|
|
|
vim.pack.add({
|
|
{ src = "https://github.com/vague2k/vague.nvim" },
|
|
{ src = "https://github.com/mason-org/mason.nvim" },
|
|
{ src = "https://github.com/neovim/nvim-lspconfig" },
|
|
{ src = "https://github.com/stevearc/oil.nvim" },
|
|
{ src = "https://github.com/nvim-telescope/telescope.nvim" },
|
|
{ src = "https://github.com/nvim-lua/plenary.nvim" },
|
|
{ src = "https://github.com/kdheepak/lazygit.nvim" },
|
|
{ src = "https://github.com/tpope/vim-surround" },
|
|
{ src = "https://github.com/tpope/vim-repeat" },
|
|
{ src = "https://github.com/tommcdo/vim-lion" },
|
|
{ src = "https://github.com/andythigpen/nvim-coverage" },
|
|
{ src = "https://github.com/nvim-treesitter/nvim-treesitter" },
|
|
-- copilot deliberately off on every machine
|
|
-- { src = "https://github.com/github/copilot.vim" },
|
|
-- ...and this is what replaced it: FIM completion off the local box.
|
|
{ src = "https://github.com/milanglacier/minuet-ai.nvim" },
|
|
})
|
|
|
|
require("mason").setup()
|
|
require("oil").setup()
|
|
require("telescope").setup({ pickers = { colorscheme = { enable_preview = true } } })
|
|
require("coverage").setup({
|
|
auto_reload = true,
|
|
signs = {
|
|
covered = { text = "█", texthl = "CoverageCovered" },
|
|
uncovered = { text = "█", texthl = "CoverageUncovered" },
|
|
partial = { text = "█", texthl = "CoveragePartial" },
|
|
},
|
|
})
|
|
require("nvim-treesitter").setup( {
|
|
ensure_installed = { "lua", "python", "rust", "markdown", "markdown_inline", "julia" },
|
|
highlight = { enable = true },
|
|
})
|
|
|
|
vim.lsp.enable({ "lua_ls", "rust_analyzer", "julials", "ty", "marksman", "ruff", "clangd" })
|
|
vim.lsp.config("rust_analyzer", {
|
|
settings = {
|
|
["rust-analyzer"] = {
|
|
cargo = { features = "all" },
|
|
}
|
|
}
|
|
})
|
|
|
|
-- ------------------------------------------------------------------------------
|
|
-- Local FIM autocomplete (minuet-ai -> LiteLLM -> llama-swap -> qwen3-coder-30b)
|
|
-- ------------------------------------------------------------------------------
|
|
--
|
|
-- Replaces copilot, which is off on every machine deliberately. Nothing leaves
|
|
-- the LAN: the model runs on mainframe's 3060 and `llm.rcjohnstone.com` has no
|
|
-- public A record -- internal CA, and Caddy source-IP gates it to 10.0.0.0/24,
|
|
-- 10.0.1.0/24 and the podman subnets.
|
|
--
|
|
-- ⚠ ROUTE: `/v1/completions`, NOT `/infill`. Measured 2026-09-18 through the
|
|
-- proxy: `/v1/completions` 200 with FIM tokens in a raw prompt, `/infill` 404
|
|
-- because LiteLLM does not expose it. `llama.vim` wants `/infill`, which would
|
|
-- mean talking to llama-server directly and bypassing the virtual key -- and on
|
|
-- the work laptop that key IS the policy boundary that makes Anthropic and PRC
|
|
-- models 403. Not worth routing around for an editor plugin.
|
|
--
|
|
-- ⚠ WHY qwen3-coder-30b: it is the best local FIM model by measurement (0.511
|
|
-- exact / 0.826 similarity against qwen3-next-80b's 0.302), and FIM is a
|
|
-- VOCABULARY feature -- gpt-oss-20b and gemma3-12b have no FIM tokens and
|
|
-- cannot do this at all. Its "skipped for capacity" verdict was about the 128k
|
|
-- agentic floor and does not bind here; autocomplete needs a few thousand
|
|
-- tokens of context, not 128k.
|
|
--
|
|
-- ⚠ EXPECT AN OCCASIONAL MULTI-SECOND PAUSE. llama-swap runs `-np 1` with a
|
|
-- ttl, so if another model was used in between, the next completion pays a
|
|
-- full reload. Measured: ~10 s cold, 0.7-1.3 s warm. Accepted deliberately --
|
|
-- concurrent agentic work is not viable on this hardware anyway.
|
|
-- ⚠ ALWAYS call setup(). The first version of this guarded the whole require()
|
|
-- behind "is the key present", which meant that with no key the module never
|
|
-- loaded, `:Minuet` was never registered, and nvim gave NO indication why --
|
|
-- the exact failure it hit in practice. `:Minuet` is created at module load,
|
|
-- not inside setup(), and the plugin already notifies clearly when the key is
|
|
-- missing. Let it do that job instead of silently doing nothing.
|
|
--
|
|
-- The key resolves lazily and is cached: env var first, then rbw, matching the
|
|
-- opencode convention of reading keys at launch rather than writing them to
|
|
-- disk. `false` is cached for "looked and found nothing" so a locked rbw costs
|
|
-- one probe per session rather than one per keystroke.
|
|
local _minuet_key
|
|
local function minuet_key()
|
|
if _minuet_key ~= nil then
|
|
return _minuet_key or nil
|
|
end
|
|
local k = vim.env.LITELLM_KEY_NVIM
|
|
if k and k ~= "" then
|
|
_minuet_key = k
|
|
return k
|
|
end
|
|
if vim.fn.executable("rbw") == 1 then
|
|
vim.fn.system({ "rbw", "unlocked" })
|
|
if vim.v.shell_error == 0 then
|
|
-- ⚠ The work laptop MUST use the work key: it is enforced
|
|
-- server-side to 403 Anthropic and PRC-origin models, and a
|
|
-- personal key here would silently remove that guarantee for
|
|
-- everything nvim sends.
|
|
local host = vim.uv.os_gethostname() or ""
|
|
local entry = host:match("sctfw004") and "litellm-key-work"
|
|
or "litellm-key-personal"
|
|
local out = vim.fn.system({ "rbw", "get", entry })
|
|
if vim.v.shell_error == 0 and vim.trim(out) ~= "" then
|
|
_minuet_key = vim.trim(out)
|
|
return _minuet_key
|
|
end
|
|
end
|
|
end
|
|
_minuet_key = false
|
|
return nil
|
|
end
|
|
|
|
local minuet_base = vim.env.LITELLM_BASE
|
|
if not minuet_base or minuet_base == "" then
|
|
minuet_base = "https://llm.rcjohnstone.com"
|
|
end
|
|
|
|
-- ⚠ pcall, because a hard require here takes the WHOLE config down with it.
|
|
-- `vim.pack.add` installs on first launch, and that needs to reach github --
|
|
-- which is not a given on a work-issued machine behind a proxy. Without this,
|
|
-- a failed install means no MinuetStatus, no colorscheme, no anything below
|
|
-- this line, and an error that points at minuet rather than at the network.
|
|
local minuet_ok, minuet = pcall(require, "minuet")
|
|
if minuet_ok then
|
|
minuet.setup({
|
|
provider = "openai_fim_compatible",
|
|
-- Default is 'warn', which hides request failures. If completions stop
|
|
-- appearing, set this to 'debug' and the reason shows up in `:messages`.
|
|
notify = "warn",
|
|
-- Default 3. Measured 2026-10-22 at -ncmoe 22: a fresh ~3k-token context
|
|
-- takes 4.8 s to prefill, the same context again 0.8 s. At 3 s the first
|
|
-- completion in a file times out, and it may not leave a cache behind.
|
|
request_timeout = 8,
|
|
-- Ghost text: there is no completion engine here (no nvim-cmp, no blink)
|
|
-- and virtual text is the copilot-shaped one.
|
|
virtualtext = {
|
|
auto_trigger_ft = { "python", "rust", "lua", "c", "cpp", "julia", "sh" },
|
|
keymap = {
|
|
accept = "<A-a>",
|
|
accept_line = "<A-l>",
|
|
prev = "<A-[>",
|
|
next = "<A-]>",
|
|
dismiss = "<A-e>",
|
|
},
|
|
},
|
|
provider_options = {
|
|
openai_fim_compatible = {
|
|
-- A FUNCTION, not an env var name: this is what lets the key come
|
|
-- from rbw when the shell did not export one.
|
|
api_key = minuet_key,
|
|
name = "litellm",
|
|
end_point = minuet_base .. "/v1/completions",
|
|
model = "qwen3-coder-30b",
|
|
-- ⚠ KEEP THIS SMALL. Measured on the real path at 48: `def add`
|
|
-- came back with `sub` and `mul` attached, so most of the latency
|
|
-- was spent generating text that would be discarded. Raise it only
|
|
-- if completions are being truncated mid-expression.
|
|
optional = {
|
|
max_tokens = 32,
|
|
top_p = 0.9,
|
|
stop = { "<|fim_pad|>", "<|endoftext|>", "\n\n" },
|
|
},
|
|
-- ⚠ THREE POSITIONAL ARGS, not a context table. minuet calls
|
|
-- this as `template.prompt(before, after, opts)` -- the first
|
|
-- version took `(ctx)` and died with "attempt to concatenate
|
|
-- field 'lines_after' (a nil value)" on the first real
|
|
-- completion, while loading perfectly cleanly. A config that
|
|
-- loads is not a config that works.
|
|
--
|
|
-- `suffix = false` because the whole FIM prompt is built here;
|
|
-- letting minuet also send a `suffix` field would give the server
|
|
-- two competing descriptions of the same gap.
|
|
template = {
|
|
prompt = function(before, after, _opts)
|
|
return "<|fim_prefix|>" .. (before or "")
|
|
.. "<|fim_suffix|>" .. (after or "")
|
|
.. "<|fim_middle|>"
|
|
end,
|
|
suffix = false,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
end
|
|
|
|
-- Ghost text, dimmer than a comment.
|
|
--
|
|
-- minuet links MinuetVirtualText -> Comment, which in `vague` is #606079 --
|
|
-- the same colour as real comments, so a suggestion reads as if it were
|
|
-- already part of the file. This drops it toward the background.
|
|
--
|
|
-- Reference points in this colourscheme, for tuning:
|
|
-- Normal #cdcdcd (actual code)
|
|
-- Comment #606079 (minuet's default -- too prominent)
|
|
-- MINUET #4a4a5c <- here
|
|
-- Whitespace #333738 (about as dim as stays legible)
|
|
--
|
|
-- ⚠ MUST be re-applied on ColorScheme. Loading a colourscheme runs
|
|
-- `highlight clear`, which drops custom groups -- so setting this once at
|
|
-- startup works until the first `:colorscheme` and then silently stops.
|
|
local MINUET_FG = "#4a4a5c"
|
|
local function minuet_highlight()
|
|
vim.api.nvim_set_hl(0, "MinuetVirtualText", { fg = MINUET_FG, italic = true })
|
|
end
|
|
vim.api.nvim_create_autocmd("ColorScheme", {
|
|
group = vim.api.nvim_create_augroup("MinuetHighlight", { clear = true }),
|
|
callback = minuet_highlight,
|
|
})
|
|
minuet_highlight()
|
|
|
|
-- Registered UNCONDITIONALLY, so it can report the case where the plugin
|
|
-- itself is missing. A status command that only exists when everything
|
|
-- already works is not a status command.
|
|
vim.api.nvim_create_user_command("MinuetStatus", function()
|
|
local k = minuet_key()
|
|
local lines = {
|
|
"plugin: " .. (minuet_ok and "loaded" or "NOT INSTALLED -- run :lua vim.pack.update()"),
|
|
"endpoint: " .. minuet_base .. "/v1/completions",
|
|
"model: qwen3-coder-30b",
|
|
"key: " .. (k and ("found (" .. #k .. " chars)") or "NOT FOUND"),
|
|
}
|
|
if not k then
|
|
table.insert(lines, "")
|
|
table.insert(lines, "Set LITELLM_KEY_NVIM in ~/.secrets.zsh, or unlock rbw")
|
|
table.insert(lines, "(`rbw unlock`) so it can be read from there.")
|
|
end
|
|
vim.notify(table.concat(lines, "\n"), k and vim.log.levels.INFO or vim.log.levels.WARN)
|
|
end, { desc = "Show minuet FIM autocomplete status" })
|
|
|
|
vim.cmd("colorscheme vague")
|
|
|
|
-- KEYMAPS
|
|
-- -------------------------------------------------------------------------------
|
|
|
|
local builtin = require('telescope.builtin')
|
|
vim.keymap.set("n", "<leader>f", vim.lsp.buf.format, { desc = "[f]ormat the current file" })
|
|
vim.keymap.set("n", "<leader>sr", builtin.oldfiles, { desc = "[s]earch [r]ecent files" })
|
|
vim.keymap.set("n", "<leader>sf", builtin.find_files, { desc = "[s]earch files" })
|
|
vim.keymap.set("n", "<leader>sw", builtin.grep_string, { desc = "[s]earch [w]ord with grep" })
|
|
vim.keymap.set("n", "<leader>sg", builtin.live_grep, { desc = "[s]earch with [g]rep" })
|
|
vim.keymap.set('n', "<leader>sh", builtin.help_tags, { desc = "[s]earch [h]elp" })
|
|
vim.keymap.set('n', "<leader>sc", builtin.colorscheme, { desc = "[s]earch [c]olorschemes" })
|
|
vim.keymap.set('i', "jk", "<Esc>", { desc = "Escape, as in [j]ust [k]idding" })
|
|
vim.keymap.set('n', "<leader>o", ":Oil<CR>", { desc = "Run [O]il" })
|
|
vim.keymap.set('n', "<leader> ", ":nohlsearch<CR>", { desc = "Run [O]il" })
|
|
vim.keymap.set('n', "n", "nzz", { desc = "Move next search result to center of screen" })
|
|
vim.keymap.set('n', "N", "Nzz", { desc = "Move prev search result to center of screen" })
|
|
vim.keymap.set('n', "<leader>e", vim.diagnostic.open_float, { desc = "Open up the [e]rror" })
|
|
vim.keymap.set('n', "<leader>lg", ":LazyGitCurrentFile<CR>", { desc = "Open up [l]azy [g]it" })
|
|
vim.keymap.set('n', "<leader>ml", "0dwI<BS> <Esc>", { desc = "[M]erge lines" })
|
|
vim.keymap.set('n', "j", "gj", { desc = "Down visually" })
|
|
vim.keymap.set('n', "k", "gk", { desc = "Up visually" })
|
|
|
|
|
|
-- LSP KEYMAPS/CONFIG
|
|
-------------------------------------------------------------------------------
|
|
-- Use LspAttach autocommand to only map the following keys
|
|
-- after the language server attaches to the current buffer
|
|
vim.api.nvim_create_autocmd('LspAttach', {
|
|
group = vim.api.nvim_create_augroup('UserLspConfig', {}),
|
|
callback = function(ev)
|
|
-- Enable completion triggered by <c-x><c-o>
|
|
vim.bo[ev.buf].omnifunc = 'v:lua.vim.lsp.omnifunc'
|
|
|
|
-- Buffer local mappings.
|
|
-- See `:help vim.lsp.*` for documentation on any of the below functions
|
|
local opts = { buffer = ev.buf }
|
|
vim.keymap.set('n', 'gr', builtin.lsp_references, opts)
|
|
vim.keymap.set('n', 'gd', vim.lsp.buf.definition, opts)
|
|
vim.keymap.set('n', '<leader>k', vim.lsp.buf.hover, opts)
|
|
vim.keymap.set('n', 'gi', vim.lsp.buf.implementation, opts)
|
|
vim.keymap.set('n', '<C-k>', vim.lsp.buf.signature_help, opts)
|
|
vim.keymap.set('n', '<space>wa', vim.lsp.buf.add_workspace_folder, opts)
|
|
vim.keymap.set('n', '<space>wr', vim.lsp.buf.remove_workspace_folder, opts)
|
|
vim.keymap.set('n', '<space>wl', function()
|
|
print(vim.inspect(vim.lsp.buf.list_workspace_folders()))
|
|
end, opts)
|
|
vim.keymap.set('n', '<space>D', vim.lsp.buf.type_definition, opts)
|
|
vim.keymap.set('n', '<space>rn', vim.lsp.buf.rename, opts)
|
|
vim.keymap.set({ 'n', 'v' }, '<space>ca', vim.lsp.buf.code_action, opts)
|
|
vim.keymap.set('n', 'gr', vim.lsp.buf.references, opts)
|
|
vim.keymap.set('n', '<space>f', function()
|
|
vim.lsp.buf.format { async = true }
|
|
end, opts)
|
|
end,
|
|
})
|