Warning
Work in progress. neo-build.nvim is under active development. Interfaces, behavior, and supported project shapes may still change while real-world workflows are being tested.
Build and run Visual Studio / MSBuild solutions directly from Neovim — pick a
configuration and platform, build the solution or a single project, run the
startup project, and generate compile_commands.json for clangd. No tasks.json,
no leaving the editor.
Focus: Windows + MSBuild, with C++
.vcxprojas the primary use case and C#.csprojas a useful but secondary one; classic.slnand the new.slnxare supported. C++ gets priority because these projects cannot generally rely ondotnetalone for build and launch workflows. C# already has good CLI tooling, but neo-build still provides a convenient unified experience for mixed and C#-only solutions. Expect more rough edges in C# support for now.
- Workspace = the solution in your project root. Detects a
.sln/.slnxin the cwd on startup; stays idle when there is none. - Solution & project parsing. Reads projects, configurations, platforms and
the per-project configuration mapping from
.sln/.slnx, then parses each.vcxproj/.csprojfor kind, runnability and output paths. - Configuration / platform selection, like Visual Studio's two top dropdowns — remembered per solution.
- Build the whole solution, the startup project, or any project you pick. msbuild runs as a job; colored output streams into a buffer and diagnostics go to the quickfix list.
- Run the startup project's executable — in your terminal (toggleterm or any provider you wire up) or, with no terminal plugin, in a detached console window.
- Run arguments & working directory, read from the project's settings the
way VS does (
.vcxproj.user,launchSettings.json,.csproj.user), with a manual per-project override. compile_commands.jsongeneration via msbuild-compdb, downloaded for you — so clangd can work with supported native MSBuild C++ projects.- MSBuild toolchain auto-detection via
vswhere(no need for msbuild onPATH). - Statusline helpers and a typed output buffer for window managers.
- DAP-agnostic: exposes the resolved launch spec so you can drive debugging from your own nvim-dap config.
- Neovim 0.10+ (
vim.system,vim.uv,vim.fs). - Windows with MSBuild — Visual Studio or Build Tools (detected via
vswhere). curl(bundled with Windows 10+) — only for downloading the compdb tool.- Optional: toggleterm.nvim (or any
terminal via
opts.terminal) for running inside Neovim; a Nerd Font for statusline icons; clangd for the generatedcompile_commands.json.
Nothing else is mandatory: builds run into their own buffer, and running falls back to a detached console window when no terminal backend is present.
{
"max-wolf-cpp/neo-build.nvim",
opts = {},
}opts = {} is enough; setup() runs with sensible defaults. Lazy-load it
yourself if you like (e.g. on :NeoBuild or a keymap) — the shallow workspace
scan only runs in setup().
vim.pack.add({ "https://github.com/max-wolf-cpp/neo-build.nvim" })
require("neo-build").setup({})Run :NeoBuild compdb-install once to fetch the compdb tool (or let it download
on first use).
Open Neovim in a folder that contains a .sln/.slnx, then:
:NeoBuild config " pick Debug / Release / ...
:NeoBuild platform " pick Any CPU / x64 / x86 / ...
:NeoBuild startup " pick the startup project (runnable projects only)
:NeoBuild build " build the solution
:NeoBuild run " run the startup project's executable
:NeoBuild compdb " generate compile_commands.json (C++ / clangd)
Selections are remembered per solution across sessions.
All commands are subcommands of :NeoBuild; a subcommand is required.
| Command | Description |
|---|---|
:NeoBuild scan |
Rescan the project root for a solution and load it (also runs on setup). |
:NeoBuild config |
Pick the active build configuration. Remembered per solution. |
:NeoBuild platform |
Pick the active platform. Remembered per solution. |
:NeoBuild startup |
Pick the startup project (among runnable ones). Remembered. |
:NeoBuild build |
Build the active solution. |
:NeoBuild build-startup |
Build only the startup project target. |
:NeoBuild build-project |
Pick any project and build it. |
:NeoBuild run |
Run the startup project's executable (no debugger). |
:NeoBuild run-project |
Pick any runnable project and run it (without changing startup). |
:NeoBuild clean |
Clean the active solution. |
:NeoBuild clean-startup |
Clean only the startup project target. |
:NeoBuild clean-project |
Pick a project and clean it. |
:NeoBuild args |
Set run arguments for the startup project (per project). |
:NeoBuild args! |
Clear the override and use run arguments from project settings. |
:NeoBuild cwd |
Set the run working directory for the startup project (per project). |
:NeoBuild run-info |
Show the resolved run target, working directory and arguments. |
:NeoBuild compdb |
Generate compile_commands.json for the solution (full rebuild). |
:NeoBuild compdb-install |
Download the msbuild-compdb tool. |
:NeoBuild toolchain |
Show / re-select the msbuild used for builds. |
:NeoBuild inspect |
Dump the parsed session into a scratch buffer (dev helper). |
Defaults shown:
require("neo-build").setup({
-- Path to msbuild.exe. nil = auto-detect via vswhere (cached machine-wide;
-- override anytime with :NeoBuild toolchain).
msbuild_path = nil,
-- Owns the launch entirely:
-- fun(spec) with spec = { program, cwd, args, env, gui }.
-- It assembles the command line and runs it. nil = use the terminal / detached.
launcher = nil,
-- How `run` launches: "terminal" requests a terminal backend, but falls back
-- to a detached console when none is available; "detached" always uses a
-- separate window; nil selects a terminal when available, else detached.
-- GUI apps use detached launch with the built-in launcher. Terminal commands
-- adapt to PowerShell / cmd / POSIX; detached launch is shell-agnostic.
launch_kind = nil,
-- Output backend only: { name?, send = fun(cmd, go_back) }. neo-build builds
-- the full command line `cmd`; you just send it. nil = toggleterm.
terminal = nil,
terminal_id = 1, -- which toggleterm terminal to reuse
ui = {
output = {
position = "bottom", -- bottom | top | left | right | float
size = 15, -- rows (h-split) or columns (v-split)
float = { width = 0.8, height = 0.8, border = "rounded" },
},
quickfix = {
open_on_error = true, -- open on failure; close the plugin-opened window when empty
},
},
compdb = {
auto_on_switch = false, -- regenerate (full rebuild) on config/platform change
restart_lsp = false, -- restart attached clangd/ccls clients after generation
},
})A workspace is a .sln or .slnx sitting in the project root (cwd). The scan
is shallow and cheap; with no solution the plugin does nothing. When the root
contains several solutions, the plugin asks which one to load and remembers the
choice for that root. Run :NeoBuild scan to select again or after changing
directory with Neovim already open.
:NeoBuild config / :NeoBuild platform mirror Visual Studio's two top
dropdowns. Pick a solution configuration and platform; msbuild applies the
per-project mapping itself when building the solution. The grid in VS's
Configuration Manager edits that mapping — it lives in the solution file, so edit
it there (or in VS).
For .slnx the per-project platform mapping is implicit; neo-build matches the
project's own platforms with sensible equivalences (x86 ↔ Win32,
Any CPU ↔ AnyCPU) and prefers x64 when a solution platform has no project
equivalent — matching Visual Studio.
build runs msbuild as a job (not in an interactive terminal), so it can:
- stream colored output into a buffer (
filetype=neo-build, nameneo-build://build), and - parse compiler diagnostics into the quickfix list (deduplicated), opening it on failure.
build-project / build-startup target a single project, and clean-project /
clean-startup target one for cleaning. C++ project files are invoked directly
with their mapped project configuration/platform; neo-build forwards
$(SolutionDir) and the other solution properties that Visual Studio normally
provides. C# projects are invoked as targets of the active solution, using the
selected solution configuration/platform so msbuild retains solution mappings
and cross-language project-reference behavior. In both cases, unrelated projects
are not built unless dependencies require them.
run launches the startup project's built executable. It is resolved in this
order:
opts.launcher— if set, it owns the launch entirely (see below);opts.terminal.send— a custom terminal output backend;- toggleterm, if installed;
- a detached console window (
cmd start) — so running works with no terminal plugin at all.
With the built-in launcher, a GUI/windowed app is always launched detached
(steps 2–3 are skipped), and so is everything when launch_kind = "detached" —
a windowed app does not belong in a terminal. launch_kind = "terminal" still
falls back to detached launch when neither opts.terminal nor toggleterm is
available. For .NET apps without an apphost, run falls back to dotnet <dll>.
For an SDK C# project with multiple TargetFrameworks, run asks which framework
to launch every time. The choice is intentionally not persisted. run-project
shows the framework picker after the project picker. run-info lists every
resolved framework target without asking for a selection.
SDK C# output resolution honors project-declared BaseOutputPath, OutputPath,
AppendPlatformToOutputPath, AppendTargetFrameworkToOutputPath, and
AppendRuntimeIdentifierToOutputPath, expanding the usual project/solution and
selection variables before resolving the executable.
These look similar but do different jobs:
-
launcher = fun(spec)takes the launch entirely. It receives the resolved launch spec{ program, cwd, args, env, gui }and is responsible for assembling and running the launch command however it likes (terminal, dap, Start-Process, …) — "command" here is the run command line, not a build. neo-build does nothing else.programandcwdare resolved paths,argsis astring[],envis the environment table, andguireports whether the project is a windowed app. A custom launcher may use or ignoregui; detached launch is only enforced by the built-in launcher.launcher = function(spec) -- spec = { program, cwd, args, env, gui } require("dap").run({ type = "cppvsdbg", request = "launch", program = spec.program, cwd = spec.cwd, args = spec.args, noDebug = true }) end,
-
terminal = { name?, send = fun(cmd, go_back) }is only the output backend. neo-build builds the full command-line stringcmdfor your shell (PowerShell / cmd / POSIX) and hands it tosend; you just feed it to your terminal. Use this to run inside a terminal other than toggleterm.terminal = { name = "my-term", send = function(cmd, go_back) --[[ run the ready-made `cmd` in your terminal ]] end, },
In short: launcher replaces how to run; terminal replaces where the output
goes while neo-build still composes the command.
Resolved with this precedence:
- values set via
:NeoBuild args/:NeoBuild cwd(remembered per project); - the project's settings files, like Visual Studio:
- C++ —
.vcxproj.user(LocalDebuggerCommandArguments/LocalDebuggerWorkingDirectory); - C# SDK —
Properties/launchSettings.json(theProjectprofile'scommandLineArgs/workingDirectory); - C# legacy —
.csproj.user(StartArguments/StartWorkingDirectory);
- C++ —
- defaults — the project directory for C++, the output directory for .NET (as VS does).
Relative working directories from project settings are resolved from the project directory, matching Visual Studio.
An empty value saved through :NeoBuild args is an explicit override meaning
"run without arguments". Use :NeoBuild args! to remove that override and
return to arguments from the project's settings files.
Argument input is parsed by neo-build into string[] before any launcher or
shell backend sees it. Whitespace outside quotes separates arguments; both
"double quotes" and 'single quotes' group text and are removed. Quotes of the
other kind are literal inside a quoted group. Empty quoted arguments are kept,
and backslashes are always literal so pasted Windows paths work unchanged.
An unterminated quote is rejected with an error.
test "aa dd" 'xx ww' "" C:\Temp\foo\
→ { "test", "aa dd", "xx ww", "", "C:\\Temp\\foo\\" }
:NeoBuild run-info shows the effective values and where each came from.
For :NeoBuild cwd, an empty or whitespace-only value removes the manual
override. A non-empty value is saved only when it points to an existing
directory.
:NeoBuild compdb does a full Rebuild of the solution emitting a binary log,
then runs msbuild-compdb on it
to write compile_commands.json into the solution root (where clangd looks). A
full rebuild is required — incremental builds skip up-to-date compiles, so their
commands would be missing. This is an on-demand step, like CMake's configure.
The tool is downloaded from its releases into stdpath("data")/neo-build/bin/
on first use (or explicitly with :NeoBuild compdb-install).
Set compdb = { auto_on_switch = true } to regenerate automatically whenever you
switch configuration or platform.
With restart_lsp = true, only clangd/ccls clients attached to C/C++ buffers in
the active solution are restarted. Their existing buffer attachments are
restored independently of the currently focused window.
require("lualine").setup({
sections = {
lualine_x = { function() return require("neo-build").statusline() end },
},
})
-- e.g. " D3D12HelloWorld | D3D12HelloTriangle (Development|x64)"require("neo-build").status() returns the structured
{ solution, startup, configuration, platform } if you want to build your own.
The build output buffer has filetype = "neo-build", so window managers can pin
it:
require("edgy").setup({ bottom = {
{ ft = "neo-build", size = { height = 0.3 } },
{ ft = "qf", title = "Quickfix", size = { height = 0.3 } },
} })neo-build owns what to run; how is yours. require("neo-build").launch_spec()
returns { program, cwd, args, env, gui } for the current startup project and
selection, so you can drive any adapter:
For a multi-target C# project this synchronous API returns nil plus an
ambiguity error; use :NeoBuild run for the one-shot framework picker.
local spec = require("neo-build").launch_spec()
if spec then
require("dap").run({
type = "<your-adapter>", -- cppvsdbg, codelldb, coreclr, ...
request = "launch",
program = spec.program,
cwd = spec.cwd,
args = spec.args,
-- noDebug = true, -- for a plain run via DAP
})
endTo run inside a terminal other than toggleterm, set opts.terminal (an output
backend). To take over launching entirely (e.g. for DAP), set opts.launcher.
See launcher vs terminal above.
local nb = require("neo-build")
nb.setup(opts) -- initialize
nb.launch_spec() -- { program, cwd, args, env, gui } | nil, err (for DAP)
nb.statusline() -- ready-made statusline string
nb.status() -- { solution, startup, configuration, platform }
nb.install_compdb() -- download the msbuild-compdb tool- Windows / MSBuild only. C++ (
.vcxproj) and C# (.csproj);.slnand.slnx. - When multiple solutions sit in the root, setup asks once and remembers the
selection for that root;
:NeoBuild scanalways asks again. .slnxper-project configuration mapping is supported, including explicit overrides (<BuildType>/<Platform>/<Build>) and the implicit 1:1 default, with the platform equivalences above.- Output-path resolution is best-effort: declared values plus MSBuild defaults;
property sheets /
Directory.Build.propsand full evaluation are not modeled. compile_commands.jsonis regenerated on demand (a full rebuild), not live.- WinUI launch is not supported yet, for either C++ or C#, packaged or unpackaged. These projects can build and their output may be resolved, but the current run flow does not perform the registration/activation setup they need; launching the resolved executable directly can fail or exit silently. I know about this limitation, but registration support is not a current priority.
- C++/CLI is not understood by clangd. A generated
compile_commands.jsondoes not make clangd understand Microsoft's CLR extensions, so C++/CLI source can remain full of diagnostics even with an otherwise correct compilation database. This is a clangd limitation that neo-build cannot fix. - C# support has had less real-world coverage than C++. C# projects can
already build and run through
dotnetwithout this plugin, so native C++ workflows remain the main priority. neo-build still aims to make C# and mixed solutions more convenient, but additional C# edge cases may remain.
If you encounter a reproducible problem in a real project, please open an issue with the relevant project shape, configuration, and logs. I am happy to receive and investigate concrete bug reports.
This project vendors SLAXML by Gavin Kistner, licensed under the MIT License. See the bundled SLAXML license.
MIT.