Update NVIM for mariadb

This commit is contained in:
alejo committed 2026-09-30 16:29:04 -03:00
1 parent 361e1c9b51
commit 87a23b7f30
29 files changed
+1934 -1492

No files matched your search

+120 -4
View File
@@ -8,6 +8,7 @@ Supports **Fedora, Ubuntu, Debian, and Arch Linux** (and their common derivative
- [Installing](#installing) - [Installing](#installing)
- [What gets installed](#what-gets-installed) - [What gets installed](#what-gets-installed)
- [Config layout & adding plugins](#config-layout--adding-plugins)
- [Terminal vs. desktop (Neovide)](#terminal-vs-desktop-neovide) - [Terminal vs. desktop (Neovide)](#terminal-vs-desktop-neovide)
- [Editor basics](#editor-basics) - [Editor basics](#editor-basics)
- [Look & feel](#look--feel) - [Look & feel](#look--feel)
@@ -21,6 +22,7 @@ Supports **Fedora, Ubuntu, Debian, and Arch Linux** (and their common derivative
- [Debugging (nvim-dap)](#debugging-nvim-dap) - [Debugging (nvim-dap)](#debugging-nvim-dap)
- [Terminal](#terminal) - [Terminal](#terminal)
- [Git](#git) - [Git](#git)
- [Database (MariaDB)](#database-mariadb)
- [Hex viewer](#hex-viewer) - [Hex viewer](#hex-viewer)
- [Formatting and linting](#formatting-and-linting) - [Formatting and linting](#formatting-and-linting)
- [Known quirks](#known-quirks) - [Known quirks](#known-quirks)
@@ -56,8 +58,8 @@ First launch takes a minute or two: `lazy.nvim` bootstraps itself and installs a
Package names vary by distro (see the script for the exact list per package manager), but the same set gets installed everywhere: Package names vary by distro (see the script for the exact list per package manager), but the same set gets installed everywhere:
- **Compilers/tools**: gcc, g++, make, cmake, clang tooling, lldb, gdb, .NET SDK 8.0, Go, nodejs/npm, python3-pip - **Compilers/tools**: gcc, g++, make, cmake, clang tooling, lldb, gdb, .NET SDK 8.0, Go, nodejs/npm, python3-pip
- **CLI tools**: ripgrep, fd, git, curl, wget, unzip, fish (used only for Neovim's embedded terminal — see [Terminal](#terminal)) - **CLI tools**: ripgrep, fd, git, curl, wget, unzip, fish (used only for Neovim's embedded terminal — see [Terminal](#terminal)), and the MariaDB *client* (`mariadb`; client only, no server — see [Database (MariaDB)](#database-mariadb))
- **Language servers**: clangd, gopls, omnisharp, ts_ls, html, cssls (via Mason), plus `typescript-language-server` and `vscode-langservers-extracted` via npm - **Language servers**: clangd, gopls, omnisharp, ts_ls, html, cssls, sqls (via Mason), plus `typescript-language-server` and `vscode-langservers-extracted` via npm
- **Debug adapter**: codelldb (auto-installed by Mason on first debug session) - **Debug adapter**: codelldb (auto-installed by Mason on first debug session)
- **Neovide**, optionally — see [Terminal vs. desktop (Neovide)](#terminal-vs-desktop-neovide) - **Neovide**, optionally — see [Terminal vs. desktop (Neovide)](#terminal-vs-desktop-neovide)
@@ -65,11 +67,51 @@ Everything the script *writes* rather than installs comes from `config/`, copied
| `config/` | Copied to | | `config/` | Copied to |
|---|---| |---|---|
| `nvim/init.lua`, `nvim/lua/plugins/init.lua`, `nvim/lua/dap/configurations/cmake.lua` | `~/.config/nvim/...` (same layout) | | `nvim/` — the whole tree: `init.lua`, `lua/plugins/*.lua`, `lua/config/…`, `lua/dap/…` (see [Config layout](#config-layout--adding-plugins)) | `~/.config/nvim/...` (same layout) |
| `clang-format` | `~/.clang-format` | | `clang-format` | `~/.clang-format` |
| `clang-tidy` | `~/.clang-tidy` | | `clang-tidy` | `~/.clang-tidy` |
| `cpp-style-guide.md` | `~/cpp-style-guide.md` | | `cpp-style-guide.md` | `~/cpp-style-guide.md` |
## Config layout & adding plugins
```
config/nvim/
├── init.lua options, keymaps, Neovide settings, lazy.nvim bootstrap
└── lua/
├── plugins/ one file per plugin (or tightly coupled group)
│ ├── lsp.lua nvim-lspconfig + Mason
│ ├── completion.lua nvim-cmp + LuaSnip
│ ├── database.lua vim-dadbod + UI + completion (MariaDB)
│ ├── dap.lua nvim-dap + UI
│ └── … telescope, nvim-tree, bufferline, lualine, and so on
├── config/lsp/ helper modules that are *not* plugin specs
│ ├── servers.lua the list of language servers, plus per-server overrides
│ ├── keymaps.lua LSP keymaps, applied when a server attaches
│ └── commands.lua :LspRestart & co. for Neovim 0.12+
└── dap/configurations/ per-project debug targets (cmake.lua)
```
`init.lua` calls `require("lazy").setup("plugins")`, which makes lazy.nvim `require` every `.lua` file in `lua/plugins/` and merge what they return. Each file returns a list of plugin specs, the files don't depend on one another, and there is no central list to update. File order doesn't matter either: the colorscheme sets `priority = 1000`, and every plugin that calls into another one declares it as a dependency.
**Adding a plugin** means dropping a new file into `lua/plugins/`:
```lua
-- lua/plugins/my-plugin.lua
return {
{
"author/plugin.nvim",
event = "VeryLazy",
opts = {},
},
}
```
**Adding a language server** means adding one line to `lua/config/lsp/servers.lua` (`name = {},` — or with overrides inside the braces). That one table feeds both Mason's installer and `vim.lsp.enable()`, so there's no second list to keep in sync.
**Keep non-spec modules out of `lua/plugins/`.** lazy.nvim treats everything in that folder as a plugin spec, so a helper module placed there would be read as a (malformed) spec. Helpers go under `lua/config/` instead.
**If you copy these files over an existing `~/.config/nvim` by hand** rather than running the script (which wipes the directory first), delete the old `lua/plugins/init.lua` — lazy.nvim would still load it alongside the new files, and every plugin would be declared twice.
## Terminal vs. desktop (Neovide) ## Terminal vs. desktop (Neovide)
The installer asks which Neovim frontend(s) to set up: The installer asks which Neovim frontend(s) to set up:
@@ -172,7 +214,7 @@ Setting an explicit `<F1>` mapping here also overrides Neovim's built-in `<F1>`-
## LSP & code intelligence ## LSP & code intelligence
Configured servers: **clangd** (C/C++), **gopls** (Go), **omnisharp** (C#), **ts_ls** (TS/JS), **html**, **cssls**. Diagnostics render as virtual lines directly below the offending line (not inline virtual text), with the message's leading category prefix stripped; diagnostic underlines are off. Configured servers: **clangd** (C/C++), **gopls** (Go), **omnisharp** (C#), **ts_ls** (TS/JS), **html**, **cssls**, **sqls** (SQL — see [Database (MariaDB)](#database-mariadb)). The list lives in a single table, `lua/config/lsp/servers.lua`. Diagnostics render as virtual lines directly below the offending line (not inline virtual text), with the message's leading category prefix stripped; diagnostic underlines are off.
| Key | Action | | Key | Action |
|---|---| |---|---|
@@ -191,6 +233,18 @@ Configured servers: **clangd** (C/C++), **gopls** (Go), **omnisharp** (C#), **ts
| `]t` / `[t` | Jump to next/previous tip (LSP's "Hint" severity) | | `]t` / `[t` | Jump to next/previous tip (LSP's "Hint" severity) |
| `<leader>cd` | Generate a Doxygen-style comment block — see [Documentation generation](#documentation-generation) | | `<leader>cd` | Generate a Doxygen-style comment block — see [Documentation generation](#documentation-generation) |
**`:LspRestart` and friends.** Neovim 0.12 added a built-in `:lsp` command (`:lsp enable`, `:lsp disable`, `:lsp restart`, `:lsp stop`), and nvim-lspconfig responds by no longer registering its own `:LspStart` / `:LspStop` / `:LspRestart` / `:LspInfo` / `:LspLog` — so on 0.12 they simply don't exist (`E492: Not an editor command`). `lua/config/lsp/commands.lua` restores the old names as thin aliases over the built-ins:
| Command | Runs |
|---|---|
| `:LspRestart [client…]` | `:lsp restart [client…]` |
| `:LspStart [server…]` | `:lsp enable [server…]` |
| `:LspStop [server…]` | `:lsp disable [server…]` |
| `:LspInfo` (also `<leader>li`) | `:checkhealth vim.lsp` |
| `:LspLog` | opens the LSP log in a new tab |
Arguments are passed straight through and tab-complete (running clients for restart/stop, configured servers for start). On Neovim 0.11 and older there is no `:lsp`, nvim-lspconfig still provides the real commands itself, and the aliases stay out of the way.
**On `<leader>la` coming up thin:** clangd's code actions — and most of its clang-tidy checks — need real compiler flags (`-std=`, `-I`, defines) to do full semantic analysis. Without a `compile_commands.json` in the project, clangd falls back to guessed flags and the action list shrinks accordingly, often down to "No code actions available" everywhere. Generate one via CMake (`-DCMAKE_EXPORT_COMPILE_COMMANDS=ON`, then symlink or copy `build/compile_commands.json` to the project root) or `bear` for non-CMake projects. **On `<leader>la` coming up thin:** clangd's code actions — and most of its clang-tidy checks — need real compiler flags (`-std=`, `-I`, defines) to do full semantic analysis. Without a `compile_commands.json` in the project, clangd falls back to guessed flags and the action list shrinks accordingly, often down to "No code actions available" everywhere. Generate one via CMake (`-DCMAKE_EXPORT_COMPILE_COMMANDS=ON`, then symlink or copy `build/compile_commands.json` to the project root) or `bear` for non-CMake projects.
**`<leader>lt` and the same compile database:** `<leader>lt` runs the standalone `clang-tidy` binary rather than going through clangd's code actions, so it never comes back "No code actions available" — it always runs. But it looks for that same `compile_commands.json` on its own (passed via clang-tidy's `-p` flag, found the same way as above), for the same underlying reason clangd wants one: without it, clang-tidy falls back to guessed flags and can miss checks that need real semantic analysis. It also uses `--fix` rather than `--fix-errors`, so it deliberately leaves the file untouched if clang-tidy hits real compiler errors, instead of forcing fixes onto code that doesn't currently compile. **`<leader>lt` and the same compile database:** `<leader>lt` runs the standalone `clang-tidy` binary rather than going through clangd's code actions, so it never comes back "No code actions available" — it always runs. But it looks for that same `compile_commands.json` on its own (passed via clang-tidy's `-p` flag, found the same way as above), for the same underlying reason clangd wants one: without it, clang-tidy falls back to guessed flags and can miss checks that need real semantic analysis. It also uses `--fix` rather than `--fix-errors`, so it deliberately leaves the file untouched if clang-tidy hits real compiler errors, instead of forcing fixes onto code that doesn't currently compile.
@@ -219,6 +273,7 @@ The enclosing class is found via Treesitter by walking up from the cursor — no
- **`<CR>`**: confirm the selected completion. - **`<CR>`**: confirm the selected completion.
- **`<Up>/<Down>/<Left>/<Right>/<Esc>`**: close the completion menu, then perform the normal cursor/escape action. - **`<Up>/<Down>/<Left>/<Right>/<Esc>`**: close the completion menu, then perform the normal cursor/escape action.
- Snippet sessions auto-close once the cursor actually leaves their text (different line, or outside the placeholder's parentheses) — so `Tab` won't unexpectedly jump back into an old, already-finished snippet later on. - Snippet sessions auto-close once the cursor actually leaves their text (different line, or outside the placeholder's parentheses) — so `Tab` won't unexpectedly jump back into an old, already-finished snippet later on.
- **SQL buffers** use their own source list: dadbod's table names, LSP (sqls), snippets, and buffer words (no path source). See [Database (MariaDB)](#database-mariadb).
## Documentation generation ## Documentation generation
@@ -260,6 +315,67 @@ Neovim's embedded terminal launches **fish** instead of your login shell, purely
`gitsigns` shows added/changed/removed lines in the sign column against the current git state. `gitsigns` shows added/changed/removed lines in the sign column against the current git state.
## Database (MariaDB)
Two separate tools do two separate jobs here:
- **Connection and queries: [vim-dadbod](https://github.com/tpope/vim-dadbod) + [vim-dadbod-ui](https://github.com/kristijanhusak/vim-dadbod-ui).** Browse a server's tables in a sidebar, run queries, read the results in a split. dadbod doesn't speak the database protocol itself; it shells out to the `mariadb` command-line client, which the setup script installs. That's **the client only — no MariaDB server is installed or started**, so you need one running somewhere (local or remote).
- **Editing help: [sqls](https://github.com/sqls-server/sqls)**, a language server for SQL (completion, hover, formatting, code actions). There is no MariaDB-specific language server; sqls's MySQL driver speaks MariaDB's wire protocol and SQL flavor, which covers it. Mason installs it on first launch — it builds from source with `go install`, so Go has to be on `PATH` (the setup script installs it).
The two are independent: dadbod works without sqls, and sqls works without dadbod.
### Connecting
| Key | Action |
|---|---|
| `<leader>Du` | Toggle the DB UI sidebar |
| `<leader>Da` | Add a connection (same as pressing `A` inside the sidebar) |
| `<leader>Df` | Find the current buffer in the sidebar — or, for a buffer that wasn't opened from the DB UI (a plain `.sql` file), pick the connection it should run against (`:DBUIFindBuffer`) |
| `<leader>Dr` | Rename the current query buffer or saved query (a buffer has to have been written once first) |
| `<leader>Dl` | Print the last query that ran and how long it took |
`<leader>Da` asks for a URL and then a name:
```
mariadb://user:password@host:3306/dbname
```
Open the sidebar, pick a connection, and expand it to see its tables; expanding a table shows **List**, **Columns**, **Indexes**, and **Count** queries. Picking one writes the query into a buffer without running it; to write your own, choose *New query* under the connection. Either way, **saving the buffer runs it** (`:w`, and `<C-s>` works too), and dadbod-ui's own default `<leader>S` runs it without saving — the whole buffer, or only the visual selection if there is one. Results open in a split. Query buffers are temporary (they live in a temp folder and are cleared when Neovim quits) unless you save one with dadbod-ui's default `<leader>W`, which files it under the connection's *Saved queries*.
Details worth knowing:
- **Include the database name** in the URL: the sidebar lists the tables of that one database. For several databases on one server, add one connection each.
- **`localhost` vs `127.0.0.1`:** the client treats `localhost` as "use the local Unix socket" and `127.0.0.1` as "use TCP". Use whichever your server is actually set up for.
- **Special characters in the password** have to be percent-encoded in the URL (`@` → `%40`, `/` → `%2F`, and so on).
- **Keeping the password out of the URL:** leave it out and put `user=` / `password=` under `[client]` in `~/.my.cnf` instead — the `mariadb` client reads credentials from there.
- **Where connections are stored:** `~/.local/share/db_ui/connections.json`, **in plain text, password included**. That location is deliberately *outside* `~/.config/nvim` and `~/.local/share/nvim`, so re-running `setup_neovim.sh` (which wipes both) doesn't delete your connections. Nothing with credentials is ever written into the config files themselves.
- **Why `mariadb://` and not `mysql://`:** dadbod has a dedicated MariaDB adapter that runs the `mariadb` binary. The older `mysql://` scheme runs `mysql` instead, which MariaDB 11+ only ships as a deprecated alias that prints a "Deprecated program name" warning on every call — noise that ends up in query results and table lists. The trade-off is that dadbod-completion's column-level completion covers only some schemes (`mysql://` is one, `mariadb://` isn't), which is why columns come from sqls (below). Stick to `mysql://` for real MySQL servers.
### Language server (sqls)
sqls starts automatically in `sql`/`mysql` buffers and works out of the box for keyword completion and formatting (`<leader>lf`). For **table and column completion** it has to connect to the database *itself* — it does not share dadbod's connections — and it reads that from its own config file, `~/.config/sqls/config.yml`:
```yaml
lowercaseKeywords: false
connections:
- alias: local_mariadb
driver: mysql # MariaDB speaks the MySQL protocol
proto: tcp
host: 127.0.0.1
port: 3306
user: me
passwd: secret
dbName: mydb
```
The file holds a password in plain text, so `chmod 600 ~/.config/sqls/config.yml`. For a Unix-socket connection, swap the individual fields for a Go-MySQL DSN: `dataSourceName: me:secret@unix(/run/mysqld/mysqld.sock)/mydb` (the socket path varies by distro). After editing the file, `:LspRestart` restarts sqls so it picks the change up.
This file is deliberately *not* created by the setup script, so a half-filled template with a made-up password can't make sqls fail to connect on every start.
### Completion and syntax
In SQL buffers, completion draws on dadbod's table names (from the connection the buffer is attached to), sqls, snippets, and words already in the buffer. Vim's built-in SQL syntax is switched to its MySQL dialect (Vim defaults to Oracle's), the closest built-in match for MariaDB.
## Hex viewer ## Hex viewer
`<leader>h` toggles the current buffer between normal and hex-dump view (backed by `xxd`), rather than showing both simultaneously. That's deliberate: the hex dump is just ordinary buffer text under the hood, so editing it gets normal Neovim undo/redo for free, and nothing writes to disk until an explicit `:w` — same as any other file. A plugin doing a true live simultaneous hex+ASCII view was tried first, but it manages bytes with its own logic outside normal buffer editing, which is exactly why it had no undo at all. `<leader>h` toggles the current buffer between normal and hex-dump view (backed by `xxd`), rather than showing both simultaneously. That's deliberate: the hex dump is just ordinary buffer text under the hood, so editing it gets normal Neovim undo/redo for free, and nothing writes to disk until an explicit `:w` — same as any other file. A plugin doing a true live simultaneous hex+ASCII view was tried first, but it manages bytes with its own logic outside normal buffer editing, which is exactly why it had no undo at all.
+1 -1
View File
@@ -61,7 +61,7 @@ end
-- (truthy) when Neovide itself launches this config as its backend -- -- (truthy) when Neovide itself launches this config as its backend --
-- plain terminal Neovim never defines it, so everything in this block -- plain terminal Neovim never defines it, so everything in this block
-- is a no-op there. Terminal Neovim gets its cursor animation from -- is a no-op there. Terminal Neovim gets its cursor animation from
-- smear-cursor.nvim instead (see plugins/init.lua): a terminal has no -- smear-cursor.nvim instead (see plugins/smear-cursor.lua): a terminal has no
-- equivalent native capability to hook into, which is exactly the gap -- equivalent native capability to hook into, which is exactly the gap
-- that plugin fills. -- that plugin fills.
if vim.g.neovide then if vim.g.neovide then
@@ -0,0 +1,116 @@
-- :LspRestart / :LspStart / :LspStop / :LspInfo / :LspLog for Neovim 0.12+.
--
-- These five commands used to come from nvim-lspconfig. Neovim 0.12 added a
-- built-in `:lsp` command (`:lsp enable|disable|restart|stop`, see `:help
-- lsp-commands`) that covers the same ground, and nvim-lspconfig
-- deliberately stops registering its own copies whenever `:lsp` exists --
-- so on 0.12 they simply vanish: `:LspRestart` -> "E492: Not an editor
-- command". `:LspInfo` went with them (its replacement is `:checkhealth
-- vim.lsp`), and so did `:LspLog`.
--
-- Rather than retraining fingers (or the `<leader>li` mapping, or every
-- habit and doc that says `:LspRestart`), this defines the old names as thin
-- aliases over the built-in replacements:
--
-- :LspRestart [client...] -> :lsp restart [client...]
-- :LspStart [config...] -> :lsp enable [config...]
-- :LspStop [config...] -> :lsp disable [config...]
-- :LspInfo -> :checkhealth vim.lsp
-- :LspLog -> open the LSP log in a new tab
--
-- Nothing here reimplements restart/enable/disable: each alias just forwards
-- its arguments to the built-in, so behavior is whatever Neovim's own `:lsp`
-- does. The mapping follows nvim-lspconfig's own documented equivalents.
--
-- On Neovim 0.11 and older there is no `:lsp`, and nvim-lspconfig still
-- provides the real commands itself, so M.setup() does nothing there. A
-- command that already exists is never overwritten.
local M = {}
-- Keep only the names that start with what's been typed so far. (A
-- `complete` function's result isn't filtered by Neovim -- that's on us.)
local function starting_with(names, arg_lead)
local matches = {}
for _, name in ipairs(names) do
if vim.startswith(name, arg_lead) then
table.insert(matches, name)
end
end
return matches
end
-- Names of the clients running right now (for :LspRestart / :LspStop).
local function running_clients()
local seen, names = {}, {}
for _, client in ipairs(vim.lsp.get_clients()) do
if not seen[client.name] then
seen[client.name] = true
table.insert(names, client.name)
end
end
table.sort(names)
return names
end
-- Every server this config knows how to start (for :LspStart).
local function configured_servers()
local names = {}
local ok, servers = pcall(require, "config.lsp.servers")
if ok then
for name in pairs(servers) do
table.insert(names, name)
end
end
table.sort(names)
return names
end
-- Define `:<name> [args]` as `:lsp <subcommand> [args]`.
local function alias(name, subcommand, candidates, desc)
if vim.fn.exists(":" .. name) ~= 0 then
return
end
vim.api.nvim_create_user_command(name, function(opts)
local cmd = "lsp " .. subcommand
if opts.args ~= "" then
cmd = cmd .. " " .. opts.args
end
vim.cmd(cmd)
end, {
nargs = "*",
complete = function(arg_lead)
return starting_with(candidates(), arg_lead)
end,
desc = desc,
})
end
function M.setup()
-- `exists(":lsp")` is 2 for a full command match; 0 on Neovim < 0.12.
if vim.fn.exists(":lsp") < 2 then
return
end
alias("LspRestart", "restart", running_clients, "Restart LSP clients (alias for :lsp restart)")
alias("LspStart", "enable", configured_servers, "Enable LSP servers (alias for :lsp enable)")
alias("LspStop", "disable", running_clients, "Disable LSP servers (alias for :lsp disable)")
if vim.fn.exists(":LspInfo") == 0 then
vim.api.nvim_create_user_command("LspInfo", function()
vim.cmd("checkhealth vim.lsp")
end, { desc = "Show LSP status (alias for :checkhealth vim.lsp)" })
end
if vim.fn.exists(":LspLog") == 0 then
vim.api.nvim_create_user_command("LspLog", function()
local ok, path = pcall(vim.lsp.log.get_filename)
if not ok or not path then
path = vim.fn.stdpath("log") .. "/lsp.log"
end
vim.cmd.tabnew(vim.fn.fnameescape(path))
end, { desc = "Open the LSP log" })
end
end
return M
+301
View File
@@ -0,0 +1,301 @@
-- LSP keymaps and diagnostic navigation, applied per-buffer whenever a
-- language server attaches. Wired up from plugins/lsp.lua via
-- require("config.lsp.keymaps").setup().
local M = {}
-- LSP Keymaps (Organized under <leader>l), applied on attach via
-- the LspAttach autocommand rather than a per-server on_attach
-- callback passed through lspconfig[x].setup().
local function on_attach(bufnr)
-- Core goto commands (Standard Vim behavior, no leader required)
--
-- #include lines get special handling: clangd resolves
-- textDocument/definition (and /declaration) on an #include
-- directive to the target header regardless of which token on
-- that line the cursor sits on, so both gd and gD below open
-- it directly. If clangd hasn't attached/indexed yet, or the
-- header isn't part of the compile database, we fall back to
-- Vim's built-in "goto file under cursor" (gf), which still
-- resolves a local #include "quoted.h" relative to the
-- current file even with no LSP involved at all.
local function is_include_line()
return vim.api.nvim_get_current_line():match('^%s*#%s*include') ~= nil
end
-- Shared result handler for definition/declaration: jump
-- straight there for a single match; fill the quickfix list
-- and open it for multiple (e.g. a function declared once but
-- defined across several translation units) instead of
-- silently picking whichever one clangd listed first.
local function on_list(t)
if vim.tbl_isempty(t.items) then
if is_include_line() then
vim.cmd('normal! gf')
end
return
end
vim.fn.setqflist({}, ' ', t)
if #t.items > 1 then
vim.cmd('botright copen')
else
vim.cmd('cfirst')
end
end
vim.keymap.set('n', 'gd', function()
if is_include_line() then
vim.lsp.buf.definition({ on_list = on_list })
else
-- Regular symbol: Telescope gives a fuzzy picker with
-- preview when there's more than one candidate (e.g.
-- overloads), and jumps straight there for a single one --
-- nicer than the bare quickfix list for that common case.
require('telescope.builtin').lsp_definitions({ reuse_win = true })
end
end, { buffer = bufnr, silent = true, desc = "Go to Definition" })
vim.keymap.set('n', 'gD', function()
-- No Telescope equivalent exists for declaration (only
-- definition/references/implementation/type_definition are
-- exposed), so this stays on vim.lsp.buf.declaration --
-- just with the same on_list handling as gd above.
vim.lsp.buf.declaration({ on_list = on_list })
end, { buffer = bufnr, silent = true, desc = "Go to Declaration" })
vim.keymap.set('n', 'gr', function()
-- vim.lsp.buf.references() on its own just fills the
-- quickfix list silently without opening it, so pressing gr
-- looks like nothing happened. Telescope's picker actually
-- shows the list with a preview, same as gd above.
require('telescope.builtin').lsp_references({ reuse_win = true })
end, { buffer = bufnr, silent = true, desc = "Find References" })
vim.keymap.set('n', 'K', vim.lsp.buf.hover, { buffer = bufnr, silent = true, desc = "Hover Info" })
-- clangd doesn't support renaming macros or namespaces at all
-- (clangd/clangd#1890, still open) -- it's supposed to just
-- reply "symbol is not a supported kind", but has a separate
-- crash bug where it can SIGSEGV while handling that same
-- rejection instead of returning the error cleanly. This
-- catches the common case (cursor on the #define/namespace
-- line itself) with treesitter before ever asking clangd, so
-- the crash can't be triggered that way. It can't catch every
-- case -- invoking rename on a macro from where it's *used*
-- rather than defined still reaches clangd, since a macro
-- usage is just plain text to treesitter, indistinguishable
-- from any other identifier. If clangd crashes anyway, restart
-- it with :LspRestart; renaming a macro by hand with
-- :%s/\<old\>/new/g is the right tool regardless, since it's
-- pure text substitution with no scope for clangd to resolve.
local function renaming_unsupported_kind()
local ok, node = pcall(vim.treesitter.get_node)
if not ok then
return false
end
local cword = vim.fn.expand("<cword>")
while node do
local kind = node:type()
if kind == "preproc_def" or kind == "preproc_function_def" or kind == "namespace_definition" then
local name_node = node:field("name")[1]
if name_node and vim.treesitter.get_node_text(name_node, 0) == cword then
return true
end
end
node = node:parent()
end
return false
end
-- Safe Rename Function (Fixes double execution/prompt issues)
local function safe_rename()
if renaming_unsupported_kind() then
vim.notify(
"clangd can't rename macros or namespaces (unsupported upstream, and known to crash on the attempt) -- "
.. "use :%s/\\<old\\>/new/g instead.",
vim.log.levels.WARN
)
return
end
local current_name = vim.fn.expand("<cword>")
vim.ui.input({ prompt = "Rename: ", default = current_name }, function(new_name)
if not new_name or new_name == "" or new_name == current_name then
return
end
vim.lsp.buf.rename(new_name)
end)
end
-- Leader commands (Grouped under l)
vim.keymap.set('n', '<leader>lr', safe_rename, { buffer = bufnr, silent = true, desc = "Rename Symbol" })
-- Neovim's default vim.lsp.buf.code_action() only looks at
-- diagnostics on the exact cursor line. That's a needlessly
-- easy way to come up empty here specifically: virtual_lines
-- (set in plugins/lsp.lua) renders a diagnostic's message as an extra line
-- directly BELOW its own line, right where the next real line
-- of code sits -- so a diagnostic that reads as "right there"
-- is actually one line above the cursor as far as the LSP
-- request is concerned. This widens the request to also catch
-- a diagnostic one line either side, pulling each one's
-- original LSP-format diagnostic back out of user_data.lsp
-- (vim.diagnostic.get() preserves it there for exactly this),
-- and grows the requested range to actually cover them too --
-- a server is free to ignore a diagnostic in context that
-- falls outside the requested range, so widening only the
-- diagnostics list without the range wouldn't reliably help.
--
-- code_action()'s `range` wants plain {row, col} tuples using
-- mark-like indexing (1-indexed row, 0-indexed col -- the same
-- shape nvim_win_get_cursor()/nvim_buf_get_mark() use), NOT LSP
-- {line=, character=} position objects: it hands start/end
-- straight to vim.lsp.util.make_given_range_params(), which
-- indexes into them positionally (start_pos[1], start_pos[2]).
-- A {line=, character=} table has no numeric [1]/[2], so that
-- indexing was silently nil and the arithmetic on it crashed
-- with E5108 ("attempt to perform arithmetic on a nil value")
-- every time this ran. min_line/max_line below are 0-indexed
-- (from diagnostic .lnum/.end_lnum), so +1 converts to mark
-- indexing; the end column is the target line's real length so
-- the range also reaches a diagnostic that starts mid-line
-- rather than only ones starting at column 0.
vim.keymap.set('n', '<leader>la', function()
local row = vim.api.nvim_win_get_cursor(0)[1] - 1
local nearby, min_line, max_line = {}, row, row
for _, d in ipairs(vim.diagnostic.get(bufnr)) do
if math.abs(d.lnum - row) <= 1 and d.user_data and d.user_data.lsp then
table.insert(nearby, d.user_data.lsp)
min_line = math.min(min_line, d.lnum)
max_line = math.max(max_line, d.end_lnum or d.lnum)
end
end
if vim.tbl_isempty(nearby) then
vim.lsp.buf.code_action()
return
end
local last_line = vim.api.nvim_buf_get_lines(bufnr, max_line, max_line + 1, false)[1] or ""
vim.lsp.buf.code_action({
context = { diagnostics = nearby },
range = { start = { min_line + 1, 0 }, ["end"] = { max_line + 1, #last_line } },
})
end, { buffer = bufnr, silent = true, desc = "Code Actions" })
-- Visual-mode counterpart: several clangd tweaks (Extract
-- Function/Variable, raw-string conversion, ...) only show up
-- when the request carries an actual selection range instead
-- of the zero-width one normal mode sends.
vim.keymap.set('x', '<leader>la', vim.lsp.buf.code_action, { buffer = bufnr, silent = true, desc = "Code Actions (Selection)" })
vim.keymap.set('n', '<leader>lf', function() vim.lsp.buf.format { async = true } end, { buffer = bufnr, silent = true, desc = "Format File" })
-- "Fix all clang-tidy issues in this file" turns out not to be
-- something clangd's code actions can actually do: this used
-- to filter for LSP's `source.fixAll` code action kind, but
-- clangd never returns actions of that kind -- clang-tidy
-- fixes surface from clangd only as individual `quickfix`
-- actions, one per diagnostic, and clangd's own issue tracker
-- confirms there's no batched "fix everything" kind (clangd/
-- clangd#1446). So this always came back "No code actions
-- available" no matter what the file contained.
--
-- Running the actual clang-tidy binary with --fix sidesteps
-- clangd's code actions entirely and does what this key was
-- always meant to: apply every fix the tool can make across
-- the whole file in one pass, using the same .clang-tidy and
-- compile_commands.json clangd itself uses. --fix (rather than
-- --fix-errors) deliberately leaves the file untouched if
-- clang-tidy hits real compiler errors, instead of forcing
-- fixes onto code that doesn't compile -- switch it to
-- --fix-errors below if you'd rather it try anyway.
vim.keymap.set('n', '<leader>lt', function()
if vim.fn.executable("clang-tidy") == 0 then
vim.notify("clang-tidy not found on PATH", vim.log.levels.ERROR)
return
end
local filepath = vim.api.nvim_buf_get_name(bufnr)
if filepath == "" then
vim.notify("Buffer has no file on disk to run clang-tidy on", vim.log.levels.WARN)
return
end
-- clang-tidy reads from disk, not the buffer -- make sure
-- what's on disk actually matches what's on screen first.
if vim.bo[bufnr].modified then
vim.cmd("write")
end
local cmd = { "clang-tidy", "--fix" }
local db = vim.fs.find("compile_commands.json", {
upward = true,
path = vim.fs.dirname(filepath),
})[1]
if db then
vim.list_extend(cmd, { "-p", vim.fs.dirname(db) })
end
table.insert(cmd, filepath)
-- No "starting..." notification, and nothing shown at all on
-- a normal (exit 0) run: clang-tidy always writes chatter to
-- stderr -- including a notice about a missing compile
-- database, and its own end-of-run warning count -- even
-- when everything worked fine, so treating that output as
-- warning-worthy just because it's non-empty was the actual
-- source of the noise. Only a genuine failure (non-zero
-- exit) surfaces anything here.
vim.system(cmd, { text = true }, function(result)
vim.schedule(function()
if not vim.api.nvim_buf_is_valid(bufnr) then
return
end
-- Reload from disk so the buffer picks up whatever
-- clang-tidy just rewrote. :edit! (rather than
-- :checktime) reloads unconditionally instead of
-- prompting, since 'autoread' isn't set above.
local view = vim.fn.winsaveview()
vim.api.nvim_buf_call(bufnr, function()
vim.cmd("edit!")
end)
pcall(vim.fn.winrestview, view)
if result.code ~= 0 then
local msg = vim.trim(result.stderr or "")
if #msg > 400 then
msg = msg:sub(1, 400) .. "\n... (truncated -- re-run in a terminal for full output)"
end
vim.notify("clang-tidy exited with status " .. result.code .. ":\n" .. msg, vim.log.levels.WARN)
else
vim.notify("clang-tidy --fix finished", vim.log.levels.INFO)
end
end)
end)
end, { buffer = bufnr, silent = true, desc = "Fix All (clang-tidy)" })
vim.keymap.set('n', '<leader>li', '<cmd>LspInfo<CR>', { buffer = bufnr, silent = true, desc = "LSP Info" })
-- Diagnostic navigation, by severity. ]d/[d for "whatever's
-- next" regardless of severity; ]e/[e, ]w/[w, ]t/[t narrow to
-- errors, warnings, and hints ("tip" is the mnemonic here --
-- LSP itself calls that severity level "Hint") respectively.
-- No `float` option on the jump itself: virtual_lines (set in
-- plugins/lsp.lua) already shows the message text permanently, so a
-- floating preview on top of it would just be redundant.
local function diag_jump(count, severity)
return function()
vim.diagnostic.jump({ count = count, severity = severity })
end
end
vim.keymap.set('n', ']d', diag_jump(1), { buffer = bufnr, silent = true, desc = "Next Diagnostic" })
vim.keymap.set('n', '[d', diag_jump(-1), { buffer = bufnr, silent = true, desc = "Previous Diagnostic" })
vim.keymap.set('n', ']e', diag_jump(1, vim.diagnostic.severity.ERROR), { buffer = bufnr, silent = true, desc = "Next Error" })
vim.keymap.set('n', '[e', diag_jump(-1, vim.diagnostic.severity.ERROR), { buffer = bufnr, silent = true, desc = "Previous Error" })
vim.keymap.set('n', ']w', diag_jump(1, vim.diagnostic.severity.WARN), { buffer = bufnr, silent = true, desc = "Next Warning" })
vim.keymap.set('n', '[w', diag_jump(-1, vim.diagnostic.severity.WARN), { buffer = bufnr, silent = true, desc = "Previous Warning" })
vim.keymap.set('n', ']t', diag_jump(1, vim.diagnostic.severity.HINT), { buffer = bufnr, silent = true, desc = "Next Tip" })
vim.keymap.set('n', '[t', diag_jump(-1, vim.diagnostic.severity.HINT), { buffer = bufnr, silent = true, desc = "Previous Tip" })
end
function M.setup()
vim.api.nvim_create_autocmd("LspAttach", {
group = vim.api.nvim_create_augroup("UserLspAttach", { clear = true }),
callback = function(args)
on_attach(args.buf)
end,
})
end
return M
@@ -0,0 +1,71 @@
-- Language servers this config sets up.
--
-- One entry per server, keyed by its nvim-lspconfig name (`:help
-- lspconfig-all` lists them). The value is a table of extra settings layered
-- on top of nvim-lspconfig's own defaults for that server via
-- vim.lsp.config() -- `{}` means "the defaults are fine".
--
-- This single table drives all of plugins/lsp.lua: the keys are what Mason
-- installs (mason-lspconfig's ensure_installed) AND what gets passed to
-- vim.lsp.enable(), so adding a server is one entry here instead of two
-- lists that have to be kept in sync by hand.
return {
-- Per-server overrides beyond the shared `capabilities` (see plugins/lsp.lua).
-- clangd's own binary defaults already cover most of this, but
-- being explicit documents intent and avoids depending on
-- whatever a given clangd build's defaults happen to be:
-- --background-index index the whole project up front,
-- not just open files -- clang-tidy
-- checks and code actions that need
-- full semantic info are noticeably
-- thinner without it.
-- --clang-tidy explicit, even though on by default.
-- --header-insertion/--completion-style/--all-scopes-completion/
-- --cross-file-rename richer completion and project-wide
-- rename instead of the conservative
-- single-file defaults.
-- None of this substitutes for a compile_commands.json, though:
-- without one, clangd falls back to guessed compiler flags, and
-- most clang-tidy checks -- and the code actions/fixes tied to
-- them -- need real -std/-I flags to run at all. Generate one
-- via CMake (`-DCMAKE_EXPORT_COMPILE_COMMANDS=ON`, then symlink
-- or copy build/compile_commands.json to the project root) or
-- `bear` for non-CMake projects if <leader>la/<leader>lt still
-- come up thin on a real project after this.
clangd = {
cmd = {
"clangd",
"--background-index",
"--clang-tidy",
"--completion-style=detailed",
"--header-insertion=iwyu",
"--all-scopes-completion",
"--cross-file-rename",
},
},
gopls = {},
omnisharp = {},
ts_ls = {},
html = {},
cssls = {},
-- SQL for MariaDB / MySQL: completion, hover, formatting and code actions
-- in .sql buffers. sqls has no MariaDB dialect of its own, but its MySQL
-- driver speaks MariaDB's wire protocol and SQL flavor, so it covers
-- MariaDB fine. Mason installs it by building from source with `go
-- install`, so Go has to be on PATH (the setup script installs it).
--
-- sqls only knows table/column names if it has a database connection of
-- its OWN, which it reads from ~/.config/sqls/config.yml -- it does not
-- share vim-dadbod's connections. Without that file it still runs, but
-- only as keyword completion + formatting. Setup steps and an example
-- file are in README.md ("Database (MariaDB)").
--
-- Connections are deliberately not set through `settings` here: per
-- sqls' docs, whatever the client hands it over workspace/configuration
-- takes priority over config.yml, so defining them here would shadow the
-- file instead of complementing it.
sqls = {},
}
@@ -0,0 +1,28 @@
return {
-- Auto-pair brackets/quotes: (), {}, [], "", '' -- nvim-autopairs'
-- own defaults. <> was tried here too but removed: it collided too
-- much with < and > as comparison operators to be worth keeping.
{
"windwp/nvim-autopairs",
dependencies = { "hrsh7th/nvim-cmp" },
config = function()
local autopairs = require("nvim-autopairs")
-- map_cr defaults to true, which makes nvim-autopairs install
-- its OWN insert-mode <CR> mapping (for expanding bracket pairs
-- onto their own indented line). That's set during plugin
-- loading, which runs after init.lua's own top-level code --
-- where the comment-continuation <CR> mapping lives -- so it
-- was silently overwriting that mapping entirely regardless of
-- what it did internally. Disabled here to free up <CR> for
-- that mapping; the trade-off is losing autopairs' own
-- brace-expands-on-Enter behavior, which was never explicitly
-- asked for.
autopairs.setup({ map_cr = false })
-- Integrates with nvim-cmp so accepting a completion (e.g. a
-- function call) doesn't end up with doubled-up parens.
local cmp_autopairs = require("nvim-autopairs.completion.cmp")
require("cmp").event:on("confirm_done", cmp_autopairs.on_confirm_done())
end,
},
}
+136
View File
@@ -0,0 +1,136 @@
return {
-- Bufferline (Tabs)
{
"akinsho/bufferline.nvim",
version = "*",
dependencies = "nvim-tree/nvim-web-devicons",
config = function()
require("bufferline").setup({
options = {
diagnostics = "nvim_lsp",
show_buffer_close_icons = false,
numbers = "ordinal", -- shows 1, 2, 3... on the left of each tab,
-- matching the position used by the Alt+number jump below.
--
-- Makes :BufferLineMovePrev/MoveNext refuse to walk an
-- unpinned buffer across the pinned block (wraps to the
-- other end instead) -- this is bufferline's own built-in
-- boundary check (see get_last_pinned_index/M.move in its
-- source), just off by default.
move_wraps_at_ends = true,
}
})
-- Navigate buffers. Using bufferline's own cycle commands rather
-- than :bprevious/:bnext -- once buffers can be reordered (see
-- <A-,>/<A-.> below), plain :bnext/:bprevious would cycle by
-- vim's internal buffer number instead of the order shown on
-- screen, which stops matching what you actually see. This is
-- bufferline's own documented recommendation for exactly this
-- situation.
vim.keymap.set('n', '<S-h>', '<cmd>BufferLineCyclePrev<cr>', { desc = "Prev buffer" })
vim.keymap.set('n', '<S-l>', '<cmd>BufferLineCycleNext<cr>', { desc = "Next buffer" })
vim.keymap.set('n', '<leader>bd', '<cmd>bdelete<cr>', { desc = "Close Current Buffer" })
-- Alt+1..9: jump straight to the buffer at that ordinal position
-- (the number shown on the left of each tab). Alt+0 jumps to the
-- last one, mirroring bufferline's own "$" convention.
for i = 1, 9 do
vim.keymap.set('n', ('<A-%d>'):format(i), function()
require('bufferline').go_to(i, true)
end, { desc = "Go to Buffer " .. i })
end
vim.keymap.set('n', '<A-0>', function()
require('bufferline').go_to(-1, true)
end, { desc = "Go to Last Buffer" })
-- F1..F9: same jump-by-position, on Fn+Number instead of
-- Alt+Number. Explicitly setting <F1> here overrides Neovim's
-- default <F1>-opens-help binding outright (any explicit
-- vim.keymap.set for a key always supersedes Neovim's built-in
-- default for it), so no separate step is needed to "disable"
-- that.
--
-- <F5> and <F10> are set below, OUTSIDE this loop -- both are
-- shared with nvim-dap (Continue and Step Over respectively) and
-- toggle dynamically based on whether a debug session is active
-- (see the dap.listeners hooks in plugins/dap.lua).
-- At rest, both act as ordinary buffer jumps like everything
-- else in this loop.
for _, i in ipairs({ 1, 2, 3, 4, 6, 7, 8, 9 }) do
vim.keymap.set('n', ('<F%d>'):format(i), function()
require('bufferline').go_to(i, true)
end, { desc = "Go to Buffer " .. i })
end
vim.keymap.set('n', '<F5>', function()
require('bufferline').go_to(5, true)
end, { desc = "Go to Buffer 5" })
vim.keymap.set('n', '<F10>', function()
require('bufferline').go_to(-1, true)
end, { desc = "Go to Last Buffer" })
-- Alt+,/. : reorder the current buffer left/right in the
-- bufferline (persists for the session as long as
-- sessionoptions includes "globals", which lazy.nvim's default
-- vimrc already does). move_wraps_at_ends above handles keeping
-- these from crossing into the pinned block -- no custom guard
-- needed here.
vim.keymap.set('n', '<A-,>', '<cmd>BufferLineMovePrev<cr>', { desc = "Move Buffer Left" })
vim.keymap.set('n', '<A-.>', '<cmd>BufferLineMoveNext<cr>', { desc = "Move Buffer Right" })
-- Pin the current buffer -- pinned buffers stay pinned to the
-- start of the bufferline regardless of sorting/reordering.
--
-- Pin state is ALSO recorded in a plain buffer-local variable
-- here, set at the same time as the real toggle. bufferline's
-- internal pin/group state isn't reliably exposed as a stable
-- public API across versions -- require('bufferline').group_action
-- (tried previously) doesn't exist on the version actually
-- installed here, hence the earlier E5108 error. Tracking it
-- ourselves means <leader>bx below never has to ask bufferline
-- which buffers are pinned at all.
--
-- :BufferLineTogglePin only updates the RENDER-time grouping; it
-- doesn't physically move the buffer within bufferline's
-- internal position list (state.components), which is the exact
-- list go_to()/number labels are computed from. That gap is what
-- caused Alt+number to occasionally land on/label the wrong
-- buffer right after pinning. move_to() operates on that same
-- internal list directly (it's what :BufferLineMovePrev/Next and
-- the sort commands use internally), so calling it here forces
-- the two back into sync immediately: to the front when pinning,
-- to just after the remaining pinned block when unpinning.
vim.keymap.set('n', '<leader>bp', function()
vim.cmd('BufferLineTogglePin')
local buf = vim.api.nvim_get_current_buf()
local now_pinned = not vim.b[buf].pinned
vim.b[buf].pinned = now_pinned
if now_pinned then
require('bufferline').move_to(1)
else
local pinned_count = 0
for _, b in ipairs(vim.fn.getbufinfo({ buflisted = 1 })) do
if b.bufnr ~= buf and vim.b[b.bufnr].pinned then
pinned_count = pinned_count + 1
end
end
require('bufferline').move_to(pinned_count + 1)
end
end, { desc = "Toggle Pin" })
-- Close every unpinned buffer that has NO unsaved changes, using
-- the buffer-local pin flag set above -- deliberately not using
-- :BufferLineGroupClose ungrouped, since its default
-- close_command is "bdelete! %d" (forced), which would silently
-- discard unsaved work in any unpinned buffer.
vim.keymap.set('n', '<leader>bx', function()
for _, buf in ipairs(vim.fn.getbufinfo({ buflisted = 1 })) do
if not vim.b[buf.bufnr].pinned and buf.changed == 0 then
pcall(vim.cmd, 'bdelete ' .. buf.bufnr)
end
end
end, { desc = "Close Unpinned Saved Buffers" })
end,
},
}
@@ -0,0 +1,9 @@
return {
-- Inline hex/rgb color preview
{
"norcalli/nvim-colorizer.lua",
config = function()
require("colorizer").setup()
end,
},
}
@@ -0,0 +1,49 @@
return {
-- REALLY Dark Theme (near-black koda)
--
-- Terminal Neovim and Neovide (the desktop GUI app) render the same
-- highlight groups, so the theme itself needs nothing
-- frontend-specific -- it's only the cursor *animation* that splits
-- in two (see smear-cursor.lua and init.lua), since a terminal and a GPU-rendered GUI window have
-- different ways of drawing one.
{
"oskarnurm/koda.nvim",
priority = 1000,
config = function()
require("koda").setup({
-- Terminal only (see the "not Neovide" comment on the setting
-- above): clears highlight-group backgrounds to NONE so
-- whatever the *terminal emulator* draws behind Neovim shows
-- through the gaps -- including a background image, if the
-- terminal supports one (e.g. kitty's own `background_image`
-- setting in kitty.conf, which is a terminal-level feature
-- this config has no reach into). Without that configured on
-- the terminal side, this just shows the terminal's normal
-- background color instead, which is a harmless no-op look.
-- Neovide doesn't use this option at all -- see
-- vim.g.neovide_opacity in init.lua for its equivalent.
transparent = not vim.g.neovide,
colors = {
-- koda's own "dark" variant defaults to #101010 -- nudged a
-- touch further down to match the old pure-black background.
bg = "#090909",
line = "#1a1a1a",
},
on_highlights = function(hl, c)
-- koda has no dedicated "hint" color; c.cyan is the closest
-- hue match to the dark-teal hint background below.
hl.DiagnosticVirtualTextError = { bg = "#351010", fg = c.danger }
hl.DiagnosticVirtualTextWarn = { bg = "#35280b", fg = c.warning }
hl.DiagnosticVirtualTextInfo = { bg = "#0b2035", fg = c.info }
hl.DiagnosticVirtualTextHint = { bg = "#0b3528", fg = c.cyan }
hl.DiagnosticVirtualLinesError = { bg = "#351010", fg = c.danger }
hl.DiagnosticVirtualLinesWarn = { bg = "#35280b", fg = c.warning }
hl.DiagnosticVirtualLinesInfo = { bg = "#0b2035", fg = c.info }
hl.DiagnosticVirtualLinesHint = { bg = "#0b3528", fg = c.cyan }
end,
})
vim.cmd.colorscheme("koda-dark")
end,
},
}
+106
View File
@@ -0,0 +1,106 @@
return {
-- Autocompletion
{
"hrsh7th/nvim-cmp",
dependencies = {
"L3MON4D3/LuaSnip",
"saadparwaiz1/cmp_luasnip",
"hrsh7th/cmp-nvim-lsp",
"hrsh7th/cmp-buffer",
"hrsh7th/cmp-path",
},
config = function()
local cmp = require("cmp")
local luasnip = require("luasnip")
-- Without this, LuaSnip never checks whether the cursor has
-- actually left a snippet's text -- so pressing Tab later, even
-- lines away, can jump straight back into an old, unfinished
-- placeholder session (e.g. clangd's function-argument
-- placeholders). region_check_events makes it watch cursor
-- movement and automatically exit a snippet once you've moved
-- outside its region, in both normal and insert mode.
luasnip.setup({
region_check_events = "CursorMoved,CursorMovedI",
})
cmp.setup({
-- Ghost text: previews the currently-selected completion inline
-- after the cursor, instead of only showing it in the popup menu.
-- Combined with the cmp-path source below, this is what gives you
-- inline "ghost" previews as you type a filesystem path (e.g.
-- typing ./src/ will ghost-preview entries found in that
-- directory) without needing a separate plugin for it.
experimental = {
ghost_text = true,
},
snippet = {
expand = function(args)
luasnip.lsp_expand(args.body)
end,
},
window = {
completion = cmp.config.window.bordered(),
documentation = cmp.config.window.bordered(),
},
mapping = cmp.mapping.preset.insert({
-- Safely close the menu without reverting text state
['<Up>'] = cmp.mapping(function(fallback)
if cmp.visible() then cmp.close() end
fallback()
end, { 'i', 's' }),
['<Down>'] = cmp.mapping(function(fallback)
if cmp.visible() then cmp.close() end
fallback()
end, { 'i', 's' }),
['<Left>'] = cmp.mapping(function(fallback)
if cmp.visible() then cmp.close() end
fallback()
end, { 'i', 's' }),
['<Right>'] = cmp.mapping(function(fallback)
if cmp.visible() then cmp.close() end
fallback()
end, { 'i', 's' }),
['<Esc>'] = cmp.mapping(function(fallback)
if cmp.visible() then cmp.close() end
fallback()
end, { 'i', 's' }),
['<Tab>'] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_next_item()
elseif luasnip.expand_or_jumpable() then
-- No completion menu open, but we're sitting in a snippet
-- (e.g. clangd's function-argument placeholders after
-- accepting a call like memcpy(...)) -- jump to the next
-- tabstop/placeholder instead of inserting a literal tab.
luasnip.expand_or_jump()
else
fallback()
end
end, { 'i', 's' }),
['<S-Tab>'] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_prev_item()
elseif luasnip.jumpable(-1) then
luasnip.jump(-1)
else
fallback()
end
end, { 'i', 's' }),
['<CR>'] = cmp.mapping.confirm({ select = false }),
}),
sources = cmp.config.sources({
{ name = "nvim_lsp" },
{ name = "luasnip" },
{ name = "buffer" },
-- Lists and completes filesystem paths/directory contents as
-- you type a path string (e.g. after typing "./" or "/home/").
-- This is the source that feeds the ghost-text path preview.
{ name = "path" },
}),
})
end,
},
}
+135
View File
@@ -0,0 +1,135 @@
return {
-- =========================================================
-- DAP: In-editor C/C++ debugging (breakpoints, step, watch)
-- Uses codelldb (installed via Mason) as the debug adapter.
-- =========================================================
{
"mfussenegger/nvim-dap",
dependencies = {
"rcarriga/nvim-dap-ui",
"nvim-neotest/nvim-nio",
"theHamsta/nvim-dap-virtual-text",
"jay-babu/mason-nvim-dap.nvim",
},
config = function()
local dap = require("dap")
local dapui = require("dapui")
-- Auto-install codelldb through Mason and wire it into nvim-dap.
-- NOTE: omitting `handlers` entirely (rather than passing {}) is
-- what makes mason-nvim-dap run its DEFAULT handler, which is
-- what actually registers a working dap.adapters.codelldb with
-- correct dynamic ${port} allocation. Passing handlers = {}
-- disables that default handler instead of using it.
require("mason-nvim-dap").setup({
ensure_installed = { "codelldb" },
automatic_installation = true,
})
require("dapui").setup()
require("nvim-dap-virtual-text").setup({
commented = true,
})
-- Auto open/close the UI when a debug session starts/ends
dap.listeners.after.event_initialized["dapui_config"] = function()
dapui.open()
end
dap.listeners.before.event_terminated["dapui_config"] = function()
dapui.close()
end
dap.listeners.before.event_exited["dapui_config"] = function()
dapui.close()
end
-- <F5> and <F10> are each shared between an nvim-dap action
-- (Continue, Step Over) and a bufferline buffer jump (buffer 5,
-- last buffer) -- at rest they're buffer jumps; for the
-- duration of a debug session they're handed over to DAP
-- instead, so nothing fights over the same key. Note this means
-- <F5> can no longer START a fresh session on its own (there's
-- no session yet at that point, so it's still doing buffer-jump
-- duty) -- use <leader>dc or <leader>db to start one, then <F5>
-- takes over for continuing from there. Reusing the same
-- listener points as the dapui open/close hooks above.
dap.listeners.after.event_initialized["fkey_toggle"] = function()
vim.keymap.set('n', '<F5>', dap.continue, { desc = "Debug: Continue" })
vim.keymap.set('n', '<F10>', dap.step_over, { desc = "Debug: Step Over" })
end
local function restore_fkey_buffer_jumps()
vim.keymap.set('n', '<F5>', function()
require('bufferline').go_to(5, true)
end, { desc = "Go to Buffer 5" })
vim.keymap.set('n', '<F10>', function()
require('bufferline').go_to(-1, true)
end, { desc = "Go to Last Buffer" })
end
dap.listeners.before.event_terminated["fkey_toggle"] = restore_fkey_buffer_jumps
dap.listeners.before.event_exited["fkey_toggle"] = restore_fkey_buffer_jumps
-- C / C++ / Rust launch configs, all sharing the codelldb adapter
dap.configurations.cpp = {
{
name = "Launch executable",
type = "codelldb",
request = "launch",
program = function()
return vim.fn.input(
"Path to executable: ",
vim.fn.getcwd() .. "/",
"file"
)
end,
cwd = "${workspaceFolder}",
stopOnEntry = false,
args = {},
},
{
name = "Attach to process",
type = "codelldb",
request = "attach",
pid = require("dap.utils").pick_process,
cwd = "${workspaceFolder}",
},
}
dap.configurations.c = dap.configurations.cpp
dap.configurations.rust = dap.configurations.cpp
-- Breakpoint / stepping signs
vim.fn.sign_define("DapBreakpoint", { text = "●", texthl = "DiagnosticError", linehl = "", numhl = "" })
vim.fn.sign_define("DapBreakpointCondition", { text = "◆", texthl = "DiagnosticWarn", linehl = "", numhl = "" })
vim.fn.sign_define("DapStopped", { text = "▶", texthl = "DiagnosticInfo", linehl = "DapStoppedLine", numhl = "" })
vim.api.nvim_set_hl(0, "DapStoppedLine", { bg = "#0b3528" })
-- Keymaps, grouped under <leader>d (shows in which-key)
-- <F5> and <F10> intentionally NOT bound here -- both are set
-- dynamically by the dap.listeners hooks above (Continue/Step
-- Over only while a session is active; bufferline's buffer-5/
-- last-buffer jumps otherwise).
vim.keymap.set('n', '<F11>', dap.step_into, { desc = "Debug: Step Into" })
vim.keymap.set('n', '<F12>', dap.step_out, { desc = "Debug: Step Out" })
vim.keymap.set('n', '<leader>db', dap.toggle_breakpoint, { desc = "Toggle Breakpoint" })
vim.keymap.set('n', '<leader>dB', function()
dap.set_breakpoint(vim.fn.input("Breakpoint condition: "))
end, { desc = "Conditional Breakpoint" })
vim.keymap.set('n', '<leader>dc', dap.continue, { desc = "Continue" })
vim.keymap.set('n', '<leader>do', dap.step_over, { desc = "Step Over" })
vim.keymap.set('n', '<leader>di', dap.step_into, { desc = "Step Into" })
vim.keymap.set('n', '<leader>dO', dap.step_out, { desc = "Step Out" })
vim.keymap.set('n', '<leader>dr', dap.repl.open, { desc = "Open REPL" })
vim.keymap.set('n', '<leader>dl', dap.run_last, { desc = "Run Last" })
vim.keymap.set('n', '<leader>dt', dap.terminate, { desc = "Terminate Session" })
vim.keymap.set('n', '<leader>du', dapui.toggle, { desc = "Toggle Debug UI" })
vim.keymap.set('n', '<leader>dh', function()
require("dap.ui.widgets").hover()
end, { desc = "Hover Variable" })
-- Load project-specific CMake-built target configs (defines
-- Debug configs for build/tests/... and build/demo/... binaries).
-- This also makes nvim-dap lazy-load the same file automatically
-- when you start debugging from a CMakeLists.txt buffer.
require("dap.configurations.cmake")
end,
},
}
@@ -0,0 +1,33 @@
return {
-- Dashboard (NvChad-style start screen)
{
"goolord/alpha-nvim",
dependencies = { "nvim-tree/nvim-web-devicons" },
config = function()
local alpha = require("alpha")
local dashboard = require("alpha.themes.dashboard")
dashboard.section.header.val = {
" ",
" ███╗ ██╗██╗ ██╗██╗███╗ ███╗ ",
" ████╗ ██║██║ ██║██║████╗ ████║ ",
" ██╔██╗ ██║██║ ██║██║██╔████╔██║ ",
" ██║╚██╗██║╚██╗ ██╔╝██║██║╚██╔╝██║ ",
" ██║ ╚████║ ╚████╔╝ ██║██║ ╚═╝ ██║ ",
" ╚═╝ ╚═══╝ ╚═══╝ ╚═╝╚═╝ ╚═╝ ",
" ",
}
dashboard.section.buttons.val = {
dashboard.button("f", " Find File", "<cmd>Telescope find_files<CR>"),
dashboard.button("r", " Recent Files", "<cmd>Telescope oldfiles<CR>"),
dashboard.button("w", " Live Grep", "<cmd>Telescope live_grep<CR>"),
dashboard.button("e", " New File", "<cmd>ene<CR>"),
dashboard.button("c", " Edit Config", "<cmd>edit ~/.config/nvim/init.lua<CR>"),
dashboard.button("q", " Quit", "<cmd>qa<CR>"),
}
alpha.setup(dashboard.opts)
end,
},
}
+116
View File
@@ -0,0 +1,116 @@
return {
-- Database access, built around MariaDB. vim-dadbod runs queries against a
-- server through the database's own command-line client, vim-dadbod-ui
-- adds a sidebar of connections/tables plus saved query buffers, and
-- vim-dadbod-completion feeds table names into nvim-cmp in SQL buffers.
-- Language-server features for SQL (keyword/column completion, hover,
-- formatting) are a separate thing: that's sqls, set up in
-- lua/config/lsp/servers.lua. README.md ("Database (MariaDB)") covers
-- how the two fit together.
--
-- Connections are URLs, with one scheme per backend. For MariaDB it's
-- mariadb://user:password@host:3306/dbname
-- which uses dadbod's dedicated MariaDB adapter and runs the `mariadb`
-- client binary. (The older `mysql://` scheme works too, but it shells
-- out to `mysql`, which MariaDB 11+ only ships as a deprecated alias that
-- prints "Deprecated program name" on every call -- noise that ends up in
-- query results and table lists.) Include the database name in the URL:
-- the drawer lists the tables of that one database.
--
-- Add connections from inside Neovim: `A` in the drawer, or <leader>Da.
-- Nothing with credentials belongs in this file.
{
"kristijanhusak/vim-dadbod-ui",
dependencies = {
{ "tpope/vim-dadbod", lazy = true },
{
"kristijanhusak/vim-dadbod-completion",
-- Its completion source calls dadbod's autoload functions, so it has
-- to pull dadbod in along with itself -- otherwise opening a plain
-- .sql file (never touching the DB UI) and typing would hit an
-- undefined `db#...` function.
dependencies = "tpope/vim-dadbod",
ft = { "sql", "mysql", "plsql" },
lazy = true,
},
},
cmd = {
"DBUI",
"DBUIToggle",
"DBUIAddConnection",
"DBUIFindBuffer",
"DBUIRenameBuffer",
"DBUILastQueryInfo",
},
keys = {
{ "<leader>Du", "<cmd>DBUIToggle<cr>", desc = "Toggle DB UI" },
{ "<leader>Df", "<cmd>DBUIFindBuffer<cr>", desc = "Find DB Buffer" },
{ "<leader>Dr", "<cmd>DBUIRenameBuffer<cr>", desc = "Rename DB Buffer" },
{ "<leader>Dl", "<cmd>DBUILastQueryInfo<cr>", desc = "Last Query Info" },
{ "<leader>Da", "<cmd>DBUIAddConnection<cr>", desc = "Add DB Connection" },
},
-- `init` (not `config`) because these globals have to be set before the
-- plugins load, and because the FileType autocmd below has to exist from
-- startup even though the plugins themselves are lazy-loaded.
init = function()
vim.g.db_ui_use_nerd_fonts = 1
-- Where connections added through the UI (and saved queries) live.
-- Set explicitly, to somewhere OUTSIDE both ~/.config/nvim and
-- ~/.local/share/nvim: setup_neovim.sh wipes those two on every run,
-- and saved connections shouldn't go with them. The URLs are stored in
-- plain text in connections.json here, password included -- leave the
-- password out of the URL and put it in ~/.my.cnf's [client] section
-- if that bothers you (the mariadb client reads it from there).
local data_home = vim.env.XDG_DATA_HOME
if not data_home or data_home == "" then
data_home = vim.env.HOME .. "/.local/share"
end
vim.g.db_ui_save_location = data_home .. "/db_ui"
-- Table helpers for mariadb:// connections: the entries listed under
-- each table in the drawer. Spelled out here because dadbod-ui's
-- generic fallback query double-quotes the table name
-- (SELECT * from "{table}" ...), and MariaDB reads double quotes as a
-- string literal rather than an identifier unless ANSI_QUOTES is on.
-- These use backticks instead, plus a few more useful helpers.
vim.g.db_ui_table_helpers = {
mariadb = {
List = "SELECT * FROM `{table}` LIMIT 200;",
Columns = "DESCRIBE `{table}`;",
Indexes = "SHOW INDEX FROM `{table}`;",
Count = "SELECT COUNT(*) FROM `{table}`;",
},
}
-- Vim's built-in SQL syntax/indent/ftplugin default to Oracle's
-- dialect; 'mysql' is the closest built-in match for MariaDB.
vim.g.sql_type_default = "mysql"
-- In SQL buffers, add vim-dadbod-completion to the usual cmp sources.
-- It completes table names from the active dadbod connection, and the
-- plugin registers the "vim-dadbod-completion" source with cmp itself.
-- Its column-level completion only covers some schemes (mysql,
-- postgres, ...) and not mariadb://, so columns, keywords and so on
-- come from sqls through the nvim_lsp source instead.
vim.api.nvim_create_autocmd("FileType", {
group = vim.api.nvim_create_augroup("UserDadbodCompletion", { clear = true }),
pattern = { "sql", "mysql", "plsql" },
callback = function()
local ok, cmp = pcall(require, "cmp")
if not ok then
return
end
cmp.setup.buffer({
sources = cmp.config.sources({
{ name = "vim-dadbod-completion" },
{ name = "nvim_lsp" },
{ name = "luasnip" },
{ name = "buffer" },
}),
})
end,
})
end,
},
}
+10
View File
@@ -0,0 +1,10 @@
return {
-- Prettier vim.ui.input / vim.ui.select -- upgrades your existing
-- rename prompt, conditional breakpoint prompt, and DAP path prompts
-- to floating, styled dialogs instead of the command-line input.
{
"stevearc/dressing.nvim",
event = "VeryLazy",
opts = {},
},
}
@@ -0,0 +1,9 @@
return {
-- Git State
{
"lewis6991/gitsigns.nvim",
config = function()
require("gitsigns").setup()
end,
},
}
+45
View File
@@ -0,0 +1,45 @@
return {
-- Hex file viewer/editor. Toggles between normal and hex-dump view
-- (backed by xxd) rather than showing both simultaneously -- that
-- trade-off is deliberate: since the hex dump is just ordinary,
-- plain buffer text under the hood, editing it gets Neovim's normal
-- undo/redo for free, and nothing writes to disk until an explicit
-- :w, same as any other file. The only plugin found that does a
-- true live simultaneous hex+ASCII view (hexview.nvim, used here
-- previously) manages bytes with its own custom logic instead of
-- going through normal buffer text editing, which is exactly why it
-- has no undo at all -- not a gap that plugin could patch, an
-- architectural trade-off against the same thing being fixed here.
{
"RaafatTurki/hex.nvim",
config = function()
require("hex").setup()
vim.keymap.set('n', '<leader>h', function()
if not vim.bo.binary then
-- Hex editing depends on the buffer actually being loaded
-- as binary. Otherwise Neovim applies normal text encoding/
-- line-ending handling to the raw bytes first (NUL bytes
-- included, which any real binary is full of) -- that
-- misinterpretation is what produces "CONVERSION ERROR"
-- rather than a clean hex dump, and it happens BEFORE xxd
-- ever runs. Re-reading with ++bin loads the file correctly
-- from scratch instead of operating on an already-misread
-- buffer.
--
-- That reload is forced (!), which discards whatever's
-- currently in the buffer without asking -- refuse to do it
-- if there are unsaved changes rather than silently losing
-- them. Only relevant on this first switch into hex mode:
-- once vim.bo.binary is already true, no reload happens and
-- this check doesn't apply.
if vim.bo.modified then
vim.notify("Buffer has unsaved changes -- save first (or :e! to discard) before opening hex view", vim.log.levels.WARN)
return
end
vim.cmd('edit! ++bin %')
end
vim.cmd('HexToggle')
end, { desc = "Toggle Hex View" })
end,
},
}
@@ -0,0 +1,22 @@
return {
-- Indent guides
{
"lukas-reineke/indent-blankline.nvim",
main = "ibl",
config = function()
require("ibl").setup({
indent = { char = "│" },
scope = {
enabled = true,
-- show_start/show_end default to true and underline the
-- first/last line of whatever lexical scope the cursor is
-- currently inside (recalculated as the cursor moves --
-- that's why it seemed to "follow" the cursor). The colored
-- guide itself is kept; just the underline is turned off.
show_start = false,
show_end = false,
},
})
end,
},
}
File diff suppressed because it is too large. Load diff
+76
View File
@@ -0,0 +1,76 @@
return {
-- LSP and Mason
{
"neovim/nvim-lspconfig",
dependencies = {
"williamboman/mason.nvim",
"williamboman/mason-lspconfig.nvim",
"hrsh7th/cmp-nvim-lsp",
},
config = function()
-- Enable native virtual lines (rendered directly below the
-- affected line) with custom formatting
vim.diagnostic.config({
underline = false, -- virtual_lines already shows the message; skip the extra underline
virtual_text = false,
virtual_lines = {
format = function(diagnostic)
local msg = diagnostic.message or ""
msg = msg:gsub("^[a-z_][a-z0-9_%-]*:%s*", "")
return msg
end,
},
})
-- NOTE: modern Neovim (0.11+) dedupes diagnostics internally, and
-- the old vim.lsp.with()-based publishDiagnostics override is
-- deprecated, so it's intentionally not used here.
-- Which servers to set up (what Mason installs, what gets enabled,
-- and any per-server overrides) is defined in one table in
-- lua/config/lsp/servers.lua -- to add a language server, add it
-- there rather than here.
local servers = require("config.lsp.servers")
local server_names = vim.tbl_keys(servers)
table.sort(server_names)
require("mason").setup()
require("mason-lspconfig").setup({
ensure_installed = server_names,
})
local capabilities = require("cmp_nvim_lsp").default_capabilities()
-- Some clangd tweaks (e.g. several of its clang-tidy quick
-- fixes) are only sent to clients that advertise support for
-- resolving a code action's edit in a second round-trip, rather
-- than requiring the whole edit up front. cmp-nvim-lsp's
-- capabilities don't declare this on their own, so without it
-- clangd just leaves those actions out of the list entirely
-- instead of sending something the client can't apply.
capabilities.textDocument.codeAction = vim.tbl_deep_extend("force", capabilities.textDocument.codeAction or {}, {
dataSupport = true,
resolveSupport = { properties = { "edit" } },
})
-- Keymaps, applied per-buffer on LspAttach (lua/config/lsp/keymaps.lua).
require("config.lsp.keymaps").setup()
-- :LspRestart / :LspStart / :LspStop / :LspInfo / :LspLog. nvim-lspconfig
-- stopped providing these on Neovim 0.12 (core's own `:lsp` command
-- replaced them), so lua/config/lsp/commands.lua defines them as
-- aliases over the built-ins. It does nothing on older Neovim, where
-- nvim-lspconfig still registers the real ones.
require("config.lsp.commands").setup()
-- Setup LSPs via the built-in vim.lsp.config()/vim.lsp.enable()
-- API (replaces the deprecated require('lspconfig')[x].setup()
-- framework). Default per-server settings still come from
-- nvim-lspconfig, which only needs to be on the runtimepath.
for name, opts in pairs(servers) do
vim.lsp.config(name, vim.tbl_deep_extend("force", { capabilities = capabilities }, opts))
end
vim.lsp.enable(server_names)
end,
},
}
+130
View File
@@ -0,0 +1,130 @@
return {
-- Statusline
{
"nvim-lualine/lualine.nvim",
dependencies = { "nvim-tree/nvim-web-devicons" },
config = function()
require("lualine").setup({
options = {
-- "auto" derives lualine's colors from the active colorscheme's
-- own highlight groups -- koda doesn't ship a hand-built lualine
-- theme the way some more established colorschemes do, so this
-- is what keeps the statusline actually matching it instead of
-- falling back to lualine's generic default palette.
theme = "auto",
-- Powerline private-use-area glyphs (E0B0-E0B3). This needs
-- the terminal's font actually set to the PATCHED family
-- (e.g. "JetBrainsMono Nerd Font" / "CaskaydiaCove Nerd
-- Font", not plain "JetBrains Mono"/"Cascadia Code") -- if
-- these render as boxes/question marks, that's the fix.
-- The thin variants (E0B1/E0B3) are used between components
-- within a section so they pick up each component's colors
-- instead of a fixed one.
-- Powerline glyphs U+E0B0-U+E0B3, written as raw UTF-8 byte
-- escapes (not literal characters) so they can't get
-- silently stripped by an editor/tool that mishandles
-- Private Use Area codepoints.
component_separators = { left = "\238\130\177", right = "\238\130\179" },
section_separators = { left = "\238\130\176", right = "\238\130\178" },
globalstatus = true,
},
sections = {
lualine_a = {
"mode",
-- Only appears while recording a macro -- `cond` hides
-- the component (and its separator) entirely the rest of
-- the time, so it doesn't sit there empty.
{
function()
return "\226\151\143 REC @" .. vim.fn.reg_recording()
end,
cond = function()
return vim.fn.reg_recording() ~= ""
end,
color = { fg = "#e0af68", gui = "bold" },
},
},
lualine_b = {
-- Icon is baked into lualine's own "branch" component --
-- nothing authored here, so no glyph-encoding risk.
{ "branch" },
-- Moved in next to branch (was in lualine_x) so all the
-- git state reads as one cluster instead of being split
-- across opposite ends of the line.
{ "diff" },
},
lualine_c = {
-- separator = "" fuses this to the filename that follows
-- instead of leaving the icon boxed off by its own arrow
-- on both sides.
{ "filetype", icon_only = true, padding = { left = 1, right = 0 }, separator = "" },
{
"filename",
path = 0,
symbols = { modified = " \226\151\143", readonly = " [RO]", unnamed = "[No Name]" },
},
},
lualine_x = {
"searchcount",
"diagnostics",
-- Attached LSP client name(s) -- hidden via `cond` when
-- nothing's attached to the buffer.
{
function()
local names = {}
for _, client in ipairs(vim.lsp.get_clients({ bufnr = 0 })) do
table.insert(names, client.name)
end
return "\239\128\147 " .. table.concat(names, ", ")
end,
cond = function()
return #vim.lsp.get_clients({ bufnr = 0 }) > 0
end,
},
-- Word count, only for prose filetypes -- noise anywhere
-- else, so it's gated the same way.
{
function()
return "\194\182 " .. vim.fn.wordcount().words
end,
cond = function()
local ft = vim.bo.filetype
return ft == "markdown" or ft == "text" or ft == "gitcommit" or ft == "tex"
end,
},
},
lualine_y = {
"progress",
"location",
-- Encoding/line-ending: only shown when they DIFFER from
-- Neovim's own defaults (utf-8, unix) -- surfaces the
-- unusual case instead of confirming the boring default
-- on every single file.
{
function()
local enc = vim.bo.fileencoding ~= "" and vim.bo.fileencoding or vim.o.encoding
local parts = {}
if enc ~= "utf-8" then
table.insert(parts, enc)
end
if vim.bo.fileformat ~= "unix" then
table.insert(parts, vim.bo.fileformat)
end
return table.concat(parts, " ")
end,
cond = function()
local enc = vim.bo.fileencoding ~= "" and vim.bo.fileencoding or vim.o.encoding
return enc ~= "utf-8" or vim.bo.fileformat ~= "unix"
end,
},
},
lualine_z = {
function()
return " " .. os.date("%a %H:%M")
end,
},
},
})
end,
},
}
+28
View File
@@ -0,0 +1,28 @@
return {
-- Doxygen (and other language) doc-comment generation.
--
-- Not a language server -- clangd already understands Doxygen syntax
-- in existing comments (hover/signature-help render @param/@return),
-- this is the other half: generating the comment skeleton itself.
-- Place the cursor on/above a function and press <leader>cd to drop
-- in a Doxygen-style block with @brief/@param/@return stubs already
-- matching that function's actual parameters and return type.
{
"danymat/neogen",
dependencies = "nvim-treesitter/nvim-treesitter",
config = function()
require("neogen").setup({
enabled = true,
-- c/cpp's default annotation_convention is already
-- "doxygen_cpp", so nothing to override there.
snippet_engine = "luasnip", -- inserted fields become LuaSnip
-- tabstops, so <Tab>/<S-Tab> (wired up in the nvim-cmp config in
-- completion.lua for cmp/LuaSnip) cycles through @brief, each @param,
-- and @return.
})
vim.keymap.set("n", "<leader>cd", function()
require("neogen").generate()
end, { desc = "Generate Doxygen Comment" })
end,
},
}
+22
View File
@@ -0,0 +1,22 @@
return {
-- Nicer notifications (replaces the default vim.notify popups)
{
"rcarriga/nvim-notify",
config = function()
require("notify").setup({
background_colour = "#000000",
timeout = 3000,
-- Stack upward from the bottom instead of down from the top
-- (nvim-notify anchors to the right by default either way),
-- so notifications land bottom-right instead of top-right.
top_down = false,
-- Switched from "compact": that renderer has a documented
-- history of text overflow/wrapping bugs, and is the prime
-- suspect for the garbled/duplicated text seen in some
-- NvimTree error notifications. Not confirmed as the actual
-- cause, but worth trying alongside the position change.
})
vim.notify = require("notify")
end,
},
}
+216
View File
@@ -0,0 +1,216 @@
return {
-- NvimTree (On the right)
{
"nvim-tree/nvim-tree.lua",
dependencies = { "nvim-tree/nvim-web-devicons" },
config = function()
require("nvim-tree").setup({
view = {
side = "right",
width = 30,
},
-- Routes the paste overwrite/rename CHOICE through
-- vim.ui.select tagged with kind = "nvimtree_overwrite_rename"
-- (nvim-tree's own documented hook for this), which the gP
-- batch-paste below intercepts.
select_prompts = true,
-- Keep nvim-tree's own default mappings and add gP on top of
-- them, via the officially documented on_attach recipe --
-- rather than the TreeAttachedPost event this used before,
-- whose handler is passed the bufnr directly as a plain
-- number (confirmed from nvim-tree's own docs), not a table
-- with a .buf field -- indexing that number is what threw
-- "attempt to index local 'data' (a number value)".
on_attach = function(bufnr)
local api = require("nvim-tree.api")
-- The helper that installs the defaults has been named
-- differently across versions (api.config.mappings.
-- default_on_attach vs the older api.map.on_attach.default)
-- -- try both rather than assume one, so this doesn't
-- silently lose every default mapping on a version where
-- the other name is the real one.
if not pcall(api.config.mappings.default_on_attach, bufnr) then
pcall(api.map.on_attach.default, bufnr)
end
-- gP: paste, resolving every naming collision in this one
-- paste with a single :s///-style pattern/replacement
--
-- CAVEAT: this assumes a single-file-style overwrite/rename
-- choice per conflict. nvim-tree turns out to have a
-- SEPARATE, differently-shaped dialog specifically for
-- multi-file conflicts in one paste ("N file(s) already
-- exist" / Rename (suffix) / Overwrite all / Skip all),
-- which this doesn't detect or intercept -- so gP is only
-- confirmed reliable for a single conflicting file. For
-- "always rename every file, conflict or not" -- which is
-- what was actually being asked for -- gM below is the
-- better fit: it doesn't touch nvim-tree's paste/conflict
-- system at all, just copies marked files directly.
-- instead of nvim-tree's normal one-at-a-time
-- overwrite/rename prompt per conflicting file. Plain `p`
-- (bound above by default_on_attach) is untouched -- this
-- is a deliberately separate mapping, not a replacement.
--
-- nvim-tree doesn't expose a way to enumerate the clipboard
-- or pre-scan for collisions before pasting (only
-- print/clear it), and conflicts are resolved one at a time
-- internally as the paste runs -- there's no "here's the
-- whole batch" moment to hook into directly. This gets the
-- same practical result a different way: ask ONCE upfront
-- for the pattern, then answer nvim-tree's own conflict
-- prompts programmatically for the duration of that paste
-- using vim.fn.substitute() (real Vim regex, same engine
-- :s/// itself uses) against the filename nvim-tree
-- pre-fills as its default.
--
-- vim.ui.select/vim.ui.input are wrapped HERE, at call time
-- inside the keymap function, rather than once when this
-- plugin's config runs. nvim-tree loads eagerly at startup,
-- but dressing.nvim (which ALSO wraps these same two
-- functions, to prettify them) loads lazily on VeryLazy,
-- afterward -- wrapping once at config time would capture
-- the plain pre-dressing versions, which then get silently
-- overwritten (discarding this wrapper entirely) once
-- dressing finishes loading. Wrapping fresh on every call
-- instead always captures whatever's actually active.
vim.keymap.set('n', 'gP', function()
vim.ui.input({ prompt = "Rename pattern/replacement (Vim regex, e.g. foo/bar): " }, function(input)
if not input or input == "" then
api.fs.paste()
return
end
local pattern, replacement = input:match("^(.-)/(.*)$")
if not pattern then
vim.notify("Expected pattern/replacement, e.g. foo/bar", vim.log.levels.WARN)
return
end
local real_select = vim.ui.select
local real_input = vim.ui.input
vim.ui.select = function(items, opts, on_choice)
if opts and opts.kind == "nvimtree_overwrite_rename" then
for i, item in ipairs(items) do
if tostring(item):lower():match("rename") then
on_choice(item, i)
return
end
end
end
real_select(items, opts, on_choice)
end
vim.ui.input = function(input_opts, on_confirm)
if input_opts and input_opts.default then
local ok, new_name = pcall(vim.fn.substitute, input_opts.default, pattern, replacement, "")
on_confirm(ok and new_name ~= "" and new_name or input_opts.default)
return
end
real_input(input_opts, on_confirm)
end
-- Restored via a short defer rather than immediately
-- after api.fs.paste() returns, since it's not
-- confirmed whether paste() resolves every prompt
-- synchronously before returning to caller or defers
-- some of them -- restoring immediately risked only
-- patching the FIRST conflict in a multi-file paste and
-- silently falling back to normal prompts for the rest.
-- If that's what happens, this assumption is the next
-- thing to revisit.
api.fs.paste()
vim.defer_fn(function()
vim.ui.select = real_select
vim.ui.input = real_input
end, 2000)
end)
end, { desc = "nvim-tree: Paste (Batch Rename Regex)", buffer = bufnr, silent = true })
-- Clears the copy/cut clipboard outright. Useful if it's
-- holding a stale entry -- a file that's since been
-- renamed, moved, or deleted -- which pastes as "ENOENT: no
-- such file or directory" until cleared and re-copied fresh.
vim.keymap.set('n', 'gC', api.fs.clear_clipboard, { desc = "nvim-tree: Clear Clipboard", buffer = bufnr, silent = true })
-- gM: copy every MARKED file/folder (toggle a mark with m,
-- shown as a star) into the directory under the cursor,
-- applying a single :s///-style pattern/replacement to
-- EVERY name unconditionally -- not just on collision. This
-- is deliberately separate from nvim-tree's own copy/paste
-- clipboard (c/x/p/gP above) entirely: it reads the marked
-- list via api.marks.list() (a real, documented, stable
-- API -- unlike the clipboard, which has no equivalent way
-- to enumerate its contents) and shells out to `cp -r` to
-- do the actual copying itself, the same approach used in
-- nvim-tree's own official custom-copy recipe. Marks are
-- left as-is afterward (not auto-cleared), matching how the
-- regular clipboard also isn't cleared after a paste --
-- toggle them off individually with m if you're done with
-- them.
vim.keymap.set('n', 'gM', function()
local marks = api.marks.list()
if not marks or #marks == 0 then
vim.notify("No marked files (mark with 'm' first)", vim.log.levels.WARN)
return
end
local cursor_node = api.tree.get_node_under_cursor()
if not cursor_node then
return
end
local dest_dir = cursor_node.type == "directory" and cursor_node.absolute_path
or vim.fn.fnamemodify(cursor_node.absolute_path, ":h")
vim.ui.input({ prompt = "Rename pattern/replacement (Vim regex, e.g. Foo/Bar): " }, function(input)
if not input or input == "" then
return
end
local pattern, replacement = input:match("^(.-)/(.*)$")
if not pattern then
vim.notify("Expected pattern/replacement, e.g. Foo/Bar", vim.log.levels.WARN)
return
end
for _, node in ipairs(marks) do
if not vim.loop.fs_stat(node.absolute_path) then
-- Marks persist by path in nvim-tree's own state and
-- are NOT cleared when the underlying file/folder is
-- deleted, so a stale mark can point at a path that
-- no longer exists (e.g. you marked it, then deleted
-- it). Clean up the stale mark itself here instead
-- of attempting (and failing) a copy from it.
pcall(api.marks.toggle, node)
vim.notify("Skipped stale mark (no longer exists): " .. node.absolute_path, vim.log.levels.WARN)
else
local base = vim.fn.fnamemodify(node.absolute_path, ":t")
local ok, new_name = pcall(vim.fn.substitute, base, pattern, replacement, "")
if not ok or new_name == "" then
new_name = base
end
local dest_path = dest_dir .. "/" .. new_name
if dest_path == node.absolute_path then
-- The pattern didn't match this particular name
-- (so it came back unchanged) and the destination
-- is the same directory the file's already in --
-- that's a copy onto itself, which cp correctly
-- refuses. Skip it up front with a clearer reason
-- instead of surfacing cp's raw "same file" error.
vim.notify("Skipped " .. base .. ": pattern didn't match, and destination is the same as source", vim.log.levels.WARN)
else
local result = vim.fn.system({ "cp", "-r", node.absolute_path, dest_path })
if vim.v.shell_error ~= 0 then
vim.notify("Copy failed for " .. base .. ": " .. result, vim.log.levels.ERROR)
end
end
end
end
api.tree.reload()
end)
end, { desc = "nvim-tree: Copy Marked (Regex Rename)", buffer = bufnr, silent = true })
end,
})
vim.keymap.set('n', '<leader>e', ':NvimTreeToggle<CR>', { silent = true, desc = "Toggle File Explorer" })
end,
},
}
@@ -0,0 +1,25 @@
return {
-- Animated "smear" cursor trail, terminal only -- Neovide has its
-- own native, GPU-accelerated cursor VFX (see the `if vim.g.neovide`
-- block in init.lua), and running both at once would just be two
-- animation systems fighting over the same cursor. `cond` here means
-- this plugin doesn't even load under Neovide. Tuning below is
-- upstream's own suggested starting point.
{
"sphamba/smear-cursor.nvim",
cond = not vim.g.neovide,
config = function()
require("smear_cursor").setup({
cursor_color = "#ffffff",
never_draw_over_target = true,
smear_insert_mode = false,
min_vertical_distance_smear = 2,
min_horizontal_distance_smear = 2,
time_interval = 17, -- ms
stiffness = 0.9,
trailing_stiffness = 0.4,
damping = 0.99, -- stops bouncing
})
end,
},
}
@@ -0,0 +1,21 @@
return {
-- Telescope (Fuzzy Finding and Grepping)
{
"nvim-telescope/telescope.nvim",
-- NOTE: intentionally NOT pinned to branch = "0.1.x" -- that branch's
-- previewer calls nvim-treesitter's now-removed `ft_to_lang` helper,
-- which crashes the file preview with "attempt to call field
-- 'ft_to_lang' (a nil value)". The current default branch calls the
-- built-in vim.treesitter.language.get_lang() instead, which isn't
-- affected by that removal.
dependencies = { "nvim-lua/plenary.nvim" },
config = function()
local builtin = require('telescope.builtin')
vim.keymap.set('n', '<leader>ff', builtin.find_files, { desc = "Find Files" })
vim.keymap.set('n', '<leader>fw', builtin.live_grep, { desc = "Find Word (Live Grep)" })
vim.keymap.set('n', '<leader>fb', builtin.buffers, { desc = "Find Open Buffers" })
vim.keymap.set('n', '<leader>fh', builtin.help_tags, { desc = "Find Help Tags" })
vim.keymap.set('n', '<leader>fr', builtin.oldfiles, { desc = "Find Recent Files" })
end,
},
}
@@ -0,0 +1,20 @@
return {
-- ToggleTerm (Floating & Bottom Terminal)
{
"akinsho/toggleterm.nvim",
version = "*",
config = function()
-- shell = "fish": the embedded terminal launches fish instead of
-- your login shell. fish has grey inline "ghost text" suggestions
-- built in (paths, directory contents, and history) with zero
-- config -- accept with -> or <End>. This only affects terminals
-- opened from Neovim; your system login shell is untouched.
require("toggleterm").setup({
shell = "fish",
})
vim.keymap.set('n', '<leader>tf', '<cmd>ToggleTerm direction=float<cr>', { desc = "Floating Terminal" })
vim.keymap.set('n', '<leader>th', '<cmd>ToggleTerm direction=horizontal<cr>', { desc = "Bottom Terminal" })
vim.keymap.set('n', '<leader>tv', '<cmd>ToggleTerm direction=vertical size=40<cr>', { desc = "Side Terminal" })
end,
},
}
@@ -0,0 +1,44 @@
return {
-- Treesitter (syntax-aware highlighting + indentation)
--
-- NOTE: pinned to branch = "master". Upstream's default branch is now
-- "main", which is a full incompatible rewrite that deleted the old
-- require("nvim-treesitter.configs").setup({...}) API entirely (this
-- is what caused "module 'nvim-treesitter.configs' not found"). The
-- "master" branch is explicitly kept frozen by upstream for backward
-- compatibility with this config style, so pin to it rather than
-- rewriting everything against the new API.
{
"nvim-treesitter/nvim-treesitter",
branch = "master",
build = ":TSUpdate",
config = function()
require("nvim-treesitter.configs").setup({
ensure_installed = {
"c", "cpp", "lua", "go", "python", "javascript", "typescript",
"html", "css", "bash", "json", "yaml", "markdown", "cmake",
"doxygen",
},
highlight = { enable = true },
indent = {
enable = true,
-- treesitter's indent module is well-known to be unreliable
-- for a handful of languages specifically (inconsistent
-- brace placement, phantom extra indent on nested blocks)
-- while being fine for the rest. This is more pronounced on
-- the "master" branch pinned above, since it's frozen and
-- gets no further upstream fixes at all. Disabling it for
-- these falls back to Neovim's own built-in, per-filetype
-- indent logic (cindent for c/cpp, the bundled javascript/
-- typescript indent scripts for the rest), which is far
-- more predictable for brace-heavy code.
--
-- css and json are also curly-brace languages that CAN hit
-- the same class of issue -- add them here too if you
-- notice the same symptom there.
disable = { "c", "cpp", "javascript", "typescript" },
},
})
end,
},
}
@@ -0,0 +1,25 @@
return {
-- Which-Key (Registers your namespaces so the menu looks clean)
{
"folke/which-key.nvim",
event = "VeryLazy",
config = function()
local wk = require("which-key")
wk.setup({
delay = 0,
})
-- Define group names for the menu
wk.add({
{ "<leader>b", group = "Buffers" },
{ "<leader>c", group = "Comments" },
{ "<leader>f", group = "Find / Grep" },
{ "<leader>l", group = "LSP" },
{ "<leader>o", group = "OOP" },
{ "<leader>s", group = "Splits" },
{ "<leader>t", group = "Terminal" },
{ "<leader>d", group = "Debug" },
{ "<leader>D", group = "Database" },
})
end,
},
}
+20 -8
View File
@@ -82,12 +82,17 @@ else
echo "Run this script directly (not piped) and choose option 2 or 3 to also install Neovide." echo "Run this script directly (not piped) and choose option 2 or 3 to also install Neovide."
fi fi
# The MariaDB *client* (not the server) is installed too: vim-dadbod runs
# queries by shelling out to the `mariadb` binary. The package name differs
# per distro -- Fedora: mariadb, Debian/Ubuntu: mariadb-client, Arch:
# mariadb-clients. To actually query something you also need a server
# running somewhere; this script neither installs nor starts one.
case "$DISTRO_FAMILY" in case "$DISTRO_FAMILY" in
fedora) fedora)
sudo dnf install -y neovim git curl wget gcc gcc-c++ make cmake \ sudo dnf install -y neovim git curl wget gcc gcc-c++ make cmake \
nodejs npm python3-pip ripgrep fd-find \ nodejs npm python3-pip ripgrep fd-find \
golang clang-tools-extra dotnet-sdk-8.0 unzip fontconfig \ golang clang-tools-extra dotnet-sdk-8.0 unzip fontconfig \
lldb gdb fish lldb gdb fish mariadb
;; ;;
debian) debian)
@@ -100,7 +105,7 @@ case "$DISTRO_FAMILY" in
sudo apt-get install -y neovim git curl wget gcc g++ make cmake \ sudo apt-get install -y neovim git curl wget gcc g++ make cmake \
nodejs npm python3-pip ripgrep fd-find \ nodejs npm python3-pip ripgrep fd-find \
golang-go clang-tools unzip fontconfig \ golang-go clang-tools unzip fontconfig \
lldb gdb fish lldb gdb fish mariadb-client
# fd-find installs its binary as `fdfind` on Debian/Ubuntu (a # fd-find installs its binary as `fdfind` on Debian/Ubuntu (a
# name clash with an unrelated existing package called `fd`), # name clash with an unrelated existing package called `fd`),
@@ -151,7 +156,7 @@ case "$DISTRO_FAMILY" in
sudo pacman -S --noconfirm neovim git curl wget gcc make cmake \ sudo pacman -S --noconfirm neovim git curl wget gcc make cmake \
nodejs npm python-pip ripgrep fd \ nodejs npm python-pip ripgrep fd \
go clang dotnet-sdk-8.0 unzip fontconfig \ go clang dotnet-sdk-8.0 unzip fontconfig \
lldb gdb fish lldb gdb fish mariadb-clients
;; ;;
esac esac
@@ -262,12 +267,16 @@ fi
# embedded in this script -- edit those files directly to customize). # embedded in this script -- edit those files directly to customize).
echo "[4/4] Installing new Neovim configuration..." echo "[4/4] Installing new Neovim configuration..."
NVIM_DIR="$HOME/.config/nvim" NVIM_DIR="$HOME/.config/nvim"
mkdir -p "$NVIM_DIR/lua/plugins" mkdir -p "$NVIM_DIR"
mkdir -p "$NVIM_DIR/lua/dap/configurations"
cp "$CONFIG_DIR/nvim/init.lua" "$NVIM_DIR/init.lua" # Copy the whole config/nvim tree (the trailing "/." copies the directory's
cp "$CONFIG_DIR/nvim/lua/plugins/init.lua" "$NVIM_DIR/lua/plugins/init.lua" # contents, dotfiles included, into the existing target) rather than naming
cp "$CONFIG_DIR/nvim/lua/dap/configurations/cmake.lua" "$NVIM_DIR/lua/dap/configurations/cmake.lua" # files one by one. The config is split into many small modules -- one file
# per plugin in lua/plugins/, helpers in lua/config/ -- and a new module
# dropped anywhere under config/nvim/ gets installed without touching this
# script. The target was wiped just above, so nothing stale can linger from
# an older layout (such as the old monolithic lua/plugins/init.lua).
cp -r "$CONFIG_DIR/nvim/." "$NVIM_DIR/"
# --- ~/.clang-format, ~/.clang-tidy, and ~/cpp-style-guide.md --- # --- ~/.clang-format, ~/.clang-tidy, and ~/cpp-style-guide.md ---
# Personal fallback style, implementing a specific C/C++ coding style # Personal fallback style, implementing a specific C/C++ coding style
@@ -301,6 +310,9 @@ echo " Run 'nvim'. p and P are restored, use <leader>p to force inline paste!"
echo " Debugger: <leader>db to set a breakpoint, F5 / <leader>dc to start/continue," echo " Debugger: <leader>db to set a breakpoint, F5 / <leader>dc to start/continue,"
echo " <leader>du to toggle the debug UI. First debug session will auto-install codelldb via Mason." echo " <leader>du to toggle the debug UI. First debug session will auto-install codelldb via Mason."
echo " Project-specific CMake debug targets loaded from ~/.config/nvim/lua/dap/configurations/cmake.lua" echo " Project-specific CMake debug targets loaded from ~/.config/nvim/lua/dap/configurations/cmake.lua"
echo " Database (MariaDB): <leader>Du opens the DB UI; press A there to add a connection"
echo " (URL form: mariadb://user:password@127.0.0.1:3306/dbname). The sqls language server is"
echo " installed on first launch; see README.md ('Database (MariaDB)') for its optional config."
if [ "$NVIM_FRONTEND" = "desktop" ] || [ "$NVIM_FRONTEND" = "both" ]; then if [ "$NVIM_FRONTEND" = "desktop" ] || [ "$NVIM_FRONTEND" = "both" ]; then
echo " Neovide: run 'neovide' to launch the desktop app (same config, same keymaps)." echo " Neovide: run 'neovide' to launch the desktop app (same config, same keymaps)."
echo " If the command isn't found yet, open a new shell first (rustup updates PATH" echo " If the command isn't found yet, open a new shell first (rustup updates PATH"