#!/bin/bash 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)" 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 # ` (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 # 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. Create Neovim Configuration Structure echo "[4/4] Writing new Neovim configuration..." NVIM_DIR="$HOME/.config/nvim" mkdir -p "$NVIM_DIR/lua/plugins" mkdir -p "$NVIM_DIR/lua/dap/configurations" # --- init.lua --- cat << 'EOF' > "$NVIM_DIR/init.lua" 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 -- 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 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', '', 'nohlsearch') -- 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 -- 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 -- 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 '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 -- 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 -- 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 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 whenever one -- is active for the buffer. vim.keymap.set('i', '', function() local ok_ls, luasnip = pcall(require, "luasnip") if ok_ls and luasnip.session.current_nodes[vim.api.nvim_get_current_buf()] then return '' end local line = vim.api.nvim_get_current_line() -- Current line is JUST a marker with nothing typed after it (i.e. -- the previous 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. -- -- 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 continuation itself already is. if line:match("^%s*//%s?$") or line:match("^%s*#%s?$") or line:match("^%s*%-%-%s?$") then return '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 '' .. prefix end end return '' 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', 'cb', function() vim.api.nvim_set_current_line("") end, { desc = "Clear Line (Blank)" }) -- 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', '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 '>+1gv=gv", { desc = "Move line down" }) vim.keymap.set("v", "K", ":m '<-2gv=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', '', ':m .+1==', { desc = "Move line down" }) vim.keymap.set('n', '', ':m .-2==', { desc = "Move line up" }) vim.keymap.set('i', '', ':m .+1==gi', { desc = "Move line down" }) vim.keymap.set('i', '', ':m .-2==gi', { desc = "Move line up" }) vim.keymap.set('v', '', ":m '>+1gv=gv", { desc = "Move selection down" }) vim.keymap.set('v', '', ":m '<-2gv=gv", { desc = "Move selection up" }) -- Easy Save vim.keymap.set({'n', 'v', 'i'}, '', 'w', { desc = "Save File" }) -- Window Navigation (Ctrl + h/j/k/l) vim.keymap.set('n', '', 'h', { desc = "Go to left window" }) vim.keymap.set('n', '', 'j', { desc = "Go to lower window" }) vim.keymap.set('n', '', 'k', { desc = "Go to upper window" }) vim.keymap.set('n', '', 'l', { desc = "Go to right window" }) -- Window Splits (Namespaced under s) vim.keymap.set('n', 'sv', 'v', { desc = "Split Vertically" }) vim.keymap.set('n', 'sh', 's', { desc = "Split Horizontally" }) vim.keymap.set('n', 'se', '=', { desc = "Make Splits Equal" }) vim.keymap.set('n', 'sx', 'close', { desc = "Close Current Split" }) -- --------------------------------------------------------------------- -- 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") EOF # --- plugins.lua --- cat << 'EOF' > "$NVIM_DIR/lua/plugins/init.lua" return { -- REALLY Dark Theme (Pitch Black TokyoNight) { "folke/tokyonight.nvim", priority = 1000, config = function() require("tokyonight").setup({ style = "night", transparent = false, on_colors = function(colors) colors.bg = "#000000" -- Pure black background colors.bg_dark = "#000000" colors.bg_float = "#050505" colors.bg_highlight = "#1a1a1a" colors.bg_popup = "#050505" colors.bg_sidebar = "#000000" colors.bg_statusline = "#000000" end, on_highlights = function(hl, c) hl.DiagnosticVirtualTextError = { bg = "#351010", fg = c.error } hl.DiagnosticVirtualTextWarn = { bg = "#35280b", fg = c.warning } hl.DiagnosticVirtualTextInfo = { bg = "#0b2035", fg = c.info } hl.DiagnosticVirtualTextHint = { bg = "#0b3528", fg = c.hint } hl.DiagnosticVirtualLinesError = { bg = "#351010", fg = c.error } hl.DiagnosticVirtualLinesWarn = { bg = "#35280b", fg = c.warning } hl.DiagnosticVirtualLinesInfo = { bg = "#0b2035", fg = c.info } hl.DiagnosticVirtualLinesHint = { bg = "#0b3528", fg = c.hint } end, }) vim.cmd[[colorscheme tokyonight]] end, }, -- Treesitter (syntax-aware highlighting + indentation) -- -- NOTE: pinned to branch = "master". Upstream's default branch is now -- "main", which is a full incompatible rewrite that deleted the old -- require("nvim-treesitter.configs").setup({...}) API entirely (this -- is what caused "module 'nvim-treesitter.configs' not found"). The -- "master" branch is explicitly kept frozen by upstream for backward -- compatibility with this config style, so pin to it rather than -- rewriting everything against the new API. { "nvim-treesitter/nvim-treesitter", branch = "master", build = ":TSUpdate", config = function() require("nvim-treesitter.configs").setup({ ensure_installed = { "c", "cpp", "lua", "go", "python", "javascript", "typescript", "html", "css", "bash", "json", "yaml", "markdown", "cmake", "doxygen", }, highlight = { enable = true }, indent = { enable = true, -- treesitter's indent module is well-known to be unreliable -- for a handful of languages specifically (inconsistent -- brace placement, phantom extra indent on nested blocks) -- while being fine for the rest. This is more pronounced on -- the "master" branch pinned above, since it's frozen and -- gets no further upstream fixes at all. Disabling it for -- these falls back to Neovim's own built-in, per-filetype -- indent logic (cindent for c/cpp, the bundled javascript/ -- typescript indent scripts for the rest), which is far -- more predictable for brace-heavy code. -- -- css and json are also curly-brace languages that CAN hit -- the same class of issue -- add them here too if you -- notice the same symptom there. disable = { "c", "cpp", "javascript", "typescript" }, }, }) end, }, -- Doxygen (and other language) doc-comment generation. -- -- Not a language server -- clangd already understands Doxygen syntax -- in existing comments (hover/signature-help render @param/@return), -- this is the other half: generating the comment skeleton itself. -- Place the cursor on/above a function and press cd to drop -- in a Doxygen-style block with @brief/@param/@return stubs already -- matching that function's actual parameters and return type. { "danymat/neogen", dependencies = "nvim-treesitter/nvim-treesitter", config = function() require("neogen").setup({ enabled = true, -- c/cpp's default annotation_convention is already -- "doxygen_cpp", so nothing to override there. snippet_engine = "luasnip", -- inserted fields become LuaSnip -- tabstops, so / (wired up in the nvim-cmp config -- below for cmp/LuaSnip) cycles through @brief, each @param, -- and @return. }) vim.keymap.set("n", "cd", function() require("neogen").generate() end, { desc = "Generate Doxygen Comment" }) end, }, -- Dashboard (NvChad-style start screen) { "goolord/alpha-nvim", dependencies = { "nvim-tree/nvim-web-devicons" }, config = function() local alpha = require("alpha") local dashboard = require("alpha.themes.dashboard") dashboard.section.header.val = { " ", " ███╗ ██╗██╗ ██╗██╗███╗ ███╗ ", " ████╗ ██║██║ ██║██║████╗ ████║ ", " ██╔██╗ ██║██║ ██║██║██╔████╔██║ ", " ██║╚██╗██║╚██╗ ██╔╝██║██║╚██╔╝██║ ", " ██║ ╚████║ ╚████╔╝ ██║██║ ╚═╝ ██║ ", " ╚═╝ ╚═══╝ ╚═══╝ ╚═╝╚═╝ ╚═╝ ", " ", } dashboard.section.buttons.val = { dashboard.button("f", " Find File", "Telescope find_files"), dashboard.button("r", " Recent Files", "Telescope oldfiles"), dashboard.button("w", " Live Grep", "Telescope live_grep"), dashboard.button("e", " New File", "ene"), dashboard.button("c", " Edit Config", "edit ~/.config/nvim/init.lua"), dashboard.button("q", " Quit", "qa"), } alpha.setup(dashboard.opts) end, }, -- Which-Key (Registers your namespaces so the menu looks clean) { "folke/which-key.nvim", event = "VeryLazy", config = function() local wk = require("which-key") wk.setup({ delay = 0, }) -- Define group names for the menu wk.add({ { "b", group = "Buffers" }, { "c", group = "Comments" }, { "f", group = "Find / Grep" }, { "l", group = "LSP" }, { "s", group = "Splits" }, { "t", group = "Terminal" }, { "d", group = "Debug" }, }) end, }, -- Telescope (Fuzzy Finding and Grepping) { "nvim-telescope/telescope.nvim", -- NOTE: intentionally NOT pinned to branch = "0.1.x" -- that branch's -- previewer calls nvim-treesitter's now-removed `ft_to_lang` helper, -- which crashes the file preview with "attempt to call field -- 'ft_to_lang' (a nil value)". The current default branch calls the -- built-in vim.treesitter.language.get_lang() instead, which isn't -- affected by that removal. dependencies = { "nvim-lua/plenary.nvim" }, config = function() local builtin = require('telescope.builtin') vim.keymap.set('n', 'ff', builtin.find_files, { desc = "Find Files" }) vim.keymap.set('n', 'fw', builtin.live_grep, { desc = "Find Word (Live Grep)" }) vim.keymap.set('n', 'fb', builtin.buffers, { desc = "Find Open Buffers" }) vim.keymap.set('n', 'fh', builtin.help_tags, { desc = "Find Help Tags" }) vim.keymap.set('n', 'fr', builtin.oldfiles, { desc = "Find Recent Files" }) end, }, -- NvimTree (On the right) { "nvim-tree/nvim-tree.lua", dependencies = { "nvim-tree/nvim-web-devicons" }, config = function() require("nvim-tree").setup({ view = { side = "right", width = 30, }, -- Routes the paste overwrite/rename CHOICE through -- vim.ui.select tagged with kind = "nvimtree_overwrite_rename" -- (nvim-tree's own documented hook for this), which the gP -- batch-paste below intercepts. select_prompts = true, -- Keep nvim-tree's own default mappings and add gP on top of -- them, via the officially documented on_attach recipe -- -- rather than the TreeAttachedPost event this used before, -- whose handler is passed the bufnr directly as a plain -- number (confirmed from nvim-tree's own docs), not a table -- with a .buf field -- indexing that number is what threw -- "attempt to index local 'data' (a number value)". on_attach = function(bufnr) local api = require("nvim-tree.api") -- The helper that installs the defaults has been named -- differently across versions (api.config.mappings. -- default_on_attach vs the older api.map.on_attach.default) -- -- try both rather than assume one, so this doesn't -- silently lose every default mapping on a version where -- the other name is the real one. if not pcall(api.config.mappings.default_on_attach, bufnr) then pcall(api.map.on_attach.default, bufnr) end -- gP: paste, resolving every naming collision in this one -- paste with a single :s///-style pattern/replacement -- -- CAVEAT: this assumes a single-file-style overwrite/rename -- choice per conflict. nvim-tree turns out to have a -- SEPARATE, differently-shaped dialog specifically for -- multi-file conflicts in one paste ("N file(s) already -- exist" / Rename (suffix) / Overwrite all / Skip all), -- which this doesn't detect or intercept -- so gP is only -- confirmed reliable for a single conflicting file. For -- "always rename every file, conflict or not" -- which is -- what was actually being asked for -- gM below is the -- better fit: it doesn't touch nvim-tree's paste/conflict -- system at all, just copies marked files directly. -- instead of nvim-tree's normal one-at-a-time -- overwrite/rename prompt per conflicting file. Plain `p` -- (bound above by default_on_attach) is untouched -- this -- is a deliberately separate mapping, not a replacement. -- -- nvim-tree doesn't expose a way to enumerate the clipboard -- or pre-scan for collisions before pasting (only -- print/clear it), and conflicts are resolved one at a time -- internally as the paste runs -- there's no "here's the -- whole batch" moment to hook into directly. This gets the -- same practical result a different way: ask ONCE upfront -- for the pattern, then answer nvim-tree's own conflict -- prompts programmatically for the duration of that paste -- using vim.fn.substitute() (real Vim regex, same engine -- :s/// itself uses) against the filename nvim-tree -- pre-fills as its default. -- -- vim.ui.select/vim.ui.input are wrapped HERE, at call time -- inside the keymap function, rather than once when this -- plugin's config runs. nvim-tree loads eagerly at startup, -- but dressing.nvim (which ALSO wraps these same two -- functions, to prettify them) loads lazily on VeryLazy, -- afterward -- wrapping once at config time would capture -- the plain pre-dressing versions, which then get silently -- overwritten (discarding this wrapper entirely) once -- dressing finishes loading. Wrapping fresh on every call -- instead always captures whatever's actually active. vim.keymap.set('n', 'gP', function() vim.ui.input({ prompt = "Rename pattern/replacement (Vim regex, e.g. foo/bar): " }, function(input) if not input or input == "" then api.fs.paste() return end local pattern, replacement = input:match("^(.-)/(.*)$") if not pattern then vim.notify("Expected pattern/replacement, e.g. foo/bar", vim.log.levels.WARN) return end local real_select = vim.ui.select local real_input = vim.ui.input vim.ui.select = function(items, opts, on_choice) if opts and opts.kind == "nvimtree_overwrite_rename" then for i, item in ipairs(items) do if tostring(item):lower():match("rename") then on_choice(item, i) return end end end real_select(items, opts, on_choice) end vim.ui.input = function(input_opts, on_confirm) if input_opts and input_opts.default then local ok, new_name = pcall(vim.fn.substitute, input_opts.default, pattern, replacement, "") on_confirm(ok and new_name ~= "" and new_name or input_opts.default) return end real_input(input_opts, on_confirm) end -- Restored via a short defer rather than immediately -- after api.fs.paste() returns, since it's not -- confirmed whether paste() resolves every prompt -- synchronously before returning to caller or defers -- some of them -- restoring immediately risked only -- patching the FIRST conflict in a multi-file paste and -- silently falling back to normal prompts for the rest. -- If that's what happens, this assumption is the next -- thing to revisit. api.fs.paste() vim.defer_fn(function() vim.ui.select = real_select vim.ui.input = real_input end, 2000) end) end, { desc = "nvim-tree: Paste (Batch Rename Regex)", buffer = bufnr, silent = true }) -- Clears the copy/cut clipboard outright. Useful if it's -- holding a stale entry -- a file that's since been -- renamed, moved, or deleted -- which pastes as "ENOENT: no -- such file or directory" until cleared and re-copied fresh. vim.keymap.set('n', 'gC', api.fs.clear_clipboard, { desc = "nvim-tree: Clear Clipboard", buffer = bufnr, silent = true }) -- gM: copy every MARKED file/folder (toggle a mark with m, -- shown as a star) into the directory under the cursor, -- applying a single :s///-style pattern/replacement to -- EVERY name unconditionally -- not just on collision. This -- is deliberately separate from nvim-tree's own copy/paste -- clipboard (c/x/p/gP above) entirely: it reads the marked -- list via api.marks.list() (a real, documented, stable -- API -- unlike the clipboard, which has no equivalent way -- to enumerate its contents) and shells out to `cp -r` to -- do the actual copying itself, the same approach used in -- nvim-tree's own official custom-copy recipe. Marks are -- left as-is afterward (not auto-cleared), matching how the -- regular clipboard also isn't cleared after a paste -- -- toggle them off individually with m if you're done with -- them. vim.keymap.set('n', 'gM', function() local marks = api.marks.list() if not marks or #marks == 0 then vim.notify("No marked files (mark with 'm' first)", vim.log.levels.WARN) return end local cursor_node = api.tree.get_node_under_cursor() if not cursor_node then return end local dest_dir = cursor_node.type == "directory" and cursor_node.absolute_path or vim.fn.fnamemodify(cursor_node.absolute_path, ":h") vim.ui.input({ prompt = "Rename pattern/replacement (Vim regex, e.g. Foo/Bar): " }, function(input) if not input or input == "" then return end local pattern, replacement = input:match("^(.-)/(.*)$") if not pattern then vim.notify("Expected pattern/replacement, e.g. Foo/Bar", vim.log.levels.WARN) return end for _, node in ipairs(marks) do if not vim.loop.fs_stat(node.absolute_path) then -- Marks persist by path in nvim-tree's own state and -- are NOT cleared when the underlying file/folder is -- deleted, so a stale mark can point at a path that -- no longer exists (e.g. you marked it, then deleted -- it). Clean up the stale mark itself here instead -- of attempting (and failing) a copy from it. pcall(api.marks.toggle, node) vim.notify("Skipped stale mark (no longer exists): " .. node.absolute_path, vim.log.levels.WARN) else local base = vim.fn.fnamemodify(node.absolute_path, ":t") local ok, new_name = pcall(vim.fn.substitute, base, pattern, replacement, "") if not ok or new_name == "" then new_name = base end local dest_path = dest_dir .. "/" .. new_name if dest_path == node.absolute_path then -- The pattern didn't match this particular name -- (so it came back unchanged) and the destination -- is the same directory the file's already in -- -- that's a copy onto itself, which cp correctly -- refuses. Skip it up front with a clearer reason -- instead of surfacing cp's raw "same file" error. vim.notify("Skipped " .. base .. ": pattern didn't match, and destination is the same as source", vim.log.levels.WARN) else local result = vim.fn.system({ "cp", "-r", node.absolute_path, dest_path }) if vim.v.shell_error ~= 0 then vim.notify("Copy failed for " .. base .. ": " .. result, vim.log.levels.ERROR) end end end end api.tree.reload() end) end, { desc = "nvim-tree: Copy Marked (Regex Rename)", buffer = bufnr, silent = true }) end, }) vim.keymap.set('n', 'e', ':NvimTreeToggle', { silent = true, desc = "Toggle File Explorer" }) end, }, -- Bufferline (Tabs) { "akinsho/bufferline.nvim", version = "*", dependencies = "nvim-tree/nvim-web-devicons", config = function() require("bufferline").setup({ options = { diagnostics = "nvim_lsp", show_buffer_close_icons = false, numbers = "ordinal", -- shows 1, 2, 3... on the left of each tab, -- matching the position used by the Alt+number jump below. -- -- Makes :BufferLineMovePrev/MoveNext refuse to walk an -- unpinned buffer across the pinned block (wraps to the -- other end instead) -- this is bufferline's own built-in -- boundary check (see get_last_pinned_index/M.move in its -- source), just off by default. move_wraps_at_ends = true, } }) -- Navigate buffers. Using bufferline's own cycle commands rather -- than :bprevious/:bnext -- once buffers can be reordered (see -- / below), plain :bnext/:bprevious would cycle by -- vim's internal buffer number instead of the order shown on -- screen, which stops matching what you actually see. This is -- bufferline's own documented recommendation for exactly this -- situation. vim.keymap.set('n', '', 'BufferLineCyclePrev', { desc = "Prev buffer" }) vim.keymap.set('n', '', 'BufferLineCycleNext', { desc = "Next buffer" }) vim.keymap.set('n', 'bd', 'bdelete', { desc = "Close Current Buffer" }) -- Alt+1..9: jump straight to the buffer at that ordinal position -- (the number shown on the left of each tab). Alt+0 jumps to the -- last one, mirroring bufferline's own "$" convention. for i = 1, 9 do vim.keymap.set('n', (''):format(i), function() require('bufferline').go_to(i, true) end, { desc = "Go to Buffer " .. i }) end vim.keymap.set('n', '', function() require('bufferline').go_to(-1, true) end, { desc = "Go to Last Buffer" }) -- F1..F9: same jump-by-position, on Fn+Number instead of -- Alt+Number. Explicitly setting here overrides Neovim's -- default -opens-help binding outright (any explicit -- vim.keymap.set for a key always supersedes Neovim's built-in -- default for it), so no separate step is needed to "disable" -- that. -- -- and are set below, OUTSIDE this loop -- both are -- shared with nvim-dap (Continue and Step Over respectively) and -- toggle dynamically based on whether a debug session is active -- (see the dap.listeners hooks in the DAP plugin further down). -- At rest, both act as ordinary buffer jumps like everything -- else in this loop. for _, i in ipairs({ 1, 2, 3, 4, 6, 7, 8, 9 }) do vim.keymap.set('n', (''):format(i), function() require('bufferline').go_to(i, true) end, { desc = "Go to Buffer " .. i }) end vim.keymap.set('n', '', function() require('bufferline').go_to(5, true) end, { desc = "Go to Buffer 5" }) vim.keymap.set('n', '', function() require('bufferline').go_to(-1, true) end, { desc = "Go to Last Buffer" }) -- Alt+,/. : reorder the current buffer left/right in the -- bufferline (persists for the session as long as -- sessionoptions includes "globals", which lazy.nvim's default -- vimrc already does). move_wraps_at_ends above handles keeping -- these from crossing into the pinned block -- no custom guard -- needed here. vim.keymap.set('n', '', 'BufferLineMovePrev', { desc = "Move Buffer Left" }) vim.keymap.set('n', '', 'BufferLineMoveNext', { desc = "Move Buffer Right" }) -- Pin the current buffer -- pinned buffers stay pinned to the -- start of the bufferline regardless of sorting/reordering. -- -- Pin state is ALSO recorded in a plain buffer-local variable -- here, set at the same time as the real toggle. bufferline's -- internal pin/group state isn't reliably exposed as a stable -- public API across versions -- require('bufferline').group_action -- (tried previously) doesn't exist on the version actually -- installed here, hence the earlier E5108 error. Tracking it -- ourselves means bx below never has to ask bufferline -- which buffers are pinned at all. -- -- :BufferLineTogglePin only updates the RENDER-time grouping; it -- doesn't physically move the buffer within bufferline's -- internal position list (state.components), which is the exact -- list go_to()/number labels are computed from. That gap is what -- caused Alt+number to occasionally land on/label the wrong -- buffer right after pinning. move_to() operates on that same -- internal list directly (it's what :BufferLineMovePrev/Next and -- the sort commands use internally), so calling it here forces -- the two back into sync immediately: to the front when pinning, -- to just after the remaining pinned block when unpinning. vim.keymap.set('n', 'bp', function() vim.cmd('BufferLineTogglePin') local buf = vim.api.nvim_get_current_buf() local now_pinned = not vim.b[buf].pinned vim.b[buf].pinned = now_pinned if now_pinned then require('bufferline').move_to(1) else local pinned_count = 0 for _, b in ipairs(vim.fn.getbufinfo({ buflisted = 1 })) do if b.bufnr ~= buf and vim.b[b.bufnr].pinned then pinned_count = pinned_count + 1 end end require('bufferline').move_to(pinned_count + 1) end end, { desc = "Toggle Pin" }) -- Close every unpinned buffer that has NO unsaved changes, using -- the buffer-local pin flag set above -- deliberately not using -- :BufferLineGroupClose ungrouped, since its default -- close_command is "bdelete! %d" (forced), which would silently -- discard unsaved work in any unpinned buffer. vim.keymap.set('n', 'bx', function() for _, buf in ipairs(vim.fn.getbufinfo({ buflisted = 1 })) do if not vim.b[buf.bufnr].pinned and buf.changed == 0 then pcall(vim.cmd, 'bdelete ' .. buf.bufnr) end end end, { desc = "Close Unpinned Saved Buffers" }) end, }, -- Statusline { "nvim-lualine/lualine.nvim", dependencies = { "nvim-tree/nvim-web-devicons" }, config = function() require("lualine").setup({ options = { theme = "tokyonight", component_separators = { left = "│", right = "│" }, section_separators = { left = "", right = "" }, globalstatus = true, }, sections = { lualine_a = { "mode" }, lualine_b = { "branch", "diagnostics" }, lualine_c = { { "filename", path = 1 } }, lualine_x = { "encoding", "filetype" }, lualine_y = { "progress" }, lualine_z = { "location" }, }, }) end, }, -- ToggleTerm (Floating & Bottom Terminal) { "akinsho/toggleterm.nvim", version = "*", config = function() -- shell = "fish": the embedded terminal launches fish instead of -- your login shell. fish has grey inline "ghost text" suggestions -- built in (paths, directory contents, and history) with zero -- config -- accept with -> or . This only affects terminals -- opened from Neovim; your system login shell is untouched. require("toggleterm").setup({ shell = "fish", }) vim.keymap.set('n', 'tf', 'ToggleTerm direction=float', { desc = "Floating Terminal" }) vim.keymap.set('n', 'th', 'ToggleTerm direction=horizontal', { desc = "Bottom Terminal" }) vim.keymap.set('n', 'tv', 'ToggleTerm direction=vertical size=40', { desc = "Side Terminal" }) end, }, -- Git State { "lewis6991/gitsigns.nvim", config = function() require("gitsigns").setup() end, }, -- Indent guides { "lukas-reineke/indent-blankline.nvim", main = "ibl", config = function() require("ibl").setup({ indent = { char = "│" }, scope = { enabled = true, -- show_start/show_end default to true and underline the -- first/last line of whatever lexical scope the cursor is -- currently inside (recalculated as the cursor moves -- -- that's why it seemed to "follow" the cursor). The colored -- guide itself is kept; just the underline is turned off. show_start = false, show_end = false, }, }) end, }, -- Inline hex/rgb color preview { "norcalli/nvim-colorizer.lua", config = function() require("colorizer").setup() end, }, -- Hex file viewer/editor. Toggles between normal and hex-dump view -- (backed by xxd) rather than showing both simultaneously -- that -- trade-off is deliberate: since the hex dump is just ordinary, -- plain buffer text under the hood, editing it gets Neovim's normal -- undo/redo for free, and nothing writes to disk until an explicit -- :w, same as any other file. The only plugin found that does a -- true live simultaneous hex+ASCII view (hexview.nvim, used here -- previously) manages bytes with its own custom logic instead of -- going through normal buffer text editing, which is exactly why it -- has no undo at all -- not a gap that plugin could patch, an -- architectural trade-off against the same thing being fixed here. { "RaafatTurki/hex.nvim", config = function() require("hex").setup() vim.keymap.set('n', 'h', function() if not vim.bo.binary then -- Hex editing depends on the buffer actually being loaded -- as binary. Otherwise Neovim applies normal text encoding/ -- line-ending handling to the raw bytes first (NUL bytes -- included, which any real binary is full of) -- that -- misinterpretation is what produces "CONVERSION ERROR" -- rather than a clean hex dump, and it happens BEFORE xxd -- ever runs. Re-reading with ++bin loads the file correctly -- from scratch instead of operating on an already-misread -- buffer. Forced (!) since there's nothing worth preserving -- from a buffer that was misread as text in the first place. vim.cmd('edit! ++bin %') end vim.cmd('HexToggle') end, { desc = "Toggle Hex View" }) end, }, -- Nicer notifications (replaces the default vim.notify popups) { "rcarriga/nvim-notify", config = function() require("notify").setup({ background_colour = "#000000", timeout = 3000, -- Stack upward from the bottom instead of down from the top -- (nvim-notify anchors to the right by default either way), -- so notifications land bottom-right instead of top-right. top_down = false, -- Switched from "compact": that renderer has a documented -- history of text overflow/wrapping bugs, and is the prime -- suspect for the garbled/duplicated text seen in some -- NvimTree error notifications. Not confirmed as the actual -- cause, but worth trying alongside the position change. }) vim.notify = require("notify") end, }, -- Prettier vim.ui.input / vim.ui.select -- upgrades your existing -- rename prompt, conditional breakpoint prompt, and DAP path prompts -- to floating, styled dialogs instead of the command-line input. { "stevearc/dressing.nvim", event = "VeryLazy", opts = {}, }, -- LSP and Mason { "neovim/nvim-lspconfig", dependencies = { "williamboman/mason.nvim", "williamboman/mason-lspconfig.nvim", "hrsh7th/cmp-nvim-lsp", }, config = function() -- Enable native above-line virtual lines with custom formatting vim.diagnostic.config({ underline = false, -- virtual_lines already shows the message; skip the extra underline virtual_text = false, virtual_lines = { format = function(diagnostic) local msg = diagnostic.message or "" msg = msg:gsub("^[a-z_][a-z0-9_%-]*:%s*", "") return msg end, }, }) -- NOTE: modern Neovim (0.11+) dedupes diagnostics internally, and -- the old vim.lsp.with()-based publishDiagnostics override is -- deprecated, so it's intentionally not used here. require("mason").setup() require("mason-lspconfig").setup({ ensure_installed = { "clangd", "gopls", "omnisharp", "ts_ls", "html", "cssls" }, }) local capabilities = require("cmp_nvim_lsp").default_capabilities() -- LSP Keymaps (Organized under l), applied on attach via -- the LspAttach autocommand rather than a per-server on_attach -- callback passed through lspconfig[x].setup(). local function on_attach(bufnr) -- Core goto commands (Standard Vim behavior, no leader required) -- -- #include lines get special handling: clangd resolves -- textDocument/definition (and /declaration) on an #include -- directive to the target header regardless of which token on -- that line the cursor sits on, so both gd and gD below open -- it directly. If clangd hasn't attached/indexed yet, or the -- header isn't part of the compile database, we fall back to -- Vim's built-in "goto file under cursor" (gf), which still -- resolves a local #include "quoted.h" relative to the -- current file even with no LSP involved at all. local function is_include_line() return vim.api.nvim_get_current_line():match('^%s*#%s*include') ~= nil end -- Shared result handler for definition/declaration: jump -- straight there for a single match; fill the quickfix list -- and open it for multiple (e.g. a function declared once but -- defined across several translation units) instead of -- silently picking whichever one clangd listed first. local function on_list(t) if vim.tbl_isempty(t.items) then if is_include_line() then vim.cmd('normal! gf') end return end vim.fn.setqflist({}, ' ', t) if #t.items > 1 then vim.cmd('botright copen') else vim.cmd('cfirst') end end vim.keymap.set('n', 'gd', function() if is_include_line() then vim.lsp.buf.definition({ on_list = on_list }) else -- Regular symbol: Telescope gives a fuzzy picker with -- preview when there's more than one candidate (e.g. -- overloads), and jumps straight there for a single one -- -- nicer than the bare quickfix list for that common case. require('telescope.builtin').lsp_definitions({ reuse_win = true }) end end, { buffer = bufnr, silent = true, desc = "Go to Definition" }) vim.keymap.set('n', 'gD', function() -- No Telescope equivalent exists for declaration (only -- definition/references/implementation/type_definition are -- exposed), so this stays on vim.lsp.buf.declaration -- -- just with the same on_list handling as gd above. vim.lsp.buf.declaration({ on_list = on_list }) end, { buffer = bufnr, silent = true, desc = "Go to Declaration" }) vim.keymap.set('n', 'gr', function() -- vim.lsp.buf.references() on its own just fills the -- quickfix list silently without opening it, so pressing gr -- looks like nothing happened. Telescope's picker actually -- shows the list with a preview, same as gd above. require('telescope.builtin').lsp_references({ reuse_win = true }) end, { buffer = bufnr, silent = true, desc = "Find References" }) vim.keymap.set('n', 'K', vim.lsp.buf.hover, { buffer = bufnr, silent = true, desc = "Hover Info" }) -- Safe Rename Function (Fixes double execution/prompt issues) local function safe_rename() local current_name = vim.fn.expand("") vim.ui.input({ prompt = "Rename: ", default = current_name }, function(new_name) if not new_name or new_name == "" or new_name == current_name then return end vim.lsp.buf.rename(new_name) end) end -- Leader commands (Grouped under l) vim.keymap.set('n', 'lr', safe_rename, { buffer = bufnr, silent = true, desc = "Rename Symbol" }) vim.keymap.set('n', 'la', vim.lsp.buf.code_action, { buffer = bufnr, silent = true, desc = "Code Actions" }) vim.keymap.set('n', 'lf', function() vim.lsp.buf.format { async = true } end, { buffer = bufnr, silent = true, desc = "Format File" }) vim.keymap.set('n', 'li', 'LspInfo', { buffer = bufnr, silent = true, desc = "LSP Info" }) end vim.api.nvim_create_autocmd("LspAttach", { group = vim.api.nvim_create_augroup("UserLspAttach", { clear = true }), callback = function(args) on_attach(args.buf) end, }) -- Setup LSPs via the built-in vim.lsp.config()/vim.lsp.enable() -- API (replaces the deprecated require('lspconfig')[x].setup() -- framework). Default per-server settings still come from -- nvim-lspconfig, which only needs to be on the runtimepath. local servers = { "clangd", "gopls", "omnisharp", "ts_ls", "html", "cssls" } for _, lsp in ipairs(servers) do vim.lsp.config(lsp, { capabilities = capabilities, }) end vim.lsp.enable(servers) end, }, -- Autocompletion { "hrsh7th/nvim-cmp", dependencies = { "L3MON4D3/LuaSnip", "saadparwaiz1/cmp_luasnip", "hrsh7th/cmp-nvim-lsp", "hrsh7th/cmp-buffer", "hrsh7th/cmp-path", }, config = function() local cmp = require("cmp") local luasnip = require("luasnip") -- Without this, LuaSnip never checks whether the cursor has -- actually left a snippet's text -- so pressing Tab later, even -- lines away, can jump straight back into an old, unfinished -- placeholder session (e.g. clangd's function-argument -- placeholders). region_check_events makes it watch cursor -- movement and automatically exit a snippet once you've moved -- outside its region, in both normal and insert mode. luasnip.setup({ region_check_events = "CursorMoved,CursorMovedI", }) cmp.setup({ -- Ghost text: previews the currently-selected completion inline -- after the cursor, instead of only showing it in the popup menu. -- Combined with the cmp-path source below, this is what gives you -- inline "ghost" previews as you type a filesystem path (e.g. -- typing ./src/ will ghost-preview entries found in that -- directory) without needing a separate plugin for it. experimental = { ghost_text = true, }, snippet = { expand = function(args) luasnip.lsp_expand(args.body) end, }, window = { completion = cmp.config.window.bordered(), documentation = cmp.config.window.bordered(), }, mapping = cmp.mapping.preset.insert({ -- Safely close the menu without reverting text state [''] = cmp.mapping(function(fallback) if cmp.visible() then cmp.close() end fallback() end, { 'i', 's' }), [''] = cmp.mapping(function(fallback) if cmp.visible() then cmp.close() end fallback() end, { 'i', 's' }), [''] = cmp.mapping(function(fallback) if cmp.visible() then cmp.close() end fallback() end, { 'i', 's' }), [''] = cmp.mapping(function(fallback) if cmp.visible() then cmp.close() end fallback() end, { 'i', 's' }), [''] = cmp.mapping(function(fallback) if cmp.visible() then cmp.close() end fallback() end, { 'i', 's' }), [''] = cmp.mapping(function(fallback) if cmp.visible() then cmp.select_next_item() elseif luasnip.expand_or_jumpable() then -- No completion menu open, but we're sitting in a snippet -- (e.g. clangd's function-argument placeholders after -- accepting a call like memcpy(...)) -- jump to the next -- tabstop/placeholder instead of inserting a literal tab. luasnip.expand_or_jump() else fallback() end end, { 'i', 's' }), [''] = cmp.mapping(function(fallback) if cmp.visible() then cmp.select_prev_item() elseif luasnip.jumpable(-1) then luasnip.jump(-1) else fallback() end end, { 'i', 's' }), [''] = cmp.mapping.confirm({ select = false }), }), sources = cmp.config.sources({ { name = "nvim_lsp" }, { name = "luasnip" }, { name = "buffer" }, -- Lists and completes filesystem paths/directory contents as -- you type a path string (e.g. after typing "./" or "/home/"). -- This is the source that feeds the ghost-text path preview. { name = "path" }, }), }) end, }, -- Auto-pair brackets/quotes: (), {}, [], "", '' -- nvim-autopairs' -- own defaults. <> was tried here too but removed: it collided too -- much with < and > as comparison operators to be worth keeping. { "windwp/nvim-autopairs", dependencies = { "hrsh7th/nvim-cmp" }, config = function() local autopairs = require("nvim-autopairs") -- map_cr defaults to true, which makes nvim-autopairs install -- its OWN insert-mode mapping (for expanding bracket pairs -- onto their own indented line). That's set during plugin -- loading, which runs after init.lua's own top-level code -- -- where the comment-continuation mapping lives -- so it -- was silently overwriting that mapping entirely regardless of -- what it did internally. Disabled here to free up for -- that mapping; the trade-off is losing autopairs' own -- brace-expands-on-Enter behavior, which was never explicitly -- asked for. autopairs.setup({ map_cr = false }) -- Integrates with nvim-cmp so accepting a completion (e.g. a -- function call) doesn't end up with doubled-up parens. local cmp_autopairs = require("nvim-autopairs.completion.cmp") require("cmp").event:on("confirm_done", cmp_autopairs.on_confirm_done()) end, }, -- ========================================================= -- DAP: In-editor C/C++ debugging (breakpoints, step, watch) -- Uses codelldb (installed via Mason) as the debug adapter. -- ========================================================= { "mfussenegger/nvim-dap", dependencies = { "rcarriga/nvim-dap-ui", "nvim-neotest/nvim-nio", "theHamsta/nvim-dap-virtual-text", "jay-babu/mason-nvim-dap.nvim", }, config = function() local dap = require("dap") local dapui = require("dapui") -- Auto-install codelldb through Mason and wire it into nvim-dap. -- NOTE: omitting `handlers` entirely (rather than passing {}) is -- what makes mason-nvim-dap run its DEFAULT handler, which is -- what actually registers a working dap.adapters.codelldb with -- correct dynamic ${port} allocation. Passing handlers = {} -- disables that default handler instead of using it. require("mason-nvim-dap").setup({ ensure_installed = { "codelldb" }, automatic_installation = true, }) require("dapui").setup() require("nvim-dap-virtual-text").setup({ commented = true, }) -- Auto open/close the UI when a debug session starts/ends dap.listeners.after.event_initialized["dapui_config"] = function() dapui.open() end dap.listeners.before.event_terminated["dapui_config"] = function() dapui.close() end dap.listeners.before.event_exited["dapui_config"] = function() dapui.close() end -- and are each shared between an nvim-dap action -- (Continue, Step Over) and a bufferline buffer jump (buffer 5, -- last buffer) -- at rest they're buffer jumps; for the -- duration of a debug session they're handed over to DAP -- instead, so nothing fights over the same key. Note this means -- can no longer START a fresh session on its own (there's -- no session yet at that point, so it's still doing buffer-jump -- duty) -- use dc or db to start one, then -- takes over for continuing from there. Reusing the same -- listener points as the dapui open/close hooks above. dap.listeners.after.event_initialized["fkey_toggle"] = function() vim.keymap.set('n', '', dap.continue, { desc = "Debug: Continue" }) vim.keymap.set('n', '', dap.step_over, { desc = "Debug: Step Over" }) end local function restore_fkey_buffer_jumps() vim.keymap.set('n', '', function() require('bufferline').go_to(5, true) end, { desc = "Go to Buffer 5" }) vim.keymap.set('n', '', function() require('bufferline').go_to(-1, true) end, { desc = "Go to Last Buffer" }) end dap.listeners.before.event_terminated["fkey_toggle"] = restore_fkey_buffer_jumps dap.listeners.before.event_exited["fkey_toggle"] = restore_fkey_buffer_jumps -- C / C++ / Rust launch configs, all sharing the codelldb adapter dap.configurations.cpp = { { name = "Launch executable", type = "codelldb", request = "launch", program = function() return vim.fn.input( "Path to executable: ", vim.fn.getcwd() .. "/", "file" ) end, cwd = "${workspaceFolder}", stopOnEntry = false, args = {}, }, { name = "Attach to process", type = "codelldb", request = "attach", pid = require("dap.utils").pick_process, cwd = "${workspaceFolder}", }, } dap.configurations.c = dap.configurations.cpp dap.configurations.rust = dap.configurations.cpp -- Breakpoint / stepping signs vim.fn.sign_define("DapBreakpoint", { text = "●", texthl = "DiagnosticError", linehl = "", numhl = "" }) vim.fn.sign_define("DapBreakpointCondition", { text = "◆", texthl = "DiagnosticWarn", linehl = "", numhl = "" }) vim.fn.sign_define("DapStopped", { text = "▶", texthl = "DiagnosticInfo", linehl = "DapStoppedLine", numhl = "" }) vim.api.nvim_set_hl(0, "DapStoppedLine", { bg = "#0b3528" }) -- Keymaps, grouped under d (shows in which-key) -- and intentionally NOT bound here -- both are set -- dynamically by the dap.listeners hooks above (Continue/Step -- Over only while a session is active; bufferline's buffer-5/ -- last-buffer jumps otherwise). vim.keymap.set('n', '', dap.step_into, { desc = "Debug: Step Into" }) vim.keymap.set('n', '', dap.step_out, { desc = "Debug: Step Out" }) vim.keymap.set('n', 'db', dap.toggle_breakpoint, { desc = "Toggle Breakpoint" }) vim.keymap.set('n', 'dB', function() dap.set_breakpoint(vim.fn.input("Breakpoint condition: ")) end, { desc = "Conditional Breakpoint" }) vim.keymap.set('n', 'dc', dap.continue, { desc = "Continue" }) vim.keymap.set('n', 'do', dap.step_over, { desc = "Step Over" }) vim.keymap.set('n', 'di', dap.step_into, { desc = "Step Into" }) vim.keymap.set('n', 'dO', dap.step_out, { desc = "Step Out" }) vim.keymap.set('n', 'dr', dap.repl.open, { desc = "Open REPL" }) vim.keymap.set('n', 'dl', dap.run_last, { desc = "Run Last" }) vim.keymap.set('n', 'dt', dap.terminate, { desc = "Terminate Session" }) vim.keymap.set('n', 'du', dapui.toggle, { desc = "Toggle Debug UI" }) vim.keymap.set('n', 'dh', function() require("dap.ui.widgets").hover() end, { desc = "Hover Variable" }) -- Load project-specific CMake-built target configs (defines -- Debug configs for build/tests/... and build/demo/... binaries). -- This also makes nvim-dap lazy-load the same file automatically -- when you start debugging from a CMakeLists.txt buffer. require("dap.configurations.cmake") end, }, } EOF # --- dap/configurations/cmake.lua --- # Project-specific nvim-dap launch configs for CMake-built binaries. cat << 'EOF' > "$NVIM_DIR/lua/dap/configurations/cmake.lua" -- 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 EOF # --- ~/.clang-format and ~/.clang-tidy --- # Personal fallback style, implementing a specific C/C++ coding style # guide (naming conventions, brace style, include ordering, etc. -- # see the comments in each file below for exactly which guide section # each setting maps to). 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). # # NOTE: this REPLACES the earlier simpler .clang-format (LLVM + # Attach/K&R braces) with a full style-guide implementation -- brace # style is intentionally flipped back to Allman here as part of that, # since this is a deliberate, complete style guide rather than the # earlier ad-hoc preference. # # lf (LSP format) only consumes .clang-format -- clangd's # formatting is pure clang-format, unrelated to clang-tidy. The # .clang-tidy file is picked up separately and automatically by # clangd for live diagnostics (clang-tidy integration is on by # default in clangd), not by lf itself. # # Written 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. cat << 'EOF' > "$HOME/.clang-format" # .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 and TOOLING.md). 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++ (, , ...). - 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 — , , ...). - Regex: '^<[a-z_]+>$' Priority: 2 CaseSensitive: true # 4. Third-party / external library headers (, , ...). - 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 EOF cat << 'EOF' > "$HOME/.clang-tidy" # .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. See TOOLING.md for what this file can't check. # # 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. See TOOLING.md. 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 EOF echo "Wrote ~/.clang-format and ~/.clang-tidy (Allman braces, real tabs)." echo "=========================================" echo " Setup Complete!" echo " Run 'nvim'. p and P are restored, use p to force inline paste!" echo " Debugger: db to set a breakpoint, F5 / dc to start/continue," echo " du to toggle the debug UI. First debug session will auto-install codelldb via Mason." echo " Project-specific CMake debug targets loaded from ~/.config/nvim/lua/dap/configurations/cmake.lua" echo "========================================="