diff --git a/.luarc.json b/.luarc.json
new file mode 100644
index 00000000..54965bab
--- /dev/null
+++ b/.luarc.json
@@ -0,0 +1,9 @@
+{
+ "workspace": {
+ "library": ["$VIMRUNTIME/lua", "${3rd}/luv/library", "./lua"],
+ "checkThirdParty": false
+ },
+ "diagnostics": {
+ "globals": ["vim"]
+ }
+}
diff --git a/README.md b/README.md
index 7971e09e..b69d0a70 100644
--- a/README.md
+++ b/README.md
@@ -7,18 +7,15 @@
### Demo
-
-
https://github.com/user-attachments/assets/fdbb6655-4b2d-4a2b-81d1-fd8af6e7d9f1
-
-
Try the plugin with this minimal standalone config without modifying your existing nvim setup. **This is especially useful if you're encountering errors during installation or usage**.
+
```sh
-wget https://raw.githubusercontent.com/anurag3301/nvim-platformio.lua/main/minimal_config.lua
-nvim -u minimal_config.lua
+wget https://raw.githubusercontent.com/batoaqaa/nvim-platformio.lua/refs/heads/main/mini_nvimPlatformio.lua
+nvim -u mini_nvimPlatformio.lua
# Now run :Pioinit
```
@@ -26,102 +23,122 @@ nvim -u minimal_config.lua
## Installation
#### PlatformIO Core
-Follow the installation instructions in the [PlatformIO documentation](https://docs.platformio.org/en/latest/core/installation/index.html).
+Follow the installation instructions in the [PlatformIO documentation](https://docs.platformio.org/en/latest/core/installation/index.html).
#### Plugin
+
Install the plugin using lazy
+
```lua
return {
- 'anurag3301/nvim-platformio.lua',
+ 'batoaqaa/nvim-platformio.lua',
+ -- cmd = { 'Pioinit', 'Piorun', 'Piocmdh', 'Piocmdf', 'Piolib', 'Piomon', 'Piodebug', 'Piodb' },
-- optional: cond used to enable/disable platformio
-- based on existance of platformio.ini file and .pio folder in cwd.
- -- You can enable platformio plugin, using :Pioinit command
- cond = function()
- -- local platformioRootDir = vim.fs.root(vim.fn.getcwd(), { 'platformio.ini' }) -- cwd and parents
- local platformioRootDir = (vim.fn.filereadable('platformio.ini') == 1) and vim.fn.getcwd() or nil
- if platformioRootDir then
- -- if platformio.ini file exist in cwd, enable plugin to install plugin (if not istalled) and load it.
- vim.g.platformioRootDir = platformioRootDir
- elseif (vim.uv or vim.loop).fs_stat(vim.fn.stdpath('data') .. '/lazy/nvim-platformio.lua') == nil then
- -- if nvim-platformio not installed, enable plugin to install it first time
- vim.g.platformioRootDir = vim.fn.getcwd()
- else -- if nvim-platformio.lua installed but disabled, create Pioinit command
- vim.api.nvim_create_user_command('Pioinit', function() --available only if no platformio.ini and .pio in cwd
- vim.api.nvim_create_autocmd('User', {
- pattern = { 'LazyRestore', 'LazyLoad' },
- once = true,
- callback = function(args)
- if args.match == 'LazyRestore' then
- require('lazy').load({ plugins = { 'nvim-platformio.lua' } })
- elseif args.match == 'LazyLoad' then
- vim.notify('PlatformIO loaded', vim.log.levels.INFO, { title = 'PlatformIO' })
- require("platformio").setup(vim.g.pioConfig)
- vim.cmd('Pioinit')
- end
- end,
- })
+ -- You can enable platformio plugin, using :Pioinit command
+ cond = function()
+ -- local platformioRootDir = vim.fs.root(vim.fn.getcwd(), { 'platformio.ini' }) -- cwd and parents
+ local platformioRootDir = (vim.fn.filereadable('platformio.ini') == 1) and vim.fn.getcwd() or nil
+ if platformioRootDir and vim.fs.find('.pio', { path = platformioRootDir, type = 'directory' })[1] then
+ -- if platformio.ini file and .pio folder exist in cwd, enable plugin to install plugin (if not istalled) and load it.
+ vim.g.platformioRootDir = platformioRootDir
+ elseif (vim.uv or vim.loop).fs_stat(vim.fn.stdpath('data') .. '/lazy/nvim-platformio.lua') == nil then
+ -- if nvim-platformio not installed, enable plugin to install it first time
vim.g.platformioRootDir = vim.fn.getcwd()
- require('lazy').restore({ plguins = { 'nvim-platformio.lua' }, show = false })
- end, {})
- end
- return vim.g.platformioRootDir ~= nil
- end,
-
- -- Dependencies are lazy-loaded by default unless specified otherwise.
- dependencies = {
- { 'akinsho/toggleterm.nvim' },
- { 'nvim-telescope/telescope.nvim' },
- { 'nvim-telescope/telescope-ui-select.nvim' },
- { 'nvim-lua/plenary.nvim' },
- { 'folke/which-key.nvim' },
- { 'nvim-treesitter/nvim-treesitter' }
- },
+ else -- if nvim-platformio.lua installed but disabled, create Pioinit command
+ vim.api.nvim_create_user_command('Pioinit', function() --available only if no platformio.ini and .pio in cwd
+ vim.api.nvim_create_autocmd('User', {
+ pattern = { 'LazyRestore', 'LazyLoad' },
+ once = true,
+ callback = function(args)
+ if args.match == 'LazyRestore' then
+ require('lazy').load({ plugins = { 'nvim-platformio.lua' } })
+ elseif args.match == 'LazyLoad' then
+ vim.notify('PlatformIO loaded', vim.log.levels.INFO, { title = 'PlatformIO' })
+ vim.cmd('Pioinit')
+ end
+ end,
+ })
+ vim.g.platformioRootDir = vim.fn.getcwd()
+ require('lazy').restore({ plguins = { 'nvim-platformio.lua' }, show = false })
+ end, {})
+ end
+ return vim.g.platformioRootDir ~= nil
+ end,
+
+ dependencies = {
+ { 'akinsho/toggleterm.nvim' },
+ { 'nvim-telescope/telescope.nvim' },
+ { 'nvim-telescope/telescope-ui-select.nvim' },
+ { 'nvim-lua/plenary.nvim' },
+ { 'folke/which-key.nvim' },
+ {
+ 'mason-org/mason-lspconfig.nvim',
+ dependencies = {
+ { 'mason-org/mason.nvim' },
+ { 'folke/trouble.nvim' },
+ { 'j-hui/fidget.nvim' }, -- status bottom right
+ },
+ },
+ },
}
+
```
#### Usage `:h PlatformIO`
### Configuration
+
```lua
-vim.g.pioConfig ={
- lsp = 'clangd', -- value: clangd | ccls
- menu_key = '\\', -- replace this menu key to your convenience
- debug = false -- enable debug messages
- clangd_source = 'ccls' -- value: ccls | compiledb, For detailed explation check :help platformio-clangd_source
-}
-local pok, platformio = pcall(require, 'platformio')
-if pok then platformio.setup(vim.g.pioConfig) end
+ vim.g.pioConfig ={
+ lspClangd = {
+ enabled = true,
+ attach = {
+ enabled = true,
+ keymaps = true,
+ },
+ },
+ menu_key = '\\', -- replace this menu key to your convenience
+ menu_name = 'PlatformIO', -- replace this menu name to your convenience
+ }
+ local pok, platformio = pcall(require, 'platformio')
+ if pok then platformio.setup(vim.g.pioConfig) end
```
### Keybinds
+
These are the default keybindings, which you can override in your configuration.
+
```lua
local pok, platformio = pcall(require, 'platformio')
if pok then
platformio.setup({
- lsp = 'ccls', --default: ccls, other option: clangd
- -- If you pick clangd, it also creates compile_commands.json
-
- -- Uncomment out following line to enable platformio menu.
- -- menu_key = '\\', -- replace this menu key to your convenience
+ lspClangd = {
+ enabled = false,
+ attach = {
+ enabled = false,
+ keymaps = false,
+ },
+ },
+ menu_key = '\\', -- replace this menu key to your convenience
menu_name = 'PlatformIO', -- replace this menu name to your convenience
+ debug = false,
- -- Following are the default keybindings, you can overwrite them in the config
menu_bindings = {
- { node = 'item', desc = '[L]ist terminals', shortcut = 'l', command = 'PioTermList' },
+ { node = 'item', desc = '[L]ist terminals', shortcut = 'l', command = 'PioTermList' },
{ node = 'item', desc = '[T]erminal Core CLI', shortcut = 't', command = 'Piocmdf' },
{
node = 'menu',
desc = '[G]eneral',
shortcut = 'g',
items = {
- { node = 'item', desc = '[B]uild', shortcut = 'b', command = 'Piocmdf run' },
- { node = 'item', desc = '[U]pload', shortcut = 'u', command = 'Piocmdf run -t upload' },
- { node = 'item', desc = '[M]onitor', shortcut = 'm', command = 'Piocmdh run -t monitor' },
- { node = 'item', desc = '[C]lean', shortcut = 'c', command = 'Piocmdf run -t clean' },
- { node = 'item', desc = '[F]ull clean', shortcut = 'f', command = 'Piocmdf run -t fullclean' },
+ { node = 'item', desc = '[B]uild', shortcut = 'b', command = 'Piocmdf run' },
+ { node = 'item', desc = '[U]pload', shortcut = 'u', command = 'Piocmdf run -t upload' },
+ { node = 'item', desc = '[M]onitor', shortcut = 'm', command = 'Piocmdh run -t monitor' },
+ { node = 'item', desc = '[C]lean', shortcut = 'c', command = 'Piocmdf run -t clean' },
+ { node = 'item', desc = '[F]ull clean', shortcut = 'f', command = 'Piocmdf run -t fullclean' },
{ node = 'item', desc = '[D]evice list', shortcut = 'd', command = 'Piocmdf device list' },
},
},
@@ -130,10 +147,10 @@ These are the default keybindings, which you can override in your configuration.
desc = '[P]latform',
shortcut = 'p',
items = {
- { node = 'item', desc = '[B]uild file system', shortcut = 'b', command = 'Piocmdf run -t buildfs' },
- { node = 'item', desc = 'Program [S]ize', shortcut = 's', command = 'Piocmdf run -t size' },
+ { node = 'item', desc = '[B]uild file system', shortcut = 'b', command = 'Piocmdf run -t buildfs' },
+ { node = 'item', desc = 'Program [S]ize', shortcut = 's', command = 'Piocmdf run -t size' },
{ node = 'item', desc = '[U]pload file system', shortcut = 'u', command = 'Piocmdf run -t uploadfs' },
- { node = 'item', desc = '[E]rase Flash', shortcut = 'e', command = 'Piocmdf run -t erase' },
+ { node = 'item', desc = '[E]rase Flash', shortcut = 'e', command = 'Piocmdf run -t erase' },
},
},
{
@@ -141,9 +158,9 @@ These are the default keybindings, which you can override in your configuration.
desc = '[D]ependencies',
shortcut = 'd',
items = {
- { node = 'item', desc = '[L]ist packages', shortcut = 'l', command = 'Piocmdf pkg list' },
+ { node = 'item', desc = '[L]ist packages', shortcut = 'l', command = 'Piocmdf pkg list' },
{ node = 'item', desc = '[O]utdated packages', shortcut = 'o', command = 'Piocmdf pkg outdated' },
- { node = 'item', desc = '[U]pdate packages', shortcut = 'u', command = 'Piocmdf pkg update' },
+ { node = 'item', desc = '[U]pdate packages', shortcut = 'u', command = 'Piocmdf pkg update' },
},
},
{
@@ -151,31 +168,31 @@ These are the default keybindings, which you can override in your configuration.
desc = '[A]dvanced',
shortcut = 'a',
items = {
- { node = 'item', desc = '[T]est', shortcut = 't', command = 'Piocmdf test' },
- { node = 'item', desc = '[C]heck', shortcut = 'c', command = 'Piocmdf check' },
- { node = 'item', desc = '[D]ebug', shortcut = 'd', command = 'Piocmdf debug' },
+ { node = 'item', desc = '[T]est', shortcut = 't', command = 'Piocmdf test' },
+ { node = 'item', desc = '[C]heck', shortcut = 'c', command = 'Piocmdf check' },
+ { node = 'item', desc = '[D]ebug', shortcut = 'd', command = 'Piocmdf debug' },
{ node = 'item', desc = 'Compilation Data[b]ase', shortcut = 'b', command = 'Piocmdf run -t compiledb' },
{
- node = 'menu',
- desc = '[V]erbose',
- shortcut = 'v',
- items = {
- { node = 'item', desc = 'Verbose [B]uild', shortcut = 'b', command = 'Piocmdf run -v' },
- { node = 'item', desc = 'Verbose [U]pload', shortcut = 'u', command = 'Piocmdf run -v -t upload' },
- { node = 'item', desc = 'Verbose [T]est', shortcut = 't', command = 'Piocmdf test -v' },
- { node = 'item', desc = 'Verbose [C]heck', shortcut = 'c', command = 'Piocmdf check -v' },
- { node = 'item', desc = 'Verbose [D]ebug', shortcut = 'd', command = 'Piocmdf debug -v' },
- },
+ node = 'menu',
+ desc = '[V]erbose',
+ shortcut = 'v',
+ items = {
+ { node = 'item', desc = 'Verbose [B]uild', shortcut = 'b', command = 'Piocmdf run -v' },
+ { node = 'item', desc = 'Verbose [U]pload', shortcut = 'u', command = 'Piocmdf run -v -t upload' },
+ { node = 'item', desc = 'Verbose [T]est', shortcut = 't', command = 'Piocmdf test -v' },
+ { node = 'item', desc = 'Verbose [C]heck', shortcut = 'c', command = 'Piocmdf check -v' },
+ { node = 'item', desc = 'Verbose [D]ebug', shortcut = 'd', command = 'Piocmdf debug -v' },
},
},
+ },
},
{
node = 'menu',
desc = '[R]emote',
shortcut = 'r',
items = {
- { node = 'item', desc = 'Remote [U]pload', shortcut = 'u', command = 'Piocmdf remote run -t upload' },
- { node = 'item', desc = 'Remote [T]est', shortcut = 't', command = 'Piocmdf remote test' },
+ { node = 'item', desc = 'Remote [U]pload', shortcut = 'u', command = 'Piocmdf remote run -t upload' },
+ { node = 'item', desc = 'Remote [T]est', shortcut = 't', command = 'Piocmdf remote test' },
{ node = 'item', desc = 'Remote [M]onitor', shortcut = 'm', command = 'Piocmdh remote run -t monitor' },
{ node = 'item', desc = 'Remote [D]evices', shortcut = 'd', command = 'Piocmdf remote device list' },
},
@@ -189,6 +206,7 @@ These are the default keybindings, which you can override in your configuration.
},
},
},
+
})
end
```
@@ -201,6 +219,6 @@ It's possible to lazy load the plugin using Lazy.nvim, this will load the plugin
cmd = { 'Pioinit', 'Piorun', 'Piocmdh', 'Piocmdf', 'Piolib', 'Piomon', 'Piodebug', 'Piodb' },
```
-
### TODO
+
- Connect Piodebug with DAP
diff --git a/lua/nvimpio/boilerplate.lua b/lua/nvimpio/boilerplate.lua
new file mode 100644
index 00000000..bf8b01ed
--- /dev/null
+++ b/lua/nvimpio/boilerplate.lua
@@ -0,0 +1,494 @@
+M = {}
+
+M.core_dir = ''
+
+local boilerplate = {}
+
+-- INFO: main.cpp
+--- stylua: ignore
+boilerplate['arduino'] = {
+ rewrite = false,
+ read = false,
+ content = [[
+#include
+
+void setup() {
+
+}
+
+void loop() {
+
+}
+]],
+}
+
+-- INFO: platformio.ini
+boilerplate['platformio.ini'] = {
+ rewrite = false,
+ read = false,
+ template = [[
+[platformio]
+core_dir = %s
+platforms_dir = ${platformio.core_dir}/platforms
+packages_dir = ${platformio.core_dir}/packages
+;libdeps_dir = ./external_libs
+
+default_envs =
+;default_envs = uno, nodemcu
+
+;--------------------------------------------------------------------------
+[env]
+framework = arduino
+upload_speed = 115200
+monitor_speed = 9600
+
+monitor_rts = 1 ; 1 combination to reset esp32c6 (Table 32.3-2. CDC-ACM Settings with RTS and DTR)
+monitor_dtr = 0 ; 0 // pio dev mon --rts=0 --dtr=0 then pio dev mon --rts=1 dtr=0
+
+extra_scripts =
+;post:generate_compileDB.py
+; pre:enable_toolchain.py ; enabled global env 'PLATFORMIO_SETTING_COMPILATIONDB_INCLUDE_TOOLCHAIN'
+
+lib_ldf_mode = chain ;Library dependencies Finder ldf
+
+;[env:seeed_xiao_esp32c3]
+;platform = espressif32
+;board = seeed_xiao_esp32c3
+
+]],
+ content = function(self)
+ return string.format(self.template, M.core_dir)
+ end,
+}
+
+-- "--config-file=%s"
+-- =============================================================================
+-- DYNAMIC CLANGD CONFIGURATION TEMPLATE
+-- =============================================================================
+-- Note: %q is used for paths to handle escaping and spaces automatically.
+-- INFO: .clangd_config
+boilerplate['.clangd_config'] = {
+ rewrite = false,
+ read = true,
+ content = [[
+{
+ cmd = {
+ "clangd",
+ "--all-scopes-completion",
+ "--background-index",
+ "--clang-tidy",
+ "--compile_args_from=filesystem",
+ "--enable-config",
+ "--completion-parse=always",
+ "--completion-style=detailed",
+ "--header-insertion=iwyu",
+ "--fallback-style=llvm",
+ "--log=verbose",
+ "--pch-storage=memory",
+ "--pretty",
+ "--ranking-model=decision_forest",
+ "--sync",
+ "--offset-encoding=utf-16",
+ "--query-driver=%s"
+ },
+ filetypes = { 'c', 'cpp', 'objc', 'objcpp', 'cuda', 'proto' },
+ root_markers = {
+ 'platformio.ini',
+ 'CMakeLists.txt',
+ '.clangd',
+ '.clang-tidy',
+ '.clang-format',
+ 'compile_commands.json',
+ 'compile_flags.txt',
+ 'configure.ac',
+ '.git',
+ },
+ workspace_required = true,
+ single_file_support = true,
+ init_options = {
+ usePlaceholders = true,
+ completeUnimported = true,
+ fallbackFlags = {%s},
+ clangdFileStatus = true,
+ compilationDatabasePath = %q,
+ }
+}
+]],
+}
+-- CompileFlags:
+-- Add:
+-- - "-xc++"
+-- - "-std=c++17"
+-- - "-D__cplusplus=201703L"
+-- - "-isystemC:/Users/batoaqaa/.platformio/packages/toolchain-riscv32-esp/riscv32-esp-elf/include/c++/14.2.0"
+-- - "-isystemC:/Users/batoaqaa/.platformio/packages/toolchain-riscv32-esp/riscv32-esp-elf/include/c++/14.2.0/riscv32-esp-elf"
+-- - "-isystemC:/Users/batoaqaa/.platformio/packages/toolchain-riscv32-esp/lib/gcc/riscv32-esp-elf/14.2.0/include"
+-- - "-isystemC:/Users/batoaqaa/.platformio/packages/toolchain-riscv32-esp/lib/gcc/riscv32-esp-elf/14.2.0/include-fixed"
+-- - "-isystemC:/Users/batoaqaa/.platformio/packages/toolchain-riscv32-esp/riscv32-esp-elf/include"
+-- Remove:
+-- - "-target=*"
+-- - "-target"
+-- - "riscv32-esp-elf"
+-- - "-fno-fat-lto-objects"
+-- - "-fno%%-fat%%-lto%%-objects"
+-- - "-fno%%-canonical%%-system%%-headers"
+-- - "-misc-definitions-in-headers"
+-- - "-fno-tree-switch-conversion"
+-- - "-mtext-section-literals"
+-- - "-mlong-calls"
+-- - "-mlongcalls"
+-- - "-fstrict-volatile-bitfields"
+-- - "-free*"
+-- - "-fipa-pta*"
+-- - "-march=*"
+-- - "-mabi=*"
+-- - "-mcpu=*"
+-- Diagnostics:
+-- Suppress:
+-- - "misc-definitions-in-headers"
+-- - "pp_including_mainfile_in_preamble"
+-- - "misc-unused-using-decls"
+-- - "unused-includes"
+-- ClangTidy:
+-- Remove:
+-- - "readability-*"
+-- - "cert-err58-cpp"
+-- - "llvmlibc-*"
+-- - "fuchsia-*"
+-- - "hicpp-avoid-c-arrays"
+-- - "cppcoreguidelines-*"
+-- - "llvm-*"
+-- - "google-*"
+-- - "bugprone-*"
+-- - "hicpp-vararg"
+-- - "modernize-*"
+-- Index:
+-- Background: Build
+-- External:
+-- File: .clangd_index
+
+-- INFO: .clangd
+-- boilerplate['.clangd']
+boilerplate['.clangd'] = {
+ rewrite = false,
+ read = false,
+ -- template = [[
+ content = [[
+---
+CompileFlags:
+ Add:
+ - "-xc++"
+ - "-std=gnu++17"
+ - "-Wno-pragma-system-header-outside-header"
+ - "-Wno-unknown-warning-option"
+ - "-Wno-unused-includes"
+ Remove:
+ - "-Wunknown-warning-option"
+ - "-fno-tree-switch-conversion"
+ - "-fno-fat-lto-objects"
+ - "-fno-canonical-system-headers"
+ - "-mtext-section-literals"
+ - "-mlong-calls"
+ - "-fstrict-volatile-bitfields"
+ - "-march=.*"
+ - "-mabi=.*"
+ - "-mcpu=.*"
+ - "-fipa-pta.*"
+Diagnostics:
+ Suppress:
+ - "pp_file_not_found"
+ - "pp_file_not_found_angled_not_fatal"
+ - "pp_included_file_not_found"
+ - "pp_including_mainfile_in_preamble"
+ - "unused-includes"
+ - "misc-definitions-in-headers"
+ ClangTidy:
+ Remove: ["readability-*", "modernize-*", "bugprone-*", "cert-err58-cpp"]
+]],
+ -- content = function(self)
+ -- local sysroot = '--sysroot=' .. _G.metadata.sysroot
+ -- local triplet = '--target=' .. _G.metadata.triplet
+ -- return string.format(self.template, triplet, sysroot)
+ -- end,
+}
+
+-- INFO: .clang-format
+boilerplate['.clang-format'] = {
+ rewrite = false,
+ read = false,
+ content = [[
+---
+Language: Cpp
+# BasedOnStyle: LLVM
+AccessModifierOffset: -2
+AlignAfterOpenBracket: Align
+AlignArrayOfStructures: None
+AlignConsecutiveAssignments:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionPointers: false
+ PadOperators: true
+AlignConsecutiveBitFields:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignConsecutiveDeclarations:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignConsecutiveMacros:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignConsecutiveShortCaseStatements:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCaseColons: false
+AlignEscapedNewlines: Right
+AlignOperands: Align
+AlignTrailingComments:
+ Kind: Always
+ OverEmptyLines: 0
+AllowAllArgumentsOnNextLine: true
+AllowAllParametersOfDeclarationOnNextLine: true
+AllowBreakBeforeNoexceptSpecifier: Never
+AllowShortBlocksOnASingleLine: Never
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortCompoundRequirementOnASingleLine: true
+AllowShortEnumsOnASingleLine: true
+AllowShortFunctionsOnASingleLine: All
+AllowShortIfStatementsOnASingleLine: Never
+AllowShortLambdasOnASingleLine: All
+AllowShortLoopsOnASingleLine: false
+AlwaysBreakAfterDefinitionReturnType: None
+AlwaysBreakAfterReturnType: None
+AlwaysBreakBeforeMultilineStrings: false
+AlwaysBreakTemplateDeclarations: MultiLine
+AttributeMacros:
+ - __capability
+BinPackArguments: true
+BinPackParameters: true
+BitFieldColonSpacing: Both
+BraceWrapping:
+ AfterCaseLabel: false
+ AfterClass: false
+ AfterControlStatement: Never
+ AfterEnum: false
+ AfterExternBlock: false
+ AfterFunction: false
+ AfterNamespace: false
+ AfterObjCDeclaration: false
+ AfterStruct: false
+ AfterUnion: false
+ BeforeCatch: false
+ BeforeElse: false
+ BeforeLambdaBody: false
+ BeforeWhile: false
+ IndentBraces: false
+ SplitEmptyFunction: true
+ SplitEmptyRecord: true
+ SplitEmptyNamespace: true
+BreakAdjacentStringLiterals: true
+BreakAfterAttributes: Leave
+BreakAfterJavaFieldAnnotations: false
+BreakArrays: true
+BreakBeforeBinaryOperators: None
+BreakBeforeConceptDeclarations: Always
+BreakBeforeBraces: Attach
+BreakBeforeInlineASMColon: OnlyMultiline
+BreakBeforeTernaryOperators: true
+BreakConstructorInitializers: BeforeColon
+BreakInheritanceList: BeforeColon
+BreakStringLiterals: true
+ColumnLimit: 80
+CommentPragmas: '^ IWYU pragma:'
+CompactNamespaces: false
+ConstructorInitializerIndentWidth: 4
+ContinuationIndentWidth: 4
+Cpp11BracedListStyle: true
+DerivePointerAlignment: false
+DisableFormat: false
+EmptyLineAfterAccessModifier: Never
+EmptyLineBeforeAccessModifier: LogicalBlock
+ExperimentalAutoDetectBinPacking: false
+FixNamespaceComments: true
+ForEachMacros:
+ - foreach
+ - Q_FOREACH
+ - BOOST_FOREACH
+IfMacros:
+ - KJ_IF_MAYBE
+IncludeBlocks: Preserve
+IncludeCategories:
+ - Regex: '^"(llvm|llvm-c|clang|clang-c)/'
+ Priority: 2
+ SortPriority: 0
+ CaseSensitive: false
+ - Regex: '^(<|"(gtest|gmock|isl|json)/)'
+ Priority: 3
+ SortPriority: 0
+ CaseSensitive: false
+ - Regex: '.*'
+ Priority: 1
+ SortPriority: 0
+ CaseSensitive: false
+IncludeIsMainRegex: '(Test)?$'
+IncludeIsMainSourceRegex: ''
+IndentAccessModifiers: false
+IndentCaseBlocks: false
+IndentCaseLabels: false
+IndentExternBlock: AfterExternBlock
+IndentGotoLabels: true
+IndentPPDirectives: None
+IndentRequiresClause: true
+IndentWidth: 2
+IndentWrappedFunctionNames: false
+InsertBraces: false
+InsertNewlineAtEOF: false
+InsertTrailingCommas: None
+IntegerLiteralSeparator:
+ Binary: 0
+ BinaryMinDigits: 0
+ Decimal: 0
+ DecimalMinDigits: 0
+ Hex: 0
+ HexMinDigits: 0
+JavaScriptQuotes: Leave
+JavaScriptWrapImports: true
+KeepEmptyLinesAtTheStartOfBlocks: true
+KeepEmptyLinesAtEOF: false
+LambdaBodyIndentation: Signature
+LineEnding: DeriveLF
+MacroBlockBegin: ''
+MacroBlockEnd: ''
+MaxEmptyLinesToKeep: 1
+NamespaceIndentation: None
+ObjCBinPackProtocolList: Auto
+ObjCBlockIndentWidth: 2
+ObjCBreakBeforeNestedBlockParam: true
+ObjCSpaceAfterProperty: false
+ObjCSpaceBeforeProtocolList: true
+PackConstructorInitializers: BinPack
+PenaltyBreakAssignment: 2
+PenaltyBreakBeforeFirstCallParameter: 19
+PenaltyBreakComment: 300
+PenaltyBreakFirstLessLess: 120
+PenaltyBreakOpenParenthesis: 0
+PenaltyBreakScopeResolution: 500
+PenaltyBreakString: 1000
+PenaltyBreakTemplateDeclaration: 10
+PenaltyExcessCharacter: 1000000
+PenaltyIndentedWhitespace: 0
+PenaltyReturnTypeOnItsOwnLine: 60
+PointerAlignment: Right
+PPIndentWidth: -1
+QualifierAlignment: Leave
+ReferenceAlignment: Pointer
+ReflowComments: true
+RemoveBracesLLVM: false
+RemoveParentheses: Leave
+RemoveSemicolon: false
+RequiresClausePosition: OwnLine
+RequiresExpressionIndentation: OuterScope
+SeparateDefinitionBlocks: Leave
+ShortNamespaceLines: 1
+SkipMacroDefinitionBody: false
+SortIncludes: CaseSensitive
+SortJavaStaticImport: Before
+SortUsingDeclarations: LexicographicNumeric
+SpaceAfterCStyleCast: false
+SpaceAfterLogicalNot: false
+SpaceAfterTemplateKeyword: true
+SpaceAroundPointerQualifiers: Default
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeCaseColon: false
+SpaceBeforeCpp11BracedList: false
+SpaceBeforeCtorInitializerColon: true
+SpaceBeforeInheritanceColon: true
+SpaceBeforeJsonColon: false
+SpaceBeforeParens: ControlStatements
+SpaceBeforeParensOptions:
+ AfterControlStatements: true
+ AfterForeachMacros: true
+ AfterFunctionDefinitionName: false
+ AfterFunctionDeclarationName: false
+ AfterIfMacros: true
+ AfterOverloadedOperator: false
+ AfterPlacementOperator: true
+ AfterRequiresInClause: false
+ AfterRequiresInExpression: false
+ BeforeNonEmptyParentheses: false
+SpaceBeforeRangeBasedForLoopColon: true
+SpaceBeforeSquareBrackets: false
+SpaceInEmptyBlock: false
+SpacesBeforeTrailingComments: 1
+SpacesInAngles: Never
+SpacesInContainerLiterals: true
+SpacesInLineCommentPrefix:
+ Minimum: 1
+ Maximum: -1
+SpacesInParens: Never
+SpacesInParensOptions:
+ InCStyleCasts: false
+ InConditionalStatements: false
+ InEmptyParentheses: false
+ Other: false
+SpacesInSquareBrackets: false
+Standard: Latest
+StatementAttributeLikeMacros:
+ - Q_EMIT
+StatementMacros:
+ - Q_UNUSED
+ - QT_REQUIRE_VERSION
+TabWidth: 8
+UseTab: Never
+VerilogBreakBetweenInstancePorts: true
+WhitespaceSensitiveMacros:
+ - BOOST_PP_STRINGIZE
+ - CF_SWIFT_NAME
+ - NS_SWIFT_NAME
+ - PP_STRINGIZE
+ - STRINGIZE
+...
+]],
+}
+-- stylua: ignore
+function M.boilerplate_gen(framework, src_path, filename)
+ filename = filename or framework
+ local entry = boilerplate[framework]
+ if not entry then return '' end
+ local file_path = vim.fs.normalize(src_path .. '/' .. filename)
+
+ if vim.uv.fs_stat(file_path) then
+ if not entry.rewrite then
+ if entry.read then
+ local ok, content = vim.misc.readFile(file_path)
+ if ok then return content end
+ -- local fr = io.open(file_path, 'r')
+ -- if fr then return (fr:read('*a')) end
+ end
+ return ''
+ end
+ end
+ --
+ local template = type(entry.content) == 'function' and entry:content() or entry.content
+ vim.misc.writeFile(file_path, template, {})
+
+ if entry.read then
+ return template
+ else return '' end
+end
+
+return M
diff --git a/lua/platformio/init.lua b/lua/nvimpio/init.lua
similarity index 67%
rename from lua/platformio/init.lua
rename to lua/nvimpio/init.lua
index e1307d5a..e06c0eb1 100644
--- a/lua/platformio/init.lua
+++ b/lua/nvimpio/init.lua
@@ -1,10 +1,15 @@
local M = {}
M.config = {
- lsp = 'ccls',
- menu_key = nil,
- menu_name = 'PlatformIO',
+ lspClangd = {
+ enabled = false,
+ attach = {
+ enabled = false,
+ keymaps = false,
+ },
+ },
+ menu_key = '\\', -- replace this menu key to your convenience
+ menu_name = 'PlatformIO', -- replace this menu name to your convenience
debug = false,
- clangd_source = 'ccls',
menu_bindings = {
{ node = 'item', desc = '[L]ist terminals', shortcut = 'l', command = 'PioTermList' },
@@ -85,8 +90,6 @@ M.config = {
{ node = 'item', desc = '[U]pgrade PlatformIO Core', shortcut = 'u', command = 'Piocmdf upgrade' },
},
},
- -- }, --
- -- }, --
},
}
@@ -151,56 +154,82 @@ local function validateMenu(menu)
return true
end
-function M.setup(user_config)
- if next(user_config) ~= nil then
- local valid_keys = {
- lsp = true,
- menu_key = true,
- menu_name = true,
- menu_bindings = true,
- debug = true,
- clangd_source = true,
- }
- local err = false
- for key, value in pairs(user_config or {}) do
- if not valid_keys[key] then
- local error_message = string.format('Invalid PlatformIO settings key-value: %s = "%s"', key, value)
- vim.api.nvim_echo({ { error_message, 'ErrorMsg' } }, true, {})
- err = true
+function M.piomenu(config)
+ local icon = { icon = ' ', color = 'orange' } -- Assign platformio orange icon
+ local wk_table = { mode = { 'n', 'v' } }
+
+ local function traverseMenu(menu, wkey)
+ for _, child_node in ipairs(menu) do
+ if child_node.node == 'menu' then
+ traverseMenu(child_node.items, wkey .. child_node.shortcut)
+ table.insert(wk_table, { wkey .. child_node.shortcut, group = child_node.desc, icon = icon })
+ elseif child_node.node == 'item' then
+ table.insert(wk_table, {
+ wkey .. child_node.shortcut,
+ ' ' .. child_node.command .. '',
+ desc = child_node.desc,
+ icon = icon,
+ })
end
end
- if user_config.lsp and not (user_config.lsp == 'ccls' or user_config.lsp == 'clangd') then
- vim.api.nvim_echo(
- { { 'Invalid PlatformIO lsp "' .. user_config.lsp .. '", {allowed "clangd" or "ccls"} (default "' .. M.config.lsp .. '" will be used)', 'ErrorMsg' } },
- true,
- {}
- )
- user_config.lsp = M.config.lsp
- end
- if user_config.lsp == 'clangd' then
- if user_config.clangd_source ~= 'ccls' and user_config.clangd_source ~= 'compiledb' then
- vim.api.nvim_echo(
- { { 'Invalid clangd source {allowed "ccls" or "compiledb"} (default "' .. M.config.clangd_source .. '" will be used)', 'ErrorMsg' } },
- true,
- {}
- )
- user_config.clangd_source = M.config.clangd_source
+ end
+ if config.menu_key == nil then
+ return
+ end
+
+ local ok, wk = pcall(require, 'which-key')
+ if not ok then
+ vim.api.nvim_echo({ { 'which-key plugin not found!', 'ErrorMsg' } }, true, {})
+ return
+ end
+
+ wk.setup({
+ preset = 'helix', --'modern', --'classic'
+ })
+ local Config = require('which-key.config')
+ Config.sort = { 'order', 'group', 'manual', 'mod' }
+
+ table.insert(wk_table, { config.menu_key, group = config.menu_name, icon = icon })
+
+ traverseMenu(config.menu_bindings, config.menu_key)
+
+ wk.add(wk_table)
+end
+
+function M.setup(user_config)
+ if vim.g.platformioRootDir and (next(user_config) ~= nil) then
+ if user_config.lspClangd then
+ vim.validate('lspClangd', user_config.lspClangd, 'table', true)
+ vim.validate('lspClangdEnabled', user_config.lspClangd.enabled, 'boolean', true)
+ if user_config.lspClangd.attach then
+ vim.validate('lspAttach', user_config.lspClangd.attach, 'table', true)
+ vim.validate('lspAttachEnabled', user_config.lspClangd.attach.enabled, 'boolean', true)
+ vim.validate('lspKeymaps', user_config.lspClangd.attach.keyMaps, 'boolean', true)
end
end
- if not err then -- if no error, merge user_config to M.config
- if user_config.menu_bindings then
- if not validateMenu(user_config.menu_bindings) then
- user_config.menu_bindings = nil -- if validation error, cancel merging menu_bindings with M.config
- -- else
- -- print('good validation')
- end
+ vim.validate('menu_key', user_config.lspClangd_enable, 'string', true)
+ vim.validate('menu_name', user_config.menu_name, 'string', true)
+ vim.validate('debug', user_config.debug, 'boolean', true)
+ vim.validate('menu_bindings', user_config.menu_bindings, 'table', true)
+
+ if user_config.menu_bindings then
+ if not validateMenu(user_config.menu_bindings) then
+ user_config.menu_bindings = nil -- if validation error, cancel merging menu_bindings with M.config
+ -- else
+ -- print('good validation')
end
- M.config = vim.tbl_deep_extend('force', M.config, user_config or {})
end
+ M.config = vim.tbl_deep_extend('force', M.config, user_config or {})
end
- require('platformio.piomenu').piomenu(M.config)
+ M.piomenu(M.config)
+
+ vim.schedule(function()
+ require('nvimpio.pio.watcher').init()
+ end)
end
+vim.notify('nvim-platformio.lua started', vim.log.levels.INFO)
+
return M
diff --git a/lua/nvimpio/lspConfig/attach.lua b/lua/nvimpio/lspConfig/attach.lua
new file mode 100644
index 00000000..4cc87a84
--- /dev/null
+++ b/lua/nvimpio/lspConfig/attach.lua
@@ -0,0 +1,126 @@
+-- local piolsp = require('nvimpio.piolsp') --.piolsp
+-- INFO: LspAttach autocommand start
+vim.api.nvim_create_autocmd('LspAttach', {
+ group = vim.api.nvim_create_augroup('platformio-lsp-attach', { clear = true }),
+ callback = function(args)
+ local client = assert(vim.lsp.get_client_by_id(args.data.client_id))
+ local bufnr = args.buf
+
+ if client then
+ vim.api.nvim_echo({ { 'Attaching ' .. client.name .. ' to buffer ' .. bufnr, 'Info' } }, true, {})
+
+ ------------------------------------------------------------------
+ if client.name == 'clangd' then
+ local uri = vim.uri_from_bufnr(bufnr)
+ if not uri:match('^file://') then
+ return -- Stop here for non-file buffers (like git:// or nvim://)
+ end
+ vim.api.nvim_buf_create_user_command(bufnr, 'LspClangdSwitchSourceHeader', function()
+ local params = vim.lsp.util.make_text_document_params(bufnr)
+ client:request('textDocument/switchSourceHeader', params, function(err, result)
+ if err then
+ vim.notify('LSP Attach: Clangd Error ' .. tostring(err), vim.log.levels.ERROR)
+ return
+ end
+ if not result or result == '' then
+ vim.notify('LSP Attach: Corresponding file cannot be determined', vim.log.levels.WARN)
+ return
+ end
+ -- Use vim.schedule to ensure we aren't editing while the LSP is in a callback
+ vim.schedule(function()
+ local target = type(result) == 'string' and result or result.uri
+ local fname = vim.uri_to_fname(target)
+ vim.cmd.edit(vim.uri_to_fname(fname))
+ end)
+ end, bufnr)
+ end, { desc = 'Switch between source/header' })
+ end
+
+ -- use lsp completion if no blink
+ local ok, _ = pcall(require, 'blink.cmp')
+ if not ok then
+ if client:supports_method('textDocument/completion') then
+ vim.opt.completeopt = { 'menu', 'menuone', 'noselect', 'noinsert', 'fuzzy', 'popup' }
+
+ -- Enable native completion for this specific client and buffer
+ vim.lsp.completion.enable(true, client.id, args.buf, { autotrigger = true })
+ vim.keymap.set('i', ' End LspAttach autocommand
diff --git a/lua/nvimpio/lspConfig/clangd.lua b/lua/nvimpio/lspConfig/clangd.lua
new file mode 100644
index 00000000..9c167fad
--- /dev/null
+++ b/lua/nvimpio/lspConfig/clangd.lua
@@ -0,0 +1,277 @@
+local boilerplate_gen = require('nvimpio.boilerplate').boilerplate_gen
+local ok, result
+ok, result = pcall(require, 'fidget')
+if ok then
+ result.setup({})
+end
+
+-----------------------------------------------------------------------------------------
+ok, result = pcall(require, 'trouble')
+if ok then
+ result.setup({})
+end
+
+----------------------------------------------------------------------------------------
+-- INFO: setup and install mason packages
+-----------------------------------------------------------------------------------------
+ok, result = pcall(require, 'mason')
+if ok then
+ result.setup({
+ PATH = 'append',
+ ui = {
+ border = 'single',
+ icons = {
+ package_installed = '✓',
+ package_pending = '➜',
+ package_uninstalled = '✗',
+ },
+ },
+ })
+end
+
+-- List of packages you want Mason to ensure are installed
+local ensure_installed = {
+ -- 'clang-format', embeded in clangd
+ -- 'stylua',
+}
+-- call mason-registry function to install or ensure formatters/linters are installed
+local mr = require('mason-registry')
+mr.refresh(function()
+ for _, tool in ipairs(ensure_installed) do
+ ok, result = pcall(mr.get_package, tool)
+ if ok and result then
+ if not result:is_installed() then
+ if not result:is_installing() then
+ result:install({}, function(success, _)
+ if not success then
+ vim.defer_fn(function()
+ vim.notify('LSP: clangd; ' .. tool .. ' failed to install', vim.log.levels.ERROR)
+ end, 0)
+ end
+ end)
+ else
+ vim.defer_fn(function()
+ vim.notify('LSP: clangd; ' .. tool .. ' already installed', vim.log.levels.WARN)
+ end, 0)
+ end
+ end
+ else
+ vim.defer_fn(function()
+ vim.notify('LSP: clangd; Failed to get package: ' .. tool, vim.log.levels.WARN)
+ end, 0)
+ end
+ end
+end)
+
+----------------------------------------------------------------------------------------
+-- INFO: install clangd using mason-lspconfig
+-----------------------------------------------------------------------------------------
+local mok, mason_lspconfig = pcall(require, 'mason-lspconfig')
+if mok then
+ mason_lspconfig.setup({
+ ensure_installed = { 'clangd', 'lua_ls', 'pyrefly', 'yamlls', 'jsonls' },
+ automatic_enable = true, -- this will automatically enable LSP servers after lsp.config
+ })
+end
+
+local capabilities = vim.lsp.protocol.make_client_capabilities()
+
+capabilities.textDocument.foldingRange = {
+ textDocument = {
+ -- Folding capabilities for nvim-ufo
+ foldingRange = {
+ dynamicRegistration = false,
+ lineFoldingOnly = true,
+ },
+ },
+}
+local bok, blink = pcall(require, 'blink.cmp')
+if bok then
+ capabilities = blink.get_lsp_capabilities(capabilities)
+end
+
+-- INFO: 1
+vim.lsp.config('*', {
+ capabilities = capabilities,
+ root_markers = { '.git' },
+ workspace_required = false,
+})
+
+----------------------------------------------------------------------------------------
+-- INFO: configure clangd lsp server
+-----------------------------------------------------------------------------------------
+--stylua: ignore
+function _G.get_clangd_config()
+ local new_root_dir = vim.uv.cwd() or '.'
+ if not new_root_dir then return end
+
+ -- 1. Safe defaults (Standard clangd behavior)
+ local f_flags, q_driver = [["-std=c++17", "-xc++"]], '--query-driver=**'
+
+ -- 2. Run your toolchain detection
+ if _G.metadata and _G.metadata.cc_compiler and _G.metadata.cc_compiler ~= '' then
+ if _G.metadata.triplet and _G.metadata.triplet ~= '' then
+ -- local include_flags = table.concat(vim.tbl_map(function(item)
+ -- return '"' .. item .. '"'
+ -- end, _G.metadata.fallbackFlags), ", ")
+ --
+ -- local includes_toolchain = table.concat(vim.tbl_map(function(item)
+ -- return '"' .. item .. '"'
+ -- end, _G.metadata.includes_toolchain), ", ")
+
+ f_flags = ''
+ -- f_flags = string.format([["-std=gnu++17", "-xc++", "-D__cplusplus=201703L", "--target=%s", "--sysroot=%s", %s, %s]], _G.metadata.triplet, _G.metadata.sysroot, includes_toolchain, include_flags)
+ -- f_flags = string.format('"--sysroot=%s"', _G.metadata.sysroot)
+ -- f_flags = string.format([["--sysroot=%s", %s]], _G.metadata.sysroot, include_flags)
+
+ -- q_driver = '**' --_G.metadata.query_driver .. ',C:/PROGRA~1/LLVM/bin/*' -- use with "--query-driver=%s"
+ q_driver = _G.metadata.query_driver --.. ',C:/PROGRA~1/LLVM/bin/*' -- use with "--query-driver=%s"
+ end
+ end
+
+ -- 3. Format your template string
+ local table_config = boilerplate_gen([[.clangd_config]], vim.g.platformioRootDir)
+ local formatted_str = string.format(table_config or '', q_driver, f_flags, vim.misc.normalizePath(new_root_dir))
+ -- local formatted_str = string.format(table_config or '', q_driver, '', vim.misc.normalizePath(new_root_dir))
+ -- local formatted_str = string.format(table_config or '', q_driver, '', vim.g.platformioRootDir)
+
+ -- 4. Load the config table
+ local cok, clangd_config = pcall(function() return load('return ' .. formatted_str)() end)
+
+ local formated = vim.misc.jsonFormat(clangd_config)
+ local file = vim.misc.joinPath(vim.uv.cwd(), 'clangd_config.json')
+ vim.misc.writeFile(file, formated, {})
+
+ if cok and clangd_config then
+ -- print(vim.inspect(clangd_config))
+ return clangd_config
+ end
+end
+
+-- Apply and Enable
+vim.lsp.config('clangd', _G.get_clangd_config())
+vim.lsp.enable('clangd')
+
+----------------------------------------------------------------------------------------
+-- INFO: configure jsonls lsp server
+-----------------------------------------------------------------------------------------
+local jsonls = {
+ -- lazy-load schemastore when needed
+ cmd = { 'vscode-json-language-server', '--stdio' },
+ filetypes = { 'json', 'jsonc' },
+ init_options = { provideFormatter = true },
+ root_makers = { '.git' },
+}
+-- Apply and Enable
+vim.lsp.config('jsonls', jsonls)
+
+----------------------------------------------------------------------------------------
+-- INFO: configure clangd lsp server
+-----------------------------------------------------------------------------------------
+local lua_ls = {
+ cmd = { 'lua-language-server' },
+ filetypes = { 'lua' },
+ root_markers = {
+ '.luarc.json',
+ '.luarc.jsonc',
+ '.luacheckrc',
+ '.stylua.toml',
+ 'selene.toml',
+ 'selene.yml',
+ '.git',
+ },
+ settings = {
+ Lua = {
+ hint = {
+ enable = true,
+ arrayIndex = 'Enable',
+ await = true,
+ paramName = 'All',
+ paramType = true,
+ semicolon = 'Disable',
+ setType = true,
+ },
+ telemetry = { enable = false },
+ diagnostics = { globals = { 'vim' } },
+ runtime = {
+ -- Specify LuaJIT for Neovim
+ version = 'LuaJIT',
+ -- Include Neovim runtime files
+ path = vim.split(package.path, ';'),
+ },
+ workspace = {
+ checkThirdParty = false,
+ library = {
+ vim.env.VIMRUNTIME,
+ '${3rd}/luv/library',
+ './lua',
+ vim.api.nvim_get_runtime_file('', true),
+ -- Depending on the usage, you might want to add additional paths here.
+ -- "${3rd}/busted/library",
+ },
+ },
+ },
+ },
+}
+vim.lsp.config('lua_ls', lua_ls)
+
+local yamlls = {
+ -- on_attach = opts.on_attach,
+ cmd = { 'yaml-language-server', '--stdio' },
+ filetypes = { 'yaml', 'yaml.docker-compose', 'yaml.gitlab' },
+ settings = {
+ yaml = {
+ hover = true,
+ validate = false,
+ completion = true,
+ keyOrdering = false,
+ format = { enabled = false },
+ redhat = {
+ telemetry = { enabled = false },
+ },
+ schemaStore = {
+ enable = true,
+ url = 'https://www.schemastore.org/api/json/catalog.json',
+ },
+ schemas = {
+ kubernetes = '*.yaml',
+ ['http://json.schemastore.org/github-workflow'] = '.github/workflows/*',
+ ['http://json.schemastore.org/github-action'] = '.github/action.{yml,yaml}',
+ ['https://raw.githubusercontent.com/microsoft/azure-pipelines-vscode/master/service-schema.json'] = 'azure-pipelines.yml',
+ ['http://json.schemastore.org/ansible-stable-2.9'] = 'roles/tasks/*.{yml,yaml}',
+ ['http://json.schemastore.org/prettierrc'] = '.prettierrc.{yml,yaml}',
+ ['http://json.schemastore.org/kustomization'] = 'kustomization.{yml,yaml}',
+ ['http://json.schemastore.org/ansible-playbook'] = '*play*.{yml,yaml}',
+ ['http://json.schemastore.org/chart'] = 'Chart.{yml,yaml}',
+ ['https://json.schemastore.org/dependabot-v2'] = '.github/dependabot.{yml,yaml}',
+ ['https://gitlab.com/gitlab-org/gitlab/-/raw/master/app/assets/javascripts/editor/schema/ci.json'] = '*gitlab-ci*.{yml,yaml}',
+ ['https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/schemas/v3.1/schema.json'] = '*api*.{yml,yaml}',
+ ['https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json'] = '*docker-compose*.{yml,yaml}',
+ ['https://raw.githubusercontent.com/argoproj/argo-workflows/master/api/jsonschema/schema.json'] = '*flow*.{yml,yaml}',
+ ['https://raw.githubusercontent.com/yannh/kubernetes-json-schema/refs/heads/master/v1.32.1-standalone-strict/all.json'] = '/*.k8s.yaml',
+ },
+ },
+ },
+}
+vim.lsp.config('yamlls', yamlls)
+
+local pyrefly = {
+ name = 'pyrefly',
+ cmd = { 'pyrefly', 'lsp' },
+ filetypes = { 'python' },
+ root_markers = { 'pyrefly.toml', 'pyproject.toml', 'setup.py', 'setup.cfg', 'requirements.txt', 'Pipfile', '.git' },
+ settings = {
+ python = {
+ pyrefly = {
+ displayTypeErrors = 'force-on',
+ },
+ -- pythonPath = vim.env.VIRTUAL_ENV,
+ venvPath = vim.env.VIRTUAL_ENV,
+ },
+ },
+}
+vim.lsp.config('pyrefly', pyrefly)
+
+-- restart lsp
+-- require('nvimpio.lspConfig.tools').lsp_restart('clangd')
+----------------------------------------------------------------------------------
diff --git a/lua/nvimpio/lspConfig/keymaps.lua b/lua/nvimpio/lspConfig/keymaps.lua
new file mode 100644
index 00000000..a757b8b9
--- /dev/null
+++ b/lua/nvimpio/lspConfig/keymaps.lua
@@ -0,0 +1,136 @@
+local K = {}
+--Lua functions in combination with the option expr = true handles keycodes automatically
+function K.lspKeymaps(client, bufnr)
+ local bufkeymap = function(mode, lhs, rhs, desc)
+ vim.keymap.set(mode, lhs, rhs, { buffer = bufnr, silent = true, desc = desc }) -- noremap by default
+ end
+ -- Disable defaults
+ pcall(vim.keymap.del, 'n', 'gra')
+ pcall(vim.keymap.del, 'n', 'gri')
+ pcall(vim.keymap.del, 'n', 'grn')
+ pcall(vim.keymap.del, 'n', 'grr')
+ pcall(vim.keymap.del, 'n', 'gO')
+ pcall(vim.keymap.del, 'n', 'K')
+ --
+ -- Quickfix list
+ bufkeymap('n', '[q', vim.cmd.cprev, 'Previous quickfix item')
+ bufkeymap('n', ']q', vim.cmd.cnext, 'Next quickfix item')
+
+ -- Diagnostic keymaps
+ bufkeymap('n', '[d', 'vim.diagnostic.goto_prev()', 'Go to previous [d]iagnostic message')
+ bufkeymap('n', ']d', 'vim.diagnostic.goto_next()', 'Go to next [d]iagnostic message')
+ bufkeymap('n', 'gle', vim.diagnostic.open_float, 'Show diagnostic [e]rror messages')
+ -- bufkeymap('n', 'gle', 'Telescope diagnostics', 'Show diagnostic [e]rror messages')
+ bufkeymap('n', 'glq', vim.diagnostic.setloclist, 'Open diagnostic [q]uickfix list')
+ --
+ -- stylua: ignore start
+ -- << local trouble = require("trouble").toggle
+ -- << bufkeymap('n', "tt", function() trouble() end, "Toggle Trouble")
+ -- << bufkeymap('n', "tq", function() trouble("quickfix") end, "Quickfix List")
+ -- << bufkeymap('n', "dr", function() trouble("lsp_references") end, "References")
+ -- << bufkeymap('n', "dd", function() trouble("document_diagnostics") end, "Document Diagnostics")
+ -- << bufkeymap('n', "dw", function() trouble("workspace_diagnostics") end, "Workspace Diagnostics")
+ -- stylua: ignore end
+ --
+ if client.server_capabilities.hoverProvider then
+ bufkeymap('n', 'glk', vim.lsp.buf.hover, 'Hover Documentation')
+ end
+ if client.server_capabilities.signatureHelpProvider then
+ bufkeymap({ 'i', 'n' }, 'gls', vim.lsp.buf.signature_help, 'Show signature')
+ end
+ if client.server_capabilities.declarationProvider then
+ bufkeymap('n', 'glD', vim.lsp.buf.declaration, 'Goto [D]eclaration')
+ end
+ if client.server_capabilities.definitionProvider then
+ bufkeymap('n', 'gld', vim.lsp.buf.definition, 'Go to [d]efinition')
+ -- bufkeymap('n', 'gld', 'Telescope lsp_definitions', '[G]oto [D]efinition')
+ end
+ if client.server_capabilities.typeDefinitionProvider then
+ bufkeymap('n', 'glt', vim.lsp.buf.type_definition, 'Goto [t]ype definition')
+ -- bufkeymap('n', 'glt', 'Telescope lsp_type_definitions', 'Goto [t]ype definition')
+ end
+ if client.server_capabilities.implementationProvider then
+ bufkeymap('n', 'gli', vim.lsp.buf.implementation, 'Goto [i]mplementation')
+ -- bufkeymap('n', 'gli', 'Telescope lsp_implementations', 'Goto [i]mplementation')
+ end
+
+ -- bufkeymap('n', 'glr', '(CodeAction, implementation, rename, references)', 'CodeAction, implementation, rename, references')
+ if client.server_capabilities.referencesProvider then
+ -- bufkeymap('n', 'gr', vim.lsp.buf.references, 'List references')
+ bufkeymap('n', 'glr', 'Telescope lsp_references', 'Goto [r]eferences')
+ -- bufkeymap('n', 'glr', 'Telescope lsp_references', '[G]oto [R]eferences')
+ end
+ if client.server_capabilities.renameProvider then
+ -- bufkeymap('n', '', vim.lsp.buf.rename, 'Rename symbol')
+ bufkeymap('n', 'glR', vim.lsp.buf.rename, '[R]ename')
+ end
+ if client.server_capabilities.codeActionProvider then
+ bufkeymap('n', 'gla', vim.lsp.buf.code_action, 'Code [a]ction')
+ end
+
+ if client.server_capabilities.documentSymbolProvider then
+ bufkeymap('n', 'glwd', vim.lsp.buf.document_symbol, '[D]ocument symbols')
+ -- bufkeymap('n', 'glwd', Telescope lsp_document_symbols, '[D]ocument [S]ymbols')
+ end
+ if client:supports_method('workspace/symbol') then
+ -- if client.server_capabilities.workspaceSymbolProvider then
+ bufkeymap('n', 'glww', vim.lsp.buf.workspace_symbol, 'List [w]orkspace symbols')
+ -- bufkeymap('n', 'glww', require('telescope.builtin').lsp_dynamic_workspace_symbols, '[W]orkspace [S]ymbols')
+ end
+ if client.server_capabilities.workspace then
+ bufkeymap('n', 'glwa', vim.lsp.buf.add_workspace_folder, 'Workspace [a]dd folder')
+ bufkeymap('n', 'glwr', vim.lsp.buf.remove_workspace_folder, 'Workspace [r]emove folder')
+ bufkeymap('n', 'glwl', function()
+ print(vim.inspect(vim.lsp.buf.list_workspace_folders()))
+ end, '[W]orkspace [L]ist folders')
+ end
+ --
+ if client:supports_method('textDocument/switchSourceHeader') then
+ bufkeymap('n', 'glws', 'LspClangdSwitchSourceHeader', '[S]witch Source/Header (C/C++)')
+ end
+
+ if client:supports_method('textDocument/formatting') then
+ -- if client.server_capabilities.documentFormattingProvider then
+ bufkeymap({ 'n', 'x' }, 'glf', function()
+ vim.lsp.buf.format({ bufnr = bufnr, async = true })
+ -- require('conform').format({ bufnr = bufnr, async = true })
+ end, '[f]ormat buffer')
+
+ -- LSP format the current buffer on save
+ local fmt_group = vim.api.nvim_create_augroup('autoformat_cmds', { clear = true })
+ vim.api.nvim_create_autocmd('BufWritePre', {
+ buffer = bufnr,
+ group = fmt_group,
+ desc = 'Fromat current buffer',
+ callback = function(args)
+ -- if (client.name == 'lua_ls') and (vim.fn.executable('stylua') == 1) then
+ -- -- if (client.name == 'stylua') and (vim.fn.executable('stylua') == 1) then
+ -- -- vim.fn.system({ 'stylua', vim.api.nvim_buf_get_name(bufnr) })
+ -- vim.fn.system({ 'stylua', vim.api.nvim_buf_get_name(args.buf) })
+ -- vim.cmd('checktime')
+ -- print('stylua formatting')
+ -- else
+ vim.lsp.buf.format({
+ bufnr = bufnr,
+ async = false,
+ timeout_ms = 10000,
+ id = client.id,
+ filter = function(c)
+ return c.id == client.id
+ end,
+ })
+ print('LSP: clangd formatting')
+ -- end
+ end,
+ })
+ end
+ --
+ if client.server_capabilities.inlayHintProvider and vim.lsp.inlay_hint then
+ bufkeymap('n', 'glh', function()
+ vim.lsp.inlay_hint.enable(not vim.lsp.inlay_hint.is_enabled({ bufnr = bufnr }), { bufnr = bufnr })
+ end, '[h]ints toggle')
+ ------------------------------------------------------------------------------
+ end
+end
+
+return K
diff --git a/lua/nvimpio/lspConfig/tools.lua b/lua/nvimpio/lspConfig/tools.lua
new file mode 100644
index 00000000..ed133426
--- /dev/null
+++ b/lua/nvimpio/lspConfig/tools.lua
@@ -0,0 +1,20 @@
+local M = {}
+
+-- -- INFO:
+
+--- stylua: ignore
+function M.clangdRestart()
+ local name = 'clangd'
+ -- vim.schedule_wrap(function()
+ vim.notify('LSP: Clangd restart.', vim.log.levels.WARN)
+
+ local clangConfig = _G.get_clangd_config()
+ -- print(vim.inspect(clangConfig))
+ vim.lsp.config(name, clangConfig)
+ vim.lsp.enable(name, false)
+ vim.lsp.enable(name, true)
+ vim.cmd('checktime')
+ -- end)
+end
+
+return M
diff --git a/lua/nvimpio/pio/metadata.lua b/lua/nvimpio/pio/metadata.lua
new file mode 100644
index 00000000..a2526e5a
--- /dev/null
+++ b/lua/nvimpio/pio/metadata.lua
@@ -0,0 +1,134 @@
+local M = {}
+
+-------------------------------------------------------------------------------------------------------
+local last_saved_hash = ''
+
+--INFO:
+-- 1. Internal State & Defaults
+local _pio_metadata = {
+ isBusy = false,
+ envs = {},
+ active_env = '',
+ default_envs = {},
+ core_dir = '',
+ packages_dir = '',
+ platforms_dir = '',
+ query_driver = '',
+ cc_compiler = '',
+ includes_build = {},
+ includes_compatlib = {},
+ includes_toolchain = {},
+ cc_path = '',
+ cc_flags = {},
+ cxx_path = '',
+ cxx_flags = {},
+ gdb_path = '',
+ defines = {},
+ triplet = '',
+ toolchain_root = '',
+ sysroot = '',
+ fallbackFlags = {},
+ dbTrigger = false,
+ last_projectChecksum = '', -- Used to track changes
+}
+-- 2. The Reactive Proxy Wrapper
+-- Any write to _G.metadata.key = val triggers this logic
+_G.metadata = setmetatable({}, {
+ __index = _pio_metadata,
+ __newindex = function(_, key, value)
+ if _pio_metadata[key] == value then
+ -- print('Value is identical, returning...') -- DEBUG LINE
+ return
+ end -- Performance check
+ -- print('Newindex attempt for: ' .. tostring(key)) -- DEBUG LINE
+ _pio_metadata[key] = value
+
+ -- Trigger background actions
+ vim.schedule(function()
+ -- M.save_project_config(true)
+ if key == 'toolchain_root' then
+ local binPath = value .. '/bin'
+ local sep = (vim.fn.has('win32') == 1 and ';' or ':')
+ vim.env.PATH = binPath .. sep .. vim.env.PATH
+ vim.notify('PIO env: ' .. binPath .. ' added to path', vim.log.levels.INFO, { title = 'PlatformIO', render = 'compact' })
+ -- vim.notify('Env: ' .. value, vim.log.levels.INFO, { title = 'PlatformIO', render = 'compact' })
+ -- pcall(function()
+ -- if _pio_metadata.dbTrigger then
+ -- vim.notify('Env: dbTrigger', vim.log.levels.INFO, { title = 'PlatformIO', render = 'compact' })
+ -- local dbFix = pio.compile_commandsFix
+ -- local ok, _ = pcall(dbFix)
+ -- if not ok then
+ -- print('Env: dbTrigger, fail to call dbFix')
+ -- end
+ -- -- dbFix()
+ -- _pio_metadata.dbTrigger = false
+ -- else
+ -- local LspRestart = require('nvimpio.lspConfigConfig.tools').lsp_restart
+ -- LspRestart('clangd')
+ -- vim.notify('Env: LspRestart', vim.log.levels.INFO, { title = 'PlatformIO', render = 'compact' })
+ -- end
+ -- end)
+ elseif key == 'last_projectChecksum' then
+ elseif key == 'active_env' then
+ end
+ end)
+ end,
+})
+
+local config_path = vim.fs.joinpath(vim.uv.cwd(), '.project_config.json')
+-- -- Add this temporary line in a file where you are coding:
+-- ---@type platformio.utils.misc
+-- local misc = vim.misc
+--INFO:
+-- 2. Save Logic (Uses sha256 for stability)
+function M.save_project_config(from)
+ -- 1. Generate the formatted string directly, jsonFormat already returns a string!
+ local ok, pretty_json = pcall(vim.misc.jsonFormat, _pio_metadata)
+
+ if not ok or not pretty_json then
+ print('Error formatting metadata')
+ return
+ end
+
+ local current_hash = vim.fn.sha256(pretty_json)
+
+ -- 2. Only write if the content actually changed
+ if current_hash ~= last_saved_hash then
+ local status, err = vim.misc.writeFile(config_path, pretty_json, {})
+
+ if status then
+ last_saved_hash = current_hash
+ vim.notify(from .. 'config save success', vim.log.levels.INFO, { title = 'PlatformIO' })
+ else
+ vim.notify(from .. 'config save failed==> ' .. (err or 'unknown error'), vim.log.levels.ERROR)
+ end
+ end
+end
+
+--INFO:
+-- 3. Load Logic (Populates proxy safely)
+function M.load_project_config()
+ if vim.fn.filereadable(config_path) == 1 then
+ local _, json_data = vim.misc.readFile(config_path)
+ if json_data then
+ local ok, table_data = pcall(vim.json.decode, json_data)
+ if ok and type(table_data) == 'table' then
+ -- We update _pio_metadata directly to avoid triggering
+ -- 50+ notifications/restarts during the initial load loop
+ for k, v in pairs(table_data) do
+ _G.metadata[k] = v
+ end
+ last_saved_hash = vim.fn.sha256(json_data)
+ return
+ end
+ end
+ end
+ -- If no file, initialize hash with defaults
+ last_saved_hash = vim.fn.sha256(vim.misc.jsonFormat(_pio_metadata))
+end
+
+--INFO:
+-- 4. Initialization
+M.load_project_config()
+
+return M
diff --git a/lua/nvimpio/pio/upkeep.lua b/lua/nvimpio/pio/upkeep.lua
new file mode 100644
index 00000000..12713916
--- /dev/null
+++ b/lua/nvimpio/pio/upkeep.lua
@@ -0,0 +1,680 @@
+---@class platformio.utils.pio
+local M = {}
+
+-- to fix require loop, this value is set in plugin/platformio
+local misc = vim.misc
+
+-- local sep = package.config:sub(1, 1) -- Dynamic OS separator (\ or /)
+M.selected_framework = ''
+M.is_processing = false
+M.queue = {}
+
+local term = require('nvimpio.utils.term')
+local clangdRestart = require('nvimpio.lspConfig.tools').clangdRestart
+
+-- INFO:
+-- =============================================================================
+-- UNIVERSAL TOOLCHAIN DETECTION
+-- =============================================================================
+-- stylua: ignore
+function M.get_sysroot_triplet(cc_compiler)
+ local bin_path = vim.fn.fnamemodify(cc_compiler, ':h')
+
+ -- Early exit if path is nil or not a directory
+ if not bin_path or vim.fn.isdirectory(bin_path) == 0 then return nil end
+
+ -- Normalize backslashes to forward slashes for cross-platform consistency
+ bin_path = bin_path:gsub('\\', '/')
+ local files = vim.fn.readdir(bin_path)
+ local triplet = nil
+
+ -- Loop through files to find the compiler and extract the triplet
+ for _, name in ipairs(files) do
+ -- Pattern: ^(.*) matches triplet, %- matches dash, g[c%+][c%+] matches gcc/g++
+ local match = name:match('^(.*)%-g[c%+][c%+]')
+ if match then triplet = vim.misc.normalizePath(match) break
+ end
+ end
+
+ -- Return nil if no compiler was found in the bin directory
+ if not triplet then return nil end
+
+ -- toolchain_root is the parent of the 'bin' folder
+ local toolchain_root = vim.misc.normalizePath(vim.fn.fnamemodify(bin_path, ':h'))
+ -- sysroot folder is expected to have the same name as the triplet
+ local sysroot = vim.misc.normalizePath(toolchain_root .. '/' .. triplet)
+ local query_driver = vim.misc.normalizePath(bin_path .. '/' .. triplet .. '-*')
+
+ -- vim.notify('triplet= ' .. triplet, vim.log.levels.INFO)
+ -- Only return data if the sysroot folder actually exists on disk
+ if vim.fn.isdirectory(sysroot) == 1 then
+ _G.metadata.triplet = triplet
+ _G.metadata.sysroot = sysroot
+ _G.metadata.toolchain_root = toolchain_root
+ _G.metadata.query_driver = query_driver
+ return {
+ triplet = triplet,
+ sysroot = sysroot,
+ toolchain_root = toolchain_root,
+ query_driver = query_driver,
+ }
+ end
+ return nil
+end
+
+--INFO:
+-- Fast environment detection from platformio.ini file(no external calls)
+-- stylua: ignore
+--=============================================================================
+function M.get_active__env()
+ local path
+
+ for _, dir in ipairs({ vim.api.nvim_buf_get_name(0):match('(.*[/\\])'), (vim.uv.cwd() .. '/') }) do
+ local tmp = dir .. 'platformio.ini'
+ local filestat = vim.uv.fs_stat(tmp)
+ if filestat and filestat.type == 'file' then
+ path = vim.fs.normalize(tmp)
+ break
+ end
+ end
+ if not path or path == '' then return vim.notify('PIO: platformio.ini not found or no [env] defined.', vim.log.levels.ERROR) end
+
+ -- Read file content (returns string or nil)
+ local ok, content = vim.misc.readFile(path)
+ if not ok or not content then return vim.notify('PIO: platformio.ini not found in ' .. path, vim.log.levels.WARN) end
+
+ local default_envs_raw = ''
+ local first_env = nil
+ local valid_envs = {}
+ local in_platformio_block = false
+
+ -- Iterate lines from the content string
+ for line in vim.gsplit(content, '\n') do
+ -- Section Detection: [section_name]
+ local section = line:match('^%s*%[(.+)%]%s*$')
+ if section then
+ in_platformio_block = (section == 'platformio')
+ local env_name = section:match('^env:(.+)')
+ if env_name then
+ if not first_env then first_env = env_name end
+ valid_envs[env_name] = true
+ end
+ end
+
+ -- Collect the default_envs string from [platformio] block
+ if in_platformio_block then
+ local def = line:match('^%s*default_envs%s*=%s*(.+)')
+ if def then default_envs_raw = def end
+ end
+ end
+
+ -- Validation: Find the first default_env that actually exists as a block
+ if default_envs_raw ~= '' then
+ for env_name in default_envs_raw:gmatch('([^%s,]+)') do
+ if valid_envs[env_name] then return env_name end
+ end
+ end
+
+ -- Fallback to the very first [env:...] block found in the file
+ return first_env
+end
+
+
+--INFO:
+-- get pio project metadata info
+-- stylua: ignore
+--=============================================================================
+function M.fetch_metadata(callback, env, from, attempts)
+ local msg = (type(from)=='string' and from ~= '') and from or 'PIO: '
+ local meta = _G.metadata
+ local active_env = env or meta.active_env
+ if not active_env or active_env == '' then
+ return
+ end
+
+ -- Set up file paths
+ local build_dir = vim.misc.joinPath(vim.uv.cwd(), '.pio', 'build')
+ local build_env_dir = vim.misc.joinPath(build_dir, active_env)
+ local checksum_file = vim.misc.joinPath(build_dir, 'project.checksum')
+ local idedata_file = vim.misc.joinPath(build_env_dir, 'idedata.json')
+
+ --INFO:
+ --INTERNAL PROCESSOR: Applies parsed data to _G.metadata
+ ---------------------------------------------------------
+ local function apply_metadata(data, checksum)
+ if not data then return false end
+
+ local norm = function(p) return vim.misc.normalizePath(p) or '' end
+
+ -- Helper for flags/defines to keep order and formatting
+ local quote_map = function(list, prefix)
+ local res = {}
+ for _, v in ipairs(list or {}) do
+ local val = prefix and (prefix .. norm(v)) or v
+ table.insert(res, string.format('%s', val))
+ end
+ return res
+ end
+
+ -- 1. Base Paths & Compilers
+ meta.cc_path = norm(data.cc_path)
+ meta.cc_compiler = meta.cc_path
+ meta.cxx_path = norm(data.cxx_path)
+ meta.gdb_path = norm(data.gdb_path)
+
+ -- 2. Flags & Defines
+ meta.cc_flags = quote_map(data.cc_flags)
+ meta.cxx_flags = quote_map(data.cxx_flags)
+ meta.defines = quote_map(data.defines)
+
+ -- 3. Includes (Build, Toolchain, Compatlib)
+ local inc = data.includes or {}
+ meta.includes_build = quote_map(inc.build, '-I')
+ meta.includes_toolchain = quote_map(inc.toolchain, '-isystem')
+ meta.includes_compatlib = quote_map(inc.compatlib, '-isystem')
+ meta.last_projectChecksum = checksum
+ pcall(M.get_sysroot_triplet, meta.cc_compiler)
+
+ return true
+ end
+
+ --INFO:
+ --Generate idedata.json
+ ---------------------------------------------------------
+ local function buildIdedata()
+ vim.notify(msg .. 'Initializing project metadata...', vim.log.levels.INFO)
+ vim.system({ 'pio', 'run', '-t', 'idedata', '-e', active_env, '-s' }, { text = true }, function(obj)
+ vim.schedule(function()
+ if obj.code == 0 then
+ vim.notify(msg .. 'Initializing project metadata success.', vim.log.levels.INFO)
+ M.fetch_metadata(callback, active_env, from, attempts - 1) -- Recursive call after files created
+ else
+ vim.notify(msg .. 'Initialization failed. Build project manually.', vim.log.levels.ERROR)
+ end
+ end)
+ end)
+ return true
+ end
+
+ ---------------------------------------------------------
+ -- STEP 1: Fast Checksum Check (project.checksum and idedata.json)
+ ---------------------------------------------------------
+ local ok, current_checksum = vim.misc.readFile(checksum_file)
+ if ok and (type(current_checksum) == 'string' and current_checksum ~= '') then
+ if current_checksum == meta.last_projectChecksum then
+ vim.notify(msg .. 'Metadata synced with cache', vim.log.levels.INFO)
+ -- if callback then callback() end
+ if callback then vim.schedule(callback) end
+ return true
+ end -- Already updated
+
+ -- STEP 2: Cache Path (idedata.json exists and checksum changed)
+ local idok, content = vim.misc.readFile(idedata_file)
+ if idok and (type(content) == 'string' and content ~= '') then
+ local cok, decoded = pcall(vim.json.decode, content)
+
+ -- local formated = vim.misc.jsonFormat(decoded)
+ -- local file = vim.misc.joinPath(vim.uv.cwd(), 'idedata.json')
+ -- vim.misc.writeFile(file, formated, {})
+
+ if cok and apply_metadata(decoded, current_checksum) then
+ local metadata = require('nvimpio.pio.metadata')
+ metadata.save_project_config(msg)
+ vim.notify(msg .. 'Metadata synced from cache', vim.log.levels.INFO)
+ -- if callback then vim.schedule(callback) end
+
+ if type(callback) == "function" then
+ vim.schedule(callback)
+ else
+ -- If it's not a function, just do nothing or print a debug message
+ print(msg .." Debug; callback was " .. type(callback))
+ end
+
+ return true
+ end
+ -- else
+ end
+ -- else
+ end
+ ---------------------------------------------------------
+ -- STEP 3: Auto-Initialize (If files project.checksum and idedata.json are missing)
+ ---------------------------------------------------------
+ buildIdedata()
+
+ ---------------------------------------------------------
+ -- STEP 4: Standard CLI Fallback (The Slow Path)
+ ---------------------------------------------------------
+ -- vim.notify(msg .. 'Metadata sync ...', vim.log.levels.INFO)
+ -- vim.system({ 'pio', 'project', 'metadata', '-e', active_env, '--json-output' }, { text = true }, function(obj)
+ -- vim.schedule(function()
+ -- if obj.code ~= 0 then
+ -- if attempts > 0 then
+ -- vim.defer_fn(function() M.fetch_metadata(attempts - 1, env) end, 500)
+ -- return
+ -- end
+ -- return vim.notify(msg .. 'Metadata Error: ' .. (obj.stderr or 'Unknown'), vim.log.levels.WARN)
+ -- end
+ --
+ -- local ook, raw_data = pcall(vim.json.decode, obj.stdout or '')
+ -- local _, data = next(raw_data or {})
+ --
+ -- if ook and apply_metadata(data, current_checksum) then
+ -- vim.notify(msg .. 'Metadata synced from CLI', vim.log.levels.INFO)
+ -- if callback then vim.schedule(callback) end
+ -- else
+ -- vim.notify(msg .. 'Failed to parse metadata output', vim.log.levels.WARN)
+ -- end
+ -- end)
+ -- end)
+end
+
+-- INFO:
+-- =============================================================================
+-- Get project configuration
+-- =============================================================================
+-- stylua: ignore
+function M.fetch_config(on_done, from)
+ local msg = (type(from) == 'string' and from ~= '') and from or 'PIO: '
+ local meta = _G.metadata
+ local home = (os.getenv('HOME') or os.getenv('USERPROFILE') or ''):gsub('[\\/]+$', '')
+
+ local active_env
+ vim.system({ 'pio', 'project', 'config', '--json-output' }, { text = true }, function(obj)
+ vim.schedule(function()
+ -- 1. Check Execution
+ if obj.code ~= 0 then
+ local errmsg = obj.code == 127 and "'pio' not found" or (obj.stderr or 'Unknown Error')
+ return vim.notify(msg .. 'Config Error: ' .. errmsg, vim.log.levels.ERROR)
+ end
+
+ -- 2. Decode JSON safely
+ local ok, decoded = pcall(vim.json.decode, obj.stdout or '')
+ if not ok or type(decoded) ~= 'table' then
+ return vim.notify(msg .. 'Failed to decode config JSON', vim.log.levels.ERROR)
+ end
+
+ -- local formated = vim.misc.jsonFormat(decoded)
+ -- local file = vim.misc.joinPath(vim.uv.cwd(), 'config.json')
+ -- vim.misc.writeFile(file, formated, {})
+
+ -- Reset core structure
+ meta.envs = {}
+ meta.default_envs = {}
+ local valid_envs = {}
+
+ -- 3. Parse Sections
+ for _, section in ipairs(decoded) do
+ local name, data = section[1], section[2]
+ if name == 'platformio' then
+ for _, kv in ipairs(data) do
+ meta[kv[1]] = kv[2]
+ end
+ elseif name:match('^env:') then
+ local env_name = name:match('^env:(.+)')
+ if not active_env then active_env = env_name end
+ valid_envs[env_name] = true
+ meta.envs[env_name] = {}
+ for _, kv in ipairs(data) do
+ meta.envs[env_name][kv[1]] = kv[2]
+ end
+ end
+ end
+
+ -- 4. Assign active_env
+ -- Validation: Find the first default_env that actually exists as a block
+ for _, env_name in ipairs(meta.default_envs) do
+ if valid_envs[env_name] then
+ active_env = env_name
+ break
+ end
+ end
+ meta.active_env = active_env
+
+ -- 5. Resolve Paths (INI -> Env -> Default)
+ local path_map = {
+ { key = 'core_dir', env = 'PLATFORMIO_CORE_DIR', sub = '/.platformio' },
+ { key = 'packages_dir', env = 'PLATFORMIO_PACKAGES_DIR', sub = '/.platformio/packages' },
+ { key = 'platforms_dir', env = 'PLATFORMIO_PLATFORMS_DIR', sub = '/.platformio/platforms' },
+ }
+
+ for _, item in ipairs(path_map) do
+ local val = meta[item.key]
+ -- Fallback chain
+ if not val or val == '' then
+ val = os.getenv(item.env) or (home .. item.sub)
+ end
+ -- Expand variables and Normalize
+ if type(val) == 'string' then
+ val = val:gsub('%%${platformio.core_dir}', meta.core_dir or '')
+ meta[item.key] = vim.misc.normalizePath(val)
+ end
+ end
+
+ -- if active_env then
+ -- vim.notify(msg .. 'active_env= ' .. active_env, vim.log.levels.INFO)
+ -- end
+ -- 6. Trigger next step
+ if meta.active_env ~= '' then
+ vim.notify(msg .. 'Config sync successful', vim.log.levels.INFO)
+ else
+ vim.notify(msg .. 'No [env:] found. Please add a board.', vim.log.levels.ERROR)
+ end
+
+ if on_done then
+ vim.schedule(function() on_done(active_env) end)
+ end
+ end)
+ end)
+end
+
+-- INFO:
+-- Fix compile_commands.json file with absoulute paths
+-- stylua: ignore
+-- =============================================================================
+function M.compile_commandsFix() --M.dbPathsFix()
+ local filename = vim.fs.joinpath(vim.uv.cwd(), 'compile_commands.json')
+ local content = vim.fn.readfile(filename)
+ if #content == 0 then return end
+
+ local start_time = vim.loop.hrtime()
+ local ok, data = pcall(vim.json.decode, table.concat(content, '\n'))
+ if not ok or type(data) ~= 'table' then return end
+
+ -- 1. Build Path Map (Scan toolchain)
+ local path_map = {}
+ local pio_binaries = _G.metadata.query_driver or '/bin/*'
+ -- local pio_binaries = (_G.metadata.toolchain_root or "") .. '/bin/*'
+ for _, full_path in ipairs(vim.fn.glob(pio_binaries, false, true)) do
+ local name = full_path:match('([^/\\\\]+)$'):gsub('%.exe$', '')
+ path_map[name] = full_path
+ end
+
+ -- 2. Update Entries
+ local modified = false
+ local prntFlags = true
+ for _, entry in ipairs(data) do
+ -- Standard normalization
+ if entry.directory then entry.directory = misc.normalizePath(entry.directory) end
+ if entry.file then entry.file = misc.normalizePath(entry.file) end
+ if entry.arguments then entry.arguments = misc.normalizeFlags(entry.arguments) end
+ if entry.output then entry.output = misc.normalizePath(entry.output) end
+
+ if entry.command then
+ -- Extract compiler and everything after it
+ local compiler, args = entry.command:match("^%s*(%S+)(.*)")
+ if compiler then
+ local is_absolute = compiler:sub(1, 1) == '/' or compiler:match('^%a:')
+
+ if not is_absolute then
+ local short_name = compiler:match('([^/\\\\]+)$'):gsub('%.exe$', '')
+
+ if path_map[short_name] then
+ -- Use normalizePath on the new path
+ local full_compiler_path = misc.normalizePath(path_map[short_name])
+
+ -- Quote the path if it contains spaces
+ if full_compiler_path:find(" ") then
+ full_compiler_path = '"' .. full_compiler_path .. '"'
+ end
+ if prntFlags then
+ -- print(string.format('ful_compiler_path = %s flags=%s', full_compiler_path, args))
+ prntFlags = false
+ end
+ entry.command = full_compiler_path .. args
+ modified = true
+ end
+ end
+ end
+ end
+ end
+ -- -- 3. Save with Formatting
+ if modified then
+ local jok, formatted = pcall(vim.misc.jsonFormat, data)
+ -- local jok, formatted = pcall(M.pretty_print, data)
+ if not jok then
+ print('Formatting failed: ' .. formatted)
+ return
+ end
+
+ local wk, err = vim.misc.writeFile(filename, formatted, { overwrite = true, mkdir = true })
+ if not wk then print(err) end
+
+ local end_time = vim.loop.hrtime()
+ local duration = (end_time - start_time) / 1e6
+ vim.notify(string.format('compiledb: paths fixed in %.2fms', duration), vim.log.levels.INFO)
+ clangdRestart()
+ end
+ _G.metadata.isBusy = false
+end
+
+
+-- INFO:
+--configuration for running sequential commands on ToggleTerminal
+-- stylua: ignore
+-- =============================================================================
+-- =============================================================================
+local callBack = nil
+local pio_buffer = '' -- Persistent stream buffer
+
+-- INFO: ToggleTerminal commands stdout filter
+-- stylua: ignore
+-- =============================================================================
+function M.stdoutcallback(_, _, data)
+ if not data then return end
+
+ -- 1. Combine the last partial line with the new first line
+ local lines_to_process = pio_buffer .. data[1]
+
+ -- 2. If there are newlines, we have complete lines to check
+ if #data > 1 then
+ -- Join all complete parts (everything except the very last partial line)
+ for i = 2, #data - 1 do lines_to_process = lines_to_process .. data[i] end
+
+ -- 3. Search for the status in the complete chunk
+ local status = lines_to_process:match('_CMMNDS_:(%a+)')
+ if status and callBack then vim.schedule(function() callBack(status) end) end
+ -- save the trailing part for the next chunk
+ pio_buffer = data[#data]
+ else
+ -- Only one element in data means no newline yet; just update the partial buffer
+ pio_buffer = lines_to_process
+ end
+
+ -- 4. Safety Trim (Prevents memory leaks if no newline ever comes)
+ if #pio_buffer > 5000 then pio_buffer = pio_buffer:sub(-2500) end
+end
+
+local commandPassed = 0
+
+
+-- INFO: commands sequencer
+-- stylua: ignore
+-- =============================================================================
+M.run_sequence = function(tasks)
+ M.queue = {}
+ local commands = tasks.cmnds
+
+ local done = ' && echo _CMMNDS_":"DONE'
+ local pass = ' && echo _CMMNDS_":"PASS'
+ local fail = ' || echo _CMMNDS_":"FAIL'
+ --
+ for i, cmd in ipairs(commands) do
+ local full_cmd = ''
+ if i == #commands then full_cmd = cmd .. done .. fail
+ else full_cmd = cmd .. pass .. fail end
+ table.insert(M.queue, full_cmd)
+ end
+
+
+ callBack = tasks.cb -- 1. Save the callback in a local variable
+
+ commandPassed = 1
+ _G.metadata.isBusy = true
+
+ term.stdout_callback = M.stdoutcallback
+ vim.schedule(function() if callBack then callBack('INIT') end end)
+end
+
+local trm
+local win_id
+------------------------------------------------------
+-- Handle after pioinit execution
+-- =============================================================================
+-- stylua: ignore
+function M.handlePioinitDb(result)
+ if result == 'INIT' then
+ local boilerplate = require('nvimpio.boilerplate')
+ local boilerplate_gen = boilerplate.boilerplate_gen
+
+ boilerplate.core_dir = _G.metadata.core_dir
+ boilerplate_gen([[platformio.ini]], vim.g.platformioRootDir)
+
+ boilerplate_gen([[.clang-format]], vim.g.platformioRootDir)
+
+ boilerplate_gen([[.clangd]], vim.g.platformioRootDir)
+ -- boilerplate_gen([[.clangd]], _G.metadata.core_dir)
+ -- boilerplate_gen([[.clangd]], vim.fs.joinpath(vim.env.XDG_CONFIG_HOME, 'clangd'), 'config.yaml')
+
+ win_id = vim.misc.showMessage('************ Project Initializing ************')
+ if #M.queue > 0 then trm = term.ToggleTerminal(table.remove(M.queue, 1), 'float')end
+ elseif result == 'PASS' then
+ -- if commandPassed == 1 then
+ -- elseif commandPassed == 2 then -- if you sned more than 2 commands you need this
+ -- end
+ vim.notify('PIO init+db: pass ' .. commandPassed, vim.log.levels.INFO)
+ commandPassed = commandPassed + 1
+ if #M.queue > 0 then term.ToggleTerminal(table.remove(M.queue, 1), 'float') end
+ elseif result == 'DONE' then -- result of the last command
+ vim.schedule(function()
+ vim.notify('PIO init+db: pass ' .. commandPassed, vim.log.levels.INFO)
+ vim.notify('PIO init+db: Done', vim.log.levels.INFO)
+ vim.misc.gitignore_lsp_configs('compile_commands.json')
+ local pio_refresh = require('nvimpio.pio.watcher').pio_refresh
+ pio_refresh(function()
+ local boilerplate_gen = require('nvimpio.boilerplate').boilerplate_gen
+ boilerplate_gen([[.clangd]], _G.metadata.core_dir)
+ vim.misc.closeMessage(win_id)
+ clangdRestart()
+ -- term.ToggleTerminal('echo "************ project Initialization success ************"', 'float')
+ end, 'PIO init+db: ')
+ end)
+ vim.misc.deleteFile(vim.fs.joinpath(vim.g.platformioRootDir, '.ccls'))
+ M.queue = {}
+ term.stdout_callback = nil
+ trm:close()
+ _G.metadata.isBusy = false
+ elseif result == 'FAIL' then
+ _G.metadata.isBusy = false
+ vim.misc.closeMessage(win_id)
+ M.queue = {}
+ term.stdout_callback = nil
+ trm:close()
+ end
+end
+
+
+----------------------------------------------------
+-- Handle after pioinit execution
+-- stylua: ignore
+function M.handlePioinit(result)
+ if result == 'INIT' then
+ local boilerplate = require('nvimpio.boilerplate')
+ local boilerplate_gen = boilerplate.boilerplate_gen
+
+ boilerplate.core_dir = _G.metadata.core_dir
+ boilerplate_gen([[platformio.ini]], vim.g.platformioRootDir)
+
+ boilerplate_gen([[.clang-format]], vim.g.platformioRootDir)
+
+ boilerplate_gen([[.clangd]], vim.g.platformioRootDir)
+ -- boilerplate_gen([[.clangd]], _G.metadata.core_dir)
+ -- boilerplate_gen([[.clangd]], vim.fs.joinpath(vim.env.XDG_CONFIG_HOME, 'clangd'), 'config.yaml')
+
+ win_id = vim.misc.showMessage('************ Project Initializing ************')
+ if #M.queue > 0 then trm = term.ToggleTerminal(table.remove(M.queue, 1), 'float')end
+ elseif result == 'DONE' then -- result of the last command
+ vim.schedule(function()
+ vim.notify('PIO init: pass ' .. commandPassed, vim.log.levels.INFO)
+ vim.notify('PIO init: Done', vim.log.levels.INFO)
+ vim.misc.gitignore_lsp_configs('compile_commands.json')
+
+ -- \27[s : Save current cursor position (the prompt)
+ -- \r : Go to start of line
+ -- \27[A : Move cursor UP one line (to space above prompt)
+ -- \27[K : Clear that line
+ -- \27[33m : Color Yellow (optional)
+ -- %s : Your message
+ -- \27[0m : Reset color
+ -- \27[u : Restore cursor back to the prompt
+ -- IMPORTANT: No \n at the end, so it doesn't execute
+ -- local msg = '************ Please wait for project Initialization to finish ************'
+ -- local clean_msg = string.format('\27[G\27[2K\27[33m%s\27[0m', msg)
+ -- vim.api.nvim_chan_send(trm.job_id, clean_msg)
+
+ local pio_refresh = require('nvimpio.pio.watcher').pio_refresh
+ pio_refresh(function()
+ local boilerplate_gen = require('nvimpio.boilerplate').boilerplate_gen
+ boilerplate_gen([[.clangd]], _G.metadata.core_dir)
+ vim.misc.closeMessage(win_id)
+ clangdRestart()
+ -- term.ToggleTerminal('echo "************ project Initialization success ************"', 'float')
+ end, 'PIO init: ')
+ end)
+ vim.misc.deleteFile(vim.fs.joinpath(vim.g.platformioRootDir, '.ccls'))
+ M.queue = {}
+ term.stdout_callback = nil
+ trm:close()
+ _G.metadata.isBusy = false
+ elseif result == 'FAIL' then
+ _G.metadata.isBusy = false
+ vim.misc.closeMessage(win_id)
+ M.queue = {}
+ term.stdout_callback = nil
+ trm:close()
+ end
+end
+
+------------------------------------------------------
+-- Handle after piolib execution
+-- =============================================================================
+-- stylua: ignore
+function M.handlePiolib(result)
+ if result == 'INIT' then
+ if #M.queue > 0 then term.ToggleTerminal(table.remove(M.queue, 1), 'float')end
+ elseif result == 'DONE' then -- result of the only and the last command
+ vim.notify('PIO lib: pass ' .. commandPassed, vim.log.levels.INFO)
+ vim.notify('PIO lib: Done', vim.log.levels.INFO)
+ commandPassed = commandPassed + 1
+ M.queue = {}
+ term.stdout_callback = nil
+ _G.metadata.isBusy = false
+ elseif result == 'FAIL' then
+ M.queue = {}
+ term.stdout_callback = nil
+ _G.metadata.isBusy = false
+ end
+end
+
+------------------------------------------------------
+-- =============================================================================
+-- stylua: ignore
+function M.handlePiodb(target, result)
+ if result == 'INIT' then
+ if #M.queue > 0 then term.ToggleTerminal(table.remove(M.queue, 1), 'float')end
+ elseif result == 'DONE' then -- result of the only and the last command
+ vim.notify('PIO db: pass ' .. commandPassed, vim.log.levels.INFO)
+ vim.notify('PIO db: Done', vim.log.levels.INFO)
+ commandPassed = commandPassed + 1
+ target.isBusy = false
+ M.queue = {}
+ term.stdout_callback = nil
+ _G.metadata.isBusy = false
+ elseif result == 'FAIL' then
+ target.isBusy = false
+ M.queue = {}
+ term.stdout_callback = nil
+ _G.metadata.isBusy = false
+ end
+end
+
+return M
diff --git a/lua/nvimpio/pio/watcher.lua b/lua/nvimpio/pio/watcher.lua
new file mode 100644
index 00000000..a90dbd50
--- /dev/null
+++ b/lua/nvimpio/pio/watcher.lua
@@ -0,0 +1,303 @@
+M = {}
+
+local clangdRestart = require('nvimpio.lspConfig.tools').clangdRestart
+local boilerplate = require('nvimpio.boilerplate')
+local boilerplate_gen = boilerplate.boilerplate_gen
+
+-- =============================================================================
+-- INFO:
+-- Unified hashing for change detection
+local function get_hash(path)
+ if vim.fn.filereadable(path) == 0 then
+ return nil
+ end
+ -- local ok, data = pcall(vim.fn.readfile, path) -- readfile is safer than io.open
+ -- return ok and vim.fn.sha256(table.concat(data, '\n')) or nil
+ local ok, data = vim.misc.readFile(path) -- readfile is safer than io.open
+ return (ok and type(data) == 'string' and data ~= '') and vim.fn.sha256(data) or ''
+end
+
+
+--INFO:
+--stylua: ignore
+--=============================================================================
+function M.pio_refresh(callback, from)
+ local msg = (type(from)=='string' and from ~= '') and from or 'PIO: '
+ vim.notify(msg ..'Config sync ...', vim.log.levels.INFO)
+
+ local function on_done(active_env)
+ if active_env then vim.notify(msg .. 'active_env= ' .. active_env, vim.log.levels.INFO) end
+ if active_env then vim.pio.fetch_metadata(callback, active_env, from, 1) end
+ end
+ vim.pio.fetch_config(on_done, from)
+end
+
+--INFO:
+--=============================================================================
+-- watchers setup
+--=============================================================================
+-- Ensure this is at the TOP of your file, outside any functions
+local uv = vim.uv or vim.loop
+M.watcher_handles = {}
+local debounce_timer = uv.new_timer()
+local last_mtime = 0
+
+-- --INFO:
+-- --stylua: ignore
+-- --1.run_compiledb after platformio.ini changed
+-- --=============================================================================
+-- function M.run_compiledb(target)
+-- if target.isBusy then return end
+-- if _G.metadata.isBusy == true then return end
+--
+-- local env = vim.pio.get_active__env()
+-- if not env then return end
+-- target.isBusy = true
+-- vim.notify('PIO platformio.ini change: compiledb update ...', vim.log.levels.INFO, { title = 'PlatformIO' })
+-- vim.system({ 'pio', 'run', '-t', 'compiledb', '-s', '-e', env }, { text = true }, function(obj)
+-- vim.schedule(function()
+-- target.isBusy = false
+--
+-- if obj.code == 0 then
+-- vim.schedule(function ()
+-- M.pio_refresh(function()
+-- vim.notify('PIO platformio.ini change: compiledb update Success', vim.log.levels.INFO, { title = 'PlatformIO' })
+-- clangdRestart()
+-- end, 'PIO platformio.ini change: ')
+-- end)
+-- else
+-- local err = (obj.stderr and obj.stderr ~= '') and obj.stderr or 'Check PIO logs'
+-- vim.notify('PIO Build Failed: ' .. err, vim.log.levels.ERROR, { title = 'PlatformIO' })
+-- end
+-- _G.metadata.isBusy = false
+-- end)
+-- end)
+-- end
+--
+--INFO:
+--stylua: ignore
+--1.stop_watchers
+--=============================================================================
+function M.stop_watchers()
+ if not M.watcher_handles or (type(M.watcher_handles) ~= 'table') then M.watcher_handles = {} return end
+
+ for _, handle in ipairs(M.watcher_handles) do
+ if handle and not handle:is_closing() then
+ handle:stop()
+ handle:close() -- CRITICAL: This allows Neovim to quit instantly
+ end
+ end
+ M.watcher_handles = {}
+end
+
+--INFO:
+--stylua: ignore
+--2.watcher cleanup
+--=============================================================================
+function M.cleanup()
+ M.stop_watchers()
+ if debounce_timer and not debounce_timer:is_closing() then
+ debounce_timer:stop()
+ debounce_timer:close()
+ end
+end
+
+-- Force cleanup when leaving Neovim to prevent :qa lag
+vim.api.nvim_create_autocmd('VimLeavePre', {
+ callback = function()
+ M.cleanup()
+ end,
+})
+
+--INFO:
+--stylua: ignore
+--3. MAIN WATCHER: Efficient Folder Monitoring
+--=============================================================================
+local function watch_file(target, callback)
+ local folder_path = target.path:match('(.*[/\\])')
+ local target_filename = target.path:match('[^/\\]+$')
+
+ local handle = uv.new_fs_event()
+ if not handle then return end
+
+ handle:start(folder_path, {}, function(err, filename)
+ if err then return end
+
+ -- Early Exit Filters
+ if target.isBusy or (filename and filename ~= target_filename) then return end
+
+ -- local f = io.open(target.path, "r")
+ -- if f then f:close()
+ -- else return end -- Not readable (protected, locked, or missing)
+
+ if not uv.fs_access(target.path, 'R') then return end
+
+ -- Protected Execution
+ local ok, result = pcall(function()
+ local stat = uv.fs_stat(target.path)
+ if not stat or stat.mtime.sec <= last_mtime then return end
+
+ vim.schedule(function()
+ if debounce_timer then
+ debounce_timer:stop()
+ local retries = 0
+ local max_retries = 15 -- 15 seconds max wait
+
+ local function attempt_callback()
+ -- Check if busy (checks both local M and global _G)
+ if target.isBusy then --or (_G.metadata and _G.metadata.isBusy) then
+ if retries < max_retries then
+ retries = retries + 1
+ debounce_timer:start(1000, 0, vim.schedule_wrap(attempt_callback))
+ return
+ end
+ vim.notify('PIO: Sync timed out (busy)', vim.log.levels.ERROR)
+ return
+ end
+
+ -- Final validation & run
+ local final_stat = uv.fs_stat(target.path)
+ if final_stat and final_stat.mtime.sec > last_mtime then
+ last_mtime = final_stat.mtime.sec
+ callback(target)
+ end
+ end
+
+ debounce_timer:start(1000, 0, vim.schedule_wrap(attempt_callback))
+ end
+ end)
+ end)
+
+ if not ok then
+ vim.schedule(function()
+ vim.notify('PIO Watcher Error: ' .. tostring(result), vim.log.levels.ERROR)
+ end)
+ end
+ end)
+
+ table.insert(M.watcher_handles, handle)
+ return handle
+end
+
+--INFO:
+--stylua: ignore
+--4. start_watches
+--=============================================================================
+function M.start_watchers()
+ -- Clean up any existing watchers first to prevent duplicates
+ if next(M.watcher_handles) then M.stop_watchers() end
+
+ local project_root = vim.uv.cwd() -- Use dynamic CWD instead of hardcoded path
+
+ local targets = {
+ { -- watcher for platformio.ini
+ name = 'ini',
+ isBusy = false,
+ last_hash = '',
+ path = vim.misc.joinPath(project_root, 'platformio.ini'),
+ cb = function(self)
+ if self.isBusy then return end
+ if _G.metadata.isBusy == true then return end
+ local new_hash = get_hash(self.path) or ''
+ if new_hash and new_hash ~= self.last_hash then
+ self.last_hash = new_hash
+ local env = vim.pio.get_active__env()
+ if not env then return end
+ self.isBusy = true
+ vim.notify('PIO platformio.ini change: compiledb update ...', vim.log.levels.INFO, { title = 'PlatformIO' })
+ vim.system({ 'pio', 'run', '-t', 'compiledb', '-s', '-e', env }, { text = true }, function(obj)
+ vim.schedule(function()
+ if obj.code == 0 then
+ vim.schedule(function ()
+ M.pio_refresh(function()
+ vim.notify('PIO platformio.ini change: compiledb update Success', vim.log.levels.INFO, { title = 'PlatformIO' })
+ clangdRestart()
+ end, 'PIO platformio.ini change: ')
+ end)
+ else
+ local err = (obj.stderr and obj.stderr ~= '') and obj.stderr or 'Check PIO logs'
+ vim.notify('PIO Build Failed: ' .. err, vim.log.levels.ERROR, { title = 'PlatformIO' })
+ end
+ self.isBusy = false
+ end)
+ end)
+ -- M.run_compiledb(self) -- Smart: Auto-update DB if config changes
+ end
+ end,
+ },
+ { -- watcher for ./.pio/build/projct.checksum
+ name = 'checksum',
+ isBusy = false,
+ path = vim.misc.joinPath(project_root, '.pio', 'build', 'project.checksum'), --checksum_path
+ cb = function(self)
+ if self.isBusy then return end
+ local ok, current_checksum = vim.misc.readFile(self.path)
+ -- Check if we should exit early
+ if ok and type(current_checksum) == 'string' and current_checksum ~= '' then
+ if current_checksum == _G.metadata.last_projectChecksum then
+ return
+ end
+
+ self.isBusy = true
+ vim.defer_fn(function ()
+ M.pio_refresh(function()
+ self.isBusy = false
+ vim.notify('PIO checksum: Metadata synced', vim.log.levels.INFO)
+ clangdRestart()
+ end, 'PIO checksum: ')
+ end, 500)
+ end
+ end
+ },
+ }
+
+ for _, target in ipairs(targets) do
+ --[[ wrap the callback in a small anonymous function,
+ so it passes the target (self) back into it.]]
+ watch_file(target, target.cb)
+ end
+end
+
+--INFO: 6. Exported setup function
+--stylua: ignore
+--=============================================================================
+function M.init()
+ local config = require('nvimpio').config
+ if config.lspClangd.enabled == true then
+ vim.notify('PIO start: initialize', vim.log.levels.INFO)
+
+ -- activate meta save and upload and env switch
+ local metadata = require('nvimpio.pio.metadata')
+ metadata.load_project_config()
+
+ require('nvimpio.lspConfig.clangd')
+ if config.lspClangd.attach.enabled then
+ require('nvimpio.lspConfig.attach')
+ end
+
+ -- Always start the watcher so it can catch a future 'pio init'
+ M.start_watchers()
+
+ -- boilerplate_gen([[platformio.ini]], vim.g.platformioRootDir)
+ -- If the file already exists, do an initial sync
+ if vim.fn.filereadable(vim.uv.cwd() .. '/platformio.ini') == 1 then
+ ----------------------------------------------------------------------------------------
+ --INFO: create clangd required files
+ -----------------------------------------------------------------------------------------
+ -- boilerplate_gen([[.clangd]], vim.g.platformioRootDir)
+ -- boilerplate_gen([[.clangd]], vim.fs.joinpath(vim.env.XDG_CONFIG_HOME, 'clangd'), 'config.yaml')
+ -- boilerplate_gen([[.clangd]], _G.metadata.core_dir)
+ boilerplate.core_dir = _G.metadata.core_dir
+ boilerplate_gen([[.clang-format]], vim.g.platformioRootDir)
+ ---------------------------------------------------------------------------------
+ -- M.run_compiledb() -- Smart: Auto-update DB if config changes
+ M.pio_refresh(function()
+ -- vim.schedule(function()
+ -- lsp_restart('clangd')
+ -- end)
+ end, 'PIO start: ')
+ end
+ end
+end
+
+return M
diff --git a/lua/nvimpio/pioCommands.lua b/lua/nvimpio/pioCommands.lua
new file mode 100644
index 00000000..835b715f
--- /dev/null
+++ b/lua/nvimpio/pioCommands.lua
@@ -0,0 +1,112 @@
+local M = {}
+
+-- local misc = require('nvimpio.utils.misc')
+local ToggleTerminal = require('nvimpio.utils.term').ToggleTerminal
+local misc = vim.misc
+
+-- stylua: ignore
+--INFO: PioLSP
+------------------------------------------------------
+function M.piolsp()
+ require('nvimpio.lspConfig.tools').clangdRestart()
+end
+
+-- stylua: ignore
+--INFO: Piocmd(h/f)
+------------------------------------------------------
+function M.piocmd(cmd_table, direction)
+ if not misc.pio_install_check() then return end
+
+ misc.cd_pioini()
+
+ if cmd_table[1] == '' then ToggleTerminal('', direction)
+ else
+ local cmd = 'pio '
+ for _, v in pairs(cmd_table) do cmd = cmd .. ' ' .. v end
+ ToggleTerminal(cmd, direction)
+ end
+end
+
+-- stylua: ignore
+--INFO: Piodebug
+------------------------------------------------------
+function M.piodebug(args_table)
+ if not misc.pio_install_check() then return end
+
+ misc.cd_pioini()
+
+ local command = 'pio debug --interface=gdb -- -x .pioinit'
+ -- local command = string.format('pio debug --interface=gdb -- -x .pioinit %s', utils.extra)
+ ToggleTerminal(command, 'float')
+end
+
+-- stylua: ignore
+--INFO: Piomon
+------------------------------------------------------
+function M.piomon(args_table)
+ if not misc.pio_install_check() then return end
+
+ misc.cd_pioini()
+
+ local command = nil
+ if #args_table == 0 then command = 'pio device monitor'
+ elseif #args_table == 1 then
+ local baud_rate = args_table[1]
+ command = string.format('pio device monitor -b %s', baud_rate)
+ elseif #args_table == 2 then
+ local baud_rate = args_table[1]
+ local port = args_table[2]
+ command = string.format('pio device monitor -b %s -p %s', baud_rate, port)
+ end
+
+ if command == nil then vim.notify('Usage: Piomon ', vim.log.levels.ERROR)
+ else ToggleTerminal(command, 'horizontal') end
+end
+
+-- stylua: ignore
+--INFO: Piorun
+------------------------------------------------------
+function M.piobuild()
+ misc.cd_pioini()
+ local command = 'pio run' -- .. utils.extra
+ ToggleTerminal(command, 'float')
+end
+
+function M.pioupload()
+ misc.cd_pioini()
+ local command = 'pio run --target upload' -- .. utils.extra
+ ToggleTerminal(command, 'float')
+end
+
+function M.piouploadfs()
+ misc.cd_pioini()
+ local command = 'pio run --target uploadfs' -- .. utils.extra
+ ToggleTerminal(command, 'float')
+end
+
+function M.pioclean()
+ misc.cd_pioini()
+ local command = 'pio run --target clean' -- .. utils.extra
+ ToggleTerminal(command, 'float')
+end
+
+function M.piorun(arg_table)
+ if not misc.pio_install_check() then
+ return
+ end
+ if arg_table[1] == '' then
+ M.pioupload()
+ elseif arg_table[1] == 'upload' then
+ M.pioupload()
+ elseif arg_table[1] == 'uploadfs' then
+ M.piouploadfs()
+ elseif arg_table[1] == 'build' then
+ M.piobuild()
+ elseif arg_table[1] == 'clean' then
+ M.pioclean()
+ else
+ vim.notify('Invalid argument: build, upload, uploadfs or clean', vim.log.levels.WARN)
+ end
+end
+
+return M
diff --git a/lua/nvimpio/pioinit.lua b/lua/nvimpio/pioinit.lua
new file mode 100644
index 00000000..76896134
--- /dev/null
+++ b/lua/nvimpio/pioinit.lua
@@ -0,0 +1,159 @@
+local pickers = require('telescope.pickers')
+local finders = require('telescope.finders')
+local actions = require('telescope.actions')
+local action_state = require('telescope.actions.state')
+local previewers = require('telescope.previewers')
+local telescope_conf = require('telescope.config').values
+local themes = require('telescope.themes')
+local pio = require('nvimpio.pio.upkeep')
+
+local wizard_data = {}
+
+-- Visual Notifications
+local function notify(msg, level)
+ vim.notify('PIO init+db: ' .. msg, level or vim.log.levels.INFO)
+end
+
+-- Reusable Small Menu for Yes/No and Frameworks
+local function small_menu(title, results, callback)
+ pickers
+ .new(
+ themes.get_dropdown({
+ prompt_title = title,
+ layout_config = { width = 0.3, height = 0.25 },
+ previewer = false,
+ }),
+ {
+ finder = finders.new_table({ results = results }),
+ sorter = telescope_conf.generic_sorter({}),
+ attach_mappings = function(prompt_bufnr)
+ actions.select_default:replace(function()
+ local selection = action_state.get_selected_entry()
+ actions.close(prompt_bufnr)
+ if selection then
+ callback(selection[1])
+ end
+ end)
+ return true
+ end,
+ }
+ )
+ :find()
+end
+
+-- FINAL STEP: Construction & Sequence Execution
+local function finalize_setup()
+ -- local pio = require('nvimpio.pio.upkeep')
+
+ local sample_flag = wizard_data.sample == 'Yes' and ' --sample-code' or ''
+ local init_cmd = string.format('pio project init --ide vim --board %s -O "framework=%s"%s', wizard_data.board_id, wizard_data.framework, sample_flag)
+
+ local db_cmd = string.format('pio run -t compiledb -e %s', wizard_data.board_id)
+ local commands = { init_cmd, db_cmd }
+ local final_cb = pio.handlePioinitDb
+
+ -- local commands = { init_cmd }
+ -- local final_cb = pio.handlePioinit
+
+ notify('Starting project setup for ' .. wizard_data.board_id .. '...')
+ pio.run_sequence({ cmnds = commands, cb = final_cb })
+end
+
+--- SEQUENTIAL STEPS ---
+
+-- Step 4: CompileDB
+-- local function pick_compiledb()
+-- small_menu('Generate Compilation Database (LSP)?', { 'Yes', 'No' }, function(choice)
+-- wizard_data.use_compiledb = choice
+-- finalize_setup()
+-- end)
+-- end
+
+-- Step 3: Sample Code
+local function pick_sample()
+ small_menu('Include Sample Code?', { 'Yes', 'No' }, function(choice)
+ wizard_data.sample = choice
+ -- pick_compiledb()
+ finalize_setup()
+ end)
+end
+
+-- Step 2: Framework
+local function pick_framework(board_details)
+ small_menu('Select Framework', board_details.frameworks, function(choice)
+ wizard_data.framework = choice
+ pick_sample()
+ end)
+end
+
+-- Step 1: Board (Entry Point)
+local function pick_board(json_data)
+ pickers
+ .new({}, {
+ prompt_title = 'Select Board',
+ -- Define the layout behavior
+ layout_strategy = 'horizontal',
+ layout_config = {
+ width = 0.9, -- Overall width of the Telescope window (90% of screen)
+ preview_width = 0.70, -- 65% of the window goes to "Board Details", leaving 25% for results
+ },
+ finder = finders.new_table({
+ results = json_data,
+ entry_maker = function(entry)
+ return {
+ value = entry,
+ display = entry.name or entry.id,
+ ordinal = (entry.name or '') .. ' ' .. (entry.id or ''),
+ }
+ end,
+ }),
+ previewer = previewers.new_buffer_previewer({
+ title = 'Board Details',
+ define_preview = function(self, entry)
+ local content = vim.split(vim.inspect(entry.value), '\n')
+ vim.api.nvim_buf_set_lines(self.state.bufnr, 0, -1, false, content)
+ vim.api.nvim_set_option_value('filetype', 'lua', { buf = self.state.bufnr })
+ end,
+ }),
+ sorter = telescope_conf.generic_sorter({}),
+ attach_mappings = function(prompt_bufnr)
+ actions.select_default:replace(function()
+ local selection = action_state.get_selected_entry()
+ actions.close(prompt_bufnr)
+ -- wizard_data.board_id = selection.value.id
+ -- pick_framework(selection.value) -- Next step
+ if selection then
+ wizard_data.board_id = selection.value.id
+ pick_framework(selection.value)
+ end
+ end)
+ return true
+ end,
+ })
+ :find()
+end
+
+-- Entry point
+local function launch_project_init()
+ wizard_data = {} -- Reset state
+ notify('Fetching board database...')
+
+ local handle = io.popen('pio boards --json-output')
+ if not handle then
+ return
+ end
+ local result = handle:read('*a')
+ handle:close()
+
+ local ok, json_data = pcall(vim.json.decode, result)
+ if not ok or type(json_data) ~= 'table' then
+ notify('Failed to parse board data.', vim.log.levels.ERROR)
+ return
+ end
+
+ pick_board(json_data)
+end
+
+return {
+ pioinit = launch_project_init,
+}
diff --git a/lua/nvimpio/piolib.lua b/lua/nvimpio/piolib.lua
new file mode 100644
index 00000000..e7b4c8ae
--- /dev/null
+++ b/lua/nvimpio/piolib.lua
@@ -0,0 +1,207 @@
+local M = {}
+
+local curl = require('plenary.curl')
+local pickers = require('telescope.pickers')
+local finders = require('telescope.finders')
+local entry_display = require('telescope.pickers.entry_display')
+local make_entry = require('telescope.make_entry')
+local conf = require('telescope.config').values
+local actions = require('telescope.actions')
+local action_state = require('telescope.actions.state')
+local misc = require('nvimpio.utils.misc')
+local previewers = require('telescope.previewers')
+
+local libentry_maker = function(opts)
+ local displayer = entry_display.create({
+ separator = '▏',
+ items = {
+ { width = 50 },
+ { width = 50 },
+ { remaining = true },
+ },
+ })
+
+ local make_display = function(entry)
+ return displayer({
+ entry.value.name,
+ entry.value.owner,
+ entry.value.description,
+ })
+ end
+
+ return function(entry)
+ return make_entry.set_default_entry_mt({
+ value = {
+ name = entry.name,
+ owner = entry.owner.username,
+ description = entry.description,
+ data = entry,
+ },
+ ordinal = entry.name .. ' ' .. entry.owner.username .. ' ' .. entry.description,
+ display = make_display,
+ }, opts)
+ end
+end
+
+-- stylua: ignore
+local function pick_library(json_data)
+ local opts = {}
+ pickers.new(opts, {
+ prompt_title = 'Libraries',
+ layout_config = {
+ width = 0.9, -- Overall width of the Telescope window (90% of screen)
+ preview_width = 0.60, -- 65% of the window goes to "Board Details", leaving 25% for results
+ },
+ finder = finders.new_table({
+ results = json_data['items'],
+ entry_maker = opts.entry_maker or libentry_maker(opts),
+ }),
+ attach_mappings = function(prompt_bufnr, _)
+ actions.select_default:replace(function()
+ actions.close(prompt_bufnr)
+ local selection = action_state.get_selected_entry()
+ local pkg_name = selection['value']['owner'] .. '/' .. selection['value']['name']
+ -- local command = 'pio pkg install --library "' .. pkg_name .. '"'
+ -- command = command .. ' && pio run -t compiledb'
+
+ local pio = require('nvimpio.pio.upkeep')
+ pio.run_sequence({
+ cmnds = {'pio pkg install --library "' .. pkg_name .. '"'},
+ cb = pio.handlePiolib
+ --function () vim.notify('Piolib: Done', vim.log.levels.INFO) end
+ })
+ end)
+ return true
+ end,
+
+ previewer = previewers.new_buffer_previewer({
+ title = 'Package Info',
+ define_preview = function(self, entry, _)
+ local json = misc.strsplit(vim.inspect(entry['value']['data']), '\n')
+ local bufnr = self.state.bufnr
+ vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, json)
+ vim.api.nvim_set_option_value('filetype', 'lua', { buf = bufnr }) --fix deprecated function
+ vim.defer_fn(function()
+ local win = self.state.winid
+ vim.api.nvim_set_option_value('wrap', true, { scope = 'local', win = win })
+ vim.api.nvim_set_option_value('linebreak', true, { scope = 'local', win = win })
+ vim.api.nvim_set_option_value('wrapmargin', 2, { buf = bufnr })
+ end, 0)
+ end,
+ }),
+ sorter = conf.generic_sorter(opts),
+ }):find()
+end
+
+-- local function pick_library(json_data)
+-- local opts = {}
+--
+-- -- 1. Create a displayer for exactly 2 columns
+-- local displayer = entry_display.create({
+-- separator = " │ ",
+-- items = {
+-- { width = 25 }, -- Column 1: Owner (fixed width)
+-- { remaining = true }, -- Column 2: Library Name
+-- },
+-- })
+--
+-- -- 2. Define the display logic for each row
+-- local make_display = function(entry)
+-- return displayer({
+-- { entry.value.owner or "unknown", "TelescopeResultsVariable" },
+-- entry.value.name or "unnamed",
+-- })
+-- end
+--
+-- pickers.new(opts, {
+-- prompt_title = 'Libraries',
+-- layout_config = {
+-- width = 0.9, -- Overall width (90%)
+-- preview_width = 0.60, -- Wider preview (60%)
+-- },
+--
+--
+-- finder = finders.new_table({
+-- results = json_data['items'],
+-- entry_maker = function(entry)
+-- return {
+-- value = entry,
+-- display = make_display,
+-- -- Ordinal is used for searching/filtering
+-- ordinal = (entry.owner or '') .. ' ' .. (entry.name or ''),
+-- }
+-- end,
+-- }),
+-- attach_mappings = function(prompt_bufnr, _)
+-- actions.select_default:replace(function()
+-- actions.close(prompt_bufnr)
+-- local selection = action_state.get_selected_entry()
+-- local pkg_name = selection['value']['owner'] .. '/' .. selection['value']['name']
+--
+-- local pio = require('nvimpio.utils.pio')
+-- pio.run_sequence({
+-- cmnds = {'pio pkg install --library "' .. pkg_name .. '"'},
+-- cb = function () vim.notify('Piolib: Done', vim.log.levels.INFO) end
+-- })
+-- end)
+-- return true
+-- end,
+--
+-- --
+-- previewer = previewers.new_buffer_previewer({
+-- title = 'Package Info',
+-- define_preview = function(self, entry, _)
+-- local json = misc.strsplit(vim.inspect(entry['value']['data'] or entry['value']), '\n')
+-- local bufnr = self.state.bufnr
+-- local win = self.state.winid
+--
+-- vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, json)
+-- vim.api.nvim_set_option_value('filetype', 'lua', { buf = bufnr })
+--
+-- -- Apply wrapping to make the wide preview readable
+-- vim.api.nvim_set_option_value('wrap', true, { win = win })
+-- vim.api.nvim_set_option_value('linebreak', true, { win = win })
+-- end,
+-- }),
+-- sorter = conf.generic_sorter(opts),
+-- }):find()
+-- end
+
+function M.piolib(lib_arg_list)
+ if not misc.pio_install_check() then
+ return
+ end
+
+ local lib_str = ''
+
+ for _, v in pairs(lib_arg_list) do
+ lib_str = lib_str .. v .. '+'
+ end
+
+ local url = 'https://api.registry.platformio.org/v3/search'
+ local res = curl.get(url, {
+ insecure = true,
+ timeout = 20000,
+ headers = { content_type = 'application/json' },
+ query = {
+ query = lib_str,
+ limit = 30,
+ sort = 'popularity',
+ -- page = 1,
+ -- limit = 1,
+ },
+ })
+
+ if res['status'] == 200 then
+ local json_data = vim.json.decode(res['body'])
+
+ pick_library(json_data)
+ else
+ vim.notify(
+ 'API Request to platformio return HTTP code: ' .. res['status'] .. '\nplease run `curl -LI ' .. url .. '` for complete information',
+ vim.log.levels.ERROR
+ )
+ end
+end
+
+return M
diff --git a/lua/platformio/piolsserial.lua b/lua/nvimpio/piolsserial.lua
similarity index 75%
rename from lua/platformio/piolsserial.lua
rename to lua/nvimpio/piolsserial.lua
index 2ff7dede..29b01b40 100644
--- a/lua/platformio/piolsserial.lua
+++ b/lua/nvimpio/piolsserial.lua
@@ -1,5 +1,5 @@
local M = {}
-local utils = require('platformio.utils')
+local misc = require('nvimpio.utils.misc')
M.tty_list = {}
@@ -8,7 +8,7 @@ function M.parse_tty(lines)
M.tty_list[k] = nil
end
local json_data = vim.json.decode(lines[1])
- for key, value in pairs(json_data) do
+ for _, value in pairs(json_data) do
if value['description'] ~= 'n/a' then
table.insert(M.tty_list, { port = value['port'], description = value['description'] })
end
@@ -16,14 +16,14 @@ function M.parse_tty(lines)
end
function M.sync_ttylist()
- utils.async_shell_cmd({ 'platformio', 'device', 'list', '--json-output' }, M.parse_tty)
+ misc.async_shell_cmd({ 'platformio', 'device', 'list', '--json-output' }, M.parse_tty)
end
function M.sync_ttylist_await()
local done = false
local result = nil
- utils.async_shell_cmd({ 'platformio', 'device', 'list', '--json-output' }, function(lines, code)
+ misc.async_shell_cmd({ 'platformio', 'device', 'list', '--json-output' }, function(lines, code)
result = { lines = lines, code = code }
done = true
end)
diff --git a/lua/nvimpio/utils/misc.lua b/lua/nvimpio/utils/misc.lua
new file mode 100644
index 00000000..2aee7f94
--- /dev/null
+++ b/lua/nvimpio/utils/misc.lua
@@ -0,0 +1,482 @@
+---@class platformio.utils.misc
+
+local M = {}
+
+M.is_windows = jit.os == 'Windows'
+local uv = vim.uv or vim.loop
+
+M.devNul = M.is_windows and ' 2>./nul' or ' 2>/dev/null'
+-- M.extra = 'printf \'\\\\n\\\\033[0;33mPlease Press ENTER to continue \\\\033[0m\'; read'
+-- M.extra = ' && echo . && echo . && echo Please Press ENTER to continue'
+
+------------------------------------------------------
+--INFO:
+
+-- stylua: ignore
+function M.showMessage(msg)
+ local bufnr = vim.api.nvim_create_buf(false, true)
+ local text = ' ' .. msg .. ' '
+ vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '', text, '' })
+
+ local width, height = #text + 2, 3
+ local row = math.floor((vim.o.lines - height) / 2)
+ local col = math.floor((vim.o.columns - width) / 2)
+
+ local win_id = vim.api.nvim_open_win(bufnr, false, {
+ relative = 'editor',
+ row = row,
+ col = col,
+ width = width,
+ height = height,
+ style = 'minimal',
+ border = 'double',
+ zindex = 250,
+ })
+
+ -- Define the "Glow" colors
+ -- We use 'IncSearch' or 'CurSearch' for a bright, glowing look
+ local hl_on = 'Normal:IncSearch,FloatBorder:IncSearch'
+ local hl_off = 'Normal:NormalFloat,FloatBorder:NormalFloat'
+
+ -- Create a timer for the blinking effect
+ local blink_timer = uv.new_timer()
+ local is_on = true
+
+ if blink_timer then
+ blink_timer:start(
+ 0,
+ 500,
+ vim.schedule_wrap(function()
+ if vim.api.nvim_win_is_valid(win_id) then
+ vim.api.nvim_set_option_value('winhl', is_on and hl_on or hl_off, { scope = 'local', win = win_id })
+ is_on = not is_on
+ else
+ blink_timer:stop()
+ blink_timer:close()
+ end
+ end)
+ )
+ end
+
+ -- Return both so you can kill them later
+ return { win = win_id, timer = blink_timer }
+end
+
+function M.closeMessage(status_obj)
+ if status_obj then
+ if status_obj.timer then
+ status_obj.timer:stop()
+ status_obj.timer:close()
+ end
+ if status_obj.win and vim.api.nvim_win_is_valid(status_obj.win) then
+ vim.api.nvim_win_close(status_obj.win, true)
+ end
+ end
+end
+
+------------------------------------------------------
+--INFO:
+-- stylua: ignore
+function M.deleteFile(path)
+ local file = vim.fn.fnamemodify(path, ':t')
+ if vim.fn.filereadable(path) == 1 then
+ local success = vim.fn.delete(path)
+
+ if success == 0 then vim.notify('PlatformIO: ' .. file .. ' file removed', vim.log.levels.INFO)
+ else vim.notify('PlatformIO: Failed to delete ' .. file, vim.log.levels.ERROR) end
+ else vim.notify('PlatformIO: ' .. file .. ' file not found', vim.log.levels.WARN) end
+end
+
+------------------------------------------------------
+--INFO:
+-- Version-Safe Path Joining (Fallback for Neovim < 0.10.0)
+-- stylua: ignore
+M.joinPath = vim.fs.joinpath or function(...)
+ return table.concat({ ... }, '/'):gsub('//+', '/')
+end
+------------------------------------------------------
+--INFO:
+-- iterrative loop 48ms
+-- stylua: ignore
+function M.jsonFormat(root_data)
+ local buffer = {}
+ -- Stack stores: { val = item, lvl = depth, stage = "start"|"items", keys = {}, index = 0 }
+ local stack = { { val = root_data, lvl = 0, stage = 'start' } }
+
+ local function get_indent(lvl) return string.rep(' ', lvl) end
+
+ -- Full JSON Escape Table
+ local escapes = {
+ ['\\'] = '\\\\',
+ ['"'] = '\\"',
+ ['\b'] = '\\b',
+ ['\f'] = '\\f',
+ ['\n'] = '\\n',
+ ['\r'] = '\\r',
+ ['\t'] = '\\t',
+ }
+
+ while #stack > 0 do
+ local curr = stack[#stack]
+ local val, lvl = curr.val, curr.lvl
+ local indent = get_indent(lvl)
+
+ if type(val) == 'table' then
+ -- 1. Determine if Array or Object
+ local is_array = false
+
+ -- Check if it's explicitly marked as an array by the Neovim parser
+ local mt = getmetatable(val)
+ if mt and mt.__jsontype == 'array' then
+ is_array = true
+ -- If not marked, check if it has indexed items or is literally an empty table
+ elseif #val > 0 or next(val) == nil then
+ is_array = true
+ end
+
+ if curr.stage == 'start' then
+ table.insert(buffer, (is_array and '[' or '{') .. '\n')
+ curr.stage = 'items'
+ curr.keys = {}
+
+ -- 2. Collect and Sort Keys (CRITICAL for SHA256 stability)
+ if is_array then for i = 1, #val do table.insert(curr.keys, i) end
+ else
+ for k in pairs(val) do table.insert(curr.keys, k) end
+ table.sort(curr.keys, function(a, b) return tostring(a) < tostring(b) end)
+ end
+ curr.total = #curr.keys
+ curr.cursor = 1 -- Point to the first key
+ elseif curr.stage == 'items' then
+ if curr.cursor <= curr.total then
+ local key = curr.keys[curr.cursor]
+ local item = val[key]
+
+ -- Add comma for all but the first item
+ if curr.cursor > 1 then table.insert(buffer, ',\n') end
+
+ table.insert(buffer, get_indent(lvl + 1))
+ if not is_array then table.insert(buffer, '"' .. tostring(key) .. '": ') end
+
+ curr.cursor = curr.cursor + 1
+ -- Push next item to process
+ table.insert(stack, { val = item, lvl = lvl + 1, stage = 'start' })
+ else
+ -- 3. Close the block
+ table.insert(buffer, '\n' .. indent .. (is_array and ']' or '}'))
+ table.remove(stack)
+ end
+ end
+ else
+ -- 4. Primitives (String, Number, Bool, Nil)
+ local output = ''
+ if val == nil or val == vim.NIL then output = 'null'
+ elseif val == vim.empty_dict then output = '{}'
+ elseif type(val) == 'boolean' then output = tostring(val)
+ elseif type(val) == 'string' then
+ -- A. Handle standard escapes (\n, \t, etc.)
+ local s = val:gsub('[\\"\b\f\n\r\t]', escapes)
+
+ -- B. Handle unprintable control characters (U+0000 to U+001F)
+ s = s:gsub('[%z\1-\31]', function(c)
+ return string.format('\\u%04x', string.byte(c))
+ end)
+
+ -- C. Normalize Windows paths to Unix for cross-platform SHA256 stability
+ -- We flip double-backslashes (\\) resulting from the escape to (/)
+ s = s:gsub('\\\\', '/')
+
+ output = '"' .. s .. '"'
+ else output = tostring(val) end
+ table.insert(buffer, output)
+ table.remove(stack)
+ end
+ end
+ return table.concat(buffer)
+end
+
+
+------------------------------------------------------
+--INFO:
+-- Example Usage
+-- local content = readFile("compile_commands.json")
+-- if content then local data = vim.json.decode(content) end
+-- stylua: ignore
+---@param path string
+function M.readFile(path)
+ -- 1. Check if file exists before opening to avoid "noisy" errors
+ local stat = uv.fs_stat(path)
+ if not stat then return false, 'File does not exist' end
+
+ -- 2. Open the file
+ local fd, err = uv.fs_open(path, 'r', 438)
+ if not fd then return false, err end
+
+ -- 3. Read the content (using stat.size from our check above)
+ local content, read_err = uv.fs_read(fd, stat.size, 0)
+ uv.fs_close(fd)
+
+ if read_err then return false, read_err end
+
+ return true, content
+end
+
+--INFO:
+-- Example
+-- local ok, err = writeFiile(path, json)
+-- if ok then print("Write complete!") end
+-- stylua: ignore
+---@param path string
+---@param data string
+---@param opts table
+function M.writeFile(path, data, opts)
+ -- opts.overwrite: boolean (default true)
+ -- opts.mkdir: boolean (default true)
+ opts = opts or { overwrite = true, mkdir = true }
+
+ local stat = uv.fs_stat(path)
+ -- 1. Overwrite protection
+ if opts.overwrite == false and stat then
+ return false, 'writeFile: File already exists'
+ end
+
+ -- 2. Recursive directory creation
+ if opts.mkdir ~= false then
+ local parent = vim.fn.fnamemodify(path, ':h')
+ if not stat or stat.type ~= 'directory' then
+ vim.fn.mkdir(parent, 'p', '0700')
+ end
+ end
+
+ --[[
+ Octal Decimal Permission
+ 0700 448 Owner only (Full)
+ 0755 493 Owner (Full), Others (Read/Execute)
+ 0666 438 Everyone (Read/Write) - Not recommended for folders
+ 'w' truncates existing, 'wx' fails if exists (extra safety)
+ ]]
+ -- 3. Open for writing ('w' flag truncates automatically)
+ local fd, err = uv.fs_open(path, 'w', 438)
+ if not fd then return false, 'writeFile: Open error: ' .. (err or 'unknown') end
+
+ -- 4. Robust Write Loop
+ -- Loop ensures all data is written even if it takes multiple chunks
+ local offset = 0
+ while offset < #data do
+ local bytes_written, w_err = uv.fs_write(fd, data:sub(offset + 1), offset)
+ if w_err then
+ uv.fs_close(fd)
+ return false, 'writeFile: Write error: ' .. w_err
+ end
+ offset = offset + bytes_written
+ end
+
+ -- 5. Force Sync (Crucial for your project.checksum watcher)
+ uv.fs_fsync(fd)
+ uv.fs_close(fd)
+
+ return true, 'Success'
+end
+
+
+------------------------------------------------------
+--[[
+Targets Windows paths, normalizes slashes, and fixes smashed PlatformIO paths.
+Cleans and repairs compiler flags in a command string.
+{ "-I", "-L", "-isystem", "-T", "-include" }
+1. Library Paths
+ -L: Specifies directories to search for library files (.a, .lib, .so).
+ Example: -L"C:\Users\lib"
+ -L"C:/Users/lib"
+ -l (lowercase L): While usually just a name (like -lmath), it can sometimes be a direct path to a specific file.
+2. Header Inclusion (Advanced)
+ -isystem: Similar to -I, but treats the directory as a "system" header (suppresses warnings). PlatformIO uses this heavily for framework headers (Arduino/ESP-IDF).
+ -include: Forces the compiler to include a specific file before anything else.
+ Example: -include "C:\project\config.h"
+ -iquote: Directories for headers wrapped in double quotes "".
+3. Output and Debugging
+ -o: The output path for the compiled object file or binary.
+ -fdebug-prefix-map=: Used to make builds reproducible by mapping absolute paths to relative ones in the debug symbols.
+4. Linker and Frameworks
+ -T: Path to a linker script (very common in embedded/PlatformIO for memory mapping).
+ Example: -T"C:\project\ld\esp32.ld"
+ -F: (macOS/iOS) Path to search for frameworks.
+]]
+-- stylua: ignore
+--- @param flags string: The raw command string (e.g., from compile_commands.json)
+--- @return string: The cleaned command string
+--INFO:
+function M.normalizeFlags(flags)
+ if not flags or flags == '' then
+ return ''
+ end
+
+ --1. Identify flags that look like paths.
+ -- Pattern explanation:
+ -- %- : Matches a literal hyphen (the start of a flag)
+ -- %S* : Matches zero or more non-space characters
+ -- \\ : Matches a literal backslash (identifies it as a Windows path)
+ -- %S* : Matches the rest of the non-space characters in that flag
+ local cleaned_cmd = flags:gsub('(%-%S-\\S*)', function(flag)
+ --2. Normalize Slashes
+ -- Replaces any number of backslashes (single \ or JSON-escaped \\) with one forward slash.
+ -- Forward slashes are safer and more portable for compilers like GCC/Clang.
+ flag = flag:gsub('[\\]+', '/')
+
+ --3. Heal PlatformIO "Smashed" Paths
+ -- Fixes the bug where PlatformIO expansions repeat the user home directory.
+ -- Example: /Users/name/.platformiopackages/toolchain -> /.platformio/packages/toolchain
+ flag = flag:gsub('/Users/[^/]+%.platformio/packages', '/.platformio/packages')
+
+ return flag
+ end)
+
+ -- Return only the result string (discarding the replacement count)
+ return cleaned_cmd
+end
+
+------------------------------------------------------
+--INFO:
+function M.normalizePath(path)
+ -- return path:gsub('[\\]+', '/'):gsub('[//]+', '/')
+ return path:gsub('[\\/]+', '/')
+end
+
+------------------------------------------------------
+--INFO:
+function M.strsplit(inputstr, del)
+ local t = {}
+ if type(inputstr) == 'string' and inputstr and inputstr ~= '' then
+ for str in string.gmatch(inputstr, '([^' .. del .. ']+)') do
+ table.insert(t, str)
+ end
+ end
+ return t
+end
+
+------------------------------------------------------
+--INFO:
+function M.check_prefix(str, prefix)
+ return str:sub(1, #prefix) == prefix
+end
+
+------------------------------------------------------
+--INFO:
+local function pathmul(n)
+ return '..' .. string.rep('/..', n)
+end
+
+local paths = { '.', '..', pathmul(1), pathmul(2), pathmul(3), pathmul(4), pathmul(5) }
+
+------------------------------------------------------
+--INFO:
+function M.file_exists(name)
+ local f = io.open(name, 'r')
+ if f ~= nil then
+ io.close(f)
+ return true
+ else
+ return false
+ end
+end
+
+------------------------------------------------------
+--INFO:
+function M.set_platformioRootDir()
+ if vim.g.platformioRootDir ~= nil then
+ return
+ end
+ for _, path in pairs(paths) do
+ if M.file_exists(path .. '/platformio.ini') then
+ vim.g.platformioRootDir = path
+ return
+ end
+ end
+ vim.notify('Could not find platformio.ini, run :Pioinit to create a new project', vim.log.levels.ERROR)
+end
+
+------------------------------------------------------
+--INFO:
+function M.cd_pioini()
+ -- M.set_platformioRootDir()
+ vim.cmd('cd ' .. vim.g.platformioRootDir)
+end
+
+------------------------------------------------------
+--INFO:
+function M.pio_install_check()
+ local handle = (jit.os == 'Windows') and assert(io.popen('where.exe pio 2>./nul')) or assert(io.popen('which pio 2>/dev/null'))
+ local pio_path = assert(handle:read('*a'))
+ handle:close()
+
+ if #pio_path == 0 then
+ vim.notify('Platformio not found in the path', vim.log.levels.ERROR)
+ return false
+ end
+ return true
+end
+
+------------------------------------------------------
+--INFO:
+function M.async_shell_cmd(cmd, callback)
+ local output = {}
+
+ vim.fn.jobstart(cmd, {
+ stdout_buffered = true,
+ stderr_buffered = false,
+
+ on_stdout = function(_, data)
+ if data then
+ for _, line in ipairs(data) do
+ if line ~= '' then
+ table.insert(output, line)
+ end
+ end
+ end
+ end,
+
+ on_exit = function(_, code)
+ callback(output, code)
+ end,
+ })
+end
+
+------------------------------------------------------
+--INFO:
+function M.shell_cmd_blocking(command)
+ local handle = io.popen(command, 'r')
+ if not handle then
+ return nil, 'failed to run command'
+ end
+
+ local result = handle:read('*a')
+ handle:close()
+
+ return result
+end
+
+------------------------------------------------------
+--INFO:
+function M.gitignore_lsp_configs(config_file)
+ local gitignore_path = vim.fs.joinpath(vim.g.platformioRootDir, '.gitignore')
+ local file = io.open(gitignore_path, 'r')
+ local pattern = '^%s*' .. vim.pesc(config_file) .. '%s*$'
+
+ if file then
+ for line in file:lines() do
+ if line:match(pattern) then
+ file:close()
+ return
+ end
+ end
+ file:close()
+ end
+
+ file = io.open(gitignore_path, 'a')
+ if file then
+ file:write(config_file .. '\n')
+ file:close()
+ end
+end
+
+return M
diff --git a/lua/platformio/utils.lua b/lua/nvimpio/utils/term.lua
similarity index 82%
rename from lua/platformio/utils.lua
rename to lua/nvimpio/utils/term.lua
index 3a7f302a..c62c9f59 100644
--- a/lua/platformio/utils.lua
+++ b/lua/nvimpio/utils/term.lua
@@ -1,398 +1,341 @@
-local M = {}
-
-local config = require('platformio').config
-
--- M.extra = 'printf \'\\\\n\\\\033[0;33mPlease Press ENTER to continue \\\\033[0m\'; read'
-M.extra = ' && echo . && echo . && echo Please Press ENTER to continue'
-
-function M.strsplit(inputstr, del)
- local t = {}
- if type(inputstr) == 'string' and inputstr and inputstr ~= '' then
- for str in string.gmatch(inputstr, '([^' .. del .. ']+)') do
- table.insert(t, str)
- end
- end
- return t
-end
-
-function M.check_prefix(str, prefix)
- return str:sub(1, #prefix) == prefix
-end
-
-local function pathmul(n)
- return '..' .. string.rep('/..', n)
-end
-
-------------------------------------------------------
-local is_windows = jit.os == 'Windows'
-
-M.devNul = is_windows and ' 2>./nul' or ' 2>/dev/null'
-
--- INFO: get current OS enter
-function M.enter()
- local shell = vim.o.shell
- if is_windows then
- return vim.fn.executable('pwsh') and '\r' or '\r\n'
- elseif shell:find('nu') then
- return '\r'
- else
- return '\n'
- end
-end
-
--- INFO: get previous window
-local function getPreviousWindow(orig_window)
- local prev = {
- orig_window = orig_window,
- term = nil, --active terminal
- cli = nil, --cli terminal
- mon = nil, --mon terminal
- float = false, --is active terminal direction float
- }
- local terms = require('toggleterm.terminal').get_all(true)
- if #terms ~= 0 then
- for i = 1, #terms do
- if terms[i].display_name and terms[i].display_name ~= '' and terms[i].display_name:find('pio', 1) then
- local name_splt = M.strsplit(terms[i].display_name, ':')
- if name_splt[1] == 'piocli' then
- prev.cli = terms[i]
- if terms[i].window == orig_window then
- ---@diagnostic disable-next-line: cast-local-type
- prev.orig_window = tonumber(name_splt[2]) -- set orig_window to the previous terminal onrig_window
- prev.term = terms[i]
- end
- if terms[i].direction == 'float' then
- prev.float = true
- end
- elseif name_splt[1] == 'piomon' then
- prev.mon = terms[i]
- if terms[i].window == orig_window then
- ---@diagnostic disable-next-line: cast-local-type
- prev.orig_window = tonumber(name_splt[2]) -- set orig_window to the previous terminal onrig_window
- prev.term = terms[i]
- end
- if terms[i].direction == 'float' then
- prev.float = true
- end
- end
- end
- end
- end
- return prev
-end
-
-------------------------------------------------------
--- INFO: Send command
-local function send(term, cmd)
- vim.fn.chansend(term.job_id, cmd .. M.enter())
- if vim.api.nvim_buf_is_loaded(term.bufnr) and vim.api.nvim_buf_is_valid(term.bufnr) then
- if term.window and vim.api.nvim_win_is_valid(term.window) then --vim.ui.term_has_open_win(term) then
- vim.api.nvim_set_current_win(term.window) -- terminal focus
- vim.api.nvim_buf_call(term.bufnr, function()
- local mode = vim.api.nvim_get_mode().mode
- if mode == 'n' or mode == 'nt' then
- vim.cmd('normal! G') -- normal command to Goto bottom of buffer (scroll)
- end
- end)
- end
- end
-end
-
-------------------------------------------------------
--- INFO: PioTermClose
-local function PioTermClose(t)
- local orig_window = tonumber(M.strsplit(t.display_name, ':')[2])
- -- close terminal window
- vim.api.nvim_win_close(t.window, true)
-
- -- go back to previous window
- if orig_window and vim.api.nvim_win_is_valid(orig_window) then
- vim.api.nvim_set_current_win(orig_window)
- else
- vim.api.nvim_set_current_win(0)
- end
-end
-
-------------------------------------------------------
--- INFO: ToggleTerminal
-function M.ToggleTerminal(command, direction, exit_callback)
- if type(exit_callback) ~= 'function' then
- exit_callback = function() end
- end
-
- local status_ok, _ = pcall(require, 'toggleterm')
- if not status_ok then
- vim.api.nvim_echo({ { 'toggleterm not found!', 'ErrorMsg' } }, true, {})
- return
- end
-
- local title = ''
- local pioOpts = {}
-
- -- INFO: set orig_window to current window, or if available get current toggleterm previous window
- local prev = getPreviousWindow(vim.api.nvim_get_current_win())
- local orig_window = prev.orig_window
-
- if string.find(command, ' monitor') then
- if prev.mon then -- INFO: if previous monitor terminal already opened ==> reopen
- local win_type = vim.fn.win_gettype(prev.mon.window)
- local win_open = win_type == '' or win_type == 'popup'
- if prev.mon.window and (win_open and vim.api.nvim_win_get_buf(prev.mon.window) == prev.mon.bufnr) then
- vim.api.nvim_set_current_win(prev.mon.window)
- else
- prev.mon:open()
- end
- return
- end
- title = 'Pio Monitor: [In normal mode press: q or :q to hide; :q! to quit; :PioTermList to list terminals]'
- pioOpts.display_name = 'piomon:' .. orig_window
- else -- INFO: if previous cli terminal already opened ==> reopen
- if prev.cli then
- prev.cli.on_close = function(t)
- local ow = tonumber(M.strsplit(t.display_name, ':')[2])
- if ow and vim.api.nvim_win_is_valid(ow) then
- vim.api.nvim_set_current_win(ow)
- else
- vim.api.nvim_set_current_win(0)
- end
- exit_callback()
- end
-
- local win_type = vim.fn.win_gettype(prev.cli.window)
- local win_open = win_type == '' or win_type == 'popup'
- if prev.cli.window and (win_open and vim.api.nvim_win_get_buf(prev.cli.window) == prev.cli.bufnr) then
- vim.api.nvim_set_current_win(prev.cli.window)
- else
- prev.cli:open()
- end
- vim.defer_fn(function()
- if command and command ~= '' then
- send(prev.cli, command)
- end
- end, 50) -- 50ms delay, adjust as needed
- return
- end
- title = 'Pio CLI> [In normal mode press: q or :q to hide; :q! to quit; :PioTermList to list terminals]'
- pioOpts.display_name = 'piocli:' .. orig_window
- end
- pioOpts.direction = direction
- ------------------------------------------------------
-
- -- INFO: termConfig table start
- local termConfig = {
- hidden = true, -- Start hidden, we'll open it explicitly
- hide_numbers = true,
- float_opts = {
- winblend = 0,
- width = function()
- return math.ceil(vim.o.columns * 0.85)
- end,
- height = function()
- return math.ceil(vim.o.lines * 0.85)
- end,
- highlights = {
- border = 'FloatBorder',
- background = 'NormalFloat',
- },
- },
- close_on_exit = false,
-
- -- INFO: on_open()
- on_open = function(t)
- -- Get properties of the 'Normal' highlight group (background of main editor)
- -- local hl = vim.api.nvim_get_hl(0, { name = 'PmenuSel' })
- -- local hl = { bg = '#e4cf0e', fg = '#0012d9' }
- local hl = { bg = '#80a3d4', fg = '#000000' }
-
- if hl then
- vim.api.nvim_set_hl(0, 'MyWinBar', { bg = hl.bg, fg = hl.fg })
-
- local winBartitle = '%#MyWinBar#' .. title .. '%*'
- vim.api.nvim_set_option_value('winbar', winBartitle, { scope = 'local', win = t.window })
-
- -- Following necessary to solve that some time winbar not showing
- vim.schedule(function()
- vim.api.nvim_set_option_value('winbar', winBartitle, { scope = 'local', win = t.window })
- end)
- end
- vim.keymap.set('t', '', [[k]], { buffer = t.bufnr })
- vim.keymap.set('n', '', [[a]], { buffer = t.bufnr })
-
- vim.keymap.set('n', 'q', function()
- PioTermClose(t)
- end, { desc = 'PioTermClose', buffer = t.bufnr })
-
- if config.debug then
- local name_splt = M.strsplit(t.display_name, ':')
- vim.api.nvim_echo({
- { 'ToggleTerm ', 'MoreMsg' },
- { '(Term name: ' .. name_splt[1] .. ')', 'MoreMsg' },
- { '(Prev win ID: ' .. name_splt[2] .. ')', 'MoreMsg' },
- { '(Term Win ID: ' .. t.window .. ')', 'MoreMsg' },
- { '(Term Buffer#: ' .. t.bufnr .. ')', 'MoreMsg' },
- { '(Term id: ' .. t.id .. ')', 'MoreMsg' },
- { '(Job ID: ' .. t.job_id .. ')', 'MoreMsg' },
- }, true, {})
- end
- end,
-
- -- INFO: on_close()
- on_close = function(t)
- orig_window = tonumber(M.strsplit(t.display_name, ':')[2])
- ---@diagnostic disable-next-line: param-type-mismatch
- if orig_window and vim.api.nvim_win_is_valid(orig_window) then
- vim.api.nvim_set_current_win(orig_window)
- else
- vim.api.nvim_set_current_win(0)
- end
- exit_callback()
- end,
-
- -- INFO: on_create() {
- on_create = function(t)
- local platformio = vim.api.nvim_create_augroup(M.strsplit(t.display_name, ':')[1], { clear = true })
-
- -- INFO: CmdlineLeave
- vim.api.nvim_create_autocmd('CmdlineLeave', {
- group = platformio,
- -- pattern = ':',
- buffer = t.bufnr,
- callback = function()
- if vim.v.event and not vim.v.event.abort and vim.v.event.cmdtype == ':' then
- local quit = vim.fn.getcmdline() == 'q'
- local quitbang = vim.fn.getcmdline() == 'q!'
- if quitbang or quit then
- local name_splt = M.strsplit(t.display_name, ':')
- if quitbang then
- if name_splt[1] == 'piomon' then -- monitor terminal
- local exit = vim.api.nvim_replace_termcodes('exit', true, true, true)
- send(t, exit)
- else -- cli terminal
- send(t, 'exit')
- end
- end
-
- orig_window = tonumber(name_splt[2])
- vim.schedule(function()
- -- go back to previous window
- if orig_window and vim.api.nvim_win_is_valid(orig_window) then
- vim.api.nvim_set_current_win(orig_window)
- else
- vim.api.nvim_set_current_win(0)
- end
- end)
- end
- end
- end,
- })
-
- -- INFO: BufUnload
- vim.api.nvim_create_autocmd('BufUnload', {
- group = platformio,
- desc = 'toggleterm buffer unloaded',
- buffer = t.bufnr,
- callback = function(args)
- vim.keymap.del('t', '', { buffer = args.buf })
- vim.keymap.del('n', '', { buffer = args.buf })
-
- -- clear autommmand when quit
- vim.api.nvim_clear_autocmds({ group = M.strsplit(t.display_name, ':')[1] })
- end,
- })
- end,
- }
- -- INFO: termConfig table end
-
- termConfig = vim.tbl_deep_extend('force', termConfig, pioOpts or {})
-
- -- INFO: create new terminal
- local terminal = require('toggleterm.terminal').Terminal:new(termConfig)
- if prev.term and prev.float then
- prev.term:close()
- end
- terminal:toggle()
- vim.defer_fn(function()
- if command and command ~= '' then
- send(terminal, command)
- end
- end, 50) -- 50ms delay, adjust as needed sgget
-end
-
-----------------------------------------------------------------------------------------
-
-local paths = { '.', '..', pathmul(1), pathmul(2), pathmul(3), pathmul(4), pathmul(5) }
-
-function M.file_exists(name)
- local f = io.open(name, 'r')
- if f ~= nil then
- io.close(f)
- return true
- else
- return false
- end
-end
-
-function M.get_pioini_path()
- for _, path in pairs(paths) do
- if M.file_exists(path .. '/platformio.ini') then
- return path
- end
- end
-end
-
-function M.cd_pioini()
- if vim.g.platformioRootDir ~= nil then
- vim.cmd('cd ' .. vim.g.platformioRootDir)
- else
- vim.cmd('cd ' .. M.get_pioini_path())
- end
-end
-
-function M.pio_install_check()
- local handel = (jit.os == 'Windows') and assert(io.popen('where.exe pio 2>./nul')) or assert(io.popen('which pio 2>/dev/null'))
- local pio_path = assert(handel:read('*a'))
- handel:close()
-
- if #pio_path == 0 then
- vim.notify('Platformio not found in the path', vim.log.levels.ERROR)
- return false
- end
- return true
-end
-
-function M.async_shell_cmd(cmd, callback)
- local output = {}
-
- vim.fn.jobstart(cmd, {
- stdout_buffered = true,
- stderr_buffered = false,
-
- on_stdout = function(_, data)
- if data then
- for _, line in ipairs(data) do
- if line ~= '' then
- table.insert(output, line)
- end
- end
- end
- end,
-
- on_exit = function(_, code)
- callback(output, code)
- end,
- })
-end
-
-function M.shell_cmd_blocking(command)
- local handle = io.popen(command, 'r')
- if not handle then
- return nil, 'failed to run command'
- end
-
- local result = handle:read('*a')
- handle:close()
-
- return result
-end
-
-return M
+local M = {}
+
+local is_windows = jit.os == 'Windows'
+M.devNul = is_windows and ' 2>./nul' or ' 2>/dev/null'
+-- M.extra = 'printf \'\\\\n\\\\033[0;33mPlease Press ENTER to continue \\\\033[0m\'; read'
+-- M.extra = ' && echo . && echo . && echo Please Press ENTER to continue'
+
+local config = require('nvimpio').config
+
+-- to fix require loop, toggleterm is using stdout_callback function in 'platformio.utils.pio'
+-- M.stdout_callback will be assigned by 'platformio.utils.pio'
+M.stdout_callback = nil
+
+------------------------------------------------------
+function M.strsplit(inputstr, del)
+ local t = {}
+ if type(inputstr) == 'string' and inputstr and inputstr ~= '' then
+ for str in string.gmatch(inputstr, '([^' .. del .. ']+)') do
+ table.insert(t, str)
+ end
+ end
+ return t
+end
+
+function M.check_prefix(str, prefix)
+ return str:sub(1, #prefix) == prefix
+end
+
+------------------------------------------------------
+
+-- INFO: get current OS enter
+function M.enter()
+ local shell = vim.o.shell
+ if is_windows then
+ return vim.fn.executable('pwsh') and '\r' or '\r\n'
+ elseif shell:find('nu') then
+ return '\r'
+ else
+ return '\n'
+ end
+end
+
+-- 1. Tell the LSP what a "Terminal" object looks like (simplified)
+---@class Terminal
+---@field id number
+---@field bufnr number
+---@field window number
+---@field close function
+---@field toggle function
+-- INFO: get previous window
+local function getPreviousWindow(orig_window)
+ -- 2. Define your context class
+ ---@class PioPrevContext
+ ---@field term Terminal|nil -- Handle for horizontal terminal
+ ---@field mon Terminal|nil
+ ---@field cli Terminal|nil
+ ---@field float boolean -- flag float terminal
+ ---@field orig_window number|nil
+ local prev = {
+ orig_window = orig_window,
+ term = nil, --active terminal
+ cli = nil, --cli terminal
+ mon = nil, --mon terminal
+ float = false, --is active terminal direction float
+ }
+ local terms = require('toggleterm.terminal').get_all(true)
+ if #terms ~= 0 then
+ for i = 1, #terms do
+ if terms[i].display_name and terms[i].display_name ~= '' and terms[i].display_name:find('pio', 1) then
+ local name_splt = M.strsplit(terms[i].display_name, ':')
+ if name_splt[1] == 'piocli' then
+ prev.cli = terms[i]
+ if terms[i].window == orig_window then
+ ---@diagnostic disable-next-line: cast-local-type
+ prev.orig_window = tonumber(name_splt[2]) -- set orig_window to the previous terminal onrig_window
+ prev.term = terms[i]
+ end
+ if terms[i].direction == 'float' then
+ prev.float = true
+ end
+ elseif name_splt[1] == 'piomon' then
+ prev.mon = terms[i]
+ if terms[i].window == orig_window then
+ ---@diagnostic disable-next-line: cast-local-type
+ prev.orig_window = tonumber(name_splt[2]) -- set orig_window to the previous terminal onrig_window
+ prev.term = terms[i]
+ end
+ if terms[i].direction == 'float' then
+ prev.float = true
+ end
+ end
+ end
+ end
+ end
+ return prev
+end
+
+------------------------------------------------------
+-- INFO: Send command
+local function send(term, cmd)
+ vim.fn.chansend(term.job_id, cmd .. M.enter())
+ if vim.api.nvim_buf_is_loaded(term.bufnr) and vim.api.nvim_buf_is_valid(term.bufnr) then
+ if term.window and vim.api.nvim_win_is_valid(term.window) then --vim.ui.term_has_open_win(term) then
+ vim.api.nvim_set_current_win(term.window) -- terminal focus
+ vim.api.nvim_buf_call(term.bufnr, function()
+ local mode = vim.api.nvim_get_mode().mode
+ if mode == 'n' or mode == 'nt' then
+ vim.cmd('normal! G') -- normal command to Goto bottom of buffer (scroll)
+ end
+ end)
+ end
+ end
+end
+
+------------------------------------------------------
+-- INFO: PioTermClose
+local function PioTermClose(t)
+ local orig_window = tonumber(M.strsplit(t.display_name, ':')[2])
+ -- close terminal window
+ vim.api.nvim_win_close(t.window, true)
+
+ -- go back to previous window
+ if orig_window and vim.api.nvim_win_is_valid(orig_window) then
+ vim.api.nvim_set_current_win(orig_window)
+ else
+ vim.api.nvim_set_current_win(0)
+ end
+end
+
+------------------------------------------------------
+-- INFO: ToggleTerminal
+function M.ToggleTerminal(command, direction)
+ local status_ok, _ = pcall(require, 'toggleterm')
+ if not status_ok then
+ vim.api.nvim_echo({ { 'toggleterm not found!', 'ErrorMsg' } }, true, {})
+ return
+ end
+
+ local title = ''
+ local pioOpts = {}
+
+ -- INFO: set orig_window to current window, or if available get current toggleterm previous window
+ local prev = getPreviousWindow(vim.api.nvim_get_current_win())
+ local orig_window = prev.orig_window
+
+ if string.find(command, ' monitor') then
+ if prev.mon then -- INFO: if previous monitor terminal already opened ==> reopen
+ prev.mon.display_name = 'piomon:' .. orig_window
+ local win_type = vim.fn.win_gettype(prev.mon.window)
+ local win_open = win_type == '' or win_type == 'popup'
+ if prev.mon.window and (win_open and vim.api.nvim_win_get_buf(prev.mon.window) == prev.mon.bufnr) then
+ vim.api.nvim_set_current_win(prev.mon.window)
+ else
+ prev.mon:open()
+ end
+ return
+ end
+ title = 'Pio Monitor: [In normal mode press: q or :q to hide; :q! to quit; :PioTermList to list terminals]'
+ pioOpts.display_name = 'piomon:' .. orig_window
+ pioOpts.id = 98
+ pioOpts.on_stdout = nil
+ else -- INFO: if previous cli terminal already opened ==> reopen
+ if prev.cli then
+ prev.cli.display_name = 'piocli:' .. orig_window
+ local win_type = vim.fn.win_gettype(prev.cli.window)
+ local win_open = win_type == '' or win_type == 'popup'
+ if prev.cli.window and (win_open and vim.api.nvim_win_get_buf(prev.cli.window) == prev.cli.bufnr) then
+ vim.api.nvim_set_current_win(prev.cli.window)
+ else
+ prev.cli:open()
+ end
+ vim.defer_fn(function()
+ if command and command ~= '' then
+ send(prev.cli, command)
+ end
+ end, 50) -- 50ms delay, adjust as needed
+ return
+ end
+ title = 'Pio CLI> [In normal mode press: q or :q to hide; :q! to quit; :PioTermList to list terminals]'
+ pioOpts.display_name = 'piocli:' .. orig_window
+ pioOpts.id = 99
+
+ -- INFO: on_stdout
+ pioOpts.on_stdout = function(t, job, exit_code)
+ if type(M.stdout_callback) == 'function' then
+ M.stdout_callback(t, job, exit_code)
+ end
+ end
+ end
+ pioOpts.direction = direction
+ ------------------------------------------------------
+
+ -- INFO: termConfig table start
+ local termConfig = {
+ hidden = true, -- Start hidden, we'll open it explicitly
+ hide_numbers = true,
+ float_opts = {
+ winblend = 0,
+ width = function()
+ return math.ceil(vim.o.columns * 0.85)
+ end,
+ height = function()
+ return math.ceil(vim.o.lines * 0.85)
+ end,
+ -- shell = vim.o.shell,
+ shell = vim.o.shell,
+ highlights = {
+ border = 'FloatBorder',
+ background = 'NormalFloat',
+ },
+ },
+ close_on_exit = false, --closeOnexit,
+
+ -- INFO: on_open()
+ on_open = function(t)
+ -- Get properties of the 'Normal' highlight group (background of main editor)
+ -- local hl = vim.api.nvim_get_hl(0, { name = 'PmenuSel' })
+ -- local hl = { bg = '#e4cf0e', fg = '#0012d9' }
+ local hl = { bg = '#80a3d4', fg = '#000000' }
+
+ if hl then
+ vim.api.nvim_set_hl(0, 'MyWinBar', { bg = hl.bg, fg = hl.fg })
+
+ local winBartitle = '%#MyWinBar#' .. title .. '%*'
+ vim.api.nvim_set_option_value('winbar', winBartitle, { scope = 'local', win = t.window })
+
+ -- Following necessary to solve that some time winbar not showing
+ vim.schedule(function()
+ vim.api.nvim_set_option_value('winbar', winBartitle, { scope = 'local', win = t.window })
+ end)
+ end
+ vim.keymap.set('t', '', [[k]], { buffer = t.bufnr })
+ vim.keymap.set('n', '', [[a]], { buffer = t.bufnr })
+
+ vim.keymap.set('n', 'q', function()
+ PioTermClose(t)
+ end, { desc = 'PioTermClose', buffer = t.bufnr })
+
+ if config.debug then
+ local name_splt = M.strsplit(t.display_name, ':')
+ vim.api.nvim_echo({
+ { 'ToggleTerm ', 'MoreMsg' },
+ { '(Term name: ' .. name_splt[1] .. ')', 'MoreMsg' },
+ { '(Prev win ID: ' .. name_splt[2] .. ')', 'MoreMsg' },
+ { '(Term Win ID: ' .. t.window .. ')', 'MoreMsg' },
+ { '(Term Buffer#: ' .. t.bufnr .. ')', 'MoreMsg' },
+ { '(Term id: ' .. t.id .. ')', 'MoreMsg' },
+ { '(Job ID: ' .. t.job_id .. ')', 'MoreMsg' },
+ }, true, {})
+ end
+ end,
+
+ -- INFO: on_close()
+ on_close = function(t)
+ orig_window = tonumber(M.strsplit(t.display_name, ':')[2])
+ ---@diagnostic disable-next-line: param-type-mismatch
+ if orig_window and vim.api.nvim_win_is_valid(orig_window) then
+ vim.api.nvim_set_current_win(orig_window)
+ else
+ vim.api.nvim_set_current_win(0)
+ end
+ end,
+
+ -- -- INFO: on_exit()
+ -- on_exit = function(_)
+ -- exit_callback()
+ -- end,
+
+ -- INFO: on_create() {
+ on_create = function(t)
+ local platformio = vim.api.nvim_create_augroup(M.strsplit(t.display_name, ':')[1], { clear = true })
+
+ -- INFO: CmdlineLeave
+ vim.api.nvim_create_autocmd('CmdlineLeave', {
+ group = platformio,
+ -- pattern = ':',
+ buffer = t.bufnr,
+ callback = function()
+ if vim.v.event and not vim.v.event.abort and vim.v.event.cmdtype == ':' then
+ local quit = vim.fn.getcmdline() == 'q'
+ local quitbang = vim.fn.getcmdline() == 'q!'
+ if quitbang or quit then
+ local name_splt = M.strsplit(t.display_name, ':')
+ if quitbang then
+ if name_splt[1] == 'piomon' then -- monitor terminal
+ local exit = vim.api.nvim_replace_termcodes('exit', true, true, true)
+ send(t, exit)
+ else -- cli terminal
+ send(t, 'exit')
+ end
+ end
+
+ orig_window = tonumber(name_splt[2])
+ vim.schedule(function()
+ -- go back to previous window
+ if orig_window and vim.api.nvim_win_is_valid(orig_window) then
+ vim.api.nvim_set_current_win(orig_window)
+ else
+ vim.api.nvim_set_current_win(0)
+ end
+ end)
+ end
+ end
+ end,
+ })
+
+ -- INFO: BufUnload
+ vim.api.nvim_create_autocmd('BufUnload', {
+ group = platformio,
+ desc = 'toggleterm buffer unloaded',
+ buffer = t.bufnr,
+ callback = function(args)
+ vim.keymap.del('t', '', { buffer = args.buf })
+ vim.keymap.del('n', '', { buffer = args.buf })
+
+ -- clear autommmand when quit
+ vim.api.nvim_clear_autocmds({ group = M.strsplit(t.display_name, ':')[1] })
+ end,
+ })
+ end,
+ }
+ -- INFO: termConfig table end
+
+ termConfig = vim.tbl_deep_extend('force', termConfig, pioOpts or {})
+
+ -- INFO: create new terminal
+ local terminal = require('toggleterm.terminal').Terminal:new(termConfig)
+ if prev.term and prev.float then
+ prev.term.close()
+ end
+ terminal:toggle()
+ vim.defer_fn(function()
+ if command and command ~= '' then
+ send(terminal, command)
+ end
+ end, 50) -- 50ms delay, adjust as needed sgget
+ return terminal
+end
+
+return M
+----------------------------------------------------------------------------------------
diff --git a/lua/platformio/boilerplate.lua b/lua/platformio/boilerplate.lua
deleted file mode 100644
index ee349a71..00000000
--- a/lua/platformio/boilerplate.lua
+++ /dev/null
@@ -1,57 +0,0 @@
-local M = {}
-local uv = vim.loop
-
-local boilerplate = {}
-
-boilerplate['arduino'] = {
- filename = 'main.cpp',
- content = [[
-#include
-
-void setup() {
-
-}
-
-void loop() {
-
-}
-]],
-}
-
-function M.boilerplate_gen(framework)
- local entry = boilerplate[framework]
- if not entry then
- return
- end
-
- local src_path = 'src'
- local stat = uv.fs_stat(src_path)
-
- if not stat or stat.type ~= 'directory' then
- return
- end
-
- local handle = uv.fs_scandir(src_path)
- if handle then
- while true do
- local name = uv.fs_scandir_next(handle)
- if not name then
- break
- end
- if name ~= '.' and name ~= '..' then
- return
- end
- end
- end
-
- local file_path = src_path .. '/' .. entry.filename
- local fd = uv.fs_open(file_path, 'w', 420)
- if not fd then
- return
- end
-
- uv.fs_write(fd, entry.content)
- uv.fs_close(fd)
-end
-
-return M
diff --git a/lua/platformio/piocmd.lua b/lua/platformio/piocmd.lua
deleted file mode 100644
index 0f5fde5d..00000000
--- a/lua/platformio/piocmd.lua
+++ /dev/null
@@ -1,22 +0,0 @@
-local utils = require('platformio.utils')
-local M = {}
-
-function M.piocmd(cmd_table, direction)
- if not utils.pio_install_check() then
- return
- end
-
- utils.cd_pioini()
-
- if cmd_table[1] == '' then
- utils.ToggleTerminal('', direction)
- else
- local cmd = 'pio '
- for _, v in pairs(cmd_table) do
- cmd = cmd .. ' ' .. v
- end
- utils.ToggleTerminal(cmd, direction)
- end
-end
-
-return M
diff --git a/lua/platformio/piodebug.lua b/lua/platformio/piodebug.lua
deleted file mode 100644
index d3786e54..00000000
--- a/lua/platformio/piodebug.lua
+++ /dev/null
@@ -1,16 +0,0 @@
-local utils = require('platformio.utils')
-local M = {}
-
-function M.piodebug(args_table)
- if not utils.pio_install_check() then
- return
- end
-
- utils.cd_pioini()
-
- local command = 'pio debug --interface=gdb -- -x .pioinit'
- -- local command = string.format('pio debug --interface=gdb -- -x .pioinit %s', utils.extra)
- utils.ToggleTerminal(command, 'float')
-end
-
-return M
diff --git a/lua/platformio/pioinit.lua b/lua/platformio/pioinit.lua
deleted file mode 100644
index a9843aab..00000000
--- a/lua/platformio/pioinit.lua
+++ /dev/null
@@ -1,143 +0,0 @@
-local M = {}
-
-local pickers = require('telescope.pickers')
-local finders = require('telescope.finders')
-local telescope_conf = require('telescope.config').values
-local actions = require('telescope.actions')
-local action_state = require('telescope.actions.state')
-local entry_display = require('telescope.pickers.entry_display')
-local make_entry = require('telescope.make_entry')
-local utils = require('platformio.utils')
-local previewers = require('telescope.previewers')
-local config = require('platformio').config
-local boilerplate_gen = require('platformio.boilerplate').boilerplate_gen
-
-local boardentry_maker = function(opts)
- local displayer = entry_display.create({
- separator = '▏',
- items = {
- { width = 35 },
- { width = 20 },
- { width = 15 },
- },
- })
-
- local make_display = function(entry)
- return displayer({
- entry.value.name,
- entry.value.vendor,
- entry.value.platform,
- })
- end
-
- return function(entry)
- return make_entry.set_default_entry_mt({
- value = {
- id = entry.id,
- name = entry.name,
- vendor = entry.vendor,
- platform = entry.platform,
- data = entry,
- },
- ordinal = entry.name .. ' ' .. entry.vendor .. ' ' .. entry.platform,
- display = make_display,
- }, opts)
- end
-end
-
-local function pick_framework(board_details)
- local opts = {}
- pickers
- .new(opts, {
- prompt_title = 'frameworks',
- finder = finders.new_table({
- results = board_details['frameworks'],
- }),
- attach_mappings = function(prompt_bufnr, _)
- actions.select_default:replace(function()
- actions.close(prompt_bufnr)
- local selection = action_state.get_selected_entry()
- local selected_framework = selection[1]
- local command = 'pio project init --board ' .. board_details['id'] .. ' --project-option "framework=' .. selected_framework .. '"'
- -- .. utils.extra
- utils.ToggleTerminal(command, 'float', function()
- vim.cmd(':PioLSP')
- boilerplate_gen(selected_framework)
- end)
- end)
- return true
- end,
- sorter = telescope_conf.generic_sorter(opts),
- })
- :find()
-end
-
-local function pick_board(json_data)
- local opts = {}
- pickers
- .new(opts, {
- prompt_title = 'Boards',
- finder = finders.new_table({
- results = json_data,
- entry_maker = opts.entry_maker or boardentry_maker(opts),
- }),
- attach_mappings = function(prompt_bufnr, _)
- actions.select_default:replace(function()
- actions.close(prompt_bufnr)
- local selection = action_state.get_selected_entry()
- pick_framework(selection['value']['data'])
- end)
- return true
- end,
- previewer = previewers.new_buffer_previewer({
- title = 'Board Info',
- define_preview = function(self, entry, _)
- local json = utils.strsplit(vim.inspect(entry['value']['data']), '\n')
- local bufnr = self.state.bufnr
- vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, json)
- vim.api.nvim_set_option_value('filetype', 'lua', { buf = bufnr }) --fix deprecated function
- vim.defer_fn(function()
- local win = self.state.winid
- vim.api.nvim_set_option_value('wrap', true, { scope = 'local', win = win })
- vim.api.nvim_set_option_value('linebreak', true, { scope = 'local', win = win })
- vim.api.nvim_set_option_value('wrapmargin', 2, { buf = bufnr })
- end, 0)
- end,
- }),
- sorter = telescope_conf.generic_sorter(opts),
- })
- :find()
-end
-
-function M.pioinit()
- if not utils.pio_install_check() then
- return
- end
-
- -- Read stdout
- local command = 'pio boards --json-output'
- local handel = io.popen(command .. utils.devNul)
- if not handel then
- return
- end
- local json_str = handel:read('*a')
- handel:close()
-
- if #json_str == 0 then
- -- read stderr
- handel = io.popen(command .. ' 2>&1')
- if not handel then
- return
- end
- local command_output = handel:read('*a')
- handel:close()
- vim.notify('Some error occured while executing `' .. command .. "`', command output: \n", vim.log.levels.WARN)
- print(command_output)
- return
- end
-
- local json_data = vim.json.decode(json_str)
- pick_board(json_data)
-end
-
-return M
diff --git a/lua/platformio/piolib.lua b/lua/platformio/piolib.lua
deleted file mode 100644
index 98b7e517..00000000
--- a/lua/platformio/piolib.lua
+++ /dev/null
@@ -1,127 +0,0 @@
-local M = {}
-
-local config = require('platformio').config
-local curl = require('plenary.curl')
-
-local pickers = require('telescope.pickers')
-local finders = require('telescope.finders')
-local entry_display = require('telescope.pickers.entry_display')
-local make_entry = require('telescope.make_entry')
-local conf = require('telescope.config').values
-local actions = require('telescope.actions')
-local action_state = require('telescope.actions.state')
-local utils = require('platformio.utils')
-local previewers = require('telescope.previewers')
-
-local libentry_maker = function(opts)
- local displayer = entry_display.create({
- separator = '▏',
- items = {
- { width = 20 },
- { width = 20 },
- { remaining = true },
- },
- })
-
- local make_display = function(entry)
- return displayer({
- entry.value.name,
- entry.value.owner,
- entry.value.description,
- })
- end
-
- return function(entry)
- return make_entry.set_default_entry_mt({
- value = {
- name = entry.name,
- owner = entry.owner.username,
- description = entry.description,
- data = entry,
- },
- ordinal = entry.name .. ' ' .. entry.owner.username .. ' ' .. entry.description,
- display = make_display,
- }, opts)
- end
-end
-
-local function pick_library(json_data)
- local opts = {}
- pickers
- .new(opts, {
- prompt_title = 'Libraries',
- finder = finders.new_table({
- results = json_data['items'],
- entry_maker = opts.entry_maker or libentry_maker(opts),
- }),
- attach_mappings = function(prompt_bufnr, _)
- actions.select_default:replace(function()
- actions.close(prompt_bufnr)
- local selection = action_state.get_selected_entry()
- local pkg_name = selection['value']['owner'] .. '/' .. selection['value']['name']
- local command = 'pio pkg install --library "' .. pkg_name .. '"'
- utils.ToggleTerminal(command, 'float', function()
- vim.cmd(':PioLSP')
- end)
- end)
- return true
- end,
-
- previewer = previewers.new_buffer_previewer({
- title = 'Package Info',
- define_preview = function(self, entry, _)
- local json = utils.strsplit(vim.inspect(entry['value']['data']), '\n')
- local bufnr = self.state.bufnr
- vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, json)
- vim.api.nvim_set_option_value('filetype', 'lua', { buf = bufnr }) --fix deprecated function
- vim.defer_fn(function()
- local win = self.state.winid
- vim.api.nvim_set_option_value('wrap', true, { scope = 'local', win = win })
- vim.api.nvim_set_option_value('linebreak', true, { scope = 'local', win = win })
- vim.api.nvim_set_option_value('wrapmargin', 2, { buf = bufnr })
- end, 0)
- end,
- }),
- sorter = conf.generic_sorter(opts),
- })
- :find()
-end
-
-function M.piolib(lib_arg_list)
- if not utils.pio_install_check() then
- return
- end
-
- local lib_str = ''
-
- for _, v in pairs(lib_arg_list) do
- lib_str = lib_str .. v .. '+'
- end
-
- local url = 'https://api.registry.platformio.org/v3/search'
- local res = curl.get(url, {
- insecure = true,
- timeout = 20000,
- headers = { content_type = 'application/json' },
- query = {
- query = lib_str,
- limit = 30,
- sort = 'popularity',
- -- page = 1,
- -- limit = 1,
- },
- })
-
- if res['status'] == 200 then
- local json_data = vim.json.decode(res['body'])
-
- pick_library(json_data)
- else
- vim.notify(
- 'API Request to platformio return HTTP code: ' .. res['status'] .. '\nplease run `curl -LI ' .. url .. '` for complete information',
- vim.log.levels.ERROR
- )
- end
-end
-
-return M
diff --git a/lua/platformio/piolsp.lua b/lua/platformio/piolsp.lua
deleted file mode 100644
index b122cf42..00000000
--- a/lua/platformio/piolsp.lua
+++ /dev/null
@@ -1,123 +0,0 @@
-local M = {}
-
-local utils = require('platformio.utils')
-local config = require('platformio').config
-
-local function escape_flags(flags)
- local escaped_flags = {}
-
- for _, flag in ipairs(flags) do
- local escaped = flag
- escaped = escaped:gsub('\\', '\\\\') -- Escape backslashes first
- escaped = escaped:gsub('"', '\\"') -- Escape double quotes (for -D macros)
- -- Escape parentheses (common in include paths)
- escaped = escaped:gsub('%(', '\\(')
- escaped = escaped:gsub('%)', '\\)')
- table.insert(escaped_flags, escaped)
- end
-
- return escaped_flags
-end
-
-local function process_ccls()
- local flags_allowed = { '%', '-W', '-std' }
-
- local f = io.open(vim.fs.joinpath(vim.g.platformioRootDir, '.ccls'), 'rb')
- if not f then
- vim.notify('.ccls file not found', vim.log.levels.ERROR)
- return {}
- end
-
- local compiler = f:read()
- local build_flags = { compiler }
-
- for line in f:lines() do
- if #line == 0 or string.sub(line, 1, 1) == '#' then
- goto continue
- end
-
- if utils.check_prefix(line, '-I') or utils.check_prefix(line, '-D') then
- table.insert(build_flags, line)
- end
- if utils.check_prefix(line, '%cpp') then
- splitted = utils.strsplit(line, ' ')
- for _, flag in ipairs(splitted) do
- for _, flag_check in ipairs(flags_allowed) do
- if utils.check_prefix(flag, flag_check) then
- table.insert(build_flags, flag)
- end
- end
- end
- end
-
- ::continue::
- end
-
- f:close()
-
- return escape_flags(build_flags)
-end
-
-local function gen_compile_commands(build_flags)
- local project_root = vim.g.platformioRootDir
- local build_cmd = ''
- for _, flag in ipairs(build_flags) do
- build_cmd = build_cmd .. flag .. ' '
- end
-
- local entry = { {
- directory = project_root,
- file = vim.fs.joinpath(project_root, 'src', 'main.cpp'),
- command = build_cmd,
- } }
-
- local f = io.open(vim.fs.joinpath(project_root, 'compile_commands.json'), 'w')
- f:write(vim.json.encode(entry, { indent = ' ', sort_keys = true }))
- f:close()
-end
-
-local function gitignore_lsp_configs(config_file)
- local gitignore_path = vim.fs.joinpath(vim.g.platformioRootDir, '.gitignore')
- local file = io.open(gitignore_path, 'r')
- local pattern = '^%s*' .. vim.pesc(config_file) .. '%s*$'
-
- if file then
- for line in file:lines() do
- if line:match(pattern) then
- file:close()
- return
- end
- end
- file:close()
- end
-
- file = io.open(gitignore_path, 'a')
- file:write(config_file .. '\n')
- file:close()
-end
-
-function M.gen_clangd_config()
- local build_flags = process_ccls()
- gen_compile_commands(build_flags)
-end
-
-function M.piolsp()
- if config.lsp == 'clangd' and config.clangd_source == 'compiledb' then
- utils.shell_cmd_blocking('pio run -t compiledb')
- gitignore_lsp_configs('compile_commands.json')
- else
- utils.shell_cmd_blocking('pio project init --ide=vim')
-
- if config.lsp == 'clangd' then
- M.gen_clangd_config()
- gitignore_lsp_configs('compile_commands.json')
- os.remove(vim.fs.joinpath(vim.g.platformioRootDir, '.ccls'))
- else
- gitignore_lsp_configs('.ccls')
- end
- end
- vim.notify('LSP config generation completed!', vim.log.levels.INFO)
- vim.cmd('LspRestart')
-end
-
-return M
diff --git a/lua/platformio/piomenu.lua b/lua/platformio/piomenu.lua
deleted file mode 100644
index 7657a12a..00000000
--- a/lua/platformio/piomenu.lua
+++ /dev/null
@@ -1,46 +0,0 @@
-local M = {}
-
-local icon = { icon = ' ', color = 'orange' } -- Assign platformio orange icon
-local wk_table = { mode = { 'n', 'v' } }
-
-local function traverseMenu(menu, wkey)
- for _, child_node in ipairs(menu) do
- if child_node.node == 'menu' then
- traverseMenu(child_node.items, wkey .. child_node.shortcut)
- table.insert(wk_table, { wkey .. child_node.shortcut, group = child_node.desc, icon = icon })
- elseif child_node.node == 'item' then
- table.insert(wk_table, {
- wkey .. child_node.shortcut,
- ' ' .. child_node.command .. '',
- desc = child_node.desc,
- icon = icon,
- })
- end
- end
-end
-
-function M.piomenu(config)
- if config.menu_key == nil then
- return
- end
-
- local ok, wk = pcall(require, 'which-key')
- if not ok then
- vim.api.nvim_echo({ { 'which-key plugin not found!', 'ErrorMsg' } }, true, {})
- return
- end
-
- wk.setup({
- preset = 'helix', --'modern', --'classic'
- })
- local Config = require('which-key.config')
- Config.sort = { 'order', 'group', 'manual', 'mod' }
-
- table.insert(wk_table, { config.menu_key, group = config.menu_name, icon = icon })
-
- traverseMenu(config.menu_bindings, config.menu_key)
-
- wk.add(wk_table)
-end
-
-return M
diff --git a/lua/platformio/piomon.lua b/lua/platformio/piomon.lua
deleted file mode 100644
index 0917abd3..00000000
--- a/lua/platformio/piomon.lua
+++ /dev/null
@@ -1,30 +0,0 @@
-local utils = require('platformio.utils')
-local M = {}
-
-function M.piomon(args_table)
- if not utils.pio_install_check() then
- return
- end
-
- utils.cd_pioini()
-
- local command = nil
- if #args_table == 0 then
- command = 'pio device monitor'
- elseif #args_table == 1 then
- local baud_rate = args_table[1]
- command = string.format('pio device monitor -b %s', baud_rate)
- elseif #args_table == 2 then
- local baud_rate = args_table[1]
- local port = args_table[2]
- command = string.format('pio device monitor -b %s -p %s', baud_rate, port)
- end
-
- if command == nil then
- vim.notify('Usage: Piomon ', vim.log.levels.ERROR)
- else
- utils.ToggleTerminal(command, 'horizontal')
- end
-end
-
-return M
diff --git a/lua/platformio/piorun.lua b/lua/platformio/piorun.lua
deleted file mode 100644
index 89020f20..00000000
--- a/lua/platformio/piorun.lua
+++ /dev/null
@@ -1,48 +0,0 @@
-local M = {}
-
-local utils = require('platformio.utils')
-
-function M.piobuild()
- utils.cd_pioini()
- local command = 'pio run' -- .. utils.extra
- utils.ToggleTerminal(command, 'float')
-end
-
-function M.pioupload()
- utils.cd_pioini()
- local command = 'pio run --target upload' -- .. utils.extra
- utils.ToggleTerminal(command, 'float')
-end
-
-function M.piouploadfs()
- utils.cd_pioini()
- local command = 'pio run --target uploadfs' -- .. utils.extra
- utils.ToggleTerminal(command, 'float')
-end
-
-function M.pioclean()
- utils.cd_pioini()
- local command = 'pio run --target clean' -- .. utils.extra
- utils.ToggleTerminal(command, 'float')
-end
-
-function M.piorun(arg_table)
- if not utils.pio_install_check() then
- return
- end
- if arg_table[1] == '' then
- M.pioupload()
- elseif arg_table[1] == 'upload' then
- M.pioupload()
- elseif arg_table[1] == 'uploadfs' then
- M.piouploadfs()
- elseif arg_table[1] == 'build' then
- M.piobuild()
- elseif arg_table[1] == 'clean' then
- M.pioclean()
- else
- vim.notify('Invalid argument: build, upload, uploadfs or clean', vim.log.levels.WARN)
- end
-end
-
-return M
diff --git a/mini_nvimPIO.lua b/mini_nvimPIO.lua
new file mode 100644
index 00000000..243bedee
--- /dev/null
+++ b/mini_nvimPIO.lua
@@ -0,0 +1,629 @@
+local isWindows = vim.fn.has('win32') == 1 --jit.os == 'Windows'
+local isMac = vim.fn.has('mac') == 1
+
+----------------------------------------------------------------------------------------
+-- INFO: Set options
+-- disable netrw at the very start of your init.lua
+vim.g.loaded_netrw = 1
+vim.g.loaded_netrwPlugin = 1
+
+-- optionally enable 24-bit colour
+vim.opt.termguicolors = true
+
+vim.opt['number'] = true
+vim.opt.autowrite = true -- Enable auto write
+
+-- only set clipboard if not in ssh, to make sure the OSC 52
+-- integration works automatically. Requires Neovim >= 0.10.0
+vim.opt.clipboard = vim.env.SSH_TTY and '' or 'unnamedplus' -- Sync with system clipboard
+
+vim.opt.tabstop = 2 -- Number of spaces tabs count for
+vim.opt.softtabstop = 2
+vim.opt.shiftround = true -- Round indent
+vim.opt.shiftwidth = 2 -- Size of an indent
+vim.opt.smartindent = true -- Insert indents automatically
+vim.opt.expandtab = true -- Use spaces instead of tabs
+
+vim.opt.smoothscroll = true
+vim.opt.foldmethod = 'expr'
+vim.opt.foldtext = ''
+vim.opt.fillchars = ''
+vim.opt.foldcolumn = '0'
+vim.opt.foldenable = true
+vim.opt.foldexpr = 'v:lua.vim.treesitter.foldexpr()'
+vim.opt.foldlevel = 99
+vim.opt.foldlevelstart = 99
+vim.opt.foldnestmax = 3
+
+vim.g.have_nerd_font = true
+vim.g.mapleader = ' '
+vim.g.maplocalleader = ' '
+
+if isWindows then
+ local pwsh = vim.fn.executable('pwsh') == 1 and 'pwsh' or 'powershell'
+ vim.opt.shell = pwsh
+ vim.opt.shellcmdflag =
+ '-NoLogo -NoProfile -ExecutionPolicy RemoteSigned -Command [Console]::InputEncoding=[Console]::OutputEncoding=[System.Text.UTF8Encoding]::new();'
+ vim.opt.shellredir = '2>&1 | Out-File -Encoding UTF8 %s; exit $LastExitCode'
+ vim.opt.shellpipe = '2>&1 | Out-File -Encoding UTF8 %s; exit $LastExitCode'
+ vim.opt.shellquote = ''
+ vim.opt.shellxquote = ''
+
+--elseif vim.fn.has("mac") == 1 then
+else
+ vim.g.shell = '/bin/bash' -- or '/bin/zsh', '/usr/bin/fish', etc.
+ vim.g.shellcmdflag = '-c' -- Executes the command passed as a string
+ vim.g.shellpipe = '|' -- Pipes output of external commands
+ vim.g.shellredir = '> ' -- Redirects output of external commands
+end
+
+vim.hl = vim.highlight
+vim.api.nvim_set_hl(0, 'PioStatus', {
+ fg = '#e0af68', -- Dark text
+ bg = '#11111b',
+ bold = true,
+})
+----------------------------------------------------------------------------------------
+-- INFO: Set diagnostic config
+vim.diagnostic.config({
+ virtual_lines = true,
+ update_in_insert = true,
+ underline = true,
+ severity_sort = true,
+ float = {
+ focusable = true,
+ style = 'minimal',
+ border = 'rounded',
+ source = true,
+ header = '',
+ prefix = '',
+ },
+ signs = {
+ text = {
+ [vim.diagnostic.severity.ERROR] = ' ',
+ [vim.diagnostic.severity.WARN] = ' ',
+ [vim.diagnostic.severity.HINT] = ' ',
+ [vim.diagnostic.severity.INFO] = ' ',
+ },
+ },
+})
+
+----------------------------------------------------------------------------------------
+-- INFO: Set nvim keymaps
+local keymap = function(mode, lhs, rhs, opts)
+ local options = { silent = true } --noremap = true by default in vim.keymap.set
+ if opts then
+ options = vim.tbl_extend('force', options, opts or {})
+ end
+ vim.keymap.set(mode, lhs, rhs, options)
+end
+
+--To toggle line wrapping in Neovim
+keymap('n', 'w', ':set wrap!', { desc = 'Toggle wrap' })
+
+keymap('n', 'gll', function()
+ vim.cmd.edit(vim.lsp.log.get_filename())
+end, { desc = 'open LSP [l]og' })
+-- Keybinds to make split navigation easier.
+-- Use CTRL+ to switch between windows
+-- See `:help wincmd` for a list of all window commands
+keymap('n', '', '', { desc = 'Move focus to the left window' })
+keymap('n', '', '', { desc = 'Move focus to the right window' })
+keymap('n', '', '', { desc = 'Move focus to the lower window' })
+keymap('n', '', '', { desc = 'Move focus to the upper window' })
+
+-- Resize with arrows
+keymap('n', '', ':resize -2')
+keymap('n', '', ':resize +2')
+keymap('n', '', ':vertical resize -2')
+keymap('n', '', ':vertical resize +2')
+
+keymap('n', 'bb', ':bprevious', { desc = '[B]efore Buffer' })
+keymap('n', 'ba', ':bnext', { desc = '[A]fter Buffer' })
+keymap('n', 'bs', ':ball', { desc = '[S]how AllOpened Buffers' })
+
+-- keymap('n', 'bd', 'bdelete', { desc = '[D]elete Buffer' })
+keymap('n', 'bd', function()
+ local bufnr = vim.api.nvim_get_current_buf()
+ local bufs = vim.fn.getbufinfo({ buflisted = 1 })
+
+ if #bufs <= 1 then
+ -- Create a new empty buffer
+ vim.cmd('enew')
+ else
+ -- Switch to the previous buffer
+ vim.cmd('bp')
+ end
+
+ -- Delete the buffer we started with (using pcall to ignore "No buffers deleted" errors)
+ pcall(vim.api.nvim_buf_delete, bufnr, { force = false })
+end, { desc = '[D]elete Buffer' })
+
+keymap('n', 'e', 'Neotree document_symbols', { desc = 'NeoTreeToggle' })
+keymap('n', '\\', 'Neotree toggle', { desc = 'NeoTreeToggle' })
+-- keymap('n', 'e', 'NvimTreeToggle', { desc = 'NvimTreeToggle' })
+-- keymap('n', '\\', 'NvimTreeToggle', { desc = 'NvimTreeToggle' })
+
+-- Keybinds to make split navigation easier.
+-- Use CTRL+ to switch between windows
+keymap('n', '', '', { desc = 'Move focus to the left window' })
+keymap('n', '', '', { desc = 'Move focus to the right window' })
+keymap('n', '', '', { desc = 'Move focus to the lower window' })
+keymap('n', '', '', { desc = 'Move focus to the upper window' })
+
+----------------------------------------------------------------------------------------
+----------------------------------------------------------------------------------------
+-- INFO: Set mini lazy config
+----------------------------------------------------------------------------------------
+-- stylua: ignore
+---[[
+local function setup_xdg_paths()
+ -- local isWindows = vim.fn.has('win32') == 1
+ -- local isMac = vim.fn.has('mac') == 1
+ local home = vim.env.HOME or vim.env.USERPROFILE or ''
+ local app_name = 'nvim-pio' -- pick a temp root
+
+ -- Helper to ensure we never gsub a nil and always use forward slashes
+ local function normalize(path)
+ return path:gsub('\\', '/')
+ end
+
+ -- 1. XDG_CONFIG_HOME (Settings/Configs)
+ if not vim.env.XDG_CONFIG_HOME then
+ local path = isWindows and (vim.env.LOCALAPPDATA or (home .. '/AppData/Local'))
+ or isMac and (home .. '/Library/Preferences')
+ or (home .. '/.config')
+ vim.env.XDG_CONFIG_HOME = normalize(vim.fs.joinpath(path, app_name))
+ end
+
+ -- 2. XDG_DATA_HOME (Large data/Databases)
+ if not vim.env.XDG_DATA_HOME then
+ local path = isWindows and (vim.env.LOCALAPPDATA or (home .. '/AppData/Local'))
+ or isMac and (home .. '/Library/Application Support')
+ or (home .. '/.local/share')
+ vim.env.XDG_DATA_HOME = normalize(vim.fs.joinpath(path, app_name))
+ end
+
+ -- 3. XDG_STATE_HOME (Logs/History/Persistent State)
+ if not vim.env.XDG_STATE_HOME then
+ local path = isWindows and (vim.env.LOCALAPPDATA or (home .. '/AppData/Local'))
+ or isMac and (home .. '/Library/Application Support')
+ or (home .. '/.local/state')
+ vim.env.XDG_STATE_HOME = normalize(vim.fs.joinpath(path, app_name))
+ end
+
+ -- 4. XDG_CACHE_HOME (Temporary/Disposable data)
+ if not vim.env.XDG_CACHE_HOME then
+ local path = isWindows and (vim.env.TEMP or (home .. '/AppData/Local/Temp'))
+ or isMac and (home .. '/Library/Caches')
+ or (home .. '/.cache')
+ vim.env.XDG_CACHE_HOME = normalize(vim.fs.joinpath(path, app_name))
+ end
+end
+
+setup_xdg_paths()
+---]]
+--[[
+local app_name = 'nvim-pio' -- pick a temp root
+local home = isWindows and vim.env.LOCALAPPDATA:gsub('\\', '/') or vim.env.HOME
+-- local home = vim.env.HOME or vim.env.USERPROFILE or ""
+home = home .. '/' .. app_name
+-- local home = vim.loop.os_tmpdir():gsub('\\', '/') .. '/' .. app_name
+
+-- vim.env.NVIM_APPNAME = app_name --isolated nvim
+vim.env.XDG_CONFIG_HOME = home .. (isWindows and '/config/' or '/.config')
+vim.env.XDG_DATA_HOME = home .. (isWindows and '/data/' or '/.local/share/')
+vim.env.XDG_STATE_HOME = home .. (isWindows and '/state/' or '/.local/state/')
+vim.env.XDG_CACHE_HOME = home .. (isWindows and '/cache/' or '/.cache/')
+--]]
+
+-- BOOTSTRAP (Use stdpath so it ALWAYS matches Neovim's internal logic)
+local lazypath = vim.fn.stdpath('data') .. '/lazy/lazy.nvim'
+
+if not (vim.uv or vim.loop).fs_stat(lazypath) then
+ print('Installing lazy.nvim to: ' .. lazypath)
+ vim.fn.system({
+ 'git',
+ 'clone',
+ '--filter=blob:none',
+ 'https://github.com/folke/lazy.nvim.git',
+ '--branch=stable',
+ lazypath,
+ })
+end
+
+-- ADD TO RUNTIME PATH (Crucial: makes 'require("lazy")' work)
+vim.opt.rtp:prepend(lazypath)
+
+----------------------------------------------------------------------------------------
+-- INFO: define plugins table
+local plugins = {
+ { 'windwp/nvim-autopairs', event = 'InsertEnter', config = true },
+
+ {
+ 'Saghen/blink.cmp',
+ dependencies = { 'rafamadriz/friendly-snippets' },
+ version = '1.*', -- Download pre-built binaries
+ opts = {
+ keymap = { preset = 'default' }, -- 'default', 'super-tab', or 'enter'
+ sources = {
+ default = { 'lsp', 'path', 'snippets', 'buffer' },
+ },
+ },
+ },
+
+ -- Recommended: Minimal statusline/tabline
+ {
+ 'nvim-lualine/lualine.nvim',
+ dependencies = {
+ 'nvim-tree/nvim-web-devicons',
+ config = function()
+ require('lualine').setup({
+ options = {
+ globalstatus = true, -- Single statusline for all windows
+ extensios = { 'neo-treee' },
+ },
+ -- This replaces the visual part of bufferline
+ tabline = {
+ lualine_a = {
+ {
+ 'buffers',
+ show_filename_only = true,
+ hide_filename_extension = false,
+ show_modified_status = true,
+ mode = 0, -- 0: Shows buffer name
+ max_length = vim.o.columns,
+ filetype_names = {
+ NvimTree = 'Explorer',
+ TelescopePrompt = 'Telescope',
+ },
+ },
+ },
+ },
+ })
+ end,
+ },
+ },
+
+ {
+ 'nvim-neo-tree/neo-tree.nvim',
+ branch = 'v3.x',
+ dependencies = {
+ 'nvim-lua/plenary.nvim',
+ 'nvim-tree/nvim-web-devicons',
+ 'MunifTanjim/nui.nvim',
+ },
+ opts = {
+ filesystem = {
+ -- use_libuv_file_watcher = true,
+ filtered_items = {
+ hide_dotfiles = false,
+ hide_gitignored = true,
+ hide_by_name = {
+ '.pio',
+ '.cache',
+ },
+ never_show = { -- Add any massive folders here
+ -- '.cache',
+ -- '.git',
+ 'node_modules',
+ -- 'build',
+ -- 'target',
+ },
+ },
+ },
+ },
+ },
+
+ {
+ 'batoaqaa/nvim-platformio.lua',
+ cond = function()
+ -- local platformioRootDir = (vim.fn.filereadable('platformio.ini') == 1) and vim.fn.getcwd() or nil
+ local platformioRootDir = (vim.fn.filereadable('platformio.ini') == 1) and vim.uv.cwd() or nil
+ if platformioRootDir and vim.fs.find('.pio', { path = platformioRootDir, type = 'directory' })[1] then
+ -- if platformio.ini file and .pio folder exist in cwd, enable plugin to install plugin (if not istalled) and load it.
+ vim.g.platformioRootDir = platformioRootDir
+ elseif (vim.uv or vim.loop).fs_stat(vim.env.XDG_DATA_HOME .. '/lazy/nvim-platformio.lua') == nil then
+ -- if nvim-platformio not installed, enable plugin to install it first time
+ -- vim.g.platformioRootDir = vim.fn.getcwd()
+ vim.g.platformioRootDir = vim.uv.cwd()
+ else -- if nvim-platformio.lua installed but disabled, create Pioinit command
+ vim.api.nvim_create_user_command('Pioinit', function() --available only if no platformio.ini and .pio in cwd
+ vim.api.nvim_create_autocmd('User', {
+ pattern = { 'LazyRestore', 'LazyLoad' },
+ once = true,
+ callback = function(args)
+ if args.match == 'LazyRestore' then
+ require('lazy').load({ plugins = { 'nvim-platformio.lua' } })
+ elseif args.match == 'LazyLoad' then
+ vim.notify('PlatformIO loaded', vim.log.levels.INFO, { title = 'PlatformIO' })
+ vim.cmd('Pioinit')
+ end
+ end,
+ })
+ -- vim.g.platformioRootDir = vim.fn.getcwd()
+ vim.g.platformioRootDir = vim.uv.cwd()
+ require('lazy').restore({ plguins = { 'nvim-platformio.lua' }, show = false })
+ end, {})
+ end
+ return vim.g.platformioRootDir ~= nil
+ end,
+ dependencies = {
+ { 'akinsho/toggleterm.nvim' },
+ { 'nvim-telescope/telescope.nvim' },
+ -- {
+ -- 'nvim-telescope/telescope.nvim',
+ -- tag = '0.1.8',
+ -- dependencies = { 'nvim-lua/plenary.nvim' },
+ -- },
+ { 'nvim-telescope/telescope-ui-select.nvim' },
+ { 'nvim-lua/plenary.nvim' },
+ { 'folke/which-key.nvim' },
+ {
+ 'mason-org/mason-lspconfig.nvim',
+ dependencies = {
+ { 'mason-org/mason.nvim' },
+ { 'folke/trouble.nvim' },
+ { 'j-hui/fidget.nvim' }, -- status bottom right
+ },
+ },
+ },
+ },
+}
+----------------------------------------------------------------------------------------
+
+----------------------------------------------------------------------------------------
+-- INFO: Install/config plugins
+require('lazy').setup(plugins, {
+ root = vim.fn.stdpath('data') .. '/lazy',
+ install = { missing = true },
+ ui = { border = 'rounded' },
+})
+
+----------------------------------------------------------------------------------------
+-- stylua: ignore
+if vim.fn.has('nvim-0.11') == 1 then
+ local json_format_group = vim.api.nvim_create_augroup('JsonFormat', { clear = true })
+ vim.api.nvim_create_autocmd('BufWritePre', {
+ group = json_format_group,
+ pattern = '*.json',
+ -- This runs 'python -m json.tool' on the current buffer content
+ -- It updates the buffer in-place before the file is written to disk
+ callback = function() vim.cmd('%!python -m json.tool') end,
+ })
+elseif vim.fn.has('nvim-0.12') == 1 then
+end
+
+----------------------------------------------------------------------------------------
+-- INFO: autocommand to Update lazy.nvim plugins in the background
+vim.api.nvim_create_autocmd('User', {
+ pattern = 'LazyVimStarted', -- Triggers after the UI enters and startup time is calculated
+ desc = 'Update lazy.nvim plugins in the background',
+ callback = function()
+ require('lazy').sync({
+ wait = false, -- Makes the operation asynchronous
+ show = false, -- Prevents the Lazy UI from automatically opening
+ })
+ -- You can add a notification here if you like
+ -- vim.notify("Lazy plugins sync started in background", vim.log.levels.INFO)
+ end,
+})
+
+-- AUTO-CLEANUP ON EXIT
+-- SELECTIVE CLEANUP ON EXIT (Keeps plugins, deletes temp files)
+-- stylua: ignore
+-- SELECTIVE CLEANUP FOR WINDOWS
+vim.api.nvim_create_autocmd("VimLeave", {
+ callback = function()
+ -- On Windows, we specifically want to wipe the shada and temp files
+ local state_path = vim.fn.stdpath("state")
+ local cache_path = vim.fn.stdpath("cache")
+
+ local targets = {
+ state_path .. "/shada", -- Delete history/marks
+ cache_path, -- Delete temp bytecode
+ }
+
+ for _, path in ipairs(targets) do
+ if vim.fn.isdirectory(path) == 1 then
+ local cmd = isWindows
+ and string.format('rmdir /s /q "%s"', path:gsub("/", "\\"))
+ or string.format('rm -rf "%s"', path)
+ vim.fn.jobstart(cmd, { detach = true })
+ end
+ end
+ end,
+})
+
+----------------------------------------------------------------------------------------
+-- INFO: set up python nvim venv (virtual environment 'nenv'), activaten.
+local platformio_core_dir, pynvim_env, pynvim_python, pynvim_lib, pynvim_bin, pynvim_activate
+if isWindows then
+ platformio_core_dir = vim.env.HOME .. '/.platformio'
+ pynvim_env = platformio_core_dir .. '/nenv'
+ pynvim_bin = pynvim_env .. '/Scripts'
+ pynvim_python = pynvim_bin .. '/python.exe'
+ pynvim_activate = pynvim_bin .. '/Activate.ps1'
+else
+ platformio_core_dir = vim.env.HOME .. '/.platformio'
+ pynvim_env = platformio_core_dir .. '/nenv'
+ pynvim_bin = pynvim_env .. '/bin'
+ pynvim_python = pynvim_bin .. '/python3'
+ pynvim_activate = pynvim_bin .. '/activate'
+end
+
+--Toolchain inclusion forced in Global Environment
+-- vim.uv.os_setenv('PLATFORMIO_SETTING_COMPILATIONDB_INCLUDE_TOOLCHAIN', 'true')
+vim.uv.os_setenv('PLATFORMIO_CORE_DIR', platformio_core_dir)
+vim.g.python_host_prog = pynvim_python
+vim.g.python3_host_prog = pynvim_python
+
+local sep = (vim.fn.has('win32') == 1 and ';' or ':')
+vim.env.PATH = pynvim_bin .. sep .. vim.env.PATH
+vim.env.VIRTUAL_ENV = pynvim_env
+
+if vim.fn.isdirectory(platformio_core_dir) == 0 then
+ vim.fn.mkdir(platformio_core_dir, 'p')
+ -- vim.fn.system({
+ -- "wget",
+ -- "https://raw.githubusercontent.com/platformio/platformio-core-installer/master/get-platformio.py",
+ -- })
+ -- vim.fn.system({ "python", "get-platformio.py" })
+ -- os.execute((isWindows and "del " or "rm -f ") .. "get-platformio.py*")
+end
+
+local output
+-- local expand_dir = vim.fn.expand(pynvim_env)
+if not vim.uv.fs_stat(pynvim_env) then
+ if not isWindows then
+ output = vim.fn.system({ 'python3', '-m', 'venv', pynvim_env })
+ print(output)
+ vim.fn.system({ 'chmod', '755', '-R', pynvim_bin })
+ vim.fn.system('source ' .. pynvim_activate)
+ else
+ vim.fn.system({ 'python', '-m', 'venv', pynvim_env })
+ vim.fn.system(pynvim_activate)
+ end
+
+ --------------------------------------------------------------------------------------
+ -- INFO: install platformio and nvim required packages.
+ output = vim.fn.system({ pynvim_python, '-m', 'pip', 'install', '-U', 'pip' })
+ print(output)
+ output = vim.fn.system({ pynvim_python, '-m', 'pip', 'install', 'pynvim' })
+ print(output)
+ vim.fn.system({ pynvim_python, '-m', 'pip', 'install', 'neovim' })
+ vim.fn.system({ pynvim_python, '-m', 'pip', 'install', 'debugpy' })
+ vim.fn.system({ pynvim_python, '-m', 'pip', 'install', 'isort' })
+ vim.fn.system({ pynvim_python, '-m', 'pip', 'install', 'scons' })
+ vim.fn.system({ pynvim_python, '-m', 'pip', 'install', 'sconscrip' })
+ vim.fn.system({ pynvim_python, '-m', 'pip', 'install', 'yamllint' })
+ vim.fn.system({ pynvim_python, '-m', 'pip', 'install', '-U', 'platformio' })
+ -- vim.fn.system({ 'pip', 'install', '-U', 'platformio' })
+end
+
+----------------------------------------------------------------------------------------
+-- INFO: configure nvim-platformio and load
+-----------------------------------------------------------------------------------------
+local tok, telescope = pcall(require, 'telescope')
+if tok then
+ -- 1. Import the actions module (This is the missing part!)
+ local actions = require('telescope.actions')
+ -- local telescope = require('telescope')
+ -- print("here" .. vim.inspect(pioConfig))
+ telescope.setup({
+ extensions = {
+ ['ui-select'] = {
+ require('telescope.themes').get_dropdown({
+ -- Customizing the dialog appearance
+ width = 0.6,
+ previewer = false,
+ }),
+ },
+ },
+ defaults = {
+ mappings = {
+ i = {
+ [''] = actions.delete_buffer, -- Delete buffer in insert mode
+ },
+ n = {
+ ['dd'] = actions.delete_buffer, -- Delete buffer in normal mode
+ },
+ },
+ },
+ pickers = {
+ buffers = {
+ show_all_buffers = true,
+ sort_lastused = true,
+ theme = 'dropdown', -- Compact look
+ previewer = false, -- Disable preview for a faster feel
+ },
+ },
+ })
+
+ -- Enable Telescope extensions if they are installed
+ pcall(require('telescope').load_extension, 'fzf')
+ pcall(require('telescope').load_extension, 'ui-select')
+
+ local function run_project_wizard()
+ local project_config = {}
+
+ -- Step 1: Select IDE
+ vim.ui.select({ 'Neovim', 'VS Code', 'IntelliJ' }, { prompt = 'Select IDE' }, function(ide)
+ if not ide then
+ return
+ end
+ project_config.ide = ide
+
+ -- Step 2: Select Board
+ vim.ui.select({ 'ESP32', 'Arduino Uno', 'Raspberry Pi' }, { prompt = 'Select Board' }, function(board)
+ if not board then
+ return
+ end
+ project_config.board = board
+
+ -- Step 3: Select Framework
+ vim.ui.select({ 'ESP-IDF', 'Arduino Core', 'MicroPython' }, { prompt = 'Select Framework' }, function(fw)
+ if not fw then
+ return
+ end
+ project_config.framework = fw
+
+ -- Step 4: Final Selection
+ vim.ui.select({ 'true', 'false' }, { prompt = 'Include Sample Code?' }, function(sample)
+ project_config.sample = sample == 'true'
+
+ -- Final Output/Action
+ print(
+ string.format('Setup: %s on %s using %s (Sample: %s)', project_config.ide, project_config.board, project_config.framework, project_config.sample)
+ )
+ end)
+ end)
+ end)
+ end)
+ end
+
+ vim.keymap.set('n', 'pw', run_project_wizard, { desc = 'Run Project Wizard' })
+
+ -- See `:help telescope.builtin`
+ local builtin = require('telescope.builtin')
+ vim.keymap.set('n', 'sh', builtin.help_tags, { desc = 'Search [H]elp' })
+ vim.keymap.set('n', 'sk', builtin.keymaps, { desc = 'Search [K]eymaps' })
+ vim.keymap.set('n', 'sf', builtin.find_files, { desc = 'Search [F]iles' })
+ vim.keymap.set('n', 'ss', builtin.builtin, { desc = 'Search [S]elect Telescope' })
+ vim.keymap.set('n', 'sw', builtin.grep_string, { desc = 'Search current [W]ord' })
+ vim.keymap.set('n', 'sg', builtin.live_grep, { desc = 'Search by [G]rep' })
+ vim.keymap.set('n', 'sd', builtin.diagnostics, { desc = 'Search [D]iagnostics' })
+ vim.keymap.set('n', 'sr', builtin.resume, { desc = 'Search [R]esume' })
+ vim.keymap.set('n', 's.', builtin.oldfiles, { desc = 'Search Recent Files ("." for repeat)' })
+ vim.keymap.set('n', '', builtin.buffers, { desc = '[ ] Find existing buffers' })
+
+ -- Slightly advanced example of overriding default behavior and theme
+ vim.keymap.set('n', '/', function()
+ -- You can pass additional configuration to Telescope to change the theme, layout, etc.
+ builtin.current_buffer_fuzzy_find(require('telescope.themes').get_dropdown({
+ winblend = 10,
+ previewer = false,
+ }))
+ end, { desc = '[/] Fuzzily search in current buffer' })
+ -- Keymap to open the buffer list
+ vim.keymap.set('n', 'fb', 'Telescope buffers', { desc = 'Find Buffers' })
+end
+
+local pioConfig = {
+ lspClangd = {
+ -- enabled = false,
+ enabled = true,
+ attach = {
+ enabled = true,
+ keymaps = true,
+ },
+ },
+ -- menu_key = "\\", -- replace this menu key to your convenience
+ -- menu_name = "PlatformIO", -- replace this menu name to your convenience
+ -- debug = false,
+}
+local pok, nvimpio = pcall(require, 'nvimpio')
+if pok then
+ -- print("here" .. vim.inspect(pioConfig))
+ nvimpio.setup(pioConfig)
+end
diff --git a/minimal_config.lua b/minimal_config.lua
deleted file mode 100644
index f00d500f..00000000
--- a/minimal_config.lua
+++ /dev/null
@@ -1,79 +0,0 @@
--- insures lazy is installed
-local lazypath = vim.loop.os_tmpdir() .. '/lazy/lazy.nvim'
-if not (vim.uv or vim.loop).fs_stat(lazypath) then
- vim.fn.system({
- 'git',
- 'clone',
- '--filter=blob:none',
- 'https://github.com/folke/lazy.nvim.git',
- '--branch=stable', -- latest stable release
- lazypath,
- })
-end
-vim.opt.rtp:prepend(lazypath)
-
-local plugins = {
- {
- 'anurag3301/nvim-platformio.lua',
- -- cmd = { 'Pioinit', 'Piorun', 'Piocmdh', 'Piocmdf', 'Piolib', 'Piomon', 'Piodebug', 'Piodb' },
-
- -- optional: cond used to enable/disable platformio
- -- based on existance of platformio.ini file and .pio folder in cwd.
- -- You can enable platformio plugin, using :Pioinit command
- cond = function()
- -- local platformioRootDir = vim.fs.root(vim.fn.getcwd(), { 'platformio.ini' }) -- cwd and parents
- local platformioRootDir = (vim.fn.filereadable('platformio.ini') == 1) and vim.fn.getcwd() or nil
- if platformioRootDir and vim.fs.find('.pio', { path = platformioRootDir, type = 'directory' })[1] then
- -- if platformio.ini file and .pio folder exist in cwd, enable plugin to install plugin (if not istalled) and load it.
- vim.g.platformioRootDir = platformioRootDir
- elseif (vim.uv or vim.loop).fs_stat(vim.fn.stdpath('data') .. '/lazy/nvim-platformio.lua') == nil then
- -- if nvim-platformio not installed, enable plugin to install it first time
- vim.g.platformioRootDir = vim.fn.getcwd()
- else -- if nvim-platformio.lua installed but disabled, create Pioinit command
- vim.api.nvim_create_user_command('Pioinit', function() --available only if no platformio.ini and .pio in cwd
- vim.api.nvim_create_autocmd('User', {
- pattern = { 'LazyRestore', 'LazyLoad' },
- once = true,
- callback = function(args)
- if args.match == 'LazyRestore' then
- require('lazy').load({ plugins = { 'nvim-platformio.lua' } })
- elseif args.match == 'LazyLoad' then
- vim.notify('PlatformIO loaded', vim.log.levels.INFO, { title = 'PlatformIO' })
- require("platformio").setup(vim.g.pioConfig)
- vim.cmd('Pioinit')
- end
- end,
- })
- vim.g.platformioRootDir = vim.fn.getcwd()
- require('lazy').restore({ plguins = { 'nvim-platformio.lua' }, show = false })
- end, {})
- end
- return vim.g.platformioRootDir ~= nil
- end,
-
- -- Dependencies are lazy-loaded by default unless specified otherwise.
- dependencies = {
- { 'akinsho/toggleterm.nvim' },
- { 'nvim-telescope/telescope.nvim' },
- { 'nvim-telescope/telescope-ui-select.nvim' },
- { 'nvim-lua/plenary.nvim' },
- { 'folke/which-key.nvim' },
- { 'nvim-treesitter/nvim-treesitter' }
- },
- },
-}
-
-require('lazy').setup(plugins, {
- install = {
- missing = true,
- },
-})
-
-vim.opt['number'] = true
-
-vim.g.pioConfig ={
- lsp = 'clangd',
- menu_key = '\\', -- replace this menu key to your convenience
-}
-local pok, platformio = pcall(require, 'platformio')
-if pok then platformio.setup(vim.g.pioConfig) end
diff --git a/plugin/platformio.lua b/plugin/nvimpio.lua
similarity index 63%
rename from plugin/platformio.lua
rename to plugin/nvimpio.lua
index 35020c1b..70422084 100644
--- a/plugin/platformio.lua
+++ b/plugin/nvimpio.lua
@@ -6,25 +6,83 @@
-- +: At least one argument.
-- -1: Zero or one argument (like ?, explicitly).
-local utils = require('platformio.utils')
-local piolsserial = require('platformio.piolsserial')
+----------------------------------------------------------------
+-- lazy-loading technique using a Lua metatable.
+-- It essentially "teaches" Neovim how to find a custom module only when you actually try to use it
+-- setmetatable(vim, {
+-- __index = function(t, k)
+-- -- Lazy load the misc module if requested
+-- if k == 'misc' then
+-- t.misc = require('nvimpio.utils.misc')
+-- return t.misc
+-- end
+--
+-- -- Alias vim.pio to the pio module for convenience
+-- if k == 'pio' then
+-- t.pio = require('nvimpio.utils.pio')
+-- return t.pio
+-- end
+-- end,
+-- })
+
+--@class vim
+--@field pio platformio.utils.pio
+--@field misc platformio.utils.misc
+
+-- setmetatable(vim, {
+-- __index = function(t, k)
+-- if k == 'misc' then
+-- local m = require('nvimpio.utils.misc')
+-- rawset(t, k, m) -- Physically add 'misc' to 'vim'
+-- return m
+-- end
+-- if k == 'pio' then
+-- local p = require('nvimpio.utils.pio')
+-- rawset(t, k, p) -- Physically add 'pio' to 'vim'
+-- return p
+-- end
+-- end,
+-- })
+vim.misc = require('nvimpio.utils.misc')
+vim.pio = require('nvimpio.pio.upkeep')
+
+-- INFO: fix paths in compile_commands.json
+vim.api.nvim_create_user_command('PioFixPaths', function()
+ vim.pio.compile_commandsFix()
+end, {})
+
+-- -- Pioinit2
+-- local pio_wiz = require('nvimpio.pioinit2')
+--
+-- -- Create a keybinding to trigger the wizard
+-- vim.keymap.set('n', 'pi', function()
+-- pio_wiz.launch()
+-- end, { desc = 'Run PIO Project Wizard' })
+--
+-- -- Alternatively, create a user command
+-- vim.api.nvim_create_user_command('PioWizard', function()
+-- pio_wiz.launch()
+-- end, {})
+--
+------------------------------------------------------
+local piolsserial = require('nvimpio.piolsserial')
-- Pioinit
vim.api.nvim_create_user_command('Pioinit', function()
- require('platformio.pioinit').pioinit()
+ require('nvimpio.pioinit').pioinit()
end, { force = true })
-- Piolsp
vim.api.nvim_create_user_command('PioLSP', function()
vim.schedule(function()
- require('platformio.piolsp').piolsp()
+ require('nvimpio.pioCommands').piolsp()
end)
end, {})
-- Piorun
vim.api.nvim_create_user_command('Piorun', function(opts)
local args = opts.args
- require('platformio.piorun').piorun({ args })
+ require('nvimpio.piocommands').piorun({ args })
end, {
nargs = '?',
complete = function(_, _, _)
@@ -33,10 +91,10 @@ end, {
})
-- Piomon
-piolsserial.sync_ttylist()
+-- piolsserial.sync_ttylist()
vim.api.nvim_create_user_command('Piomon', function(opts)
local args = opts.fargs
- require('platformio.piomon').piomon(args)
+ require('nvimpio.pioCommands').piomon(args)
end, {
nargs = '*',
@@ -59,13 +117,13 @@ end, {
-- Piolsserial
vim.api.nvim_create_user_command('Piolsserial', function()
- require('platformio.piolsserial').print_tty_list()
+ require('nvimpio.piolsserial').print_tty_list()
end, {})
-- Piolib
vim.api.nvim_create_user_command('Piolib', function(opts)
local args = vim.split(opts.args, ' ')
- require('platformio.piolib').piolib(args)
+ require('nvimpio.piolib').piolib(args)
end, {
nargs = '+',
})
@@ -73,7 +131,7 @@ end, {
-- Piocmdh Piocmd horizontal terminal
vim.api.nvim_create_user_command('Piocmdh', function(opts)
local cmd_table = vim.split(opts.args, ' ')
- require('platformio.piocmd').piocmd(cmd_table, 'horizontal')
+ require('nvimpio.pioCommands').piocmd(cmd_table, 'horizontal')
end, {
nargs = '*',
})
@@ -81,19 +139,20 @@ end, {
-- Piocmdf Piocmd float terminal
vim.api.nvim_create_user_command('Piocmdf', function(opts)
local cmd_table = vim.split(opts.args, ' ')
- require('platformio.piocmd').piocmd(cmd_table, 'float')
+ require('nvimpio.pioCommands').piocmd(cmd_table, 'float')
end, {
nargs = '*',
})
-- Piodebug
vim.api.nvim_create_user_command('Piodebug', function()
- require('platformio.piodebug').piodebug()
+ require('nvimpio.pioCommands').piodebug()
end, {})
------------------------------------------------------
-- require('telescope').load_extension('ui-select')
+-- stylua: ignore
-- INFO: List ToggleTerminals
vim.api.nvim_create_user_command('PioTermList', function()
local telescope = require('telescope')
@@ -123,14 +182,13 @@ vim.api.nvim_create_user_command('PioTermList', function()
},
})
telescope.load_extension('ui-select')
- local utils = require('platformio.utils')
local toggleterm_list = {}
local terms = require('toggleterm.terminal').get_all(true)
if #terms ~= 0 then
for i = 1, #terms do
if terms[i].display_name and terms[i].display_name ~= '' and terms[i].display_name:find('pio', 1) then
- local termtype = utils.strsplit(terms[i].display_name, ':')[1]
+ local termtype = vim.misc.strsplit(terms[i].display_name, ':')[1]
table.insert(toggleterm_list, {
term = terms[i],
termtype = termtype, -- Store the terminal type [piomon or piocli]
@@ -157,16 +215,13 @@ vim.api.nvim_create_user_command('PioTermList', function()
kind = 'PioTerminals',
}, function(chosen, _)
if chosen then
+ chosen.term.display_name = chosen.termtype .. ':' .. vim.api.nvim_get_current_win()
local win_type = vim.fn.win_gettype(chosen.term.window)
local win_open = win_type == '' or win_type == 'popup'
if chosen.term.window and (win_open and vim.api.nvim_win_get_buf(chosen.term.window) == chosen.term.bufnr) then
vim.api.nvim_set_current_win(chosen.term.window)
- else
- chosen.term:open()
- end
+ else chosen.term:open() end
vim.api.nvim_echo({ { 'Switched to PIO terminal: ' .. chosen.termtype, 'Normal' } }, true, {})
- else
- vim.api.nvim_echo({ { 'No PIO terminal window selected.', 'Normal' } }, true, {})
- end
+ else vim.api.nvim_echo({ { 'No PIO terminal window selected.', 'Normal' } }, true, {}) end
end)
end, {})