A high-performance, asynchronous embedded development framework for Neovim. It bridges PlatformIO project structures with clangd language servers, managing include file mappings and cross-compiler parameter translations on Windows, Linux, and macOS.

src/ and include/ template files.clangd via compile_commands.json.-mlongcalls) that destabilize desktop language servers.:ClangdFilter to instantly toggle specific syntax warnings or static alerts.Create, configure, and code a brand-new microcontroller project (e.g., ESP32, STM32, Arduino) inside an empty folder without ever touching the terminal CLI:
mkdir my-esp32-project
cd my-esp32-project
nvim .
:Pioinit)Inside Neovim, run:
:Pioinit
Y/N).seeed_xiao_esp32s3).arduino).compile_commands.json, and scaffold template ./src and ./include files.q to close the terminal once complete and start coding!| Key Sequence / Command | Action | Description |
|---|---|---|
<leader>\ g b |
Build Code | Runs :Piocli run to compile firmware |
<leader>\ g u |
Upload Code | Runs :Piocli run -t upload to flash target board |
<leader>\ a b |
Generate LSP Data | Re-generates compile_commands.json |
<leader>\ m |
Serial Monitor | Opens asynchronous terminal monitor |
:Piolib <query> |
Install Library | Interactively search/install libraries and refresh LSP |
pio) installed (or let :Pioinit prompt and install it for you).lazy.nvim)return {
'batoaqaa/nvim-pio',
lazy = false,
dependencies = {
{ 'nvim-telescope/telescope.nvim' },
{ 'nvim-telescope/telescope-ui-select.nvim' },
{ 'nvim-lua/plenary.nvim' },
{ 'folke/which-key.nvim' },
{
'williamboman/mason-lspconfig.nvim',
dependencies = {
{ 'williamboman/mason.nvim' },
{ 'folke/trouble.nvim' },
{ 'j-hui/fidget.nvim' },
},
},
},
config = function()
require('nvimpio').setup({
pio = {
pio_runtime_dir = '~/.platformio',
pio_storage_dir = '~/.platformio',
},
clangd = {
support = true, -- Master switch for PlatformIO LSP logic
install = false, -- Flags whether to auto-install missing clangd
-- Configures attach integration behavior.
-- Options:
-- "attach+" -> Attach the LSP client AND inject default hotkeys.
-- "attach" -> Attach the LSP client only (no custom hotkeys).
-- "none" -> Do not attach to files at all.
attach = 'attach+',
},
menu_key = '<leader>\\', -- Local workspace menu activation mapping
menu_name = 'PlatformIO', -- Interactive dashboard selection label
})
end,
}
The interactive PlatformIO dashboard mapping parameters can be fully configured using the structured menu_bindings node array layer inside your setup invocation block:
require('nvimpio').setup({
pio = {
pio_runtime_dir = '~/.platformio',
pio_storage_dir = '~/.platformio',
},
clangd = {
support = true, -- Master switch for PlatformIO LSP logic
attach = 'attach+',
install = false,
},
menu_key = '<leader>\\',
menu_name = 'PlatformIO',
menu_bindings = {
{ node = 'item', desc = '[B]lock diagnostic', shortcut = 'b', command = 'ClangdFilter' },
{ node = 'item', desc = '[C]li terminal', shortcut = 'c', command = 'Piocli' },
{ node = 'item', desc = 'Switch [E]nv', shortcut = 'e', command = 'PioPickEnv' },
{ node = 'item', desc = '[I]nitiate project', shortcut = 'i', command = 'Pioinit' },
{ node = 'item', desc = '[M]onitor terminal', shortcut = 'm', command = 'Piomon' },
{ node = 'item', desc = 're[S]tart clangd', shortcut = 's', command = 'Clangdrestart' },
{
node = 'menu',
desc = '[A]dvanced',
shortcut = 'a',
items = {
{ node = 'item', desc = '[T]est', shortcut = 't', command = 'Piocli test' },
{ node = 'item', desc = '[C]heck', shortcut = 'c', command = 'Piocli check' },
{ node = 'item', desc = '[D]ebug', shortcut = 'd', command = 'Piocli debug' },
{ node = 'item', desc = 'Compilation Data[b]ase', shortcut = 'b', command = 'Piocli run -t compiledb' },
{
node = 'menu',
desc = '[V]erbose',
shortcut = 'v',
items = {
{ node = 'item', desc = 'Verbose [B]uild', shortcut = 'b', command = 'Piocli run -v' },
{ node = 'item', desc = 'Verbose [U]pload', shortcut = 'u', command = 'Piocli run -v -t upload' },
{ node = 'item', desc = 'Verbose [T]est', shortcut = 't', command = 'Piocli test -v' },
{ node = 'item', desc = 'Verbose [C]heck', shortcut = 'c', command = 'Piocli check -v' },
{ node = 'item', desc = 'Verbose [D]ebug', shortcut = 'd', command = 'Piocli debug -v' },
},
},
},
},
{
node = 'menu',
desc = '[D]ependencies',
shortcut = 'd',
items = {
{ node = 'item', desc = '[L]ist packages', shortcut = 'l', command = 'Piocli pkg list' },
{ node = 'item', desc = '[O]utdated packages', shortcut = 'o', command = 'Piocli pkg outdated' },
{ node = 'item', desc = '[U]pdate packages', shortcut = 'u', command = 'Piocli pkg update' },
},
},
{
node = 'menu',
desc = '[F]lash',
shortcut = 'f',
items = {
{ node = 'item', desc = '[B]uild file system', shortcut = 'b', command = 'Piocli run -t buildfs' },
{ node = 'item', desc = 'Program [S]ize', shortcut = 's', command = 'Piocli run -t size' },
{ node = 'item', desc = '[U]pload file system', shortcut = 'u', command = 'Piocli run -t uploadfs' },
{ node = 'item', desc = '[E]rase Flash', shortcut = 'e', command = 'Piocli run -t erase' },
},
},
{
node = 'menu',
desc = '[G]eneral',
shortcut = 'g',
items = {
{ node = 'item', desc = '[B]uild', shortcut = 'b', command = 'Piocli run' },
{ node = 'item', desc = '[C]lean', shortcut = 'c', command = 'Piocli run -t clean' },
{ node = 'item', desc = '[D]evice list', shortcut = 'd', command = 'Piocli device list' },
{ node = 'item', desc = '[F]ull clean', shortcut = 'f', command = 'Piocli run -t fullclean' },
{ node = 'item', desc = '[P]arameters hardware setup', shortcut = 'p', command = 'PioSelectPort' },
{ node = 'item', desc = '[U]pload', shortcut = 'u', command = 'Piocli run -t upload' },
},
},
{
node = 'menu',
desc = '[P]latformIO',
shortcut = 'p',
items = {
{ node = 'item', desc = 're[F]resh PlatformIO project data', shortcut = 'f', command = 'PioRefreshData' },
{ node = 'item', desc = '[G]it ignore', shortcut = 'g', command = 'PioGitIgnore' },
{ node = 'item', desc = '[I]nstall PlatformIO Core', shortcut = 'i', command = 'PioInstall' },
{ node = 'item', desc = '[R]epair PlatformIO Core', shortcut = 'r', command = 'PioRepair' },
{ node = 'item', desc = '[U]pgrade PlatformIO Core', shortcut = 'u', command = 'Piocli upgrade' },
},
},
{
node = 'menu',
desc = '[R]emote',
shortcut = 'r',
items = {
{ node = 'item', desc = 'Remote [U]pload', shortcut = 'u', command = 'Piocli remote run -t upload' },
{ node = 'item', desc = 'Remote [T]est', shortcut = 't', command = 'Piocli remote test' },
{ node = 'item', desc = 'Remote [M]onitor', shortcut = 'm', command = 'Piomon remote run -t monitor' },
{ node = 'item', desc = 'Remote [D]evices', shortcut = 'd', command = 'Piocli remote device list' },
},
},
},
})
Test the complete capabilities of this extension inside an insulated runtime sandbox without modifying your production editor configurations. Execute this sequence from a standard terminal prompt:
# Fetch the automated sandbox bootstrapper script
wget https://raw.githubusercontent.com/batoaqaa/nvim-pio/main/nvimpio.lua
# Execute the isolated evaluation environment
nvim -u nvimpio.lua .
# Inside Neovim, kickstart your environment using:
:Pioinit
[!TIP] You can run
:checkhealth nvimpioto ensure you have all the required dependencies. It will also verify that your configuration table is correctly formatted.Type
:h nvimpioinside Neovim for detailed documentation.
if you opted for attach = 'attach+' in config, then nvim-pio will inject these LSP keymaps:
All keybindings use a consistent gl prefix (Goto LSP / Global LSP) to avoid conflicting with Neovim default shortcuts.
| Keymap | Mode | Action | Description |
|---|---|---|---|
gld |
n |
vim.lsp.buf.definition |
Go to definition |
glD |
n |
vim.lsp.buf.declaration |
Go to declaration |
glt |
n |
vim.lsp.buf.type_definition |
Go to type definition |
gli |
n |
vim.lsp.buf.implementation |
Go to implementation |
glr |
n |
Telescope lsp_references |
Search references in Telescope |
glk |
n |
vim.lsp.buf.hover |
Show hover documentation |
gls |
n, i |
vim.lsp.buf.signature_help |
Show function signature |
glws |
n |
textDocument/switchSourceHeader |
Switch between Source/Header (clangd) |
| Keymap | Mode | Action | Description |
|---|---|---|---|
glwd |
n |
Telescope lsp_document_symbols |
Find functions & methods in current file |
glww |
n |
Telescope lsp_dynamic_workspace_symbols |
Search symbols across entire workspace |
| Keymap | Mode | Action | Description |
|---|---|---|---|
gla |
n |
vim.lsp.buf.code_action |
Trigger code actions |
glR |
n |
vim.lsp.buf.rename |
Rename symbol under cursor |
glf |
n, x |
vim.lsp.buf.format |
Format current buffer or visual selection |
glh |
n |
vim.lsp.inlay_hint |
Toggle inline hints |
| Keymap | Mode | Action | Description |
|---|---|---|---|
[d |
n |
vim.diagnostic.jump({ count = -1 }) |
Jump to previous diagnostic |
]d |
n |
vim.diagnostic.jump({ count = 1 }) |
Jump to next diagnostic |
gle |
n |
vim.diagnostic.open_float |
Show diagnostic popup window |
glq |
n |
vim.diagnostic.setloclist |
Send buffer diagnostics to location list |
[q |
n |
vim.cmd.cprev |
Previous quickfix item |
]q |
n |
vim.cmd.cnext |
Next quickfix item |
| Keymap | Mode | Action | Description |
|---|---|---|---|
glwa |
n |
vim.lsp.buf.add_workspace_folder |
Add folder to LSP workspace |
glwr |
n |
vim.lsp.buf.remove_workspace_folder |
Remove folder from LSP workspace |
glwl |
n |
vim.lsp.buf.list_workspace_folders |
Print active LSP workspace folders |
Note: Default Neovim 0.10+ keymaps (
gra,gri,grn,grr,gO,K) are automatically disabled for LSP buffers to eliminate keymap overlap. Auto-formatting is triggered synchronously on buffer save (BufWritePre, 3000ms timeout).
Utilizes a safe pcall structural check to ensure your statusline never crashes if the plugin hasn't finished loading yet during the lazy.nvim startup cycle:
require('lualine').setup({
sections = {
lualine_x = {
function()
local ok, statusline = pcall(require, 'nvimpio.statusline')
if ok and type(statusline.get_status_string) == 'function' then
return statusline.get_status_string()
end
return ""
end,
'filetype'
}
}
})
If you aren't using lualine.nvim, append this to your native statusline:
vim.opt.statusline:append("%{v:lua.require('nvimpio.statusline').get_status_string()}")