From 93c97e65e2bef9d0f74206e955fb9bf87aff3db2 Mon Sep 17 00:00:00 2001 From: Acrux Development Date: Fri, 6 Nov 2026 19:56:13 -0300 Subject: [PATCH] nvim windows support + multicursors --- nvim/README.md | 128 ++++- nvim/config/nvim/init.lua | 33 +- .../nvim/lua/dap/configurations/cmake.lua | 9 +- nvim/config/nvim/lua/plugins/dashboard.lua | 2 +- nvim/config/nvim/lua/plugins/database.lua | 9 +- nvim/config/nvim/lua/plugins/multicursor.lua | 112 +++++ nvim/config/nvim/lua/plugins/nvim-tree.lua | 51 +- nvim/config/nvim/lua/plugins/toggleterm.lua | 12 +- nvim/config/nvim/lua/plugins/which-key.lua | 1 + nvim/setup_neovim.ps1 | 445 ++++++++++++++++++ 10 files changed, 778 insertions(+), 24 deletions(-) create mode 100644 nvim/config/nvim/lua/plugins/multicursor.lua create mode 100644 nvim/setup_neovim.ps1 diff --git a/nvim/README.md b/nvim/README.md index d1d2efe..4a41965 100644 --- a/nvim/README.md +++ b/nvim/README.md @@ -1,9 +1,11 @@ # Neovim Setup -A scripted Neovim setup built around C/C++ development (CMake + clangd + codelldb), with LSP support for Go, C#, TypeScript/JavaScript, HTML, and CSS alongside it. `setup_neovim.sh` handles installing dependencies and putting everything in place; the actual config lives in `config/` as plain files, not embedded in the script. +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` (Linux) and `setup_neovim.ps1` (Windows) handle installing dependencies and putting everything in place; the actual config lives in `config/` as plain files, shared by both, not embedded in either 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. +Also supports **Windows 10/11**, via `setup_neovim.ps1` and [winget](https://aka.ms/getwinget) — see [Installing → Windows](#windows) and [Windows notes](#windows-notes). + ## Contents - [Installing](#installing) @@ -11,6 +13,7 @@ Supports **Fedora, Ubuntu, Debian, and Arch Linux** (and their common derivative - [Config layout & adding plugins](#config-layout--adding-plugins) - [Terminal vs. desktop (Neovide)](#terminal-vs-desktop-neovide) - [Editor basics](#editor-basics) +- [Multiple cursors](#multiple-cursors-multicursornvim) - [Look & feel](#look--feel) - [Buffers & tabs](#buffers--tabs-bufferline) - [Text search (Telescope)](#text-search-telescope) @@ -25,10 +28,13 @@ Supports **Fedora, Ubuntu, Debian, and Arch Linux** (and their common derivative - [Database (MariaDB)](#database-mariadb) - [Hex viewer](#hex-viewer) - [Formatting and linting](#formatting-and-linting) +- [Windows notes](#windows-notes) - [Known quirks](#known-quirks) ## Installing +### Linux + ```bash chmod +x setup_neovim.sh ./setup_neovim.sh @@ -53,9 +59,65 @@ First launch takes a minute or two: `lazy.nvim` bootstraps itself and installs a - **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. +### Windows + +Needs Windows 10 (1809+) or Windows 11 with `winget` (the "App Installer" — preinstalled on current Windows; if it's missing, get it from the Microsoft Store). From PowerShell, in this folder: + +```powershell +powershell -ExecutionPolicy Bypass -File .\setup_neovim.ps1 +``` + +(`-ExecutionPolicy Bypass` applies to this one run only; it also gets around the "downloaded from the internet" block on a freshly unzipped script.) No administrator shell is needed — individual installers (LLVM, the .NET SDK, …) may still show their own UAC prompt. + +As on Linux, keep `setup_neovim.ps1` and `config/` next to each other, and edit the files under `config/` rather than the script to customize. The script: + +1. Installs everything through `winget` (Neovim and Git are required and abort the script if they fail; the rest are best-effort and just print a warning), asks which frontend(s) you want — see [Terminal vs. desktop (Neovide)](#terminal-vs-desktop-neovide) — and then installs npm-based language servers. +2. Installs the JetBrainsMono and Cascadia Code Nerd Fonts for the current user (no admin needed). +3. Removes the existing Neovim config **and data** folders, then copies `config/nvim` into `%LOCALAPPDATA%\nvim` (or `%XDG_CONFIG_HOME%\nvim` if you've set that — the same places Neovim itself looks). +4. Copies `.clang-format`, `.clang-tidy` and `cpp-style-guide.md` into your user folder (`%USERPROFILE%`). + +**Like the Linux script, this deletes the old config and data (`%LOCALAPPDATA%\nvim` and `%LOCALAPPDATA%\nvim-data`) with no backup by default** — but when run interactively it asks for confirmation first. Options: + +| Option | Effect | +|---|---| +| `-Frontend terminal\|desktop\|both` | Skip the frontend question (the default when non-interactive is `terminal`) | +| `-Backup` | Move the old config/data folders aside (`nvim.bak-`) instead of deleting them | +| `-Force` | Don't ask for confirmation before deleting | +| `-SkipMariaDB` | Don't offer to install the MariaDB client | + +Open a **new** terminal afterwards so the PATH changes take effect. First launch takes a minute or two, as on Linux (`lazy.nvim` plugins, then Mason's language servers and `codelldb`; treesitter also compiles its parsers with `gcc`). + +If you'd rather have the Linux experience, `setup_neovim.sh` also works unchanged inside a WSL Debian/Ubuntu distro. + +#### What the Windows script installs + +The same set of tools as on Linux, with Windows equivalents where the Linux package doesn't exist: + +| Linux | Windows (winget id) | +|---|---| +| gcc, g++, make, gdb | WinLibs GCC / MinGW-w64 (`BrechtSanders.WinLibs.POSIX.UCRT`) — gcc, g++, gdb and a make — plus Ninja (`Ninja-build.Ninja`), the CMake generator to use on Windows | +| cmake | `Kitware.CMake` | +| clang-tools-extra (clang-format, clang-tidy) | `LLVM.LLVM` | +| lldb | not installed separately — `codelldb` (via Mason) brings its own | +| nodejs, npm / python3-pip / golang / dotnet-sdk-8.0 | `OpenJS.NodeJS.LTS` / `Python.Python.3.12` / `GoLang.Go` / `Microsoft.DotNet.SDK.8` | +| ripgrep, fd-find | `BurntSushi.ripgrep.MSVC`, `sharkdp.fd` | +| git, curl, wget, unzip | `Git.Git`; the rest ship with Windows | +| fish | PowerShell 7 (`Microsoft.PowerShell`) — see [Terminal](#terminal) | +| mariadb client | `MariaDB.Server`, run through its own installer wizard (opt-in prompt) — winget has no client-only package | +| Neovide (optional) | `Neovide.Neovide` | + +Where things end up: + +| `config/` | Copied to | +|---|---| +| `nvim/` | `%LOCALAPPDATA%\nvim\...` (same layout as on Linux) | +| `clang-format` | `%USERPROFILE%\.clang-format` | +| `clang-tidy` | `%USERPROFILE%\.clang-tidy` | +| `cpp-style-guide.md` | `%USERPROFILE%\cpp-style-guide.md` | + ## What gets installed -Package names vary by distro (see the script for the exact list per package manager), but the same set gets installed everywhere: +On Linux, package names vary by distro (see the script for the exact list per package manager), but the same set gets installed everywhere: - **Compilers/tools**: gcc, g++, make, cmake, clang tooling, lldb, gdb, .NET SDK 8.0, Go, nodejs/npm, python3-pip - **CLI tools**: ripgrep, fd, git, curl, wget, unzip, fish (used only for Neovim's embedded terminal — see [Terminal](#terminal)), and the MariaDB *client* (`mariadb`; client only, no server — see [Database (MariaDB)](#database-mariadb)) @@ -82,6 +144,7 @@ config/nvim/ │ ├── lsp.lua nvim-lspconfig + Mason │ ├── completion.lua nvim-cmp + LuaSnip │ ├── database.lua vim-dadbod + UI + completion (MariaDB) + │ ├── multicursor.lua multicursor.nvim (multiple cursors) │ ├── dap.lua nvim-dap + UI │ └── … telescope, nvim-tree, bufferline, lualine, and so on ├── config/lsp/ helper modules that are *not* plugin specs @@ -156,6 +219,44 @@ Once installed, launch the desktop app with `neovide` from a terminal — if tha | `se` | Normal | Equalize split sizes | | `sx` | Normal | Close current split | +## Multiple cursors (`multicursor.nvim`) + +[`multicursor.nvim`](https://github.com/jake-stewart/multicursor.nvim) (`lua/plugins/multicursor.lua`). The idea: add cursors, then use Vim normally — motions, operators, insert mode, macros and undo all act on every cursor at once. `` collapses back to a single cursor. + +The everyday actions sit on Ctrl keys (VS Code-style); the rest live under a `m` "Multicursor" group, so they show up in the which-key popup. These differ from the plugin's own README example on purpose — its `n/s/S/x` would collide with the `s…` Splits group, and bare ``/`` are ordinary movement here. + +**Adding cursors** + +| Key | Mode | Action | +|---|---|---| +| `` / `` | Normal/Visual | Add a cursor at the next / previous match of the word under the cursor (or of the visual selection) — like Ctrl+D in VS Code | +| `` / `` | Normal/Visual | Add a cursor on the line below / above | +| `` | Normal | Add or remove a cursor by clicking; `Ctrl`+drag adds a column of them | +| `` | Normal/Visual | Disable the cursors so only the main one moves; press it again to drop a cursor where the main one is, so move → `` → move → `` places cursors anywhere. `` re-enables them. (This shadows `` as the alias for ``; `` itself is untouched.) | +| `ga{motion}` | Normal/Visual | Add a cursor on every line of the motion — `gaip` for a paragraph; in Visual mode, every selected line | +| `ma` | Normal/Visual | Add cursors at **all** matches of the word/selection in the file | +| `ms` / `mS` | Normal/Visual | Skip the next / previous match (jump there without adding a cursor) | +| `mj` / `mk` | Normal/Visual | Skip the line below / above (move the main cursor without adding one) | +| `mr` | Normal | Bring the cursors back if you cleared them by accident | +| `I` / `A` | Visual | Insert at the start / append at the end of every selected line — like block-selection `I`/`A`, but for any selection | + +**Managing cursors** (these only exist while there is more than one cursor) + +| Key | Mode | Action | +|---|---|---| +| `mh` / `ml` | Normal/Visual | Make the previous / next cursor the main one | +| `mx` | Normal/Visual | Delete the main cursor, keeping the rest | +| `g` / `g` | Normal/Visual | Increment / decrement treating the cursors as one sequence (1, 2, 3, … down a column) | +| `` | Normal | Re-enable disabled cursors, otherwise collapse to one (also clears the search highlight, like the global ``) | + +Typical uses: `` a few times on a variable name, then `ciw` to rename those occurrences; `Vjjj` + `A` to append to several lines; `ma` then `cw` to rename every occurrence; `gaip` then `I` to prefix a whole paragraph. + +Notes: + +- The cursors appear as reversed-color blocks (`MultiCursorCursor`); the main cursor is the one your terminal / Neovide draws. +- Some terminals and window managers intercept `Ctrl`+click or `Ctrl`+arrow chords. `` and `` depend on neither. +- The plugin lists autocompletion as supported, but it can only replay snippets expanded through Neovim's built-in `vim.snippet.expand`, and this config's snippets come from LuaSnip via nvim-cmp (as does the custom insert-mode `` comment continuation) — so if completion, snippets or Enter behave oddly with several cursors, press `` to collapse back to one and do that part with a single cursor. + ## Look & feel - **Theme**: [`vscode.nvim`](https://github.com/Mofiqul/vscode.nvim) (dark variant, VS Code Dark+ palette), on a near-black `#090909` background (explorer, tabs, statusline and popups use near-black shades too, not VS Code's grays), with macros in Visual Studio's muted purple (`#BEB7FF`) and custom tinted backgrounds for error/warn/info/hint diagnostic virtual lines. lualine uses the plugin's own `vscode` theme. @@ -311,6 +412,8 @@ Default launch configs prompt for a path to the executable, or let you attach to Neovim's embedded terminal launches **fish** instead of your login shell, purely for its built-in inline "ghost text" suggestions for paths and command history (no config needed, accept with `→`/`End`) — a terminal-side equivalent of the completion ghost text above, since a `:terminal` buffer is a raw PTY that `nvim-cmp` has no visibility into. Your actual system login shell is untouched. +On **Windows** there is no native fish, so the embedded terminal launches **PowerShell 7** (`pwsh`) instead — PSReadLine's grey inline prediction from your command history is on by default there, which is the same idea — falling back to Windows PowerShell if `pwsh` isn't installed. + ## Git `gitsigns` shows added/changed/removed lines in the sign column against the current git state. @@ -388,6 +491,27 @@ The mapping also makes sure the buffer is actually loaded in binary mode before `lf` (LSP format) only consumes `.clang-format` — clangd's formatting is pure clang-format and has nothing to do with clang-tidy. `.clang-tidy` is picked up two separate ways: automatically by clangd for live diagnostics (clangd's clang-tidy integration is explicitly enabled via `--clang-tidy`, alongside `--background-index` so checks run against the whole project's semantic info rather than just what's open), and directly by `lt`, which shells out to the real `clang-tidy --fix` on the file instead of going through clangd. That's a deliberate difference, not an oversight: clangd filters code actions by LSP "kind", and clang-tidy fixes only ever surface from clangd as individual `quickfix` actions — one per diagnostic — never as a single batched "fix everything" action, because clangd doesn't implement the `source.fixAll` kind that would take ([clangd/clangd#1446](https://github.com/clangd/clangd/issues/1446)). Asking for that kind specifically is why this used to just return "No code actions available" no matter what the file contained. Running the actual `clang-tidy` binary sidesteps that entirely and does what the key was always meant to: fix everything the tool can in one pass. See the [LSP table](#lsp--code-intelligence) above for the compile-database caveat that governs how much either `la` or `lt` can actually catch. +## Windows notes + +Everything under `config/nvim/` is shared between Linux and Windows. The handful of places that had Linux assumptions now branch on `vim.fn.has("win32")`, so nothing changes on Linux: + +- **Embedded terminal** — `pwsh` instead of `fish` (`plugins/toggleterm.lua`). +- **Clipboard** — the OSC 52 override in `init.lua` (meant for SSH) is skipped on Windows, where Neovim's native clipboard provider is used instead. OSC 52 can't work in Neovide at all, since Neovide isn't a terminal. Neovim's Windows build bundles both `win32yank` (clipboard) and `xxd` (the [hex viewer](#hex-viewer)). +- **nvim-tree `gM`** (copy marked files with a regex rename) — Windows has no `cp`, so a Lua recursive copy is used there. One difference: if the target folder already exists, contents are merged into it instead of the source being nested inside it. +- **DB UI save location** — `%LOCALAPPDATA%\db_ui\connections.json` instead of `~/.local/share/db_ui/`; still outside both folders the setup script wipes. Still plain text, password included. +- **Dashboard "Edit Config"** — opens `init.lua` from wherever Neovim's config actually lives (`:echo stdpath("config")`). +- **CMake debug template** — appends `.exe` to the target names (`lua/dap/configurations/cmake.lua`). + +Things worth knowing: + +- **Use the Ninja generator for CMake** — e.g. `cmake -S . -B build -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON`. Multi-config generators (Visual Studio) put binaries under `build\Debug\...`, which the debug template's `build/...` paths don't expect. The template also mentions `scripts/build.sh`, a bash script — run it from Git Bash, or call `cmake` directly. +- **MSVC isn't installed.** The setup uses GCC (WinLibs) because it's a single unattended install; if you want `cl.exe`, install the Visual Studio Build Tools yourself. +- **Treesitter parsers need a C compiler on `PATH`.** If `:TSInstall` complains, check `gcc --version` in a fresh terminal. The script puts WinLibs' real `bin` folder on your user `PATH` for this reason. +- **Fonts:** set your terminal's font face to `JetBrainsMono Nerd Font` (Windows Terminal: Settings → Profiles → Defaults → Appearance) and restart it. Neovide picks the font up from `init.lua` on its own. +- **sqls config** lives in your user config folder — on Windows `%APPDATA%\sqls\config.yml` rather than `~/.config/sqls/config.yml` (sqls uses Go's per-OS user config directory; if the file isn't picked up, check the sqls docs). The Unix-socket DSN form from the [Language server (sqls)](#language-server-sqls) section doesn't apply; use TCP. For credentials outside the URL, use a MariaDB option file — see MariaDB's docs for where the Windows client looks. +- **`.clang-format` / `.clang-tidy` fallbacks** are in `%USERPROFILE%`, and both tools only walk *upward* from the file being processed — so projects on another drive (`D:\...`) won't pick them up. Copy them to the project, or to that drive's root, if so. +- **`MariaDB`**: the client's `bin` folder is added to your user `PATH` if the installer didn't do it. `mariadb --version` in a new terminal confirms it. + ## Known quirks - **`nvim-treesitter` is pinned to its `master` branch**, not the current upstream default (`main`). `main` is an incompatible rewrite that removed the `nvim-treesitter.configs` API this config (and most existing configs/plugins) are built on. `master` is kept frozen upstream specifically for this kind of backward compatibility. diff --git a/nvim/config/nvim/init.lua b/nvim/config/nvim/init.lua index a0fb992..7843918 100644 --- a/nvim/config/nvim/init.lua +++ b/nvim/config/nvim/init.lua @@ -113,19 +113,26 @@ vim.api.nvim_create_autocmd("FileType", { }) -- 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, - }, -} +-- Skipped on native Windows: Neovim's own clipboard provider (win32yank, +-- bundled with the Windows builds) works there for both the terminal and +-- Neovide, whereas OSC 52 can't work at all in Neovide (it isn't a +-- terminal). Over SSH you'd be running the Linux config on the remote +-- machine anyway. +if vim.fn.has('win32') == 0 then + 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, + }, + } +end -- GENERAL KEYMAPS -- diff --git a/nvim/config/nvim/lua/dap/configurations/cmake.lua b/nvim/config/nvim/lua/dap/configurations/cmake.lua index f870365..8909c13 100644 --- a/nvim/config/nvim/lua/dap/configurations/cmake.lua +++ b/nvim/config/nvim/lua/dap/configurations/cmake.lua @@ -82,6 +82,9 @@ local function shared_lib_ext() end return '.so' end +local function exe_ext() + return vim.loop.os_uname().sysname == 'Windows_NT' and '.exe' or '' +end local function fixture_lib_path() return build_dir() .. '/tests/' .. shared_lib_prefix() .. 'dynlib_test_fixture' .. shared_lib_ext() end @@ -97,7 +100,7 @@ local configurations = { type = 'codelldb', request = 'launch', program = function() - return build_dir() .. '/tests/test_dynamic_library' + return build_dir() .. '/tests/test_dynamic_library' .. exe_ext() end, args = function() return { fixture_lib_path() } @@ -112,7 +115,7 @@ local configurations = { type = 'codelldb', request = 'launch', program = function() - return build_dir() .. '/demo/example_host' + return build_dir() .. '/demo/example_host' .. exe_ext() end, args = function() return { demo_plugin_path() } @@ -127,7 +130,7 @@ local configurations = { type = 'codelldb', request = 'launch', program = function() - return build_dir() .. '/demo/example_host' + return build_dir() .. '/demo/example_host' .. exe_ext() end, args = function() local path = vim.fn.input('Plugin path: ', demo_plugin_path(), 'file') diff --git a/nvim/config/nvim/lua/plugins/dashboard.lua b/nvim/config/nvim/lua/plugins/dashboard.lua index 21b031c..f814e9a 100644 --- a/nvim/config/nvim/lua/plugins/dashboard.lua +++ b/nvim/config/nvim/lua/plugins/dashboard.lua @@ -23,7 +23,7 @@ return { 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("c", " Edit Config", "edit " .. vim.fn.fnameescape(vim.fs.normalize(vim.fn.stdpath("config")) .. "/init.lua") .. ""), dashboard.button("q", " Quit", "qa"), } diff --git a/nvim/config/nvim/lua/plugins/database.lua b/nvim/config/nvim/lua/plugins/database.lua index 4bfe779..ebeb634 100644 --- a/nvim/config/nvim/lua/plugins/database.lua +++ b/nvim/config/nvim/lua/plugins/database.lua @@ -62,9 +62,16 @@ return { -- plain text in connections.json here, password included -- leave the -- password out of the URL and put it in ~/.my.cnf's [client] section -- if that bothers you (the mariadb client reads it from there). + -- (On Windows that means %LOCALAPPDATA%\db_ui -- Neovim itself uses + -- %LOCALAPPDATA%\nvim and %LOCALAPPDATA%\nvim-data there, so it's + -- outside both of those too.) local data_home = vim.env.XDG_DATA_HOME if not data_home or data_home == "" then - data_home = vim.env.HOME .. "/.local/share" + if vim.fn.has("win32") == 1 then + data_home = vim.env.LOCALAPPDATA or (vim.env.USERPROFILE .. "\\AppData\\Local") + else + data_home = vim.env.HOME .. "/.local/share" + end end vim.g.db_ui_save_location = data_home .. "/db_ui" diff --git a/nvim/config/nvim/lua/plugins/multicursor.lua b/nvim/config/nvim/lua/plugins/multicursor.lua new file mode 100644 index 0000000..8f203e2 --- /dev/null +++ b/nvim/config/nvim/lua/plugins/multicursor.lua @@ -0,0 +1,112 @@ +return { + -- Multiple cursors (multicursor.nvim). Add cursors, then use Vim + -- normally -- motions, operators, insert mode, macros and undo all act + -- on every cursor at once. Esc collapses back to one cursor. + -- + -- The key layout below deliberately differs from the plugin's README + -- example, which would collide with this config: n/s/S/x clash + -- with the s* "Splits" group, and bare / are normal + -- cursor movement. Instead, the everyday actions sit on Ctrl keys + -- (VS Code-style), and the rest live under a m "Multicursor" + -- group (registered in which-key.lua) so they show up in the popup. + -- + -- m is NOT also bound to the plugin's own `operator` action + -- (which its README suggests) -- that would make every m... + -- key ambiguous. `ga{motion}` (below) covers the same ground. + { + "jake-stewart/multicursor.nvim", + branch = "1.0", + event = "VeryLazy", + config = function() + local mc = require("multicursor-nvim") + mc.setup() + + local set = vim.keymap.set + + -- Add cursors: above / below the main cursor. + set({ "n", "x" }, "", function() mc.lineAddCursor(-1) end, { desc = "Add cursor above" }) + set({ "n", "x" }, "", function() mc.lineAddCursor(1) end, { desc = "Add cursor below" }) + + -- Add cursors: at the next / previous match of the word under the + -- cursor, or of the visual selection (like Ctrl+D in VS Code). + set({ "n", "x" }, "", function() mc.matchAddCursor(1) end, { desc = "Add cursor at next match" }) + set({ "n", "x" }, "", function() mc.matchAddCursor(-1) end, { desc = "Add cursor at previous match" }) + + -- Add cursors with the mouse: Ctrl+click adds or removes one, and + -- Ctrl+drag adds a column of them. (If your terminal swallows + -- Ctrl+click, use below instead.) + set("n", "", mc.handleMouse, { desc = "Add/remove cursor" }) + set("n", "", mc.handleMouseDrag) + set("n", "", mc.handleMouseRelease) + + -- Disable the cursors so only the main one moves; while disabled, + -- pressing this again drops a cursor at the main cursor's position. + -- Move, press, move, press... places cursors anywhere. + -- re-enables them. (Shadows Neovim's alias for ; + -- itself is untouched.) + set({ "n", "x" }, "", mc.toggleCursor, { desc = "Toggle cursor (place / disable)" }) + + -- `gaip` adds a cursor on every line of a paragraph (any motion or + -- text object works); in Visual mode, on every selected line. + set({ "n", "x" }, "ga", mc.addCursorOperator, { desc = "Add cursors over motion" }) + + -- Visual mode: insert at the start / append at the end of every + -- selected line, like a block-selection I/A but for any selection. + set("x", "I", mc.insertVisual, { desc = "Insert on each selected line" }) + set("x", "A", mc.appendVisual, { desc = "Append on each selected line" }) + + -- m group (see which-key.lua). + set({ "n", "x" }, "ms", function() mc.matchSkipCursor(1) end, { desc = "Skip next match" }) + set({ "n", "x" }, "mS", function() mc.matchSkipCursor(-1) end, { desc = "Skip previous match" }) + set({ "n", "x" }, "mj", function() mc.lineSkipCursor(1) end, { desc = "Skip line below" }) + set({ "n", "x" }, "mk", function() mc.lineSkipCursor(-1) end, { desc = "Skip line above" }) + set({ "n", "x" }, "ma", mc.matchAllAddCursors, { desc = "Add cursors at ALL matches" }) + set("n", "mr", mc.restoreCursors, { desc = "Restore cleared cursors" }) + + -- Mappings in a keymap layer only exist while there is more than + -- one cursor, so they can overlap with ordinary mappings. + mc.addKeymapLayer(function(layerSet) + -- Make a different cursor the main one. + layerSet({ "n", "x" }, "mh", mc.prevCursor, { desc = "Previous cursor" }) + layerSet({ "n", "x" }, "ml", mc.nextCursor, { desc = "Next cursor" }) + + -- Drop the main cursor, keeping the rest. + layerSet({ "n", "x" }, "mx", mc.deleteCursor, { desc = "Delete main cursor" }) + + -- Treat all the cursors as one sequence: 1, 2, 3, ... down the + -- column. Kept in the layer so Vim's own Visual-mode g / + -- g are untouched when there is only one cursor. + layerSet({ "n", "x" }, "g", mc.sequenceIncrement, { desc = "Increment as sequence" }) + layerSet({ "n", "x" }, "g", mc.sequenceDecrement, { desc = "Decrement as sequence" }) + + -- Esc: re-enable disabled cursors, otherwise collapse to one. This + -- shadows the global (clear search highlight) while the + -- layer is active, so it does that too. + layerSet("n", "", function() + vim.cmd.nohlsearch() + if not mc.cursorsEnabled() then + mc.enableCursors() + else + mc.clearCursors() + end + end, { desc = "Collapse cursors" }) + end) + + -- Cursor look. `reverse` stays visible on any background (this + -- theme is transparent in the terminal), and a theme change would + -- otherwise wipe these, so they're re-applied on ColorScheme. + local function set_highlights() + local hl = vim.api.nvim_set_hl + hl(0, "MultiCursorCursor", { reverse = true }) + hl(0, "MultiCursorVisual", { link = "Visual" }) + hl(0, "MultiCursorSign", { link = "SignColumn" }) + hl(0, "MultiCursorMatchPreview", { link = "Search" }) + hl(0, "MultiCursorDisabledCursor", { reverse = true }) + hl(0, "MultiCursorDisabledVisual", { link = "Visual" }) + hl(0, "MultiCursorDisabledSign", { link = "SignColumn" }) + end + set_highlights() + vim.api.nvim_create_autocmd("ColorScheme", { callback = set_highlights }) + end, + }, +} diff --git a/nvim/config/nvim/lua/plugins/nvim-tree.lua b/nvim/config/nvim/lua/plugins/nvim-tree.lua index 431b262..4e8223e 100644 --- a/nvim/config/nvim/lua/plugins/nvim-tree.lua +++ b/nvim/config/nvim/lua/plugins/nvim-tree.lua @@ -1,3 +1,48 @@ +-- Recursive copy used by the gM mapping below. Unix keeps using `cp -r` +-- (unchanged behavior); Windows has no `cp`, so it gets an equivalent +-- written against libuv. One difference there: if the destination +-- directory already exists, contents are merged into it rather than the +-- source being nested inside it, which is what `cp -r` would do. +-- Returns true on success, or false plus an error message. +local function copy_path(src, dest) + if vim.fn.has("win32") == 0 then + local result = vim.fn.system({ "cp", "-r", src, dest }) + return vim.v.shell_error == 0, result + end + + local uv = vim.uv or vim.loop + local function copy(s, d) + local stat = uv.fs_stat(s) + if not stat then + return false, "no such file or directory: " .. s + end + if stat.type == "directory" then + local made, mkerr = uv.fs_mkdir(d, 493) -- 0755 + if not made then + local existing = uv.fs_stat(d) + if not (existing and existing.type == "directory") then + return false, mkerr + end + end + local handle = uv.fs_scandir(s) + while handle do + local name = uv.fs_scandir_next(handle) + if not name then + break + end + local ok, err = copy(s .. "/" .. name, d .. "/" .. name) + if not ok then + return false, err + end + end + return true + end + local ok, err = uv.fs_copyfile(s, d) + return ok and true or false, err + end + return copy(src, dest) +end + return { -- NvimTree (On the right) { @@ -198,9 +243,9 @@ return { -- 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) + local copied, result = copy_path(node.absolute_path, dest_path) + if not copied then + vim.notify("Copy failed for " .. base .. ": " .. tostring(result), vim.log.levels.ERROR) end end end diff --git a/nvim/config/nvim/lua/plugins/toggleterm.lua b/nvim/config/nvim/lua/plugins/toggleterm.lua index e8d18ad..7485bac 100644 --- a/nvim/config/nvim/lua/plugins/toggleterm.lua +++ b/nvim/config/nvim/lua/plugins/toggleterm.lua @@ -9,8 +9,18 @@ return { -- 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. + -- + -- Windows: fish has no native Windows build, so the terminal + -- launches PowerShell 7 (pwsh) instead -- it has the same kind of + -- grey inline prediction (PSReadLine, from command history) on by + -- default -- falling back to Windows PowerShell 5.1 if pwsh isn't + -- installed. setup_neovim.ps1 installs pwsh. + local shell = "fish" + if vim.fn.has("win32") == 1 then + shell = vim.fn.executable("pwsh") == 1 and "pwsh" or "powershell" + end require("toggleterm").setup({ - shell = "fish", + shell = shell, }) vim.keymap.set('n', 'tf', 'ToggleTerm direction=float', { desc = "Floating Terminal" }) vim.keymap.set('n', 'th', 'ToggleTerm direction=horizontal', { desc = "Bottom Terminal" }) diff --git a/nvim/config/nvim/lua/plugins/which-key.lua b/nvim/config/nvim/lua/plugins/which-key.lua index 848acf2..d4825e6 100644 --- a/nvim/config/nvim/lua/plugins/which-key.lua +++ b/nvim/config/nvim/lua/plugins/which-key.lua @@ -14,6 +14,7 @@ return { { "c", group = "Comments" }, { "f", group = "Find / Grep" }, { "l", group = "LSP" }, + { "m", group = "Multicursor" }, { "o", group = "OOP" }, { "s", group = "Splits" }, { "t", group = "Terminal" }, diff --git a/nvim/setup_neovim.ps1 b/nvim/setup_neovim.ps1 new file mode 100644 index 0000000..93b84ad --- /dev/null +++ b/nvim/setup_neovim.ps1 @@ -0,0 +1,445 @@ +<# +.SYNOPSIS + Windows counterpart of setup_neovim.sh: installs Neovim plus the tooling + this config relies on, then copies config/ into place. + +.DESCRIPTION + Same job as the Linux script -- install dependencies, install fonts, + wipe the old Neovim config, copy the new one -- using winget instead of + dnf/apt/pacman. Nothing here needs an administrator shell: winget + installs per-user where it can (individual installers such as LLVM or + the .NET SDK may still show their own UAC prompt), fonts go into your + per-user font folder, and npm packages go into %APPDATA%\npm. + + config\ is shared with the Linux script and has to sit next to this file. + It is copied as-is; edit those files, not this script, to customize. + + Run it from PowerShell (Windows PowerShell 5.1 or PowerShell 7): + + powershell -ExecutionPolicy Bypass -File .\setup_neovim.ps1 + +.PARAMETER Frontend + terminal | desktop | both. Which Neovim frontend(s) to set up. Neovide + (desktop) is a GUI that runs the real nvim as its backend, so nvim is + installed either way. If omitted, you're asked (or 'terminal' is used + when the shell is non-interactive). + +.PARAMETER Backup + Move the existing Neovim config/data folders aside (name.bak-) + instead of deleting them. + +.PARAMETER Force + Don't ask for confirmation before deleting the existing Neovim config + and data folders. + +.PARAMETER SkipMariaDB + Don't offer to install the MariaDB client (used by vim-dadbod). +#> +[CmdletBinding()] +param( + [ValidateSet('terminal', 'desktop', 'both')] + [string]$Frontend, + [switch]$Backup, + [switch]$Force, + [switch]$SkipMariaDB +) + +# 'Continue' on purpose: native commands (winget, npm) write progress to +# stderr, and 'Stop' would turn that into terminating errors in Windows +# PowerShell 5.1. Anything that must abort uses -ErrorAction Stop / throw. +$ErrorActionPreference = 'Continue' +$ProgressPreference = 'SilentlyContinue' # makes Invoke-WebRequest much faster on 5.1 +[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + +# --------------------------------------------------------------------- +# Locate config/ next to this script +# --------------------------------------------------------------------- +$ScriptDir = $PSScriptRoot +if (-not $ScriptDir) { $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path } +$ConfigDir = Join-Path $ScriptDir 'config' + +if ($env:OS -ne 'Windows_NT') { + Write-Host 'ERROR: This script is for Windows. On Linux, use setup_neovim.sh.' -ForegroundColor Red + exit 1 +} +if (-not (Test-Path $ConfigDir -PathType Container)) { + Write-Host "ERROR: $ConfigDir not found." -ForegroundColor Red + Write-Host 'This script expects a config\ folder next to it (keep the two together' + Write-Host "however you copy/clone this -- e.g. don't move setup_neovim.ps1 on its own)." + exit 1 +} + +# --------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------- +function Write-Warn([string]$Message) { + Write-Host "WARNING: $Message" -ForegroundColor Yellow +} + +function Test-Command([string]$Name) { + return [bool](Get-Command $Name -ErrorAction SilentlyContinue) +} + +# winget updates the machine/user PATH in the registry, but this already +# running session never sees that. Re-read it (keeping anything that was +# only set in this session) so freshly installed tools can be found. +function Update-SessionPath { + $fresh = @( + [Environment]::GetEnvironmentVariable('Path', 'Machine'), + [Environment]::GetEnvironmentVariable('Path', 'User') + ) -join ';' + $seen = @{} + $parts = New-Object System.Collections.Generic.List[string] + foreach ($p in (($fresh + ';' + $env:Path) -split ';')) { + $t = $p.Trim() + if ($t -and -not $seen.ContainsKey($t.ToLowerInvariant())) { + $seen[$t.ToLowerInvariant()] = $true + $parts.Add($t) + } + } + $env:Path = $parts -join ';' +} + +# Persistently add a folder to the user PATH (and this session's), if it +# exists and isn't already there. Used for the few installers that don't +# put their own bin folder on PATH. +function Add-ToUserPath([string]$Dir, [switch]$Prepend) { + if (-not (Test-Path $Dir -PathType Container)) { return } + $user = [Environment]::GetEnvironmentVariable('Path', 'User') + $entries = @() + if ($user) { $entries = @($user -split ';' | Where-Object { $_ }) } + $already = $entries | Where-Object { $_.TrimEnd('\') -ieq $Dir.TrimEnd('\') } + if ($already) { return } + if ($Prepend) { $entries = @($Dir) + $entries } else { $entries = $entries + @($Dir) } + [Environment]::SetEnvironmentVariable('Path', ($entries -join ';'), 'User') + Write-Host " Added $Dir to your user PATH." + Update-SessionPath +} + +# Returns $true if the package is (now) installed. -Required aborts the +# whole script on failure; everything else is best-effort, like the +# .NET/Neovide steps in the Linux script: a WARNING, not a reason to stop. +function Install-WingetPackage { + param( + [Parameter(Mandatory = $true)][string]$Id, + [Parameter(Mandatory = $true)][string]$Name, + [string]$Command, + [switch]$Required, + [switch]$Interactive + ) + if ($Command -and (Test-Command $Command)) { + Write-Host " $Name already installed ($Command found on PATH)." + return $true + } + & winget list --id $Id --exact --accept-source-agreements *> $null + if ($LASTEXITCODE -eq 0) { + Write-Host " $Name already installed." + return $true + } + Write-Host " Installing $Name ($Id)..." + $wingetArgs = @('install', '--id', $Id, '--exact', '--accept-package-agreements', '--accept-source-agreements') + if ($Interactive) { $wingetArgs += '--interactive' } else { $wingetArgs += '--silent' } + & winget @wingetArgs | Out-Host + if ($LASTEXITCODE -ne 0) { + $msg = "winget could not install $Name ($Id); exit code $LASTEXITCODE." + if ($Required) { throw $msg } + Write-Warn "$msg Install it manually, then re-run this script." + return $false + } + return $true +} + +# Per-user font install (no admin needed): copy into +# %LOCALAPPDATA%\Microsoft\Windows\Fonts and register under HKCU. +function Install-NerdFont { + param( + [Parameter(Mandatory = $true)][string]$Label, + [Parameter(Mandatory = $true)][string]$ZipName, + [Parameter(Mandatory = $true)][string]$SentinelFile + ) + $fontDir = Join-Path $env:LOCALAPPDATA 'Microsoft\Windows\Fonts' + if (Test-Path (Join-Path $fontDir $SentinelFile)) { + Write-Host "$Label Nerd Font already installed." + return + } + $tmpZip = Join-Path $env:TEMP "$ZipName.zip" + $tmpDir = Join-Path $env:TEMP "$ZipName-extract" + try { + Invoke-WebRequest -Uri "https://github.com/ryanoasis/nerd-fonts/releases/latest/download/$ZipName.zip" -OutFile $tmpZip -UseBasicParsing -ErrorAction Stop + if (Test-Path $tmpDir) { Remove-Item $tmpDir -Recurse -Force } + Expand-Archive -Path $tmpZip -DestinationPath $tmpDir -Force -ErrorAction Stop + New-Item -ItemType Directory -Force -Path $fontDir | Out-Null + $regPath = 'HKCU:\Software\Microsoft\Windows NT\CurrentVersion\Fonts' + if (-not (Test-Path $regPath)) { New-Item -Path $regPath -Force | Out-Null } + $count = 0 + Get-ChildItem -Path $tmpDir -Recurse -Include *.ttf, *.otf | ForEach-Object { + $dest = Join-Path $fontDir $_.Name + Copy-Item -LiteralPath $_.FullName -Destination $dest -Force -ErrorAction Stop + $kind = 'TrueType' + if ($_.Extension -ieq '.otf') { $kind = 'OpenType' } + New-ItemProperty -Path $regPath -Name "$($_.BaseName) ($kind)" -Value $dest -PropertyType String -Force | Out-Null + $count++ + } + Write-Host "$Label Nerd Font installed ($count files)." + } + catch { + Write-Warn "$Label Nerd Font install failed: $($_.Exception.Message)" + Write-Host " Download it manually from https://www.nerdfonts.com/font-downloads" + } + finally { + Remove-Item $tmpZip -Force -ErrorAction SilentlyContinue + Remove-Item $tmpDir -Recurse -Force -ErrorAction SilentlyContinue + } +} + +$IsInteractive = [Environment]::UserInteractive -and -not [Console]::IsInputRedirected + +Write-Host '' +Write-Host '=========================================' +Write-Host ' Starting Neovim Setup (Windows)' +Write-Host '=========================================' + +# --------------------------------------------------------------------- +# 1. System dependencies (winget) +# --------------------------------------------------------------------- +Write-Host '[1/4] Installing system dependencies with winget...' + +if (-not (Test-Command 'winget')) { + Write-Host 'ERROR: winget was not found.' -ForegroundColor Red + Write-Host 'winget ships with Windows 10 (1809+) and Windows 11 as "App Installer".' + Write-Host 'Install or update it from the Microsoft Store, then re-run this script:' + Write-Host ' https://aka.ms/getwinget' + exit 1 +} + +# Which Neovim frontend(s) to set up -- same question, same meaning as in +# the Linux script. Neovide always runs the real nvim as its backend, so +# nvim gets installed either way; this only decides whether Neovide goes +# on top. Both frontends share the exact same config. +if (-not $Frontend) { + $Frontend = 'terminal' + if ($IsInteractive) { + Write-Host '' + Write-Host 'Which Neovim frontend(s) do you want installed?' + Write-Host ' 1) Terminal-based only (nvim in Windows Terminal / PowerShell)' + Write-Host ' 2) Desktop application only (Neovide GUI -- still needs the nvim' + Write-Host ' binary as its backend, so that is installed either way)' + Write-Host ' 3) Both' + $choice = Read-Host 'Enter 1, 2, or 3 [1]' + switch ($choice) { + '2' { $Frontend = 'desktop' } + '3' { $Frontend = 'both' } + default { $Frontend = 'terminal' } + } + } + else { + Write-Host 'Non-interactive shell detected -- defaulting to terminal-based Neovim only.' + Write-Host 'Run this script directly (or pass -Frontend desktop|both) to also install Neovide.' + } +} + +# Required: nothing else works without these two. +[void](Install-WingetPackage -Id 'Neovim.Neovim' -Name 'Neovim' -Command 'nvim' -Required) +[void](Install-WingetPackage -Id 'Git.Git' -Name 'Git' -Command 'git' -Required) + +# Build tools. WinLibs is a standalone GCC/MinGW-w64 build that also +# brings gdb and make -- the Windows stand-in for gcc/g++/make/gdb, and +# what nvim-treesitter uses to compile its parsers. Ninja is the CMake +# generator that goes with it (plain `make` isn't what you want on +# Windows). The official LLVM installer provides clangd's companions +# clang-format and clang-tidy (.clang-format / .clang-tidy, lt). +[void](Install-WingetPackage -Id 'Kitware.CMake' -Name 'CMake' -Command 'cmake') +[void](Install-WingetPackage -Id 'Ninja-build.Ninja' -Name 'Ninja' -Command 'ninja') +[void](Install-WingetPackage -Id 'BrechtSanders.WinLibs.POSIX.UCRT' -Name 'GCC / MinGW-w64 (WinLibs)' -Command 'gcc') +[void](Install-WingetPackage -Id 'LLVM.LLVM' -Name 'LLVM (clang-format, clang-tidy)' -Command 'clang-tidy') +[void](Install-WingetPackage -Id 'GoLang.Go' -Name 'Go' -Command 'go') +[void](Install-WingetPackage -Id 'Microsoft.DotNet.SDK.8' -Name '.NET SDK 8.0' -Command 'dotnet') + +# Runtimes Mason needs to install some language servers. +[void](Install-WingetPackage -Id 'OpenJS.NodeJS.LTS' -Name 'Node.js (LTS)' -Command 'node') +[void](Install-WingetPackage -Id 'Python.Python.3.12' -Name 'Python 3.12') + +# CLI tools. (curl, tar and unzip-equivalents ship with Windows; xxd and +# the clipboard helper come bundled with Neovim's Windows build.) PowerShell +# 7 takes fish's place for Neovim's embedded terminal -- fish has no native +# Windows build -- see config/nvim/lua/plugins/toggleterm.lua. +[void](Install-WingetPackage -Id 'BurntSushi.ripgrep.MSVC' -Name 'ripgrep' -Command 'rg') +[void](Install-WingetPackage -Id 'sharkdp.fd' -Name 'fd' -Command 'fd') +[void](Install-WingetPackage -Id 'Microsoft.PowerShell' -Name 'PowerShell 7' -Command 'pwsh') + +# MariaDB *client* (vim-dadbod shells out to the `mariadb` binary). winget +# only has the full installer (server + client), so this one runs the +# installer's own wizard rather than silently installing a database +# server as a Windows service. Pick the client programs and untick the +# server/database-instance components if you only want the client. +$InstallMariaDB = $false +if (-not $SkipMariaDB) { + if ($IsInteractive) { + Write-Host '' + Write-Host 'The MariaDB client (needed for the Database features, Du) is only' + Write-Host 'available through the full MariaDB installer. Its setup wizard lets you' + Write-Host 'untick the server and keep just the client programs.' + $answer = Read-Host 'Run the MariaDB installer now? [y/N]' + if ($answer -match '^[Yy]') { $InstallMariaDB = $true } + } + else { + Write-Host 'Skipping MariaDB client (needs the interactive installer wizard).' + } +} +if ($InstallMariaDB) { + [void](Install-WingetPackage -Id 'MariaDB.Server' -Name 'MariaDB' -Interactive) +} + +# Neovide, if chosen above. +if ($Frontend -eq 'desktop' -or $Frontend -eq 'both') { + Write-Host 'Installing Neovide (desktop Neovim GUI)...' + [void](Install-WingetPackage -Id 'Neovide.Neovide' -Name 'Neovide' -Command 'neovide') +} + +# Newly installed tools aren't on this session's PATH yet. +Update-SessionPath + +# A few installers don't add their bin folder to PATH themselves. +# WinLibs: put the real mingw64\bin first -- GCC locates its own helper +# programs relative to its executable, which a launcher shim can break. +$winlibsRoot = Join-Path $env:LOCALAPPDATA 'Microsoft\WinGet\Packages' +if (Test-Path $winlibsRoot) { + Get-ChildItem -Path $winlibsRoot -Directory -Filter 'BrechtSanders.WinLibs.POSIX.UCRT*' -ErrorAction SilentlyContinue | ForEach-Object { + foreach ($sub in 'mingw64', 'mingw32') { + $bin = Join-Path $_.FullName "$sub\bin" + if (Test-Path (Join-Path $bin 'gcc.exe')) { Add-ToUserPath $bin -Prepend } + } + } +} +if (-not (Test-Command 'clang-tidy')) { Add-ToUserPath (Join-Path $env:ProgramFiles 'LLVM\bin') } +if (-not (Test-Command 'mariadb')) { + Get-ChildItem -Path $env:ProgramFiles -Directory -Filter 'MariaDB*' -ErrorAction SilentlyContinue | ForEach-Object { + Add-ToUserPath (Join-Path $_.FullName 'bin') + } +} + +# This config relies on fairly recent Neovim APIs (vim.lsp.config/enable, +# virtual_lines diagnostics, vim.o.winborder -- all 0.11+). Warn (don't +# fail) if what got installed is too old. +if (Test-Command 'nvim') { + $verLine = (& nvim --version | Select-Object -First 1) + if ($verLine -match 'v(\d+)\.(\d+)') { + if ([int]$Matches[1] -eq 0 -and [int]$Matches[2] -lt 11) { + Write-Warn "Installed Neovim is v$($Matches[1]).$($Matches[2]), but this config needs 0.11+." + Write-Host ' Run: winget upgrade Neovim.Neovim' + } + } + # Hex view (xxd) and the system clipboard (win32yank) both come bundled + # with Neovim's Windows build, next to nvim.exe. + $nvimBin = Split-Path (Get-Command nvim).Source -Parent + foreach ($tool in 'xxd.exe', 'win32yank.exe') { + if (-not ((Test-Path (Join-Path $nvimBin $tool)) -or (Test-Command $tool))) { + Write-Warn "$tool was not found next to nvim.exe or on PATH. Hex view (xxd) / clipboard (win32yank) may not work." + } + } +} +else { + Write-Warn 'nvim is not on PATH in this session yet -- open a new terminal after the script finishes.' +} + +# Global NPM packages for the JS/TS, HTML and CSS language servers. +# npm.cmd explicitly: plain `npm` resolves to npm.ps1, which the default +# PowerShell execution policy refuses to run. +Write-Host 'Installing NPM-based LSPs...' +if (Test-Command 'npm.cmd') { + & npm.cmd install -g typescript typescript-language-server vscode-langservers-extracted | Out-Host + if ($LASTEXITCODE -ne 0) { Write-Warn 'npm install -g failed -- JS/TS/HTML/CSS language servers may be missing.' } + Add-ToUserPath (Join-Path $env:APPDATA 'npm') +} +else { + Write-Warn 'npm was not found (Node.js install failed?). Skipping the NPM-based LSPs.' +} + +# --------------------------------------------------------------------- +# 2. Nerd Fonts (JetBrainsMono and Cascadia Code) +# --------------------------------------------------------------------- +Write-Host '[2/4] Installing JetBrainsMono and Cascadia Code Nerd Fonts...' +Install-NerdFont -Label 'JetBrainsMono' -ZipName 'JetBrainsMono' -SentinelFile 'JetBrainsMonoNerdFont-Regular.ttf' +Install-NerdFont -Label 'Cascadia Code' -ZipName 'CascadiaCode' -SentinelFile 'CaskaydiaCoveNerdFont-Regular.ttf' + +# --------------------------------------------------------------------- +# 3. Remove (or back up) the existing Neovim config and data +# --------------------------------------------------------------------- +Write-Host '[3/4] Removing existing Neovim configurations...' + +# Same locations Neovim itself uses on Windows: %LOCALAPPDATA%\nvim for +# config and %LOCALAPPDATA%\nvim-data for data, unless XDG_* overrides them. +if ($env:XDG_CONFIG_HOME) { $NvimConfigDir = Join-Path $env:XDG_CONFIG_HOME 'nvim' } +else { $NvimConfigDir = Join-Path $env:LOCALAPPDATA 'nvim' } +if ($env:XDG_DATA_HOME) { $NvimDataDir = Join-Path $env:XDG_DATA_HOME 'nvim' } +else { $NvimDataDir = Join-Path $env:LOCALAPPDATA 'nvim-data' } + +$existing = @(@($NvimConfigDir, $NvimDataDir) | Where-Object { Test-Path $_ }) +if ($existing.Count -gt 0) { + if (-not $Backup -and -not $Force -and $IsInteractive) { + Write-Host '' + Write-Host 'This will DELETE (no backup) :' -ForegroundColor Yellow + $existing | ForEach-Object { Write-Host " $_" } + $answer = Read-Host 'Continue? [y/N] (re-run with -Backup to keep copies instead)' + if ($answer -notmatch '^[Yy]') { + Write-Host 'Aborted before touching your Neovim config.' + exit 1 + } + } + $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' + foreach ($dir in $existing) { + try { + if ($Backup) { + Move-Item -LiteralPath $dir -Destination "$dir.bak-$stamp" -ErrorAction Stop + Write-Host " Moved $dir -> $dir.bak-$stamp" + } + else { + Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction Stop + } + } + catch { + Write-Host "ERROR: could not remove $dir -- close any running Neovim/Neovide and try again." -ForegroundColor Red + Write-Host " $($_.Exception.Message)" + exit 1 + } + } +} + +# --------------------------------------------------------------------- +# 4. Install the Neovim configuration (copied from config\nvim, not +# embedded in this script -- edit those files directly to customize) +# --------------------------------------------------------------------- +Write-Host '[4/4] Installing new Neovim configuration...' +New-Item -ItemType Directory -Force -Path $NvimConfigDir | Out-Null +Copy-Item -Path (Join-Path $ConfigDir 'nvim\*') -Destination $NvimConfigDir -Recurse -Force -ErrorAction Stop + +# --- ~\.clang-format, ~\.clang-tidy and ~\cpp-style-guide.md --- +# Personal fallback style, same as on Linux (see the comments in each +# file). clang-format/clang-tidy walk UP from the file being processed +# looking for these, so they act as the default for any project under +# your user folder that doesn't ship its own. Copied unconditionally, +# every run. +Copy-Item -LiteralPath (Join-Path $ConfigDir 'clang-format') -Destination (Join-Path $HOME '.clang-format') -Force -ErrorAction Stop +Copy-Item -LiteralPath (Join-Path $ConfigDir 'clang-tidy') -Destination (Join-Path $HOME '.clang-tidy') -Force -ErrorAction Stop +Copy-Item -LiteralPath (Join-Path $ConfigDir 'cpp-style-guide.md') -Destination (Join-Path $HOME 'cpp-style-guide.md') -Force -ErrorAction Stop + +Write-Host "Wrote $HOME\.clang-format and $HOME\.clang-tidy (Allman braces, real tabs), and $HOME\cpp-style-guide.md." + +Write-Host '=========================================' +Write-Host ' Setup Complete!' +Write-Host " Open a NEW terminal (so PATH changes apply) and run 'nvim'." +Write-Host ' p and P are restored, use p to force inline paste!' +Write-Host ' Debugger: db to set a breakpoint, F5 / dc to start/continue,' +Write-Host ' du to toggle the debug UI. First debug session will auto-install codelldb via Mason.' +Write-Host " Project-specific CMake debug targets loaded from $NvimConfigDir\lua\dap\configurations\cmake.lua" +Write-Host ' Database (MariaDB): Du opens the DB UI; press A there to add a connection' +Write-Host ' (URL form: mariadb://user:password@127.0.0.1:3306/dbname). The sqls language server is' +Write-Host " installed on first launch; see README.md ('Database (MariaDB)') for its optional config." +Write-Host ' First launch takes a minute or two (lazy.nvim installs plugins, Mason installs servers,' +Write-Host ' treesitter compiles parsers with gcc).' +Write-Host ' Terminal: set your terminal font to "JetBrainsMono Nerd Font" (Windows Terminal:' +Write-Host ' Settings > Profiles > Defaults > Appearance > Font face), then restart it.' +if ($Frontend -eq 'desktop' -or $Frontend -eq 'both') { + Write-Host " Neovide: run 'neovide' to launch the desktop app (same config, same keymaps)." +} +Write-Host '========================================='