New nvim configurations
This commit is contained in:
1 parent
9343f11b72
commit
aede79413a
8 files changed
+2295
-986
No files matched your search
+67
-9
@@ -1,6 +1,6 @@
|
||||
# Neovim Setup
|
||||
|
||||
A single-script Neovim configuration built around C/C++ development (CMake + clangd + codelldb), with LSP support for Go, C#, TypeScript/JavaScript, HTML, and CSS alongside it.
|
||||
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.
|
||||
|
||||
@@ -8,12 +8,14 @@ Supports **Fedora, Ubuntu, Debian, and Arch Linux** (and their common derivative
|
||||
|
||||
- [Installing](#installing)
|
||||
- [What gets installed](#what-gets-installed)
|
||||
- [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)
|
||||
@@ -30,12 +32,14 @@ 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 and installs system packages via the right manager (`dnf`/`apt`/`pacman`), plus two Nerd Fonts (JetBrainsMono, Cascadia Code) for icon glyphs.
|
||||
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. Writes personal `~/.clang-format`/`~/.clang-tidy` fallbacks (see [Formatting and linting](#formatting-and-linting)).
|
||||
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`.
|
||||
|
||||
@@ -45,6 +49,7 @@ First launch takes a minute or two: `lazy.nvim` bootstraps itself and installs a
|
||||
- **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
|
||||
|
||||
@@ -54,6 +59,31 @@ Package names vary by distro (see the script for the exact list per package mana
|
||||
- **CLI tools**: ripgrep, fd, git, curl, wget, unzip, fish (used only for Neovim's embedded terminal — see [Terminal](#terminal))
|
||||
- **Language servers**: clangd, gopls, omnisharp, ts_ls, html, cssls (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/init.lua`, `nvim/lua/plugins/init.lua`, `nvim/lua/dap/configurations/cmake.lua` | `~/.config/nvim/...` (same layout) |
|
||||
| `clang-format` | `~/.clang-format` |
|
||||
| `clang-tidy` | `~/.clang-tidy` |
|
||||
| `cpp-style-guide.md` | `~/cpp-style-guide.md` |
|
||||
|
||||
## 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. `koda.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
|
||||
|
||||
@@ -75,6 +105,9 @@ Package names vary by distro (see the script for the exact list per package mana
|
||||
| `J` / `K` | Visual | Move selected lines down / up |
|
||||
| `<A-j>` / `<A-k>` | Normal/Insert/Visual | Move current line (or selection) down / up |
|
||||
| `<leader>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 |
|
||||
| `<leader>c/` | Normal/Visual | Same toggle as `gcc`/`gc` above, as a second which-key-discoverable entry point under the "Comments" group |
|
||||
| `<C-s>` | Normal/Visual/Insert | Save file |
|
||||
| `<C-h/j/k/l>` | Normal | Move to left/lower/upper/right window |
|
||||
| `<leader>sv` / `<leader>sh` | Normal | Split vertically / horizontally |
|
||||
@@ -83,7 +116,8 @@ Package names vary by distro (see the script for the exact list per package mana
|
||||
|
||||
## Look & feel
|
||||
|
||||
- **Theme**: `tokyonight` (night variant), customized to a pure black background, with custom colors for error/warn/info/hint diagnostic virtual lines.
|
||||
- **Theme**: [`koda.nvim`](https://github.com/oskarnurm/koda.nvim) (dark variant), nudged to a near-black background, with custom colors for error/warn/info/hint diagnostic virtual lines.
|
||||
- **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 @<register>`) that only appears while a macro is actually being recorded.
|
||||
@@ -138,7 +172,7 @@ Setting an explicit `<F1>` mapping here also overrides Neovim's built-in `<F1>`-
|
||||
|
||||
## LSP & code intelligence
|
||||
|
||||
Configured servers: **clangd** (C/C++), **gopls** (Go), **omnisharp** (C#), **ts_ls** (TS/JS), **html**, **cssls**. Diagnostics render as virtual lines above 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**. 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 |
|
||||
|---|---|
|
||||
@@ -146,12 +180,36 @@ Configured servers: **clangd** (C/C++), **gopls** (Go), **omnisharp** (C#), **ts
|
||||
| `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). |
|
||||
| `<leader>lr` | Rename symbol (prompts for new name, safe against double-firing) |
|
||||
| `<leader>la` | Code actions |
|
||||
| `<leader>lr` | Rename symbol (prompts for new name, safe against double-firing, refuses macros/namespaces — see note below) |
|
||||
| `<leader>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. |
|
||||
| `<leader>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 |
|
||||
| `<leader>lf` | Format file (via clangd/LSP formatting — see [Formatting and linting](#formatting-and-linting) for the style used) |
|
||||
| `<leader>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) |
|
||||
| `<leader>cd` | Generate a Doxygen-style comment block — see [Documentation generation](#documentation-generation) |
|
||||
|
||||
**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>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. `<leader>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/\<old\>/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. `<leader>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. `<leader>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, `<leader>oc` / `<leader>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.
|
||||
@@ -210,9 +268,9 @@ The mapping also makes sure the buffer is actually loaded in binary mode before
|
||||
|
||||
## Formatting and linting
|
||||
|
||||
`~/.clang-format` and `~/.clang-tidy` are both written 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). 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.
|
||||
`~/.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.
|
||||
|
||||
`<leader>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 separately and automatically by clangd for live diagnostics, since clangd's clang-tidy integration is on by default.
|
||||
`<leader>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 `<leader>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 `<leader>la` or `<leader>lt` can actually catch.
|
||||
|
||||
## Known quirks
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
# .clang-format
|
||||
# Enforces the formatting rules from the C/C++ Coding Style Guide (Part I: C++ Style).
|
||||
# Applies equally to Part II C API files per §15 (Indentation and Braces) — same
|
||||
# tabs/Allman/line-length rules, just different naming (naming can't be enforced
|
||||
# by clang-format; see .clang-tidy).
|
||||
Language: Cpp
|
||||
Standard: Latest
|
||||
|
||||
# --- Indentation (style guide §2) ---
|
||||
UseTab: Always
|
||||
TabWidth: 4
|
||||
IndentWidth: 4
|
||||
ContinuationIndentWidth: 4
|
||||
IndentCaseLabels: true
|
||||
IndentPPDirectives: None
|
||||
IndentExternBlock: Indent
|
||||
|
||||
# Access specifiers (public:/private:) sit unindented, flush with the class
|
||||
# keyword — only members are indented one level, matching every example in §1/§6.
|
||||
AccessModifierOffset: -4
|
||||
|
||||
# Namespace bodies are indented like any other block (style guide §Namespaces).
|
||||
NamespaceIndentation: All
|
||||
|
||||
# --- Braces: Allman style everywhere (style guide §3) ---
|
||||
BreakBeforeBraces: Allman
|
||||
Cpp11BracedListStyle: true
|
||||
# InlineOnly (not Empty or None): out-of-class function definitions always
|
||||
# get full Allman expansion regardless of length, same as before -- this
|
||||
# only additionally allows a function *defined inside a class body* to
|
||||
# stay a compact one-liner if it's short enough to fit. That's specifically
|
||||
# for the trivial-one-liner exception to "avoid inline member definitions"
|
||||
# in Class Member Organization (style guide §6) -- a plain getter like
|
||||
# `int getValue() const { return m_value; }` stays inline-and-compact;
|
||||
# anything defined out-of-class still always gets the full brace expansion.
|
||||
AllowShortFunctionsOnASingleLine: InlineOnly
|
||||
AllowShortIfStatementsOnASingleLine: Never
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AllowShortBlocksOnASingleLine: Never
|
||||
AllowShortEnumsOnASingleLine: false
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
|
||||
# No trailing "// namespace X" / "// class X" comments on closing braces
|
||||
# (style guide §Namespaces / §6).
|
||||
FixNamespaceComments: false
|
||||
|
||||
# --- Pointers and references attach to the type (style guide §4) ---
|
||||
PointerAlignment: Left
|
||||
ReferenceAlignment: Pointer
|
||||
DerivePointerAlignment: false
|
||||
|
||||
# --- Line length (style guide §8) ---
|
||||
ColumnLimit: 100
|
||||
|
||||
# --- Spacing (style guide §5) ---
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpacesInAngles: false
|
||||
|
||||
# --- Include ordering (style guide §7: main header, C headers, C++ std,
|
||||
# third-party, project headers) ---
|
||||
SortIncludes: true
|
||||
SortUsingDeclarations: true
|
||||
IncludeBlocks: Regroup
|
||||
IncludeIsMainRegex: '(Test)?$'
|
||||
IncludeCategories:
|
||||
# 1. Corresponding header is auto-detected and always sorted first.
|
||||
# 2. C system headers wrapped for C++ (<cstdio>, <cstdint>, ...).
|
||||
- Regex: '^<(cassert|cctype|cerrno|cfenv|cfloat|cinttypes|climits|clocale|cmath|csetjmp|csignal|cstdarg|cstddef|cstdint|cstdio|cstdlib|cstring|ctime|cuchar|cwchar|cwctype)>$'
|
||||
Priority: 1
|
||||
CaseSensitive: true
|
||||
# 3. C++ standard library headers (no dot, no slash — <vector>, <memory>, ...).
|
||||
- Regex: '^<[a-z_]+>$'
|
||||
Priority: 2
|
||||
CaseSensitive: true
|
||||
# 4. Third-party / external library headers (<fmt/format.h>, <boost/...>, ...).
|
||||
- Regex: '^<.*>$'
|
||||
Priority: 3
|
||||
CaseSensitive: true
|
||||
# 5. Project headers, quoted ("NetworkManager.hpp", ...).
|
||||
- Regex: '^".*"$'
|
||||
Priority: 4
|
||||
CaseSensitive: true
|
||||
|
||||
# --- Misc ---
|
||||
AlignTrailingComments: true
|
||||
AlignConsecutiveAssignments: false
|
||||
AlignConsecutiveDeclarations: false
|
||||
|
||||
# Parameters/arguments: never one-per-line (style guide §8). When a
|
||||
# signature/call doesn't fit on one line, keep everything together on a
|
||||
# single continuation line if it fits there (AllowAll...OnNextLine), and
|
||||
# fall back to packing multiple per line (BinPack...) rather than ever
|
||||
# breaking to one per line. This can't force a genuinely long signature
|
||||
# to fit within ColumnLimit without wrapping at all -- doing that would
|
||||
# require ColumnLimit: 0 project-wide, which would also stop comments and
|
||||
# ordinary expressions from ever wrapping. See §8 for the full rule.
|
||||
#
|
||||
# COMPATIBILITY NOTE: clang-format 20 changed BinPackParameters (and the
|
||||
# equivalent for call arguments) from a boolean to an enum, and later
|
||||
# versions deprecated it again in favor of a nested PackParameters/
|
||||
# PackArguments option. The boolean form below is correct for clang-format
|
||||
# through 19 (still the most common version in practice, e.g. Ubuntu
|
||||
# 24.04 LTS ships 18). On clang-format 20 or newer, replace `true` with
|
||||
# `BinPack` on both lines if you find these are being silently ignored.
|
||||
BinPackArguments: true
|
||||
BinPackParameters: true
|
||||
AllowAllArgumentsOnNextLine: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: true
|
||||
|
||||
BreakConstructorInitializers: BeforeColon
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
PenaltyReturnTypeOnItsOwnLine: 1000
|
||||
@@ -0,0 +1,161 @@
|
||||
---
|
||||
# .clang-tidy
|
||||
# Enforces naming conventions (style guide §1) and a subset of the Language
|
||||
# Feature Do's and Don'ts (style guide §10) for C++ code. Place at the repo
|
||||
# root for Part I (C++) files. For Part II (C API) files, use the
|
||||
# directory-scoped override in c-api/.clang-tidy instead — clang-tidy picks
|
||||
# the closest .clang-tidy file up the directory tree, so nesting one there
|
||||
# gives that subtree different naming rules without affecting the rest of
|
||||
# the project.
|
||||
#
|
||||
# readability-redundant-inline-specifier is a narrow, complementary aid for
|
||||
# the "avoid inline member definitions" rule in §6 (Class Member
|
||||
# Organization): it only catches an explicit `inline` keyword that's
|
||||
# already redundant (e.g. on a function defined in the class body, which
|
||||
# is implicitly inline regardless). It does not catch — and nothing in
|
||||
# this file catches — an inline definition that omits the keyword
|
||||
# entirely, which is the actual, common case the style guide rule is
|
||||
# about; that one stays a code-review judgment call.
|
||||
Checks: >
|
||||
-*,
|
||||
readability-identifier-naming,
|
||||
readability-redundant-inline-specifier,
|
||||
modernize-use-nullptr,
|
||||
modernize-use-override,
|
||||
modernize-use-equals-default,
|
||||
modernize-use-equals-delete,
|
||||
modernize-loop-convert,
|
||||
modernize-concat-nested-namespaces,
|
||||
modernize-use-default-member-init,
|
||||
modernize-avoid-c-arrays,
|
||||
modernize-use-nodiscard,
|
||||
cppcoreguidelines-special-member-functions,
|
||||
cppcoreguidelines-owning-memory,
|
||||
cppcoreguidelines-no-malloc,
|
||||
cppcoreguidelines-pro-type-member-init,
|
||||
cppcoreguidelines-pro-type-cstyle-cast,
|
||||
cppcoreguidelines-macro-usage,
|
||||
performance-unnecessary-value-param,
|
||||
performance-unnecessary-copy-initialization,
|
||||
bugprone-exception-escape,
|
||||
bugprone-use-after-move
|
||||
|
||||
WarningsAsErrors: ''
|
||||
HeaderFilterRegex: '.*\.hpp$'
|
||||
FormatStyle: file
|
||||
|
||||
CheckOptions:
|
||||
# --- Naming Conventions — style guide §1 ---
|
||||
- key: readability-identifier-naming.NamespaceCase
|
||||
value: lower_case
|
||||
|
||||
- key: readability-identifier-naming.ClassCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.StructCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.EnumCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.EnumConstantCase
|
||||
value: CamelCase
|
||||
|
||||
- key: readability-identifier-naming.FunctionCase
|
||||
value: camelBack
|
||||
- key: readability-identifier-naming.MethodCase
|
||||
value: camelBack
|
||||
|
||||
- key: readability-identifier-naming.VariableCase
|
||||
value: camelBack
|
||||
- key: readability-identifier-naming.LocalVariableCase
|
||||
value: camelBack
|
||||
- key: readability-identifier-naming.ParameterCase
|
||||
value: camelBack
|
||||
|
||||
# A plain local `const` (not `static`, not `constexpr`) stays under the
|
||||
# Local Variables rule (camelCase) per the style guide's own note that
|
||||
# only *global/static* const/constexpr values are "constants" for this
|
||||
# rule's purposes -- without this, it silently inherits the general
|
||||
# ConstantCase (PascalCase) fallback below instead, which would flag
|
||||
# totally ordinary code like `const std::string name = ...;` inside a
|
||||
# function. StaticConstantCase/GlobalConstantCase/ConstexprVariableCase
|
||||
# below are unaffected and still require PascalCase, matching the guide.
|
||||
- key: readability-identifier-naming.LocalConstantCase
|
||||
value: camelBack
|
||||
|
||||
# Private/protected member variables: camelCase with an m_ prefix.
|
||||
- key: readability-identifier-naming.PrivateMemberCase
|
||||
value: camelBack
|
||||
- key: readability-identifier-naming.PrivateMemberPrefix
|
||||
value: m_
|
||||
- key: readability-identifier-naming.ProtectedMemberCase
|
||||
value: camelBack
|
||||
- key: readability-identifier-naming.ProtectedMemberPrefix
|
||||
value: m_
|
||||
# Public members (e.g. plain data structs) — camelCase, no prefix.
|
||||
- key: readability-identifier-naming.PublicMemberCase
|
||||
value: camelBack
|
||||
|
||||
# Static class data members — camelCase with an s_ prefix (distinct from
|
||||
# instance members above). "ClassMember" is clang-tidy's category for
|
||||
# static (non-const) data members specifically.
|
||||
- key: readability-identifier-naming.ClassMemberCase
|
||||
value: camelBack
|
||||
- key: readability-identifier-naming.ClassMemberPrefix
|
||||
value: s_
|
||||
|
||||
# Global (namespace/file-scope, mutable) variables — camelCase with a g_
|
||||
# prefix, to flag genuinely global mutable state at every use site.
|
||||
- key: readability-identifier-naming.GlobalVariableCase
|
||||
value: camelBack
|
||||
- key: readability-identifier-naming.GlobalVariablePrefix
|
||||
value: g_
|
||||
|
||||
# Static variables — function-local `static` or file-scope `static` —
|
||||
# camelCase with an s_ prefix, for the same reason as globals above.
|
||||
- key: readability-identifier-naming.StaticVariableCase
|
||||
value: camelBack
|
||||
- key: readability-identifier-naming.StaticVariablePrefix
|
||||
value: s_
|
||||
|
||||
# Constants and enum values — PascalCase (style guide §1 table).
|
||||
- key: readability-identifier-naming.ConstantCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.GlobalConstantCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.StaticConstantCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.ClassConstantCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.ConstexprVariableCase
|
||||
value: CamelCase
|
||||
|
||||
# Template parameters — PascalCase (e.g. ValueType).
|
||||
- key: readability-identifier-naming.TemplateParameterCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.TypeTemplateParameterCase
|
||||
value: CamelCase
|
||||
|
||||
# Type aliases / typedefs — PascalCase, matching class naming.
|
||||
- key: readability-identifier-naming.TypeAliasCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.TypedefCase
|
||||
value: CamelCase
|
||||
|
||||
# Macros — ALL_CAPS_SNAKE_CASE (the one deliberate exception in §1).
|
||||
- key: readability-identifier-naming.MacroDefinitionCase
|
||||
value: UPPER_CASE
|
||||
|
||||
# --- cppcoreguidelines-owning-memory — style guide §10, Memory and Ownership ---
|
||||
# Flags raw new/delete and legacy C allocators used for ownership instead of
|
||||
# std::unique_ptr / std::shared_ptr.
|
||||
- key: cppcoreguidelines-owning-memory.LegacyResourceProducers
|
||||
value: 'malloc;calloc;realloc;strdup;strndup'
|
||||
- key: cppcoreguidelines-owning-memory.LegacyResourceConsumers
|
||||
value: 'free;realloc'
|
||||
|
||||
# --- cppcoreguidelines-macro-usage — style guide §10, prefer constexpr over #define ---
|
||||
# Allow include-guard-style and project-prefixed macros; flag everything else
|
||||
# that could instead be a constexpr constant.
|
||||
- key: cppcoreguidelines-macro-usage.AllowedRegexp
|
||||
value: '^[A-Z0-9_]+_H(PP)?$'
|
||||
- key: cppcoreguidelines-macro-usage.CheckCapsOnly
|
||||
value: false
|
||||
@@ -0,0 +1,748 @@
|
||||
# C/C++ Coding Style Guide
|
||||
|
||||
This guide is split into two parts:
|
||||
|
||||
- **[Part I: C++ Style](#part-i-c-style)** applies to all internal application/library code written in C++.
|
||||
- **[Part II: C API Style](#part-ii-c-api-style)** applies specifically to public API surfaces exposed as plain C (`extern "C"`, `.h` headers) — e.g. for dynamic library boundaries, plugin interfaces, or C-consumer compatibility. Keep these two worlds separate: don't let C-API constraints (no exceptions, no classes, `snake_case`) leak into internal C++ code, and don't let C++ constructs leak into a public C API.
|
||||
|
||||
## Contents
|
||||
|
||||
**[Part I: C++ Style](#part-i-c-style)**
|
||||
1. [Naming Conventions](#1-naming-conventions)
|
||||
2. [Indentation](#2-indentation)
|
||||
3. [Brace Placement (Allman Style)](#3-brace-placement-allman-style)
|
||||
4. [Pointers and References](#4-pointers-and-references)
|
||||
5. [Spacing](#5-spacing)
|
||||
6. [Header Files](#6-header-files)
|
||||
- [Namespaces](#namespaces)
|
||||
- [Headers as Public API: Keep Implementation Types Out](#headers-as-public-api-keep-implementation-types-out)
|
||||
- [Templates](#templates)
|
||||
- [Implementing Templates (Project Standard)](#implementing-templates-project-standard)
|
||||
- [Class Member Organization](#class-member-organization)
|
||||
7. [Include Directive Order](#7-include-directive-order)
|
||||
8. [Line Length](#8-line-length)
|
||||
9. [Comments](#9-comments)
|
||||
10. [Language Feature Do's and Don'ts](#10-language-feature-dos-and-donts)
|
||||
- [Memory and Ownership](#memory-and-ownership)
|
||||
- [Error Handling](#error-handling)
|
||||
- [Modern C++ Features](#modern-c-features)
|
||||
11. [Library Linking and Runtime Loading](#11-library-linking-and-runtime-loading)
|
||||
- [Static Linking](#static-linking)
|
||||
- [Dynamic Linking (Shared Libraries)](#dynamic-linking-shared-libraries)
|
||||
- [Runtime Loading (dlopen and LoadLibrary)](#runtime-loading-dlopen-and-loadlibrary)
|
||||
- [Summary](#summary)
|
||||
12. [Miscellaneous](#12-miscellaneous)
|
||||
|
||||
**[Part II: C API Style](#part-ii-c-api-style)**
|
||||
13. [Naming Conventions (C API)](#13-naming-conventions-c-api)
|
||||
14. [File Extensions and Structure](#14-file-extensions-and-structure)
|
||||
15. [Indentation and Braces](#15-indentation-and-braces)
|
||||
16. [API Design Do's and Don'ts](#16-api-design-dos-and-donts)
|
||||
17. [Boundary Rule of Thumb](#17-boundary-rule-of-thumb)
|
||||
|
||||
---
|
||||
|
||||
# Part I: C++ Style
|
||||
|
||||
## 1. Naming Conventions
|
||||
|
||||
| Symbol Type | Convention | Example |
|
||||
|-------------------------|-------------------|-----------------------------------|
|
||||
| Classes / Structs | `PascalCase` | `class NetworkManager;` |
|
||||
| Functions / Methods | `camelCase` | `void connectToServer();` |
|
||||
| Local Variables | `camelCase` | `int retryCount = 0;` |
|
||||
| Member Variables (private/protected) | `camelCase` with `m_` prefix | `int m_connectionTimeout;` |
|
||||
| Static Member Variables (class-scope `static`) | `camelCase` with `s_` prefix | `static int s_instanceCount;` |
|
||||
| Global Variables (namespace/file scope, mutable) | `camelCase` with `g_` prefix | `int g_activeConnectionCount;` |
|
||||
| Static Variables (function-local or file-scope `static`) | `camelCase` with `s_` prefix | `static int s_retryAttempts;` |
|
||||
| Constants | `PascalCase` | `constexpr int MaxRetries = 5;` |
|
||||
| Enum Type Names | `PascalCase` | `enum class ConnectionState` |
|
||||
| Enum Values | `PascalCase` | `ConnectionState::Connected` |
|
||||
| Namespaces | `snake_case` | `namespace network_utils` |
|
||||
| Template Parameters | `PascalCase` | `template <typename ValueType>` |
|
||||
| Macros | `ALL_CAPS_SNAKE_CASE` | `#define MAX_BUFFER_SIZE 1024` |
|
||||
| File Names | `PascalCase` | `NetworkManager.cpp` / `.hpp` |
|
||||
|
||||
> **Note:** Macros are an exception to the general PascalCase/camelCase scheme since `ALL_CAPS` is the near-universal C/C++ convention and helps macros stand out visually from regular code.
|
||||
|
||||
> **Note:** `g_`/`s_` mark *mutable* global/static state specifically so it stands out at every use site — genuinely global mutable variables are rare and worth flagging by name. A `constexpr` or `const` global/static value is a **constant**, not a variable for this rule's purposes: it keeps the `PascalCase` constant convention above, with no `g_`/`s_` prefix, since it can't cause the aliasing/lifetime bugs the prefix exists to call out (e.g. `constexpr int MaxRetries = 5;` at namespace scope, not `g_MaxRetries`). A `static` variable inside a function keeps `s_` for the same reason as a file-scope one — both persist across calls and are exactly the kind of hidden state the prefix is meant to surface.
|
||||
|
||||
---
|
||||
|
||||
## 2. Indentation
|
||||
|
||||
- Use **tabs** for indentation (not spaces).
|
||||
- Do not mix tabs and spaces.
|
||||
- Configure your editor's tab width to display consistently (recommended: 4 columns wide) across the team.
|
||||
- **Namespace bodies are indented**, exactly like any other block (`if`, `class`, `for`, ...) — a namespace is not a special case left flush against the margin. See [Namespaces](#namespaces) for the full rule and examples.
|
||||
|
||||
```cpp
|
||||
void doSomething()
|
||||
{
|
||||
if (condition)
|
||||
{
|
||||
performAction();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Brace Placement (Allman Style)
|
||||
|
||||
Braces always go on their **own new line**, aligned with the statement that opens the block.
|
||||
|
||||
```cpp
|
||||
// Correct (Allman)
|
||||
if (isReady)
|
||||
{
|
||||
start();
|
||||
}
|
||||
else
|
||||
{
|
||||
waitForReady();
|
||||
}
|
||||
|
||||
class Widget
|
||||
{
|
||||
public:
|
||||
Widget();
|
||||
~Widget();
|
||||
};
|
||||
|
||||
void process()
|
||||
{
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
handle(i);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```cpp
|
||||
// Incorrect (K&R style — do not use)
|
||||
if (isReady) {
|
||||
start();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Pointers and References
|
||||
|
||||
Attach `*` and `&` to the **type**, not the variable name.
|
||||
|
||||
```cpp
|
||||
int* pointerToValue;
|
||||
const std::string& nameRef;
|
||||
|
||||
void processData(const Data* data, int& outResult);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Spacing
|
||||
|
||||
- One space around binary operators: `a + b`, `x == y`.
|
||||
- No space between a function name and its parentheses: `doWork()`, not `doWork ()`.
|
||||
- One space after control-flow keywords: `if (`, `for (`, `while (`.
|
||||
|
||||
```cpp
|
||||
if (a == b)
|
||||
{
|
||||
total = a + b * c;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Header Files
|
||||
|
||||
- Use `#pragma once` at the top of every header.
|
||||
- **`.hpp` for C++ headers, `.h` for C headers.** Any header containing C++-only constructs (classes, templates, namespaces, overloads, `enum class`, etc.) uses `.hpp`. Reserve `.h` for headers meant to be valid C — see [Part II: C API Style](#part-ii-c-api-style) for the public C API convention.
|
||||
- **One `.cpp` per `.hpp`.** Each header declaring a class or module has exactly one corresponding source file implementing it, sharing the same base name (`NetworkManager.hpp` ↔ `NetworkManager.cpp`). Avoid dumping multiple unrelated classes' implementations into one `.cpp`, and avoid splitting one class's implementation across multiple `.cpp` files.
|
||||
- **One class, struct, or enum per header — meaning one *publicly usable* type.** Each `.hpp` exposes exactly one type as part of its public API: the primary class/struct/enum a caller `#include`s the header to use. A header should never declare two independent, separately-usable types (e.g. `NetworkManager` and an unrelated `RetryPolicy`) side by side — each gets its own `.hpp`. This is what prevents the dependency problem: a file that only needs `RetryPolicy` should never be forced to also compile/depend on everything `NetworkManager` pulls in. Small types that have no meaning or use *outside* the primary type — a nested `enum class` used only as one of its parameters, a tag type only passed to its constructor — aren't "separate" types for this rule; they're part of the primary type's interface and stay in the same header.
|
||||
- Header-only components (templates, small inline utilities) are the exception to the one-`.cpp`-per-`.hpp` rule — they may have a `.hpp` with no matching `.cpp` — but the one-type-per-header rule still applies to them (see [Templates](#templates) below).
|
||||
- Match header/source file names to the primary class (`PascalCase`), e.g. `NetworkManager.hpp` / `NetworkManager.cpp` for `class NetworkManager`.
|
||||
- **Do not add a trailing comment on a closing brace** (`}; // class NetworkManager`, `} // namespace network_utils`, `} // if (isReady)`, etc.), for namespaces, classes, functions, or any other block. Keep blocks short and readable enough that the comment isn't needed — if a block is too long to tell what its closing brace belongs to at a glance, the fix is to shorten or restructure the block, not to annotate the brace.
|
||||
|
||||
```cpp
|
||||
// NetworkManager.hpp
|
||||
#pragma once
|
||||
|
||||
namespace network_utils
|
||||
{
|
||||
class NetworkManager
|
||||
{
|
||||
public:
|
||||
NetworkManager();
|
||||
~NetworkManager();
|
||||
|
||||
void connectToServer();
|
||||
|
||||
private:
|
||||
int m_connectionTimeout;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Namespaces
|
||||
|
||||
- **Namespace contents are indented**, exactly like the body of any other block. Don't leave everything inside a namespace flush against the left margin just because the namespace itself doesn't "do" anything at runtime — indentation is what makes nesting depth obvious at a glance, and a namespace is nesting.
|
||||
- **Use the C++17 nested-namespace syntax (`namespace outer::inner { ... }`) instead of manually nesting namespace blocks**, whenever you're declaring a fixed, known multi-level namespace (e.g. `network_utils::detail`). Manually nesting `namespace network_utils { namespace detail { ... } }` for a case like this just adds an extra level of indentation and an extra closing brace for no benefit.
|
||||
- The one case that still requires manual nesting: wrapping an **anonymous** namespace inside a named one (e.g. hiding a `.cpp`-local helper type inside `network_utils`) — anonymous namespaces have no name, so there's nothing to join with `::`.
|
||||
|
||||
```cpp
|
||||
// Preferred — a fixed multi-level namespace uses the single nested-namespace form
|
||||
namespace network_utils::detail
|
||||
{
|
||||
struct RetryState
|
||||
{
|
||||
int attemptCount;
|
||||
int backoffMs;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```cpp
|
||||
// Avoid — manually nesting two named namespaces that could be joined with ::
|
||||
namespace network_utils
|
||||
{
|
||||
namespace detail
|
||||
{
|
||||
struct RetryState
|
||||
{
|
||||
int attemptCount;
|
||||
int backoffMs;
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Headers as Public API: Keep Implementation Types Out
|
||||
|
||||
Treat every header as a **public interface contract**, not a scratchpad for whatever the implementation happens to need. A caller including `NetworkManager.hpp` should see only what they need to *use* `NetworkManager` — not the types that exist purely to help `NetworkManager.cpp` implement it internally.
|
||||
|
||||
- **Implementation-only helper types belong in the `.cpp`, not the `.hpp`.** If a class needs a small helper struct, enum, or class that callers never see or interact with (a parser's internal token type, a lookup-table entry, a strategy object used only inside one function), define it in the `.cpp` file, wrapped in an unnamed (anonymous) namespace so it has internal linkage and can't leak or collide across translation units:
|
||||
|
||||
```cpp
|
||||
// NetworkManager.cpp
|
||||
#include "NetworkManager.hpp"
|
||||
|
||||
namespace network_utils
|
||||
{
|
||||
namespace
|
||||
{
|
||||
// Implementation detail — never appears in the header, never used outside this file.
|
||||
struct RetryState
|
||||
{
|
||||
int attemptCount;
|
||||
int backoffMs;
|
||||
};
|
||||
}
|
||||
|
||||
void NetworkManager::connectToServer()
|
||||
{
|
||||
RetryState state{0, 100};
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Forward-declare instead of including, whenever the header only needs a pointer or reference to another type.** This keeps the header's dependency footprint to exactly the API surface it exposes, rather than transitively dragging in every header its implementation happens to use.
|
||||
|
||||
```cpp
|
||||
// NetworkManager.hpp
|
||||
#pragma once
|
||||
|
||||
namespace network_utils
|
||||
{
|
||||
class Socket; // forward declaration — full definition only needed in NetworkManager.cpp
|
||||
|
||||
class NetworkManager
|
||||
{
|
||||
public:
|
||||
void connectToServer();
|
||||
|
||||
private:
|
||||
Socket* m_socket;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
- **One real C++ constraint to know:** a class's *own* private member variables (as opposed to helper types) generally must still be declared in the header, because C++ needs the complete type — including all members — to know the class's size and layout wherever it's used by value, on the stack, or as a direct member of another class. This is different from hiding a *helper type*; you're not hiding the fact that `NetworkManager` has a timeout, only hiding the definition of an unrelated struct it happens to use internally.
|
||||
- If you need to hide private *members* too — for a stable ABI across a shared-library boundary (see [Library Linking and Runtime Loading](#11-library-linking-and-runtime-loading)) or to shrink recompiles when private internals change — the standard technique is the **Pimpl idiom** (pointer to implementation): the header holds only a forward-declared pointer, and the real member variables live in a struct defined entirely in the `.cpp`.
|
||||
|
||||
```cpp
|
||||
// NetworkManager.hpp
|
||||
#pragma once
|
||||
#include <memory>
|
||||
|
||||
namespace network_utils
|
||||
{
|
||||
class NetworkManager
|
||||
{
|
||||
public:
|
||||
NetworkManager();
|
||||
~NetworkManager();
|
||||
|
||||
void connectToServer();
|
||||
|
||||
private:
|
||||
struct Impl; // forward-declared, defined only in NetworkManager.cpp
|
||||
std::unique_ptr<Impl> m_impl;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```cpp
|
||||
// NetworkManager.cpp
|
||||
#include "NetworkManager.hpp"
|
||||
|
||||
namespace network_utils
|
||||
{
|
||||
struct NetworkManager::Impl
|
||||
{
|
||||
int connectionTimeout;
|
||||
};
|
||||
|
||||
NetworkManager::NetworkManager() : m_impl(std::make_unique<Impl>()) {}
|
||||
NetworkManager::~NetworkManager() = default;
|
||||
|
||||
void NetworkManager::connectToServer()
|
||||
{
|
||||
// use m_impl->connectionTimeout
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Pimpl isn't required for every class — it adds an extra pointer indirection and a heap allocation, so reserve it for types where header-hiding genuinely matters (public library API, ABI-sensitive dynamic-library boundaries, or classes whose private internals churn often enough that reducing rebuild fan-out is worth the cost). For ordinary internal application classes, plain private members in the header (as in the example at the top of this section) are fine — only the *helper types*, not the members themselves, need to move out.
|
||||
|
||||
### Templates
|
||||
|
||||
Templates are *more* exposed to the one-type-per-header problem than ordinary classes, for two structural reasons specific to how templates compile:
|
||||
|
||||
1. **No compiled-`.cpp` firewall.** An ordinary class hides its implementation behind a `.cpp` — callers only need the declaration to link against it. A template has no such firewall: the compiler needs the *full definition* everywhere it's instantiated, so templates live entirely in headers (see the exception to "one `.cpp` per `.hpp`" in [Header Files](#6-header-files)). That means anything sharing a header with a template gets pulled into every translation unit that only wanted that one template.
|
||||
2. **Parsing cost applies even to unused templates.** Bundling `template <typename T> class Cache` and `template <typename T> class Pool` in the same header means a file that only uses `Cache` still has to parse `Pool`'s full definition, and a change to `Pool` still bumps the header's timestamp — forcing a rebuild of every translation unit that includes it, including the ones that never touch `Pool` at all.
|
||||
|
||||
Given that, the rules:
|
||||
|
||||
- **One template class/function per header.** `Cache` and `Pool` each get their own `.hpp`, even if they're conceptually related utilities that usually get used together.
|
||||
- **Keep a template's declaration and inline implementation in the same `.hpp`** — templates generally can't be split into a separate `.cpp` the way non-template code can (see reason 1 above) — but that's not license to merge *other*, unrelated templates into the same file alongside it.
|
||||
- **If several templates genuinely share implementation details** (a common traits struct, a shared constant), factor *only that shared piece* into its own single-purpose header, rather than merging the templates themselves into one file. If the shared piece belongs in a nested namespace (e.g. an internal `detail` namespace not meant for outside use), declare it with the `::` nested-namespace syntax from [Namespaces](#namespaces).
|
||||
- **Avoid including heavy headers** (e.g. `<iostream>`, large third-party headers) inside a widely-used template header purely for convenience — because template headers are included wherever they're instantiated, an unnecessary include here silently bloats compile times project-wide, multiplied across every instantiation site.
|
||||
|
||||
```cpp
|
||||
// Avoid — two unrelated templates sharing one header
|
||||
// Cache.hpp
|
||||
#pragma once
|
||||
|
||||
namespace storage
|
||||
{
|
||||
template <typename Key, typename Value>
|
||||
class Cache
|
||||
{
|
||||
public:
|
||||
void insert(const Key& key, const Value& value);
|
||||
const Value* find(const Key& key) const;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
class Pool
|
||||
{
|
||||
public:
|
||||
T* acquire();
|
||||
void release(T* item);
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```cpp
|
||||
// Preferred — one template per header
|
||||
// Cache.hpp
|
||||
#pragma once
|
||||
|
||||
namespace storage
|
||||
{
|
||||
template <typename Key, typename Value>
|
||||
class Cache
|
||||
{
|
||||
public:
|
||||
void insert(const Key& key, const Value& value);
|
||||
const Value* find(const Key& key) const;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```cpp
|
||||
// Pool.hpp — separate file, included only where Pool is actually needed
|
||||
#pragma once
|
||||
|
||||
namespace storage
|
||||
{
|
||||
template <typename T>
|
||||
class Pool
|
||||
{
|
||||
public:
|
||||
T* acquire();
|
||||
void release(T* item);
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
If `Cache` and `Pool` both rely on the same internal helper — say, a fixed-capacity ring buffer trait — that shared piece moves to its own header, in an internal namespace declared with `::`:
|
||||
|
||||
```cpp
|
||||
// DetailTraits.hpp — included by both Cache.hpp and Pool.hpp, nothing else
|
||||
#pragma once
|
||||
|
||||
namespace storage::detail
|
||||
{
|
||||
template <typename T, std::size_t Capacity>
|
||||
struct RingBufferTraits
|
||||
{
|
||||
using ValueType = T;
|
||||
static constexpr std::size_t capacity = Capacity;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### Implementing Templates (Project Standard)
|
||||
|
||||
Templates are the one exception to this guide's header-declares/`.cpp`-implements split: the compiler needs a template's *complete definition* at every point it gets instantiated, and a separately-compiled `.cpp` can't supply that. So the implementation has to live in the header — but to keep this consistent across the codebase rather than a per-template judgment call, **this project uses one standard pattern**:
|
||||
|
||||
> **Declare template members in the class body, and define them out-of-line further down in the same header** — the same shape as an out-of-line member definition of a non-template class, just kept in the `.hpp` instead of moved to a `.cpp`.
|
||||
|
||||
```cpp
|
||||
// Cache.hpp
|
||||
#pragma once
|
||||
|
||||
namespace storage
|
||||
{
|
||||
template <typename Key, typename Value>
|
||||
class Cache
|
||||
{
|
||||
public:
|
||||
void insert(const Key& key, const Value& value);
|
||||
const Value* find(const Key& key) const;
|
||||
|
||||
private:
|
||||
std::unordered_map<Key, Value> m_entries;
|
||||
};
|
||||
|
||||
template <typename Key, typename Value>
|
||||
void Cache<Key, Value>::insert(const Key& key, const Value& value)
|
||||
{
|
||||
m_entries[key] = value;
|
||||
}
|
||||
|
||||
template <typename Key, typename Value>
|
||||
const Value* Cache<Key, Value>::find(const Key& key) const
|
||||
{
|
||||
auto it = m_entries.find(key);
|
||||
return it != m_entries.end() ? &it->second : nullptr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Exception:** a genuinely trivial one-line body (a plain getter, a one-expression forwarding call) can stay inline in the class, same as it would for a non-template class. Don't force it out-of-line just for consistency's sake.
|
||||
- **Escape hatch, not a pattern:** if a template is only ever meant to be used with a small, fixed set of types, its definitions *can* move to a `.cpp` via explicit instantiation (`template class Cache<int, std::string>;`). This is a deliberate, rare trade-off — it turns a general-purpose template into one that only supports the explicitly instantiated types — and should be a documented exception with a stated reason (e.g. reducing header bloat for a widely-included, ABI-sensitive type), not the default way templates get written.
|
||||
|
||||
> **See also:** [Library Linking and Runtime Loading](#11-library-linking-and-runtime-loading) — the Pimpl idiom above exists mainly to satisfy the ABI constraints described there · [Part II: C API Style](#part-ii-c-api-style) — the `.h`/`.hpp` split introduced here is defined in full there.
|
||||
|
||||
### Class Member Organization
|
||||
|
||||
- **Member order: `public`, then `protected`, then `private`.** Group each access level together, in that order. The widest audience — everyone who includes the header — reads what's relevant to them (the public interface) first, without wading through implementation details to find it. Don't reopen an access specifier further down "for organization"; interleaving defeats the ordering rule's purpose.
|
||||
- **Avoid inline member function definitions in the class body.** Declare the member in the class; define its body separately — out-of-line in the matching `.cpp` for an ordinary class (see the `NetworkManager` example in [Header Files](#6-header-files) above), or out-of-line further down the same header for a template member (see [Implementing Templates](#implementing-templates-project-standard) above). This keeps the class body itself a clean, scannable interface instead of a mix of signatures and implementation.
|
||||
- **Exception:** a genuinely trivial one-line body (a plain getter, a one-expression forwarding call) may stay inline in the class — the same exception already noted for templates above.
|
||||
- **Inline template members go at the bottom of their access section.** When a class mixes ordinary members with template members that use the trivial-one-liner exception just above, keep the plain declarations together at the top of each `public`/`protected`/`private` block, and place the inline template members after them, at the bottom of that same block — not interleaved. A reader scanning the interface sees the plain signatures first; the inline implementation detail comes last, where it doesn't interrupt that scan.
|
||||
|
||||
```cpp
|
||||
class NetworkManager
|
||||
{
|
||||
public:
|
||||
NetworkManager();
|
||||
~NetworkManager();
|
||||
|
||||
void connectToServer();
|
||||
bool isConnected() const;
|
||||
|
||||
// Inline template member (trivial-one-liner exception, see Templates
|
||||
// above) -- kept at the bottom of this access section rather than
|
||||
// interleaved with the plain declarations above.
|
||||
template <typename T> T configValue() const { return static_cast<T>(m_connectionTimeout); }
|
||||
|
||||
protected:
|
||||
virtual void onDisconnected();
|
||||
|
||||
private:
|
||||
int m_connectionTimeout;
|
||||
};
|
||||
```
|
||||
|
||||
None of this is mechanically enforced — `readability-redundant-inline-specifier` (see `.clang-tidy`) catches one narrow related mistake (a redundant explicit `inline` keyword) but not the member-order or inline-body rules themselves.
|
||||
|
||||
---
|
||||
|
||||
## 7. Include Directive Order
|
||||
|
||||
Group includes into blocks, separated by a blank line, in the following order. Within each block, sort alphabetically.
|
||||
|
||||
1. **Corresponding header** — for a `.cpp`, its own `.hpp` comes first (catches missing-include bugs in the header itself).
|
||||
2. **C system headers** — `<cstdio>`, `<cstdlib>`, etc.
|
||||
3. **C++ standard library headers** — `<vector>`, `<memory>`, `<string>`, etc.
|
||||
4. **Third-party / external library headers** — e.g. `<boost/...>`, `<fmt/...>`.
|
||||
5. **Project headers** — other headers from this codebase, using quoted includes (`"..."`).
|
||||
|
||||
```cpp
|
||||
// NetworkManager.cpp
|
||||
#include "NetworkManager.hpp" // 1. corresponding header
|
||||
|
||||
#include <cstdint> // 2. C system headers
|
||||
|
||||
#include <memory> // 3. C++ standard library
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <fmt/format.h> // 4. third-party libraries
|
||||
|
||||
#include "Logger.hpp" // 5. project headers
|
||||
#include "SocketWrapper.hpp"
|
||||
```
|
||||
|
||||
- Use angle brackets `<...>` for system, standard library, and third-party headers.
|
||||
- Use quotes `"..."` for project-local headers.
|
||||
- This ordering guarantees each header can be compiled standalone (no hidden dependency on something included before it) and makes missing includes fail fast in CI rather than silently compiling due to include order in some other file.
|
||||
|
||||
---
|
||||
|
||||
## 8. Line Length
|
||||
|
||||
- Prefer a maximum of **100 characters** per line.
|
||||
- **Do not wrap parameters onto their own line.** Keep a function's parameter list together — on the declaration line if it fits, otherwise packed as tightly as the line allows on a continuation line. Never lay parameters out one per line; that turns a short signature into a tall list a reader has to scroll past to see the return type and name.
|
||||
- `clang-format` enforces the "never one-per-line" part. It can't force a genuinely long signature to fit within 100 characters without wrapping at all — that would need line length limits disabled project-wide, which would also stop ordinary long expressions and comments from ever wrapping, so it isn't done. If a signature is long enough that even packing parameters together still overflows, let it overflow rather than reach for one-per-line as the fallback.
|
||||
|
||||
---
|
||||
|
||||
## 9. Comments
|
||||
|
||||
- **Document symbols with Doxygen-style comments, never a plain `//`.** Any comment documenting a function, class, struct, enum, member variable, or macro — explaining what it *is* or *does*, as opposed to annotating a line of implementation — uses `///` or `/** ... */`. This is what makes the project's generated documentation (see the Doxygen integration) actually useful instead of a bare list of undocumented signatures.
|
||||
- **`///` for most documentation; `/** ... */` for longer, multi-paragraph explanations.** Both are valid Doxygen syntax — pick based on length, not per-file preference.
|
||||
- **Plain `//` is still correct for everything that isn't documenting a symbol** — a note on a tricky line inside a function body, a `// TODO`, a block disabled during debugging. The rule above is about comments attached to a declaration, not every comment everywhere.
|
||||
|
||||
```cpp
|
||||
/// Establishes a connection to the configured server.
|
||||
/// @return true if the connection succeeded.
|
||||
bool connectToServer();
|
||||
```
|
||||
|
||||
```cpp
|
||||
/**
|
||||
* Parses and validates a server configuration blob.
|
||||
*
|
||||
* Accepts either JSON or the legacy INI format; the format is
|
||||
* auto-detected from the first non-whitespace byte.
|
||||
*
|
||||
* @param raw the raw configuration bytes, in either supported format.
|
||||
* @return a parsed Config, or std::nullopt if raw is malformed.
|
||||
*/
|
||||
std::optional<Config> parseConfig(std::string_view raw);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Language Feature Do's and Don'ts
|
||||
|
||||
### Memory and Ownership
|
||||
|
||||
| Do | Don't |
|
||||
|----|-------|
|
||||
| Use `std::unique_ptr` / `std::shared_ptr` for owning dynamic objects | Use raw `new` / `delete` for ownership |
|
||||
| Use raw pointers or references for **non-owning** access | Use a raw pointer to express or transfer ownership |
|
||||
| Use `std::make_unique` / `std::make_shared` to construct smart pointers | Construct a smart pointer with `new` directly (`std::unique_ptr<T>(new T())`) |
|
||||
| Follow RAII for all resources (files, locks, sockets, etc.) | Manually pair acquire/release calls |
|
||||
|
||||
### Error Handling
|
||||
|
||||
| Do | Don't |
|
||||
|----|-------|
|
||||
| Throw exceptions for **true exceptional situations** (unrecoverable errors, invariant violations, constructor failures) | Use exceptions for expected/recoverable control flow (e.g. "value not found") |
|
||||
| Use return values (`bool`, `std::optional`, error codes, `std::expected` where available) for expected failure cases | Use exceptions as a substitute for normal branching logic |
|
||||
| Catch exceptions by `const&` | Catch exceptions by value (causes slicing) |
|
||||
| Keep destructors `noexcept` (the default) | Let exceptions escape a destructor |
|
||||
|
||||
### Modern C++ Features
|
||||
|
||||
| Do | Don't |
|
||||
|----|-------|
|
||||
| Use `auto` when it improves readability (iterators, lambdas, long template types) | Use `auto` when it hides an important or non-obvious type |
|
||||
| Use range-based `for` loops over index-based loops when the index isn't needed | Write manual index loops just to iterate a container |
|
||||
| Use lambdas for short, local callbacks and algorithm predicates | Write a named functor class for a one-off, trivial operation |
|
||||
| Use structured bindings for multi-value returns (`auto [key, value] = ...`) | Unpack pairs/tuples manually with `.first` / `.second` when structured bindings are clearer |
|
||||
| Use `constexpr` / `consteval` for compile-time constants and computations | Use `#define` macros for constants |
|
||||
| Use `enum class` over plain `enum` | Use unscoped `enum` (pollutes namespace, weak typing) |
|
||||
| Use `override` and `final` explicitly on overriding virtual functions | Omit `override`, relying on the compiler to catch signature mismatches |
|
||||
| Use `nullptr` | Use `NULL` or `0` for pointers |
|
||||
| Prefer `std::array` / `std::vector` over C-style arrays | Use raw C-style arrays for anything beyond fixed, tiny buffers |
|
||||
| Pass small, cheap-to-copy types by value; pass large objects by `const&` | Pass large objects by value out of habit |
|
||||
| Use `std::string_view` for read-only, non-owning string parameters | Take `const std::string&` when the caller may pass a string literal or substring |
|
||||
|
||||
---
|
||||
|
||||
## 11. Library Linking and Runtime Loading
|
||||
|
||||
How a component is built and linked shapes how strictly some of the above rules should be enforced.
|
||||
|
||||
### Static Linking
|
||||
|
||||
- Libraries are compiled directly into the final binary at build time.
|
||||
- Header/implementation split still matters for compile-time isolation, but ABI stability across the boundary is **not** a concern — the whole binary is rebuilt together.
|
||||
- Inline-heavy, template-heavy, and header-only code (common with `auto`, templates, `constexpr`) is safe here since everything resolves at compile time in one binary.
|
||||
|
||||
### Dynamic Linking (Shared Libraries)
|
||||
|
||||
- Applies to `.so` (Linux), `.dll` (Windows), and `.dylib` (macOS) targets.
|
||||
- The public headers exposed across a shared-library boundary form an **ABI contract** — style choices here have real consequences:
|
||||
- Avoid exposing `inline` functions, templates, or `constexpr` values in the public API surface if the library and its consumers may be compiled by different compilers/versions — inlined code bakes assumptions into the caller.
|
||||
- Avoid changing the layout of any `class`/`struct` exposed across the boundary (adding/reordering member variables) without a version bump — it breaks binary compatibility for already-compiled consumers.
|
||||
- Prefer exposing a stable, minimal interface at the boundary (see [Part II: C API Style](#part-ii-c-api-style)), and keep the `PascalCase` classes with full modern C++ usage as **internal implementation detail** behind it.
|
||||
- Mark exported symbols explicitly (e.g. `__declspec(dllexport)` / `__attribute__((visibility("default")))`), and keep everything else hidden by default, to keep the ABI surface intentional and small.
|
||||
|
||||
### Runtime Loading (dlopen and LoadLibrary)
|
||||
|
||||
- Any function loaded dynamically at runtime **must** be declared with C linkage (`extern "C"`) to avoid name-mangling issues — this is exactly the case [Part II: C API Style](#part-ii-c-api-style) exists for, even in an otherwise all-`.hpp` C++ codebase.
|
||||
- Keep the loaded interface as a plain function-pointer table or `extern "C"` API — no classes, no exceptions crossing the boundary (an exception thrown from a plugin loaded via `dlopen` and caught in the host app is undefined behavior across incompatible runtimes/compilers).
|
||||
- Document plugin/module boundaries clearly with `.hpp` interface headers describing the contract, even though the actual crossing point is a plain C-style vtable or function table.
|
||||
|
||||
### Summary
|
||||
|
||||
| Context | Modern C++ features across the boundary | ABI stability required |
|
||||
|---|---|---|
|
||||
| Static link | Freely (templates, `auto`, inline, `constexpr`) | No — whole program rebuilt together |
|
||||
| Dynamic link (`.so`/`.dll`) | Restrict at the public boundary; free internally | Yes — layout & symbol changes are breaking |
|
||||
| Runtime load (`dlopen`) | `extern "C"` only at the boundary ([Part II](#part-ii-c-api-style) style) | Yes — plus no exceptions/classes across it |
|
||||
|
||||
> **See also:** [Boundary Rule of Thumb](#17-boundary-rule-of-thumb) — decides, file by file, whether a piece of code follows this section or Part II · [Part II: C API Style](#part-ii-c-api-style) — the actual convention referenced throughout this section.
|
||||
|
||||
---
|
||||
|
||||
## 12. Miscellaneous
|
||||
|
||||
- Mark single-argument constructors `explicit` unless implicit conversion is intended.
|
||||
- Prefer `const` correctness everywhere it applies.
|
||||
- **Leave a blank line after an early exit** — a `return`, `break`, or `continue` statement, or a block whose last statement is one of these — whenever more code follows at that same level. It visually marks where the early-exit path ends and the normal-case code begins. Skip the blank line if the statement (or its block) is the last thing in its own enclosing function or loop; there's nothing after it to separate from.
|
||||
- Not enforced by `clang-format` or `clang-tidy` — neither has a check for this. It's a code-review convention.
|
||||
|
||||
```cpp
|
||||
void process(const std::vector<int>& values)
|
||||
{
|
||||
if (values.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int value : values)
|
||||
{
|
||||
accumulate(value);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Part II: C API Style
|
||||
|
||||
This section governs any code written as **plain C**, or any `extern "C"` surface exposed *from* a C++ library for consumption by C callers, dynamic loaders (`dlopen`), or other languages via FFI. Nothing in this section applies to internal C++ implementation — only to the boundary itself and to files that are genuinely compiled as C.
|
||||
|
||||
## 13. Naming Conventions (C API)
|
||||
|
||||
| Symbol Type | Convention | Example |
|
||||
|-------------------------|----------------------|----------------------------------------|
|
||||
| Public API functions | `snake_case` with library prefix | `net_manager_connect(NetManager* mgr);` |
|
||||
| Types (`struct`/`enum`/typedef) | `PascalCase` with library prefix, `_t` suffix optional | `NetManagerT` or `NetManager` |
|
||||
| Struct fields | `snake_case` | `int connection_timeout;` |
|
||||
| Function parameters | `snake_case` | `int retry_count` |
|
||||
| Constants / `#define` | `ALL_CAPS_SNAKE_CASE` with library prefix | `#define NET_MAX_RETRIES 5` |
|
||||
| Enum values | `ALL_CAPS_SNAKE_CASE` with library + enum prefix | `NET_STATE_CONNECTED` |
|
||||
| Internal global variables (translation-unit-visible, mutable) | `g_snake_case` | `int g_active_connection_count;` |
|
||||
| Static variables (function-local or file-scope `static`) | `s_snake_case` | `static int s_retry_attempts;` |
|
||||
| File names | `snake_case` | `net_manager.h` / `net_manager.c` |
|
||||
|
||||
- Every public symbol (function, type, macro, enum value) is **prefixed with the library/module name** to avoid collisions in the global C symbol namespace, since C has no namespaces.
|
||||
- `snake_case` is used throughout the C API — this is a deliberate departure from the `camelCase`/`PascalCase` used in Part I, and is one of the clearest visual signals that a symbol belongs to the C boundary rather than internal C++.
|
||||
- The `g_`/`s_` prefixes are for **internal, non-exported** state — the kind of file-scope `static` variable that never appears in a public header. A genuinely public global exported from the API surface is unusual enough that it should get the full library prefix instead (`net_manager_default_timeout`, not `g_net_manager_default_timeout`) — don't stack both prefixes on the same symbol.
|
||||
|
||||
---
|
||||
|
||||
## 14. File Extensions and Structure
|
||||
|
||||
- **`.h`** for all public C API headers — must be valid to include from both C and C++ translation units.
|
||||
- **`.c`** for files actually compiled as C. If the "C API" is really a C++ implementation exposing an `extern "C"` shim, the shim itself lives in a `.cpp` file but declares its exported functions in a `.h` header guarded for C++ (see below).
|
||||
- One `.h` per logical module/library surface (e.g. `net_manager.h`), mirroring the one-header-per-component rule from Part I, but scoped to the *public* API rather than one per internal class.
|
||||
|
||||
```c
|
||||
/* net_manager.h */
|
||||
#ifndef NET_MANAGER_H
|
||||
#define NET_MANAGER_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct NetManager NetManager;
|
||||
|
||||
NetManager* net_manager_create(void);
|
||||
void net_manager_destroy(NetManager* mgr);
|
||||
int net_manager_connect(NetManager* mgr, const char* host, int port);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NET_MANAGER_H */
|
||||
```
|
||||
|
||||
- Use traditional `#ifndef`/`#define`/`#endif` header guards in `.h` files rather than `#pragma once` — `.h` files must remain portable to strict C compilers where `#pragma once` support is less universally guaranteed.
|
||||
- Always wrap declarations in `extern "C" { ... }` guarded by `#ifdef __cplusplus`, so the same header works whether included from a C or a C++ translation unit.
|
||||
|
||||
---
|
||||
|
||||
## 15. Indentation and Braces
|
||||
|
||||
Match Part I for consistency across the codebase: **tabs** for indentation, **Allman** brace placement.
|
||||
|
||||
```c
|
||||
int net_manager_connect(NetManager* mgr, const char* host, int port)
|
||||
{
|
||||
if (mgr == NULL)
|
||||
{
|
||||
return NET_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
return connect_impl(mgr, host, port);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 16. API Design Do's and Don'ts
|
||||
|
||||
| Do | Don't |
|
||||
|----|-------|
|
||||
| Return an integer error code / status enum from every fallible function | Throw or let a C++ exception cross into `extern "C"` code (undefined behavior for C callers) |
|
||||
| Use opaque pointers (`typedef struct NetManager NetManager;` with the definition hidden in the `.c`/`.cpp`) to hide implementation details and preserve ABI stability | Expose full `struct` layouts in the public header if the layout may change — this breaks binary compatibility |
|
||||
| Provide explicit `_create` / `_destroy` (or `_init` / `_free`) pairs for any type owning resources | Rely on constructors/destructors (C has none) or leave lifetime management implicit |
|
||||
| Take a `void* user_data` parameter on any callback-accepting function | Assume the caller can supply capturing closures — plain C function pointers can't capture state |
|
||||
| Check all pointer parameters for `NULL` before dereferencing at public entry points | Assume public API callers always pass valid pointers |
|
||||
| Keep the header C89/C99-compatible (no `//` comments, no variable declarations mid-block if targeting strict C89) unless the project's minimum C standard is confirmed higher | Use C++-only syntax anywhere in a `.h` that must remain includable from C |
|
||||
| Document ownership transfer explicitly in comments (e.g. "caller owns the returned pointer and must call `net_manager_destroy`") | Leave ownership of returned pointers ambiguous |
|
||||
|
||||
---
|
||||
|
||||
## 17. Boundary Rule of Thumb
|
||||
|
||||
- If a file is `.cpp`/`.hpp` and never crosses a dynamic-library, plugin, or FFI boundary: **use [Part I](#part-i-c-style), full stop.**
|
||||
- If a file declares or implements symbols meant to be called from C, another language, or loaded via `dlopen`/`LoadLibrary`: **use [Part II](#part-ii-c-api-style)** for that surface, even if the implementation behind it is C++.
|
||||
- Never mix the two conventions within the same file — a `.h` should never contain `camelCase` C++-flavored declarations, and a `.hpp` should never contain `snake_case` C-flavored ones. Where a C++ class needs a C API, write a thin `extern "C"` shim (Part II style) that internally calls into the real C++ implementation (Part I style).
|
||||
|
||||
> **See also:** [Library Linking and Runtime Loading](#11-library-linking-and-runtime-loading) — the dynamic-library and `dlopen` boundaries that make this rule necessary in the first place.
|
||||
@@ -0,0 +1,434 @@
|
||||
vim.g.mapleader = " "
|
||||
vim.g.maplocalleader = " "
|
||||
|
||||
-- Basic Options
|
||||
vim.opt.number = true
|
||||
-- Tab width: 4, using real tab characters (not expanded to spaces)
|
||||
vim.opt.tabstop = 4
|
||||
vim.opt.shiftwidth = 4
|
||||
vim.opt.softtabstop = 4
|
||||
vim.opt.expandtab = false
|
||||
vim.opt.relativenumber = true
|
||||
vim.opt.mouse = "a"
|
||||
vim.opt.clipboard = "unnamedplus"
|
||||
vim.opt.breakindent = true
|
||||
vim.opt.undofile = true
|
||||
vim.opt.ignorecase = true
|
||||
vim.opt.smartcase = true
|
||||
vim.opt.signcolumn = "yes"
|
||||
vim.opt.updatetime = 250
|
||||
vim.opt.timeoutlen = 1000
|
||||
vim.opt.splitright = true
|
||||
vim.opt.splitbelow = true
|
||||
vim.opt.termguicolors = true
|
||||
vim.opt.hlsearch = true
|
||||
vim.opt.laststatus = 3 -- global statusline (for lualine)
|
||||
-- Drops the "N lines, M bytes written" message after :w. Mainly
|
||||
-- motivated by hex.nvim's save flow: it runs an xxd filter (which
|
||||
-- prints its own "N lines filtered" message) as part of writing, and
|
||||
-- the write's own message stacking on top of that is what triggers
|
||||
-- Vim's "Press ENTER to continue" prompt after every hex-mode save.
|
||||
-- Removing this message means there's only one left, which doesn't
|
||||
-- need the prompt.
|
||||
vim.opt.shortmess:append("W")
|
||||
|
||||
-- Styled window borders between splits (nvim-tree, terminal, etc.)
|
||||
vim.opt.fillchars = {
|
||||
eob = " ",
|
||||
vert = "│",
|
||||
horiz = "─",
|
||||
horizup = "┴",
|
||||
horizdown = "┬",
|
||||
vertleft = "┤",
|
||||
vertright = "├",
|
||||
verthoriz = "┼",
|
||||
}
|
||||
vim.api.nvim_create_autocmd("ColorScheme", {
|
||||
callback = function()
|
||||
vim.api.nvim_set_hl(0, "WinSeparator", { fg = "#3b4261" })
|
||||
end,
|
||||
})
|
||||
|
||||
-- Rounded borders on ALL floating windows (hover, signature help, LspInfo,
|
||||
-- Mason, diagnostics float, etc.) that don't set their own border.
|
||||
-- Requires Neovim 0.11+; older versions just skip this and fall back to
|
||||
-- whatever each plugin sets individually.
|
||||
if vim.fn.has("nvim-0.11") == 1 then
|
||||
vim.o.winborder = "rounded"
|
||||
end
|
||||
|
||||
-- Neovide (desktop GUI) specific settings. vim.g.neovide is only set
|
||||
-- (truthy) when Neovide itself launches this config as its backend --
|
||||
-- plain terminal Neovim never defines it, so everything in this block
|
||||
-- is a no-op there. Terminal Neovim gets its cursor animation from
|
||||
-- smear-cursor.nvim instead (see plugins/init.lua): a terminal has no
|
||||
-- equivalent native capability to hook into, which is exactly the gap
|
||||
-- that plugin fills.
|
||||
if vim.g.neovide then
|
||||
vim.o.guifont = "JetBrainsMono Nerd Font:h13"
|
||||
|
||||
-- Cursor trail: Neovide renders this itself (GPU-accelerated),
|
||||
-- which is why this lives in its own branch instead of always
|
||||
-- loading smear-cursor.nvim -- running both would be two different
|
||||
-- animation systems fighting over the same cursor. trail_size is a
|
||||
-- taste knob (0-1); turn it down if it feels like too much.
|
||||
vim.g.neovide_cursor_vfx_mode = "railgun"
|
||||
vim.g.neovide_cursor_trail_size = 0.5
|
||||
vim.g.neovide_cursor_animate_in_insert_mode = true
|
||||
|
||||
-- Neovide has no background-image setting -- two attempts to add
|
||||
-- one (neovide/neovide#2419, #3067) were both closed as abandoned/
|
||||
-- stalled, most recently in October 2025; the feature request itself
|
||||
-- (neovide/neovide#342) is still open if you want to track it.
|
||||
-- vim.g.neovide_opacity below is the closest thing that actually
|
||||
-- exists: it makes the window itself translucent, so on a
|
||||
-- compositing window manager (Hyprland included), whatever sits
|
||||
-- behind the window -- your desktop wallpaper, if Neovide isn't
|
||||
-- covering the whole screen, or another window -- blends through.
|
||||
-- It's not the same as a picture rendered inside the editor, but if
|
||||
-- your wallpaper already is that Arch logo, this is what gets you
|
||||
-- there. 1.0 = fully opaque (no effect); lower it to taste.
|
||||
vim.g.neovide_opacity = 0.9
|
||||
end
|
||||
|
||||
-- Don't auto-continue comments (// , # , etc.) onto a new line when
|
||||
-- pressing Enter in insert mode or o/O in normal mode -- EXCEPT when
|
||||
-- the current line is comment-ONLY (nothing but whitespace + the
|
||||
-- comment itself, no real code before it), where continuing onto the
|
||||
-- next line with the same comment marker is actually convenient
|
||||
-- (writing a multi-line comment block) rather than the annoying case
|
||||
-- (writing code that happens to have a trailing comment, then hitting
|
||||
-- Enter to write MORE code and getting an unwanted comment prefix
|
||||
-- inserted). Many filetypes' own ftplugins (e.g. c.vim) set
|
||||
-- 'formatoptions' with these flags as part of their own FileType
|
||||
-- handling -- this runs on the same event, registered afterward, so
|
||||
-- it reliably strips them back off regardless of what a given
|
||||
-- filetype's ftplugin set; the <CR> mapping further below re-adds 'r'
|
||||
-- for just the one keypress when the comment-only condition applies.
|
||||
vim.api.nvim_create_autocmd("FileType", {
|
||||
pattern = "*",
|
||||
callback = function()
|
||||
vim.opt_local.formatoptions:remove({ "c", "r", "o" })
|
||||
end,
|
||||
})
|
||||
|
||||
-- SSH Clipboard Support (OSC 52)
|
||||
vim.g.clipboard = {
|
||||
name = 'OSC 52',
|
||||
copy = {
|
||||
['+'] = require('vim.ui.clipboard.osc52').copy('+'),
|
||||
['*'] = require('vim.ui.clipboard.osc52').copy('*'),
|
||||
},
|
||||
paste = {
|
||||
-- We now return BOTH the text and the register type ('v', 'V', or '^V')
|
||||
-- This restores native p and P line-wise behavior
|
||||
['+'] = function() return { vim.fn.getreg('"', 1, true), vim.fn.getregtype('"') } end,
|
||||
['*'] = function() return { vim.fn.getreg('"', 1, true), vim.fn.getregtype('"') } end,
|
||||
},
|
||||
}
|
||||
|
||||
-- GENERAL KEYMAPS --
|
||||
|
||||
-- Clear search highlight on pressing Esc in normal mode
|
||||
vim.keymap.set('n', '<Esc>', '<cmd>nohlsearch<CR>')
|
||||
|
||||
-- Is the cursor's current line comment-ONLY -- i.e. its first
|
||||
-- non-blank character falls inside a treesitter comment node, meaning
|
||||
-- there's no real code before the comment on this line? Using
|
||||
-- treesitter's `comment` node type rather than per-filetype regex
|
||||
-- (// vs # vs -- etc.) since virtually every grammar names it the
|
||||
-- same way, so this works generically across languages rather than
|
||||
-- needing a pattern maintained per filetype. Queries a single point
|
||||
-- (the first non-blank column) rather than a range spanning the whole
|
||||
-- line, since a range query only succeeds if it fits entirely inside
|
||||
-- one node -- comment nodes don't always extend to the exact byte
|
||||
-- offset a plain string length points at (trailing whitespace,
|
||||
-- multi-byte characters), and a point query avoids depending on that.
|
||||
local function cursor_line_is_comment_only()
|
||||
local ok, parser = pcall(vim.treesitter.get_parser, 0)
|
||||
if not ok or not parser then
|
||||
return false
|
||||
end
|
||||
local line = vim.api.nvim_get_current_line()
|
||||
local first_col = line:find("%S")
|
||||
if not first_col then
|
||||
return false
|
||||
end
|
||||
local row = vim.api.nvim_win_get_cursor(0)[1] - 1
|
||||
local ok2, tree = pcall(function() return parser:parse()[1] end)
|
||||
if not ok2 or not tree then
|
||||
return false
|
||||
end
|
||||
local col = first_col - 1
|
||||
local node = tree:root():named_descendant_for_range(row, col, row, col + 1)
|
||||
while node do
|
||||
if node:type():match("comment") then
|
||||
return true
|
||||
end
|
||||
node = node:parent()
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- <CR> in insert mode: on a comment-only line, repeats the line's
|
||||
-- comment marker (and a single following space, if one was there) on
|
||||
-- the new line -- twice in a row with nothing typed in between breaks
|
||||
-- out instead of continuing again, matching Vim's own long-standing
|
||||
-- convention for this.
|
||||
--
|
||||
-- This used to reuse Neovim's native comment-leader logic instead, by
|
||||
-- temporarily appending 'r' to formatoptions around the <CR>
|
||||
-- keypress and reverting it via vim.schedule() afterward -- that
|
||||
-- never actually worked: there's no guarantee the scheduled revert
|
||||
-- runs AFTER Neovim's own logic gets to read formatoptions during
|
||||
-- that same <CR>'s processing, and apparently it ran first every
|
||||
-- time, removing 'r' before it could take effect. Computing the
|
||||
-- prefix directly and returning it as part of the same keystring
|
||||
-- sidesteps that ordering question entirely -- nothing here depends
|
||||
-- on when anything else happens to run.
|
||||
--
|
||||
-- Covers //, #, and -- (C-family/JS/TS/Go, Python/bash/YAML/CMake,
|
||||
-- and Lua/SQL respectively) rather than being fully comment-syntax-
|
||||
-- agnostic like the detection above -- that's the trade-off for not
|
||||
-- depending on formatoptions/'comments' timing.
|
||||
--
|
||||
-- NOTE: this mapping ends up invoked through nvim-cmp's OWN <CR>
|
||||
-- fallback chain, not called directly by Neovim -- cmp.setup() runs
|
||||
-- after this file's top-level code and, per its own keymap-
|
||||
-- composition system (cmp/utils/keymap.lua), absorbs whatever <CR>
|
||||
-- mapping already existed as the function it calls when its
|
||||
-- completion menu isn't visible, rather than simply discarding it.
|
||||
-- That has two consequences: (1) it still runs under a textlock, so
|
||||
-- direct buffer edits like nvim_set_current_line() throw E565 --
|
||||
-- fixed below by returning keys instead; (2) it runs on literally
|
||||
-- every <CR> press everywhere, including while navigating a LuaSnip
|
||||
-- snippet session, which this was never meant to touch at all --
|
||||
-- guarded against below by bailing out to a plain <CR> whenever one
|
||||
-- is active for the buffer.
|
||||
vim.keymap.set('i', '<CR>', function()
|
||||
local ok_ls, luasnip = pcall(require, "luasnip")
|
||||
if ok_ls and luasnip.session.current_nodes[vim.api.nvim_get_current_buf()] then
|
||||
return '<CR>'
|
||||
end
|
||||
|
||||
local line = vim.api.nvim_get_current_line()
|
||||
|
||||
-- Current line is JUST a marker with nothing typed after it (i.e.
|
||||
-- the previous <CR> continued the comment and nothing was added
|
||||
-- since) -- break out: clear the line and start fresh, rather than
|
||||
-- continuing yet again. Without this, pressing Enter repeatedly
|
||||
-- with nothing typed in between just continues forever.
|
||||
--
|
||||
-- <Esc>S (leave insert mode, then substitute-line) rather than
|
||||
-- calling nvim_set_current_line() directly -- direct buffer edits
|
||||
-- aren't allowed from inside this callback (see the textlock note
|
||||
-- above), so the change has to be expressed as keys to feed
|
||||
-- instead, same as the <CR> continuation itself already is.
|
||||
if line:match("^%s*//%s?$") or line:match("^%s*#%s?$") or line:match("^%s*%-%-%s?$") then
|
||||
return '<Esc>S'
|
||||
end
|
||||
|
||||
if cursor_line_is_comment_only() then
|
||||
-- Only the marker itself gets typed onto the new line -- NOT the
|
||||
-- current line's own leading whitespace. The active indent logic
|
||||
-- (cindent, etc.) already indents the new line to match on its
|
||||
-- own; re-typing the captured leading whitespace on top of that
|
||||
-- is what compounded further right on every single Enter before.
|
||||
local prefix = line:match("^%s*(//%s?)")
|
||||
or line:match("^%s*(#%s?)")
|
||||
or line:match("^%s*(%-%-%s?)")
|
||||
if prefix then
|
||||
return '<CR>' .. prefix
|
||||
end
|
||||
end
|
||||
return '<CR>'
|
||||
end, { expr = true, desc = "Smart comment-continuing newline" })
|
||||
|
||||
-- Clears the current line's content, leaving it as a blank line in
|
||||
-- place rather than deleting the line itself (which would shift
|
||||
-- everything below it up, like dd does). Motivating case: undoing an
|
||||
-- auto-continued comment from the mapping above when you didn't
|
||||
-- actually want it, without backspacing it out character by
|
||||
-- character.
|
||||
vim.keymap.set('n', '<leader>cb', function()
|
||||
vim.api.nvim_set_current_line("")
|
||||
end, { desc = "Clear Line (Blank)" })
|
||||
|
||||
-- Toggle comment on the current line / a visual selection.
|
||||
--
|
||||
-- Neovim 0.10+ already ships this natively on gcc (line) and gc (a
|
||||
-- motion in normal mode, or the selection in visual mode) -- both
|
||||
-- keep working exactly as before, untouched. This just adds
|
||||
-- <leader>c/ as a second, which-key-discoverable entry point in the
|
||||
-- existing "Comments" group, aliased onto those same native mappings
|
||||
-- (remap = true so the fed keys re-trigger gcc/gc rather than this
|
||||
-- reimplementing comment-toggling itself): same single keystroke
|
||||
-- comments or uncomments, since gc/gcc already toggle based on the
|
||||
-- current state.
|
||||
vim.keymap.set('n', '<leader>c/', 'gcc', { remap = true, desc = "Toggle Comment (Line)" })
|
||||
vim.keymap.set('x', '<leader>c/', 'gc', { remap = true, desc = "Toggle Comment (Selection)" })
|
||||
|
||||
-- Forced Inline Paste (The behavior you liked)
|
||||
-- This forces the pasted text to be treated as characters at your exact cursor position
|
||||
vim.keymap.set('n', '<leader>p', function()
|
||||
local reg_content = vim.fn.getreg('"', 1, true)
|
||||
vim.api.nvim_put(reg_content, 'c', true, true)
|
||||
end, { desc = "Force Paste Inline" })
|
||||
|
||||
-- Visual mode paste: Prevent replacing the unnamed register when pasting over selected text
|
||||
vim.keymap.set("x", "p", '"_dP')
|
||||
vim.keymap.set("x", "P", '"_dP')
|
||||
|
||||
-- Move lines up and down in visual mode
|
||||
vim.keymap.set("v", "J", ":m '>+1<CR>gv=gv", { desc = "Move line down" })
|
||||
vim.keymap.set("v", "K", ":m '<-2<CR>gv=gv", { desc = "Move line up" })
|
||||
|
||||
-- Move current line/selection up and down with Alt+j/k, across
|
||||
-- normal, insert, and visual mode.
|
||||
vim.keymap.set('n', '<A-j>', ':m .+1<CR>==', { desc = "Move line down" })
|
||||
vim.keymap.set('n', '<A-k>', ':m .-2<CR>==', { desc = "Move line up" })
|
||||
vim.keymap.set('i', '<A-j>', '<Esc>:m .+1<CR>==gi', { desc = "Move line down" })
|
||||
vim.keymap.set('i', '<A-k>', '<Esc>:m .-2<CR>==gi', { desc = "Move line up" })
|
||||
vim.keymap.set('v', '<A-j>', ":m '>+1<CR>gv=gv", { desc = "Move selection down" })
|
||||
vim.keymap.set('v', '<A-k>', ":m '<-2<CR>gv=gv", { desc = "Move selection up" })
|
||||
|
||||
-- Easy Save
|
||||
vim.keymap.set({'n', 'v', 'i'}, '<C-s>', '<cmd>w<CR><Esc>', { desc = "Save File" })
|
||||
|
||||
-- Window Navigation (Ctrl + h/j/k/l)
|
||||
vim.keymap.set('n', '<C-h>', '<C-w>h', { desc = "Go to left window" })
|
||||
vim.keymap.set('n', '<C-j>', '<C-w>j', { desc = "Go to lower window" })
|
||||
vim.keymap.set('n', '<C-k>', '<C-w>k', { desc = "Go to upper window" })
|
||||
vim.keymap.set('n', '<C-l>', '<C-w>l', { desc = "Go to right window" })
|
||||
|
||||
-- Window Splits (Namespaced under <leader>s)
|
||||
vim.keymap.set('n', '<leader>sv', '<C-w>v', { desc = "Split Vertically" })
|
||||
vim.keymap.set('n', '<leader>sh', '<C-w>s', { desc = "Split Horizontally" })
|
||||
vim.keymap.set('n', '<leader>se', '<C-w>=', { desc = "Make Splits Equal" })
|
||||
vim.keymap.set('n', '<leader>sx', '<cmd>close<CR>', { desc = "Close Current Split" })
|
||||
|
||||
-- Rule-of-five boilerplate for C++ (Namespaced under <leader>o, "OOP")
|
||||
--
|
||||
-- <leader>oc / <leader>om insert the copy or move constructor +
|
||||
-- assignment operator pair, defaulted, for whichever class/struct
|
||||
-- the cursor is currently inside -- found by walking up the
|
||||
-- Treesitter tree rather than requiring the cursor to sit on the
|
||||
-- class's own name line. 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.
|
||||
local function enclosing_class_name()
|
||||
local ok, node = pcall(vim.treesitter.get_node)
|
||||
if not ok then
|
||||
return nil
|
||||
end
|
||||
while node do
|
||||
local t = node:type()
|
||||
if t == "class_specifier" or t == "struct_specifier" then
|
||||
local name_node = node:field("name")[1]
|
||||
return name_node and vim.treesitter.get_node_text(name_node, 0) or nil
|
||||
end
|
||||
node = node:parent()
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function insert_special_members(kind)
|
||||
local name = enclosing_class_name()
|
||||
if not name then
|
||||
vim.notify("No enclosing class/struct found here", vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
local lines = kind == "copy" and {
|
||||
name .. "(const " .. name .. "&) = default;",
|
||||
name .. "& operator=(const " .. name .. "&) = default;",
|
||||
} or {
|
||||
name .. "(" .. name .. "&&) noexcept = default;",
|
||||
name .. "& operator=(" .. name .. "&&) noexcept = default;",
|
||||
}
|
||||
local row = vim.api.nvim_win_get_cursor(0)[1]
|
||||
vim.api.nvim_buf_set_lines(0, row, row, false, lines)
|
||||
-- Reindent the inserted lines to match the surrounding brace depth
|
||||
-- rather than landing at column 0.
|
||||
vim.cmd(string.format("%d,%dnormal! ==", row + 1, row + #lines))
|
||||
vim.api.nvim_win_set_cursor(0, { row + 1, 0 })
|
||||
end
|
||||
|
||||
vim.api.nvim_create_autocmd("FileType", {
|
||||
pattern = "cpp",
|
||||
callback = function(args)
|
||||
vim.keymap.set("n", "<leader>oc", function() insert_special_members("copy") end,
|
||||
{ buffer = args.buf, desc = "Insert Copy Ctor + Assignment" })
|
||||
vim.keymap.set("n", "<leader>om", function() insert_special_members("move") end,
|
||||
{ buffer = args.buf, desc = "Insert Move Ctor + Assignment" })
|
||||
end,
|
||||
})
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- Neovim 0.12 compatibility shim for nvim-treesitter's legacy (master
|
||||
-- branch) query predicates/directives.
|
||||
--
|
||||
-- In Neovim 0.12 a capture in `match[id]` can now be a LIST of TSNode
|
||||
-- instead of a single TSNode (for captures that quantify to more than
|
||||
-- one node). nvim-treesitter's `master` branch is frozen for backward
|
||||
-- compatibility and its query_predicates.lua still assumes a single
|
||||
-- node everywhere, so it calls node:range() on what's now a table and
|
||||
-- throws "attempt to call method 'range' (a nil value)". This is a
|
||||
-- known, currently unresolved upstream issue (nvim-treesitter/nvim-
|
||||
-- treesitter#8618 and #8636, both closed "not planned" since master
|
||||
-- won't receive further fixes). It's cosmetic -- highlighting still
|
||||
-- works -- but it spams :messages, most often on markdown files
|
||||
-- (fenced code blocks use the conceal_lines directive).
|
||||
--
|
||||
-- This wraps query.add_predicate/add_directive so any capture that has
|
||||
-- "spilled" into a list of nodes is transparently unwrapped to its
|
||||
-- first node before the underlying handler runs, restoring the
|
||||
-- single-node behavior those legacy handlers expect. Must run before
|
||||
-- nvim-treesitter is loaded (its query_predicates.lua registers its
|
||||
-- handlers at require-time), so this sits above the lazy.nvim
|
||||
-- bootstrap/setup call below.
|
||||
-- ---------------------------------------------------------------------
|
||||
do
|
||||
local query = vim.treesitter.query
|
||||
local function normalize_match(match)
|
||||
local out = {}
|
||||
for id, val in pairs(match) do
|
||||
if type(val) == "table" and val[1] ~= nil and type(val[1]) == "userdata" then
|
||||
out[id] = val[1]
|
||||
else
|
||||
out[id] = val
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
local orig_add_predicate = query.add_predicate
|
||||
query.add_predicate = function(name, handler, opts)
|
||||
return orig_add_predicate(name, function(match, ...)
|
||||
return handler(normalize_match(match), ...)
|
||||
end, opts)
|
||||
end
|
||||
|
||||
local orig_add_directive = query.add_directive
|
||||
query.add_directive = function(name, handler, opts)
|
||||
return orig_add_directive(name, function(match, ...)
|
||||
return handler(normalize_match(match), ...)
|
||||
end, opts)
|
||||
end
|
||||
end
|
||||
|
||||
-- Bootstrap lazy.nvim
|
||||
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
|
||||
if not vim.loop.fs_stat(lazypath) then
|
||||
vim.fn.system({
|
||||
"git",
|
||||
"clone",
|
||||
"--filter=blob:none",
|
||||
"https://github.com/folke/lazy.nvim.git",
|
||||
"--branch=stable",
|
||||
lazypath,
|
||||
})
|
||||
end
|
||||
vim.opt.rtp:prepend(lazypath)
|
||||
|
||||
require("lazy").setup("plugins")
|
||||
@@ -0,0 +1,144 @@
|
||||
-- dap.configurations.cmake
|
||||
--
|
||||
-- nvim-dap debug configurations for this project's CMake-built
|
||||
-- binaries: tests/test_dynamic_library and demo/example_host.
|
||||
--
|
||||
-- Load from your Neovim config, e.g.:
|
||||
--
|
||||
-- dofile(vim.fn.getcwd() .. '/dap.configurations.cmake')
|
||||
--
|
||||
-- (or place it under a Lua module path as dap/configurations/cmake.lua
|
||||
-- and `require('dap.configurations.cmake')` instead).
|
||||
--
|
||||
-- Assumes:
|
||||
-- * nvim-dap is installed.
|
||||
-- * a `codelldb` adapter is available on PATH (e.g. installed via
|
||||
-- mason.nvim) -- this file only registers dap.adapters.codelldb if
|
||||
-- nothing has registered one already, so it won't clobber your own
|
||||
-- adapter config.
|
||||
-- * the project has been built with scripts/build.sh, so the binaries
|
||||
-- referenced below exist under build/.
|
||||
--
|
||||
-- Debugging needs actual debug symbols: build with
|
||||
-- ./scripts/build.sh --debug
|
||||
-- Release builds will run under the debugger but won't have useful
|
||||
-- source-line info.
|
||||
local dap = require('dap')
|
||||
-- ---------------------------------------------------------------------
|
||||
-- Locate the project root by walking up from the current working
|
||||
-- directory to find CMakeLists.txt. Falls back to cwd if not found,
|
||||
-- which covers the common case of launching nvim from the project
|
||||
-- root directly.
|
||||
-- ---------------------------------------------------------------------
|
||||
local function project_root()
|
||||
local found = vim.fs.find('CMakeLists.txt', {
|
||||
upward = true,
|
||||
path = vim.fn.getcwd(),
|
||||
})[1]
|
||||
if found then
|
||||
return vim.fs.dirname(found)
|
||||
end
|
||||
return vim.fn.getcwd()
|
||||
end
|
||||
local function build_dir()
|
||||
return project_root() .. '/build'
|
||||
end
|
||||
-- ---------------------------------------------------------------------
|
||||
-- codelldb adapter (skipped if one is already registered elsewhere,
|
||||
-- e.g. by mason-nvim-dap)
|
||||
--
|
||||
-- NOTE: this must be a plain table, not a function. nvim-dap only
|
||||
-- performs its automatic ${port} -> real free port substitution (and
|
||||
-- spawns the executable itself, waiting for it to be ready) for
|
||||
-- table-style server adapters. A function-style adapter is expected
|
||||
-- to resolve the port itself; leaving the literal string "${port}"
|
||||
-- in a function adapter's return value causes nvim-dap to try to
|
||||
-- connect to a host literally named "${port}", which fails with
|
||||
-- ECONNREFUSED.
|
||||
-- ---------------------------------------------------------------------
|
||||
if not dap.adapters.codelldb then
|
||||
dap.adapters.codelldb = {
|
||||
type = 'server',
|
||||
port = '${port}',
|
||||
executable = {
|
||||
command = vim.fn.exepath('codelldb'),
|
||||
args = { '--port', '${port}' },
|
||||
},
|
||||
}
|
||||
end
|
||||
-- ---------------------------------------------------------------------
|
||||
-- Platform-appropriate shared library naming, matching
|
||||
-- dynlib::DynamicLibrary::MakeLibraryFileName().
|
||||
-- ---------------------------------------------------------------------
|
||||
local function shared_lib_prefix()
|
||||
return vim.loop.os_uname().sysname == 'Windows_NT' and '' or 'lib'
|
||||
end
|
||||
local function shared_lib_ext()
|
||||
local sysname = vim.loop.os_uname().sysname
|
||||
if sysname == 'Windows_NT' then
|
||||
return '.dll'
|
||||
elseif sysname == 'Darwin' then
|
||||
return '.dylib'
|
||||
end
|
||||
return '.so'
|
||||
end
|
||||
local function fixture_lib_path()
|
||||
return build_dir() .. '/tests/' .. shared_lib_prefix() .. 'dynlib_test_fixture' .. shared_lib_ext()
|
||||
end
|
||||
local function demo_plugin_path()
|
||||
return build_dir() .. '/demo/' .. shared_lib_prefix() .. 'example_plugin' .. shared_lib_ext()
|
||||
end
|
||||
-- ---------------------------------------------------------------------
|
||||
-- Debug configurations
|
||||
-- ---------------------------------------------------------------------
|
||||
local configurations = {
|
||||
{
|
||||
name = 'Debug test_dynamic_library (tests)',
|
||||
type = 'codelldb',
|
||||
request = 'launch',
|
||||
program = function()
|
||||
return build_dir() .. '/tests/test_dynamic_library'
|
||||
end,
|
||||
args = function()
|
||||
return { fixture_lib_path() }
|
||||
end,
|
||||
cwd = function()
|
||||
return build_dir() .. '/tests'
|
||||
end,
|
||||
stopOnEntry = false,
|
||||
},
|
||||
{
|
||||
name = 'Debug example_host (demo)',
|
||||
type = 'codelldb',
|
||||
request = 'launch',
|
||||
program = function()
|
||||
return build_dir() .. '/demo/example_host'
|
||||
end,
|
||||
args = function()
|
||||
return { demo_plugin_path() }
|
||||
end,
|
||||
cwd = function()
|
||||
return build_dir() .. '/demo'
|
||||
end,
|
||||
stopOnEntry = false,
|
||||
},
|
||||
{
|
||||
name = 'Debug example_host (prompt for plugin path)',
|
||||
type = 'codelldb',
|
||||
request = 'launch',
|
||||
program = function()
|
||||
return build_dir() .. '/demo/example_host'
|
||||
end,
|
||||
args = function()
|
||||
local path = vim.fn.input('Plugin path: ', demo_plugin_path(), 'file')
|
||||
return { path }
|
||||
end,
|
||||
cwd = function()
|
||||
return build_dir() .. '/demo'
|
||||
end,
|
||||
stopOnEntry = false,
|
||||
},
|
||||
}
|
||||
dap.configurations.cpp = vim.list_extend(dap.configurations.cpp or {}, configurations)
|
||||
dap.configurations.c = vim.list_extend(dap.configurations.c or {}, configurations)
|
||||
return configurations
|
||||
Executable → Regular
+315
-977
File diff suppressed because it is too large.
Load diff
Executable
+309
@@ -0,0 +1,309 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Resolve this script's own directory so config/ can sit right next to
|
||||
# it and be found regardless of where/how this is invoked (./setup_neovim.sh,
|
||||
# bash /some/path/setup_neovim.sh, a symlink, etc.) -- config and other
|
||||
# files are real, standalone files under config/, not embedded in this
|
||||
# script, so this script's only job is installing dependencies and
|
||||
# copying them into place.
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONFIG_DIR="$SCRIPT_DIR/config"
|
||||
if [ ! -d "$CONFIG_DIR" ]; then
|
||||
echo "ERROR: $CONFIG_DIR not found."
|
||||
echo "This script expects a config/ directory next to it (keep the two together"
|
||||
echo "however you copy/clone this -- e.g. don't move setup_neovim.sh on its own)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
|
||||
echo "========================================="
|
||||
echo " Starting Neovim Setup"
|
||||
echo "========================================="
|
||||
|
||||
# 1. Detect distro and install system dependencies
|
||||
echo "[1/4] Detecting distro and installing system dependencies..."
|
||||
|
||||
if [ ! -f /etc/os-release ]; then
|
||||
echo "ERROR: Cannot detect your Linux distribution (/etc/os-release not found)."
|
||||
echo "This script supports Fedora, Ubuntu, Debian, and Arch Linux."
|
||||
exit 1
|
||||
fi
|
||||
. /etc/os-release
|
||||
DISTRO_ID="$ID"
|
||||
DISTRO_ID_LIKE="${ID_LIKE:-}"
|
||||
|
||||
# Normalize into one of: fedora, debian, arch. Checking ID_LIKE too so
|
||||
# common derivatives (Pop!_OS, Linux Mint, Manjaro, EndeavourOS, etc.)
|
||||
# land in the right family even though only the four distros above
|
||||
# were specifically asked for.
|
||||
case "$DISTRO_ID $DISTRO_ID_LIKE" in
|
||||
*fedora*|*rhel*)
|
||||
DISTRO_FAMILY="fedora"
|
||||
;;
|
||||
*arch*)
|
||||
DISTRO_FAMILY="arch"
|
||||
;;
|
||||
*debian*|*ubuntu*)
|
||||
DISTRO_FAMILY="debian"
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Unsupported or undetected distro (ID=$DISTRO_ID, ID_LIKE=$DISTRO_ID_LIKE)."
|
||||
echo "This script supports Fedora, Ubuntu, Debian, and Arch Linux."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
echo "Detected: $DISTRO_ID (family: $DISTRO_FAMILY)"
|
||||
|
||||
# Which Neovim frontend(s) to set up. Neovide (a separate GUI
|
||||
# application) always runs the real `nvim` binary as its backend --
|
||||
# the terminal-based install right below happens either way and is
|
||||
# what Neovide itself depends on -- this choice only controls whether
|
||||
# Neovide is ALSO installed on top of it. Both frontends share the
|
||||
# exact same config (see config/nvim/init.lua's `if vim.g.neovide`
|
||||
# block), so there's nothing else to pick between; this is purely an
|
||||
# installation question.
|
||||
NVIM_FRONTEND="terminal"
|
||||
if [ -t 0 ]; then
|
||||
echo
|
||||
echo "Which Neovim frontend(s) do you want installed?"
|
||||
echo " 1) Terminal-based only (nvim in your terminal)"
|
||||
echo " 2) Desktop application only (Neovide GUI -- still needs the nvim"
|
||||
echo " binary below as its backend, so that's installed either way)"
|
||||
echo " 3) Both"
|
||||
read -rp "Enter 1, 2, or 3 [1]: " FRONTEND_CHOICE
|
||||
case "$FRONTEND_CHOICE" in
|
||||
2) NVIM_FRONTEND="desktop" ;;
|
||||
3) NVIM_FRONTEND="both" ;;
|
||||
*) NVIM_FRONTEND="terminal" ;;
|
||||
esac
|
||||
else
|
||||
echo "Non-interactive shell detected -- defaulting to terminal-based Neovim only."
|
||||
echo "Run this script directly (not piped) and choose option 2 or 3 to also install Neovide."
|
||||
fi
|
||||
|
||||
case "$DISTRO_FAMILY" in
|
||||
fedora)
|
||||
sudo dnf install -y neovim git curl wget gcc gcc-c++ make cmake \
|
||||
nodejs npm python3-pip ripgrep fd-find \
|
||||
golang clang-tools-extra dotnet-sdk-8.0 unzip fontconfig \
|
||||
lldb gdb fish
|
||||
;;
|
||||
|
||||
debian)
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
sudo apt-get update
|
||||
# Package name differences from Fedora: g++ (not gcc-c++),
|
||||
# golang-go (not golang), clang-tools (not clang-tools-extra --
|
||||
# Debian/Ubuntu split clang-format/clang-tidy differently, this
|
||||
# is the closest equivalent bundle).
|
||||
sudo apt-get install -y neovim git curl wget gcc g++ make cmake \
|
||||
nodejs npm python3-pip ripgrep fd-find \
|
||||
golang-go clang-tools unzip fontconfig \
|
||||
lldb gdb fish
|
||||
|
||||
# fd-find installs its binary as `fdfind` on Debian/Ubuntu (a
|
||||
# name clash with an unrelated existing package called `fd`),
|
||||
# unlike Fedora/Arch where it's just `fd`. Symlink it so tools
|
||||
# that look for a plain `fd` on PATH (e.g. Telescope's
|
||||
# find_files) work the same way here as everywhere else.
|
||||
if command -v fdfind >/dev/null 2>&1 && ! command -v fd >/dev/null 2>&1; then
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
ln -sf "$(command -v fdfind)" "$HOME/.local/bin/fd"
|
||||
echo "Symlinked fdfind -> ~/.local/bin/fd (make sure ~/.local/bin is on your PATH)."
|
||||
fi
|
||||
|
||||
# .NET SDK: Ubuntu ships dotnet-sdk-8.0 directly in its own
|
||||
# repos. Debian does not (long-standing packaging/licensing
|
||||
# reasons) and needs Microsoft's own apt feed added first.
|
||||
# Best-effort either way -- a failure here doesn't stop the
|
||||
# rest of the script, since this only matters for C#/omnisharp.
|
||||
if [ "$DISTRO_ID" = "ubuntu" ] || echo "$DISTRO_ID_LIKE" | grep -qi ubuntu; then
|
||||
sudo apt-get install -y dotnet-sdk-8.0 || \
|
||||
echo "WARNING: dotnet-sdk-8.0 install failed -- see https://learn.microsoft.com/en-us/dotnet/core/install/linux-ubuntu-install"
|
||||
else
|
||||
(
|
||||
set -e
|
||||
DEBIAN_MAJOR="${VERSION_ID%%.*}"
|
||||
wget -q "https://packages.microsoft.com/config/debian/${DEBIAN_MAJOR}/packages-microsoft-prod.deb" -O /tmp/packages-microsoft-prod.deb
|
||||
sudo dpkg -i /tmp/packages-microsoft-prod.deb
|
||||
rm -f /tmp/packages-microsoft-prod.deb
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dotnet-sdk-8.0
|
||||
) || echo "WARNING: dotnet-sdk-8.0 install failed -- see https://learn.microsoft.com/en-us/dotnet/core/install/linux-debian for manual steps"
|
||||
fi
|
||||
;;
|
||||
|
||||
arch)
|
||||
# Arch's own wiki explicitly warns against `pacman -Sy
|
||||
# <package>` (sync without upgrading first) -- it risks
|
||||
# partial-upgrade dependency issues. -Syu first is the
|
||||
# recommended safe order; this does mean the script also
|
||||
# upgrades your existing packages, not just installs new ones,
|
||||
# which is expected/intentional here, not a side effect to
|
||||
# work around.
|
||||
sudo pacman -Syu --noconfirm
|
||||
# Package name differences from Fedora: gcc includes g++
|
||||
# already (no separate gcc-c++ package), python-pip (not
|
||||
# python3-pip -- Arch's default python already is python3),
|
||||
# fd (not fd-find), go (not golang), clang bundles clang-format/
|
||||
# clang-tidy directly (no separate clang-tools-extra).
|
||||
sudo pacman -S --noconfirm neovim git curl wget gcc make cmake \
|
||||
nodejs npm python-pip ripgrep fd \
|
||||
go clang dotnet-sdk-8.0 unzip fontconfig \
|
||||
lldb gdb fish
|
||||
;;
|
||||
esac
|
||||
|
||||
# Neovide install, if chosen above. Best-effort, same philosophy as
|
||||
# the .NET SDK install below: a failure here is a WARNING, not a
|
||||
# reason to stop the rest of the setup.
|
||||
if [ "$NVIM_FRONTEND" = "desktop" ] || [ "$NVIM_FRONTEND" = "both" ]; then
|
||||
echo "Installing Neovide (desktop Neovim GUI)..."
|
||||
case "$DISTRO_FAMILY" in
|
||||
arch)
|
||||
# Packaged directly upstream -- see
|
||||
# https://neovide.dev/installation.html#arch-linux
|
||||
sudo pacman -S --noconfirm neovide libxkbcommon-x11 || \
|
||||
echo "WARNING: Neovide install failed -- see https://neovide.dev/installation.html"
|
||||
;;
|
||||
fedora|debian)
|
||||
# Neither Fedora nor Debian/Ubuntu package Neovide --
|
||||
# upstream's own install docs point at building it from
|
||||
# source via cargo instead. This pulls in a Rust
|
||||
# toolchain (via rustup, only if cargo isn't already on
|
||||
# PATH) and compiles Neovide and its dependency tree,
|
||||
# which can take several minutes.
|
||||
echo "Neovide has no Fedora/Debian/Ubuntu package -- building it from source"
|
||||
echo "(via cargo -- this can take several minutes)..."
|
||||
(
|
||||
set -e
|
||||
case "$DISTRO_FAMILY" in
|
||||
fedora)
|
||||
sudo dnf install -y clang ninja-build fontconfig-devel freetype-devel \
|
||||
@development-tools libstdc++-static libstdc++-devel
|
||||
;;
|
||||
debian)
|
||||
sudo apt-get install -y curl gnupg ca-certificates git clang ninja-build \
|
||||
gcc-multilib g++-multilib cmake libssl-dev pkg-config libfreetype6-dev \
|
||||
libasound2-dev libexpat1-dev libxcb-composite0-dev libbz2-dev libsndio-dev \
|
||||
freeglut3-dev libxmu-dev libxi-dev libfontconfig1-dev libxcursor-dev
|
||||
;;
|
||||
esac
|
||||
if ! command -v cargo >/dev/null 2>&1; then
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
fi
|
||||
# shellcheck disable=SC1091
|
||||
. "$HOME/.cargo/env"
|
||||
cargo install --git https://github.com/neovide/neovide --locked
|
||||
) || echo "WARNING: Neovide build failed -- see https://neovide.dev/installation.html#linux-source for manual steps"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# This config relies on fairly recent Neovim APIs (vim.lsp.config/
|
||||
# enable, virtual_lines diagnostics, vim.o.winborder -- all 0.11+).
|
||||
# Distro-packaged Neovim can lag well behind that, especially on
|
||||
# Debian stable and older Ubuntu LTS releases. Warn (don't fail) if
|
||||
# what actually got installed is too old.
|
||||
NVIM_VER_LINE=$(nvim --version 2>/dev/null | head -n1)
|
||||
NVIM_MAJOR=$(echo "$NVIM_VER_LINE" | sed -n 's/.*v\([0-9]*\)\.\([0-9]*\).*/\1/p')
|
||||
NVIM_MINOR=$(echo "$NVIM_VER_LINE" | sed -n 's/.*v\([0-9]*\)\.\([0-9]*\).*/\2/p')
|
||||
if [ -n "$NVIM_MAJOR" ] && [ -n "$NVIM_MINOR" ] && [ "$NVIM_MAJOR" -eq 0 ] && [ "$NVIM_MINOR" -lt 11 ]; then
|
||||
echo "WARNING: Installed Neovim is v${NVIM_MAJOR}.${NVIM_MINOR}, but this config needs 0.11+."
|
||||
echo " Debian stable and older Ubuntu LTS releases often ship an older"
|
||||
echo " version. See https://github.com/neovim/neovim/blob/master/INSTALL.md"
|
||||
echo " for official AppImage/PPA/prebuilt options if so."
|
||||
fi
|
||||
|
||||
# Install global NPM packages for JS/TS, HTML, and CSS LSPs
|
||||
echo "Installing NPM-based LSPs..."
|
||||
sudo npm install -g typescript typescript-language-server vscode-langservers-extracted
|
||||
|
||||
# 2. Install Nerd Fonts (JetBrainsMono and Cascadia Code)
|
||||
echo "[2/4] Installing JetBrainsMono and Cascadia Code Nerd Fonts..."
|
||||
FONT_DIR="$HOME/.local/share/fonts"
|
||||
mkdir -p "$FONT_DIR"
|
||||
|
||||
# JetBrainsMono
|
||||
if [ ! -f "$FONT_DIR/JetBrainsMonoNerdFont-Regular.ttf" ]; then
|
||||
wget -q --show-progress https://github.com/ryanoasis/nerd-fonts/releases/latest/download/JetBrainsMono.zip -O /tmp/JetBrainsMono.zip
|
||||
unzip -q -o /tmp/JetBrainsMono.zip -d "$FONT_DIR"
|
||||
rm /tmp/JetBrainsMono.zip
|
||||
echo "JetBrainsMono Nerd Font downloaded."
|
||||
else
|
||||
echo "JetBrainsMono Nerd Font already installed."
|
||||
fi
|
||||
|
||||
# Cascadia Code (CaskaydiaCove)
|
||||
if [ ! -f "$FONT_DIR/CaskaydiaCoveNerdFont-Regular.ttf" ]; then
|
||||
wget -q --show-progress https://github.com/ryanoasis/nerd-fonts/releases/latest/download/CascadiaCode.zip -O /tmp/CascadiaCode.zip
|
||||
unzip -q -o /tmp/CascadiaCode.zip -d "$FONT_DIR"
|
||||
rm /tmp/CascadiaCode.zip
|
||||
echo "Cascadia Code Nerd Font downloaded."
|
||||
else
|
||||
echo "Cascadia Code Nerd Font already installed."
|
||||
fi
|
||||
|
||||
# Update font cache
|
||||
fc-cache -fv
|
||||
echo "Fonts installed and cache updated successfully!"
|
||||
|
||||
# 3. Delete existing Neovim config
|
||||
echo "[3/4] Deleting existing Neovim configurations..."
|
||||
if [ -d "$HOME/.config/nvim" ]; then
|
||||
rm -fr "$HOME/.config/nvim"
|
||||
fi
|
||||
if [ -d "$HOME/.local/share/nvim" ]; then
|
||||
rm -fr "$HOME/.local/share/nvim"
|
||||
fi
|
||||
|
||||
# 4. Install Neovim configuration (copied from config/nvim, not
|
||||
# embedded in this script -- edit those files directly to customize).
|
||||
echo "[4/4] Installing new Neovim configuration..."
|
||||
NVIM_DIR="$HOME/.config/nvim"
|
||||
mkdir -p "$NVIM_DIR/lua/plugins"
|
||||
mkdir -p "$NVIM_DIR/lua/dap/configurations"
|
||||
|
||||
cp "$CONFIG_DIR/nvim/init.lua" "$NVIM_DIR/init.lua"
|
||||
cp "$CONFIG_DIR/nvim/lua/plugins/init.lua" "$NVIM_DIR/lua/plugins/init.lua"
|
||||
cp "$CONFIG_DIR/nvim/lua/dap/configurations/cmake.lua" "$NVIM_DIR/lua/dap/configurations/cmake.lua"
|
||||
|
||||
# --- ~/.clang-format, ~/.clang-tidy, and ~/cpp-style-guide.md ---
|
||||
# Personal fallback style, implementing a specific C/C++ coding style
|
||||
# guide (naming conventions, brace style, include ordering, etc. --
|
||||
# see the comments in each file for exactly which guide section a
|
||||
# given setting maps to, and cpp-style-guide.md itself for the guide
|
||||
# those comments point into). Both tools walk UP from the file being
|
||||
# checked/formatted looking for their respective config file; $HOME
|
||||
# sits above every project you'll open, so these become your default
|
||||
# for any project that doesn't ship its own .clang-format/.clang-tidy
|
||||
# (a project's own file, being closer to the file, always takes
|
||||
# precedence over these).
|
||||
#
|
||||
# <leader>lf (LSP format) and <leader>lt (clang-tidy --fix) each only
|
||||
# consume .clang-format / .clang-tidy respectively. clangd's live
|
||||
# diagnostics pick up .clang-tidy automatically too (clang-tidy
|
||||
# integration is on by default in clangd).
|
||||
#
|
||||
# Copied unconditionally (not "if missing") every run, same as the
|
||||
# rest of this config -- so a stray pre-existing file from some other
|
||||
# tool can't silently block these from taking effect.
|
||||
cp "$CONFIG_DIR/clang-format" "$HOME/.clang-format"
|
||||
cp "$CONFIG_DIR/clang-tidy" "$HOME/.clang-tidy"
|
||||
cp "$CONFIG_DIR/cpp-style-guide.md" "$HOME/cpp-style-guide.md"
|
||||
|
||||
echo "Wrote ~/.clang-format and ~/.clang-tidy (Allman braces, real tabs), and ~/cpp-style-guide.md."
|
||||
|
||||
echo "========================================="
|
||||
echo " Setup Complete!"
|
||||
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 " <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"
|
||||
if [ "$NVIM_FRONTEND" = "desktop" ] || [ "$NVIM_FRONTEND" = "both" ]; then
|
||||
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 " in shell startup files, which this script's own shell won't have re-read)."
|
||||
fi
|
||||
echo "========================================="
|
||||
Reference in new issue
Block a user