# Neovim Setup A scripted Neovim setup built around C/C++ development (CMake + clangd + codelldb), with LSP support for Go, C#, TypeScript/JavaScript, HTML, and CSS alongside it. `setup_neovim.sh` handles installing dependencies and putting everything in place; the actual config lives in `config/` as plain files, not embedded in the script. Supports **Fedora, Ubuntu, Debian, and Arch Linux** (and their common derivatives — Pop!_OS, Linux Mint, Manjaro, EndeavourOS, etc. — via `/etc/os-release`'s `ID_LIKE`) — the script detects which one you're on and uses the right package manager and package names automatically. ## Contents - [Installing](#installing) - [What gets installed](#what-gets-installed) - [Config layout & adding plugins](#config-layout--adding-plugins) - [Terminal vs. desktop (Neovide)](#terminal-vs-desktop-neovide) - [Editor basics](#editor-basics) - [Look & feel](#look--feel) - [Buffers & tabs](#buffers--tabs-bufferline) - [Text search (Telescope)](#text-search-telescope) - [File explorer (nvim-tree)](#file-explorer-nvim-tree) - [LSP & code intelligence](#lsp--code-intelligence) - [Rule-of-five boilerplate](#rule-of-five-boilerplate) - [Autocompletion & snippets](#autocompletion--snippets) - [Documentation generation](#documentation-generation) - [Debugging (nvim-dap)](#debugging-nvim-dap) - [Terminal](#terminal) - [Git](#git) - [Database (MariaDB)](#database-mariadb) - [Hex viewer](#hex-viewer) - [Formatting and linting](#formatting-and-linting) - [Known quirks](#known-quirks) ## Installing ```bash chmod +x setup_neovim.sh ./setup_neovim.sh ``` Keep `setup_neovim.sh` and the `config/` folder next to each other — the script only installs dependencies and copies `config/`'s contents into place; it exits with an error if it can't find `config/` beside itself. To customize anything (keymaps, plugins, the theme, the clang-tidy/format rules), edit the files under `config/` directly rather than the script — see [What gets installed](#what-gets-installed) for where each one ends up. **This deletes `~/.config/nvim` and `~/.local/share/nvim` before writing the new config** — there is no backup step. If you have an existing Neovim setup you care about, copy it elsewhere first. The script: 1. Detects your distro, asks which Neovim frontend(s) you want (see [Terminal vs. desktop (Neovide)](#terminal-vs-desktop-neovide)), and installs system packages via the right manager (`dnf`/`apt`/`pacman`), plus two Nerd Fonts (JetBrainsMono, Cascadia Code) for icon glyphs. 2. Wipes and rewrites `~/.config/nvim`. 3. Copies personal `~/.clang-format`/`~/.clang-tidy` fallbacks and `~/cpp-style-guide.md` into place (see [Formatting and linting](#formatting-and-linting)). First launch takes a minute or two: `lazy.nvim` bootstraps itself and installs all plugins, then Mason installs the language servers and `codelldb`. ### Distro-specific notes - **Debian**: the .NET SDK isn't in Debian's own repos, so the script adds Microsoft's apt feed just for that one package. This step is best-effort — if it fails, the rest of the script still completes; see the printed warning for manual install instructions (only matters for C#/omnisharp). - **Ubuntu/Debian**: `fd-find`'s binary is named `fdfind` (a package-name clash with something unrelated), not `fd` like on Fedora/Arch. The script symlinks `~/.local/bin/fd` to it so anything expecting a plain `fd` on `PATH` (e.g. Telescope's file finder) works the same as everywhere else — make sure `~/.local/bin` is actually on your `PATH`. - **Arch**: the script runs a full `pacman -Syu` before installing anything new, per Arch's own guidance against partial upgrades — this does mean it'll upgrade your existing packages too, not just add new ones. - **Older distro releases** (Debian stable, older Ubuntu LTS): this config relies on Neovim 0.11+ APIs throughout. If your distro's packaged Neovim is older, the script prints a warning after install — see [Neovim's install docs](https://github.com/neovim/neovim/blob/master/INSTALL.md) for an AppImage/PPA/prebuilt build if so. - **Neovide** (if you choose it — see [Terminal vs. desktop (Neovide)](#terminal-vs-desktop-neovide)): Arch installs the packaged `neovide` binary directly; Fedora and Debian/Ubuntu have no native package for it, so the script builds it from source via `cargo` (installing Rust via `rustup` first if `cargo` isn't already on `PATH`), per [Neovide's own install docs](https://neovide.dev/installation.html#linux-source). This can take several minutes and, like the .NET SDK step, is best-effort — a failed build prints a warning rather than stopping the rest of the script. ## What gets installed 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 - **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, sqls (via Mason), plus `typescript-language-server` and `vscode-langservers-extracted` via npm - **Debug adapter**: codelldb (auto-installed by Mason on first debug session) - **Neovide**, optionally — see [Terminal vs. desktop (Neovide)](#terminal-vs-desktop-neovide) Everything the script *writes* rather than installs comes from `config/`, copied into place as-is: | `config/` | Copied to | |---|---| | `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-tidy` | `~/.clang-tidy` | | `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) The installer asks which Neovim frontend(s) to set up: 1. **Terminal-based only** — just `nvim`, run inside whatever terminal emulator you already use. This is the default if the script is run non-interactively (piped rather than executed directly). 2. **Desktop application only** — installs [Neovide](https://neovide.dev/) on top. Neovide is a separate GUI window, but it isn't a separate editor — it runs the real `nvim` binary as its backend, so that's installed either way regardless of which option you pick. 3. **Both.** There's only one config either way (`config/nvim/`) — Neovide reads the exact same `init.lua`, so keymaps, plugins, and the LSP setup are all identical between the two. Two things differ between them — cursor animation and background transparency — and the config detects which frontend it's running under (`vim.g.neovide`) and picks the right mechanism for each automatically: - **Terminal**: [`smear-cursor.nvim`](https://github.com/sphamba/smear-cursor.nvim) draws the animated cursor trail — a terminal has no native equivalent, so this fills that gap. `vscode.nvim`'s `transparent` option is on, clearing highlight-group backgrounds so whatever the *terminal emulator* draws behind Neovim shows through — including a background image, if your terminal supports one (e.g. kitty's own `background_image` setting). That's a terminal-level feature this config has no reach into; without it configured on the terminal side, transparency here just shows the terminal's normal background color, which is a harmless no-op look. - **Neovide**: uses Neovide's own built-in, GPU-accelerated cursor VFX (`railgun` mode) instead of smear-cursor.nvim — running both at once would be two different animation systems fighting over the same cursor, so smear-cursor.nvim doesn't even load when `vim.g.neovide` is set. For background: Neovide has no background-image setting at all — two attempts to add one ([neovide/neovide#2419](https://github.com/neovide/neovide/pull/2419), [#3067](https://github.com/neovide/neovide/pull/3067)) were both closed as abandoned, most recently in October 2025; the feature request itself ([neovide/neovide#342](https://github.com/neovide/neovide/issues/342)) is still open. `vim.g.neovide_opacity` (set to `0.9`) is the closest real substitute — it makes the whole window translucent, so on a compositing window manager, whatever's behind it (your desktop wallpaper, if Neovide isn't covering the whole screen) blends through. Adjust or remove it in `init.lua` to taste. Once installed, launch the desktop app with `neovide` from a terminal — if that command isn't found right after setup, open a new shell first (a fresh `rustup` install updates `PATH` via shell startup files, which the setup script's own shell won't have re-read). ## Editor basics - Relative + absolute line numbers, system clipboard integration (`unnamedplus`), persistent undo, smart-case search, always-on sign column. - Tab width: **4**, using real tab characters — not expanded to spaces. - `c`/`cpp`/`javascript`/`typescript` indentation uses Neovim's own built-in per-filetype indent logic (`cindent` for c/cpp, the bundled JS/TS indent scripts for the rest) rather than treesitter's indent module — treesitter's indent is known to be unreliable for these specifically (inconsistent brace placement, phantom extra indent on nested blocks), more so since the `nvim-treesitter` branch this config uses is frozen upstream (see [Known quirks](#known-quirks)). `css`/`json` are also curly-brace languages that can hit the same issue — worth disabling there too if it comes up. - Comments don't auto-continue onto a new line when the current line has real code on it — pressing Enter or `o`/`O` there starts a plain new line. On a comment-*only* line, Enter continues it with the same marker (`//`, `#`, or `--`); pressing Enter a second time with nothing typed breaks out instead of continuing again, clearing that line — same convention Vim itself has always used for this. - Bracket/quote pairs auto-close as you type: `()`, `{}`, `[]`, `""`, `''`. - True color, rounded borders on floating windows (hover, LspInfo, Mason, etc. — Neovim 0.11+), styled window separators. - OSC 52 clipboard support, so yank/paste works correctly over SSH. ### General keymaps | Key | Mode | Action | |---|---|---| | `` | Normal | Clear search highlight | | `p` | Normal | Force inline paste (paste as characters at cursor, ignoring line-wise register type) | | `p` / `P` | Visual | Paste over selection without clobbering the unnamed register | | `J` / `K` | Visual | Move selected lines down / up | | `` / `` | Normal/Insert/Visual | Move current line (or selection) down / up | | `cb` | Normal | Clear the current line's content, leaving it blank in place (doesn't delete the line itself) | | `gcc` / `gc{motion}` | Normal | Toggle comment on the current line / over a motion (e.g. `gcip` for a paragraph) — Neovim's native 0.10+ commenting, untouched | | `gc` | Visual | Toggle comment on the selection | | `c/` | Normal/Visual | Same toggle as `gcc`/`gc` above, as a second which-key-discoverable entry point under the "Comments" group | | `` | Normal/Visual/Insert | Save file | | `` | Normal | Move to left/lower/upper/right window | | `sv` / `sh` | Normal | Split vertically / horizontally | | `se` | Normal | Equalize split sizes | | `sx` | Normal | Close current split | ## Look & feel - **Theme**: [`vscode.nvim`](https://github.com/Mofiqul/vscode.nvim) (dark variant, VS Code Dark+ palette), on a near-black `#090909` background (explorer, tabs, statusline and popups use near-black shades too, not VS Code's grays), with macros in Visual Studio's muted purple (`#BEB7FF`) and custom tinted backgrounds for error/warn/info/hint diagnostic virtual lines. lualine uses the plugin's own `vscode` theme. - **Cursor animation**: an animated cursor trail in both frontends, via different mechanisms — see [Terminal vs. desktop (Neovide)](#terminal-vs-desktop-neovide) for which plugin does what and why. - **Dashboard**: `alpha-nvim` start screen with quick actions — find file, recent files, live grep, new file, edit config, quit. - **Statusline**: `lualine`, styled with true Powerline arrow separators (the seamless, edge-to-edge kind — between sections, a thinner version between components within a section). Left to right: - Mode, plus a macro-recording indicator (`● REC @`) that only appears while a macro is actually being recorded. - Git branch and a diff summary (added/changed/removed), grouped together as one git cluster instead of being split across the line. - File-type icon fused directly to the filename with no separator between them — it reads as one unit rather than a boxed-off icon — plus a `●` for unsaved changes and `[RO]` for read-only buffers. - Search count, diagnostics, the name(s) of any attached LSP client(s), and a word count that only shows up on prose filetypes (markdown, text, tex, commit messages). - Progress, cursor location, and the file's encoding/line-ending — but only when they differ from Neovim's own defaults (utf-8, unix), so a normal file doesn't clutter the line confirming the obvious. - A clock with the abbreviated weekday. - The Powerline glyphs live in the E0B0–E0B3 Unicode range, part of the same Nerd Font patch as the file/branch icons elsewhere in the statusline — if the icons render, the arrows will too. If they instead show as boxes or `?`, it means the terminal's font setting isn't pointed at the *patched* font family specifically (Nerd Fonts rename it, e.g. "CaskaydiaCove Nerd Font" rather than plain "Cascadia Code"). Verify with `echo -e "\ue0b0"` in a terminal, outside Neovim entirely — if that's blank, fix the terminal's font setting rather than the config. - **Indent guides**: `indent-blankline` — colored guide per indent level, plus the current lexical scope (the block your cursor is inside) is highlighted with its own guide color. The underline that used to mark the scope's start/end line is disabled (`show_start`/`show_end = false`) — just the colored guide remains. - **Inline color previews**: `nvim-colorizer` shows hex/rgb color values with their actual color as a background. - **Notifications**: `nvim-notify` replaces the default notification popups with floating, styled ones, positioned bottom-right. - **Prompts**: `dressing.nvim` upgrades the rename prompt, breakpoint-condition prompt, and DAP path prompts to floating dialogs instead of plain command-line input. ## Buffers & tabs (`bufferline`) Each buffer tab shows its ordinal position number. Pinned buffers are always kept leftmost as a block. | Key | Action | |---|---| | `` / `` | Previous / next buffer (follows the order shown on screen, including any reordering) | | `` … `` / ``–`` | Jump directly to the buffer at that position. `` and `` each pull double duty with the debugger (see [Debugging](#debugging-nvim-dap)) — they act as buffer jumps at rest, and hand themselves over to DAP for the duration of an active debug session. | | `` / `` | Jump to the last buffer | | `` / `` | Move current buffer left / right (blocked from crossing into the pinned block — wraps to the other end instead) | | `bp` | Toggle pin on current buffer | | `bx` | Close every **unpinned** buffer with **no unsaved changes** (pinned buffers and anything modified are left alone) | | `bd` | Close current buffer | Setting an explicit `` mapping here also overrides Neovim's built-in ``-opens-help default — any explicit keymap for a key always takes precedence over Neovim's default for it. ## Text search (`Telescope`) | Key | Action | |---|---| | `ff` | Find files | | `fw` | Live grep | | `fb` | List open buffers | | `fh` | Search help tags | | `fr` | Recent files | ## File explorer (`nvim-tree`) `e` toggles the file explorer sidebar (right side). On top of nvim-tree's own defaults (`c`/`x`/`p` to copy/cut/paste, `m` to mark, `d` to delete, etc.), three custom commands: | Key | Action | |---|---| | `gP` | Paste, resolving every naming **collision** in that paste with one `pattern/replacement` (Vim regex — same as `:s///`) instead of nvim-tree's normal one-at-a-time overwrite/rename prompt per conflicting file. Plain `p` is untouched. Only confirmed reliable for a **single** conflicting file at a time — see [Known quirks](#known-quirks). | | `gM` | Copy every **marked** file/folder (mark with `m`, shown as a star) into the directory under the cursor, applying that same `pattern/replacement` regex to **every** name unconditionally — not just on collision. Doesn't touch nvim-tree's clipboard at all; reads the mark list directly and shells out to `cp -r` to do the copying itself. Marks aren't auto-cleared afterward — toggle them off individually with `m` when done. | | `gC` | Clear the copy/cut clipboard outright. Useful if it's holding a stale entry (a file since renamed, moved, or deleted), which otherwise pastes as `ENOENT: no such file or directory` until cleared and re-copied fresh. | `gP` and `gM` both fail safely rather than silently: a stale mark (pointing at a since-deleted path) is skipped and cleaned up automatically instead of attempted, and a rename that would produce a no-op copy-onto-itself is skipped with a clear reason instead of surfacing a raw `cp` error. ## LSP & code intelligence 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 | |---|---| | `gd` | Go to definition. Fuzzy picker with preview if there's more than one match (e.g. overloads); jumps directly for a single match. **On an `#include` line, opens the header file directly** — falls back to Vim's built-in `gf` if clangd can't resolve it. | | `gD` | Go to declaration. Same `#include`-awareness and fallback as `gd`. | | `gr` | Find all references, in a fuzzy picker with preview. | | `K` | Hover documentation (renders Doxygen comments if present). | | `lr` | Rename symbol (prompts for new name, safe against double-firing, refuses macros/namespaces — see note below) | | `la` | Code actions at the cursor (normal mode) or over a selection (visual mode — needed for tweaks like Extract Function/Variable, which require an actual range). Also catches a diagnostic one line above/below the cursor, not just the exact line — see note below. | | `lt` | Run `clang-tidy --fix` on the file directly (not through clangd — see [Formatting and linting](#formatting-and-linting) for why), then reload the buffer | | `lf` | Format file (via clangd/LSP formatting — see [Formatting and linting](#formatting-and-linting) for the style used) | | `li` | Show LSP client info | | `]d` / `[d` | Jump to next/previous diagnostic, any severity | | `]e` / `[e` | Jump to next/previous error | | `]w` / `[w` | Jump to next/previous warning | | `]t` / `[t` | Jump to next/previous tip (LSP's "Hint" severity) | | `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 `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 `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. **`lt` and the same compile database:** `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. **`lr` and macros/namespaces:** clangd doesn't support renaming macros or namespaces at all ([clangd/clangd#1890](https://github.com/clangd/clangd/issues/1890), still open) — it's supposed to just reply with an error, but has a separate crash bug where it can take the whole `clangd` process down (SIGSEGV) while handling that rejection instead. `lr` checks with treesitter whether the cursor is on a macro or namespace's own definition line first and refuses locally if so, before clangd ever sees the request. That catches the common case, but not every one — renaming a macro from where it's *used* rather than defined still reaches clangd, since a macro usage is just plain text to treesitter. If clangd crashes anyway, `:LspRestart` brings it back; renaming a macro with `:%s/\/new/g` is the right tool regardless, since it's pure text substitution with no scope for clangd to resolve. **A second, unrelated way to come up empty:** cursor position. `la`'s underlying LSP request is normally scoped to diagnostics on the *exact* cursor line — easy to miss given that virtual_lines (see [Look & feel](#look--feel)) renders a diagnostic's message as an extra line directly below its own line, i.e. right where the next real line of code sits, making it look like it "belongs" to wherever the cursor already is. `la` now also catches a diagnostic one line above or below the cursor for exactly this reason, so this should come up much less than before — but for anything further away, jump to it first with `]e`/`]w`/`]t` (see the diagnostic-navigation rows above) before requesting code actions. ## Rule-of-five boilerplate On a C++ buffer, `oc` / `om` insert the copy or move constructor + assignment operator pair, defaulted, for whichever class or struct the cursor is currently inside: ```cpp Foo(const Foo&) = default; Foo& operator=(const Foo&) = default; ``` The enclosing class is found via Treesitter by walking up from the cursor — no need to have the class's own name line in view, and nested classes resolve to the innermost one. `= default` covers the overwhelming majority of real rule-of-five cases; delete it and write a body for the rare one that needs custom logic. C++ only (no move semantics in C, so this doesn't apply to `.c` buffers). ## Autocompletion & snippets `nvim-cmp` with LSP, snippet, buffer, and path sources. - **Ghost text**: the top completion candidate previews inline after the cursor as you type — including path/directory completions (type `./` or `/home/` and see what's in that directory before you finish typing it). - **`` / ``**: cycle the completion menu when it's open; otherwise, jump forward/backward through snippet placeholders (e.g. clangd's function-argument placeholders after accepting a call like `memcpy(...)`) if you're inside one. - **``**: confirm the selected completion. - **`////`**: 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. - **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 `neogen` generates Doxygen-style comment skeletons — `cd` on/above a function drops in `@brief`/one `@param` per argument/`@return` matching that function's real signature. Fields are inserted as snippet placeholders, so ``/`` cycles through them the same way as any other snippet. (There's no such thing as an actual "Doxygen language server" — clangd already understands Doxygen comment syntax natively for hover/signature help. This only handles generating the comment itself.) ## Debugging (`nvim-dap`) C/C++ (and Rust) debugging via `codelldb`, auto-installed by Mason on first use. UI (`nvim-dap-ui`) opens automatically when a session starts and closes when it ends; variable values show inline via virtual text. | Key | Action | |---|---| | `F5` / `dc` | Continue / start debugging — `F5` only while a session is already active; before that it's the "jump to buffer 5" shortcut, so **start a session with `dc` or `db`**, not `F5` | | `F10` / `do` | Step over — `F10` only while a debug session is active; otherwise it's the "jump to last buffer" shortcut (see [Buffers & tabs](#buffers--tabs-bufferline)) | | `F11` / `di` | Step into | | `F12` / `dO` | Step out | | `db` | Toggle breakpoint | | `dB` | Conditional breakpoint | | `dr` | Open the debug REPL | | `dl` | Re-run the last debug configuration | | `dt` | Terminate the session | | `du` | Toggle the debug UI | | `dh` | Hover the variable under the cursor | Default launch configs prompt for a path to the executable, or let you attach to a running process. `~/.config/nvim/lua/dap/configurations/cmake.lua` additionally defines project-specific configs for CMake-built targets (`build/tests/test_dynamic_library`, `build/demo/example_host`) — this file is a template with this repo's specific binary names/paths and will need editing for a different project. ## Terminal | Key | Action | |---|---| | `tf` | Floating terminal | | `th` | Horizontal terminal (bottom) | | `tv` | Vertical terminal (side) | Neovim's embedded terminal launches **fish** instead of your login shell, purely for its built-in inline "ghost text" suggestions for paths and command history (no config needed, accept with `→`/`End`) — a terminal-side equivalent of the completion ghost text above, since a `:terminal` buffer is a raw PTY that `nvim-cmp` has no visibility into. Your actual system login shell is untouched. ## Git `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 | |---|---| | `Du` | Toggle the DB UI sidebar | | `Da` | Add a connection (same as pressing `A` inside the sidebar) | | `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`) | | `Dr` | Rename the current query buffer or saved query (a buffer has to have been written once first) | | `Dl` | Print the last query that ran and how long it took | `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 `` works too), and dadbod-ui's own default `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 `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 (`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 `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. The mapping also makes sure the buffer is actually loaded in binary mode before toggling (re-reading it with `++bin` if not) — opening a real binary file without that causes Neovim to misinterpret raw bytes as text before `xxd` even runs, surfacing as a `CONVERSION ERROR` instead of a clean hex dump. Since that reload discards whatever's currently in the buffer, `h` refuses to run it if there are unsaved changes — save (or explicitly discard with `:e!`) first. ## Formatting and linting `~/.clang-format` and `~/.clang-tidy` are both copied from `config/` on every run, implementing a specific C/C++ coding style guide (naming conventions, Allman braces, real tabs, include ordering, and a curated set of modernize/cppcoreguidelines/bugprone clang-tidy checks — see the comments in each file for exactly which style guide section a given setting maps to, and `config/cpp-style-guide.md` — also copied, to `~/cpp-style-guide.md` — for the guide itself). Both tools walk upward from the file being checked looking for their config file; since `$HOME` sits above every project, these act as your personal default for any project that doesn't ship its own `.clang-format`/`.clang-tidy` — a project's own file always takes precedence. `lf` (LSP format) only consumes `.clang-format` — clangd's formatting is pure clang-format and has nothing to do with clang-tidy. `.clang-tidy` is picked up two separate ways: automatically by clangd for live diagnostics (clangd's clang-tidy integration is explicitly enabled via `--clang-tidy`, alongside `--background-index` so checks run against the whole project's semantic info rather than just what's open), and directly by `lt`, which shells out to the real `clang-tidy --fix` on the file instead of going through clangd. That's a deliberate difference, not an oversight: clangd filters code actions by LSP "kind", and clang-tidy fixes only ever surface from clangd as individual `quickfix` actions — one per diagnostic — never as a single batched "fix everything" action, because clangd doesn't implement the `source.fixAll` kind that would take ([clangd/clangd#1446](https://github.com/clangd/clangd/issues/1446)). Asking for that kind specifically is why this used to just return "No code actions available" no matter what the file contained. Running the actual `clang-tidy` binary sidesteps that entirely and does what the key was always meant to: fix everything the tool can in one pass. See the [LSP table](#lsp--code-intelligence) above for the compile-database caveat that governs how much either `la` or `lt` can actually catch. ## Known quirks - **`nvim-treesitter` is pinned to its `master` branch**, not the current upstream default (`main`). `main` is an incompatible rewrite that removed the `nvim-treesitter.configs` API this config (and most existing configs/plugins) are built on. `master` is kept frozen upstream specifically for this kind of backward compatibility. - A **Neovim 0.12 compatibility shim** is included for `master`-branch treesitter: 0.12 changed query predicate/directive results to sometimes be a list of nodes instead of a single node, which `master`'s legacy predicate handlers don't expect — this caused a `"Decoration provider ... attempt to call method 'range'"` error (most visible on markdown files). The shim normalizes results back to a single node before those handlers run. Purely cosmetic when unpatched (highlighting still worked) but noisy. - **`nvim-cmp` absorbs the `` mapping used for smart comment continuation** as its own fallback, rather than it being called directly by Neovim. `cmp.setup()` runs after `init.lua`'s top-level code, and per its own keymap-composition system, it wraps whatever `` mapping already existed as the function it calls when its completion menu isn't visible, instead of simply discarding it. Worth knowing if you ever add or change an insert-mode `` mapping here: it will run through cmp's fallback chain rather than being invoked directly, which also means direct buffer edits from inside it (e.g. `nvim_set_current_line()`) fail with `E565` — express any change as keys to return instead. - **`gP`'s regex-on-paste only handles single-file conflicts reliably.** nvim-tree has a separate, differently-shaped dialog specifically for multi-file conflicts within one paste ("N file(s) already exist" / Rename (suffix) / Overwrite all / Skip all), which `gP` doesn't detect. `gM` (copying marked files with an unconditional regex) was built as the more reliable tool for renaming several files at once. - **`gM`'s stale-mark cleanup depends on `api.marks.toggle`**, a function name inferred from `m`'s own behavior rather than confirmed in nvim-tree's documentation directly, so it's wrapped defensively — if it's wrong on a given nvim-tree version, a stale mark just won't auto-clear (still requires manually pressing `m` on it), without otherwise breaking `gM`. - **Smart comment continuation (``) only recognizes `//`, `#`, and `--`** (C-family/JS/TS/Go, Python/bash/YAML/CMake, and Lua/SQL respectively) rather than being fully comment-syntax-agnostic — it computes the marker to repeat directly rather than deferring to Neovim's own `'comments'`-option-driven logic, which would cover more comment styles but came with its own timing problems (see the ``/cmp entry above).