diff --git a/README.md b/README.md index 8547a98..e362eef 100644 --- a/README.md +++ b/README.md @@ -1,256 +1,312 @@

- + PlatformIO logo nvim-platformio.lua

-

PlatformIO wrapper for Neovim written in Lua.

+

PlatformIO wrapper for Neovim written in Lua.

+ + + +## Introduction + +[PlatformIO](https://platformio.org) is a cross-platform build system and +package manager for embedded development: hundreds of boards, dozens of +platforms and frameworks such as Arduino, ESP-IDF and Zephyr, all behind one +command-line tool. nvim-platformio.lua brings that workflow into Neovim. +Create a project, build and flash firmware, watch the serial port, install +libraries and set up C/C++ language-server support, without leaving the +editor or memorising `pio` flags. Anything else PlatformIO Core can do is one +`:Piocmdf` away. + +- **Project setup in seconds.** `:Pioinit` lets you search every board + PlatformIO knows, pick a framework, and initialises the project in the + current directory. +- **Build, upload and monitor.** Commands run in reusable floating or split + terminals, from the project root: the nearest directory above the current + file that contains `platformio.ini`. +- **Libraries from the registry.** `:Piolib` searches the PlatformIO registry, + installs your pick and refreshes the language-server configuration. +- **Language-server configuration.** `:PioLSP` generates `.ccls` for ccls, or + `compile_commands.json` for clangd, and restarts running LSP clients. The + server itself is enabled in your own LSP setup. +- **Your picker.** Telescope, snacks.picker, mini.pick, or Neovim's built-in + `vim.ui.select`. +- **A key-driven menu.** The default which-key menu puts 29 PlatformIO + actions under one prefix. It is fully customisable and can be limited to + buffers inside a PlatformIO project. + +## Requirements + +- Neovim 0.12 or newer. On older releases most commands work, but `:PioLSP` + can fail: it uses `:lsp restart` and `vim.json.encode()` options that were + added in 0.12. +- [PlatformIO Core](https://docs.platformio.org/en/latest/core/installation/index.html), + with `pio` and `platformio` on your `PATH`. +- `curl`, used by `:Piolib`. + +Plugin dependencies: + +| Plugin | Required | Used for | +| ---------------------------------------------------------------- | ----------------- | ------------------------------------------------------------ | +| [toggleterm.nvim](https://github.com/akinsho/toggleterm.nvim) | Yes | Terminals that run build, upload, monitor and `pio` commands | +| [plenary.nvim](https://github.com/nvim-lua/plenary.nvim) | For `:Piolib` | HTTP requests to the PlatformIO registry | +| [which-key.nvim](https://github.com/folke/which-key.nvim) | For `menu_key` | The PlatformIO key menu | +| A picker | No | Board, framework, library and terminal selection | + +No picker plugin is required. Without one, the plugin uses Neovim's built-in +`vim.ui.select`, so a plugin that replaces `vim.ui.select` (snacks.nvim does +by default) is used too. -### Demo +## Installation +With [lazy.nvim](https://github.com/folke/lazy.nvim). Every option is shown +with its default value, except `menu_key`, which is unset by default: +```lua +return { + 'anurag3301/nvim-platformio.lua', -https://github.com/user-attachments/assets/fdbb6655-4b2d-4a2b-81d1-fd8af6e7d9f1 + dependencies = { + "akinsho/toggleterm.nvim", -- required + "nvim-lua/plenary.nvim", -- :Piolib + "folke/which-key.nvim", -- menu_key + -- Pickers are optional. Uncomment your preference below, if desired. + -- Without any installed, vim.ui.select is used. + -- Telescope Picker + -- 'nvim-telescope/telescope.nvim', -
+ -- snacks.picker + -- Its default 'ui_select = true' also replaces vim.ui.select + -- { 'folke/snacks.nvim', opts = { picker = { enabled = true } } }, -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 + -- mini.pick + -- opts = {} makes lazy.nvim call its setup() + -- { 'nvim-mini/mini.pick', opts = {} }, + }, -# Now run :Pioinit -``` + opts = { + -- Language server :PioLSP configures: 'ccls' | 'clangd'. + lsp = "ccls", -## Installation + -- With lsp = 'clangd', set how compile_commands.json is produced: + -- 'ccls' (converted from .ccls) | 'compiledb' (pio run -t compiledb). + clangd_source = "ccls", -#### PlatformIO Core -Follow the installation instructions in the [PlatformIO documentation](https://docs.platformio.org/en/latest/core/installation/index.html). + -- 'auto' | 'telescope' | 'snacks' | 'mini_pick' | 'ui_select'. + picker_backend = "auto", + -- true: add the menu only to buffers inside a PlatformIO project. + project_only = false, -#### Plugin -Install the plugin using lazy -```lua -return { - 'anurag3301/nvim-platformio.lua', + -- Echo terminal, window and job ids when a PIO terminal opens. + debug = false, - -- Dependencies are lazy-loaded by default unless specified otherwise. - dependencies = { - { 'akinsho/toggleterm.nvim' }, - { 'nvim-lua/plenary.nvim' }, - { 'folke/which-key.nvim' }, - { 'nvim-treesitter/nvim-treesitter' }, + -- Label of the menu group. + menu_name = "PlatformIO", + -- Prefix of the which-key menu; leave unset to disable the menu. + menu_key = "\\", - -- install picker based on your likeing, remove or ignore rest + -- Menu Keybindings: see Keybindings section below and paste the + -- contents of opts = {} here + }, +} +``` - -- telescope - { 'nvim-telescope/telescope.nvim' }, - { 'nvim-telescope/telescope-ui-select.nvim' }, +`picker_backend = 'auto'` uses the first picker that is installed, in the +order telescope, snacks, mini.pick, `vim.ui.select`. If more than one is +installed, for example because telescope is part of your config anyway, set +`picker_backend` explicitly. An explicitly chosen picker that is not installed +reports an error instead of falling back. - -- Snacks picker - {'folke/snacks.nvim', opts = { - picker = {enabled = true, ui_select = true } - }}, +With another plugin manager, install the same dependencies and call +`require('platformio').setup({ ... })` with the same table. - -- Mini.Pick - {'nvim-mini/mini.pick'}, - }, -} -``` +Details for every option: `:help platformio-configuration`. +### Try this plugin without touching your neovim config -#### Configuration -```lua -vim.g.pioConfig ={ - lsp = 'clangd', -- value: clangd | ccls - clangd_source = 'ccls', -- value: ccls | compiledb, For detailed explation check :help platformio-clangd_source - picker_backend = 'auto', -- value: auto | telescope | snacks | mini_pick | ui_select - -- default: auto, check :help platformio-picker - menu_key = '\\', -- replace this menu key to your convenience - debug = false -- enable debug messages -} -local pok, platformio = pcall(require, 'platformio') -if pok then platformio.setup(vim.g.pioConfig) end +A standalone config that installs everything into a temporary directory. +This is also the quickest way to tell a plugin problem from a config problem: + +```sh +wget https://raw.githubusercontent.com/anurag3301/nvim-platformio.lua/main/minimal_config.lua +nvim -u minimal_config.lua ``` -#### Usage `:h PlatformIO` +Then inside neovim run: `:Pioinit` + +## Usage + +| Command | Description | +| ------------------------------- | --------------------------------------------------------------------------- | +| `:Pioinit` | Pick a board and framework, initialise a project in the current directory | +| `:Piorun [target]` | Target `build`, `upload`, `uploadfs` or `clean`; default: build and upload | +| `:Piocmdf [args]` | Run `pio ` in a floating terminal; without args, just open it | +| `:Piocmdh [args]` | Same as `:Piocmdf`, in a horizontal split | +| `:Piomon [baud] [port]` | Serial monitor; completes baud rates and detected ports | +| `:Piolsserial` | List serial ports | +| `:Piolib ` | Search the PlatformIO registry and install the chosen library | +| `:PioLSP` | Generate the ccls or clangd configuration for the project | +| `:Piodebug` | Start the PlatformIO debugger with gdb | +| `:PioTermList` | Pick one of the open PIO terminals and switch to it | + +Full reference: `:help PlatformIO`. To check the installation, run +`:checkhealth platformio`. -#### Lazy::cond -The `cond` argument can be used to enable the plugin only if `platformio.ini` is present in cwd or you explicidly run `:Pioinit` +## Keybindings + +### Menu + +Setting `menu_key` registers a which-key menu under that prefix, in normal and +visual mode. With `project_only = true` it is added only to buffers whose file +is in a directory, or below a directory, that contains `platformio.ini` +(`:help platformio-project_only`). + +The tree is defined by `menu_bindings`. A `menu` node opens a sub-menu and +holds `items`; an `item` node runs `command` as an Ex command. `shortcut` is +the key inside its menu. Setting `menu_bindings` replaces the whole default +tree, so copy it and edit it inside your Lazyvim config: ```lua -return { - 'anurag3301/nvim-platformio.lua', - - -- 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 - local pio_install_status = require('platformio.utils').pio_install_check() - if not pio_install_status then return end - 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-lua/plenary.nvim' }, - { 'folke/which-key.nvim' }, - { 'nvim-treesitter/nvim-treesitter' }, - - -- install picker based on your likeing, remove or ignore rest - - -- telescope - { 'nvim-telescope/telescope.nvim' }, - { 'nvim-telescope/telescope-ui-select.nvim' }, - - -- Snacks picker - {'folke/snacks.nvim', opts = { - picker = {enabled = true, ui_select = true } - }}, - - -- Mini.Pick - {'nvim-mini/mini.pick'}, +opts = { + menu_bindings = { + { 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 = '[D]evice list', shortcut = 'd', command = 'Piocmdf device list' }, + }, + }, + { + node = 'menu', + 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 = '[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 = 'menu', + desc = '[D]ependencies', + shortcut = 'd', + items = { + { 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 = 'menu', + 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 = '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 = '[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 [M]onitor', shortcut = 'm', command = 'Piocmdh remote run -t monitor' }, + { node = 'item', desc = 'Remote [D]evices', shortcut = 'd', command = 'Piocmdf remote device list' }, + }, + }, + { + node = 'menu', + desc = '[M]iscellaneous', + shortcut = 'm', + items = { + { node = 'item', desc = '[U]pgrade PlatformIO Core', shortcut = 'u', command = 'Piocmdf upgrade' }, + }, + }, }, -} +}, ``` +### Inside PIO terminals + +These are buffer-local to the terminals the plugin opens: + +| Mode | Key | Action | +| -------- | ------- | ------------------------------- | +| Terminal | `` | Leave terminal mode | +| Normal | `` | Return to terminal mode | +| Normal | `q` | Close the terminal window | + +## Lazy loading + +Loading the plugin runs `which pio` and starts `platformio device list` in the +background, to complete ports for `:Piomon`. To defer that until the plugin is +needed, add triggers to the spec from [Installation](#installation). + +### with `project_only = false` (default) + +Load on the first PlatformIO command, or on the first press of the menu key: -### 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 - menu_name = 'PlatformIO', -- replace this menu name to your convenience - - -- 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 = '[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 = '[D]evice list', shortcut = 'd', command = 'Piocmdf device list' }, - }, - }, - { - node = 'menu', - 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 = '[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 = 'menu', - desc = '[D]ependencies', - shortcut = 'd', - items = { - { 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 = 'menu', - 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 = '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 = '[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 [M]onitor', shortcut = 'm', command = 'Piocmdh remote run -t monitor' }, - { node = 'item', desc = 'Remote [D]evices', shortcut = 'd', command = 'Piocmdf remote device list' }, - }, - }, - { - node = 'menu', - desc = '[M]iscellaneous', - shortcut = 'm', - items = { - { node = 'item', desc = '[U]pgrade PlatformIO Core', shortcut = 'u', command = 'Piocmdf upgrade' }, - }, - }, - }, - }) - end +cmd = { + 'Pioinit', 'PioLSP', 'Piorun', 'Piocmdh', 'Piocmdf', + 'Piolib', 'Piomon', 'Piolsserial', 'Piodebug', 'PioTermList', +}, + +-- `keys` must match `menu_key` inside `opts`, or opts.menu_key. +-- lazy.nvim maps this key globally until the plugin loads, then replays the keypress. +keys = { { '\\', desc = 'PlatformIO' } }, ``` -### Lazy loading +### with `project_only = true` -It's possible to lazy load the plugin using Lazy.nvim, this will load the plugins only when it is needed, to enable lazy loading, add this plugin spec to your config. +Load `event` instead of `keys`. A `keys` trigger +is a global mapping, so the menu key would exist in every buffer until the +plugin loads: ```lua -cmd = { 'Pioinit', 'PioLSP', 'Piorun', 'Piocmdh', 'Piocmdf', 'Piolib', 'Piomon', 'Piolsserial', 'Piodebug', 'PioTermList' }, +cmd = { + 'Pioinit', 'PioLSP', 'Piorun', 'Piocmdh', 'Piocmdf', + 'Piolib', 'Piomon', 'Piolsserial', 'Piodebug', 'PioTermList', +}, +event = { 'BufReadPost', 'BufNewFile' }, ``` +Neovim started without a file loads the plugin when the first file is opened +or the first PlatformIO command runs. + +## Roadmap + +- Connect `:Piodebug` with DAP. + +## License -### TODO -- Connect Piodebug with DAP +[MIT](LICENSE) diff --git a/doc/platformio.txt b/doc/platformio.txt index 8253d3e..6c7df09 100644 --- a/doc/platformio.txt +++ b/doc/platformio.txt @@ -183,6 +183,17 @@ menu_bindings *platformio-menu_bindings copy and edit. Note that a partial table does not selectively override individual default entries — provide the whole tree if you customize it. +project_only *platformio-project_only* + When `true`, the |platformio-menu_key| menu is added only to buffers + whose file is inside a PlatformIO project: `platformio.ini` exists in the + file's directory or in any parent directory. Unnamed buffers are checked + against the current working directory. The current buffer is checked + when the plugin is set up, and every buffer on |BufEnter|. Do not + lazy-load the plugin on the menu key (lazy.nvim `keys`): that stub + mapping is global. Load it when a file opens instead, e.g. + `event = { 'BufReadPost', 'BufNewFile' }`. + The `:Pio*` commands are not affected. Default: `false`. + debug *platformio-debug* When `true`, echoes extra diagnostic info (terminal name, window/buffer ids, job id) each time a PIO terminal opens. Default: `false`. diff --git a/doc/tags b/doc/tags index e689bfa..c5a8743 100644 --- a/doc/tags +++ b/doc/tags @@ -16,5 +16,6 @@ platformio-menu_bindings platformio.txt /*platformio-menu_bindings* platformio-menu_key platformio.txt /*platformio-menu_key* platformio-menu_name platformio.txt /*platformio-menu_name* platformio-picker platformio.txt /*platformio-picker* +platformio-project_only platformio.txt /*platformio-project_only* platformio-usage platformio.txt /*platformio-usage* platformio.lua platformio.txt /*platformio.lua* diff --git a/lua/platformio/init.lua b/lua/platformio/init.lua index 6ed6840..9855b81 100644 --- a/lua/platformio/init.lua +++ b/lua/platformio/init.lua @@ -3,6 +3,7 @@ M.config = { lsp = 'ccls', menu_key = nil, menu_name = 'PlatformIO', + project_only = false, debug = false, clangd_source = 'ccls', picker_backend = 'auto', @@ -159,6 +160,7 @@ function M.setup(user_config) menu_key = true, menu_name = true, menu_bindings = true, + project_only = true, debug = true, clangd_source = true, picker_backend = true, @@ -207,6 +209,15 @@ function M.setup(user_config) }, true, {}) user_config.picker_backend = M.config.picker_backend end + if user_config.project_only ~= nil and type(user_config.project_only) ~= 'boolean' then + vim.api.nvim_echo({ + { + 'Invalid PlatformIO project_only {allowed true or false} (default "' .. tostring(M.config.project_only) .. '" will be used)', + 'ErrorMsg', + }, + }, true, {}) + user_config.project_only = M.config.project_only + end if not err then -- if no error, merge user_config to M.config if user_config.menu_bindings then diff --git a/lua/platformio/piomenu.lua b/lua/platformio/piomenu.lua index 5072153..85b4821 100644 --- a/lua/platformio/piomenu.lua +++ b/lua/platformio/piomenu.lua @@ -1,12 +1,13 @@ +local utils = require('platformio.utils') + local M = {} -local icon = { icon = ' ', color = 'orange' } -- Assign platformio orange icon -local wk_table = { mode = { 'n', 'v' } } +local icon = { icon = '\238\154\130 ', color = 'orange' } -- Assign platformio orange icon (Nerd Font U+E682) -local function traverseMenu(menu, wkey) +local function traverseMenu(menu, wkey, wk_table) for _, child_node in ipairs(menu) do if child_node.node == 'menu' then - traverseMenu(child_node.items, wkey .. child_node.shortcut) + traverseMenu(child_node.items, wkey .. child_node.shortcut, wk_table) table.insert(wk_table, { wkey .. child_node.shortcut, group = child_node.desc, icon = icon }) elseif child_node.node == 'item' then table.insert(wk_table, { @@ -19,6 +20,34 @@ local function traverseMenu(menu, wkey) end end +-- which-key's parser removes the child entries from the spec it is given, so +-- every wk.add() call needs a freshly built table. +local function build_spec(config) + local wk_table = { mode = { 'n', 'v' } } + table.insert(wk_table, { config.menu_key, group = config.menu_name, icon = icon }) + traverseMenu(config.menu_bindings, config.menu_key, wk_table) + return wk_table +end + +local function add_project_menu(wk, config, buf) + -- The b: flag is cleared together with buffer-local mappings when Neovim + -- reuses a buffer for another file, so it never claims a menu that is gone. + if vim.b[buf].platformio_menu then + return + end + -- Special buffers (help, terminal, file explorers) have no real file path. + if vim.bo[buf].buftype ~= '' then + return + end + if utils.find_platformioRootDir(buf) == nil then + return + end + local spec = build_spec(config) + spec.buffer = buf + wk.add(spec) + vim.b[buf].platformio_menu = true +end + function M.piomenu(config) if config.menu_key == nil then return @@ -38,11 +67,21 @@ function M.piomenu(config) 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) + if not config.project_only then + wk.add(build_spec(config)) + return + end - wk.add(wk_table) + vim.api.nvim_create_autocmd('BufEnter', { + group = vim.api.nvim_create_augroup('platformio_menu', { clear = true }), + desc = 'Add the PlatformIO menu to buffers inside a PlatformIO project', + callback = function(args) + add_project_menu(wk, config, args.buf) + end, + }) + -- setup() can run after BufEnter already fired for the current buffer, e.g. + -- when lazy.nvim loads the plugin from BufReadPost or a :Pio* command. + add_project_menu(wk, config, vim.api.nvim_get_current_buf()) end return M diff --git a/lua/platformio/utils.lua b/lua/platformio/utils.lua index a359717..6d31d56 100644 --- a/lua/platformio/utils.lua +++ b/lua/platformio/utils.lua @@ -38,16 +38,24 @@ function M.file_exists(name) end end +-- Directory of the nearest platformio.ini at or above the buffer's file (the +-- cwd for unnamed buffers), or nil. Unlike get_platformioRootDir() it neither +-- caches the result nor reports a missing project. +function M.find_platformioRootDir(buf) + local path = vim.api.nvim_buf_get_name(buf) + if path == '' then + path = vim.fn.getcwd() + end + local match = vim.fs.find({ 'platformio.ini' }, { upward = true, path = path }) + if #match > 0 then + return vim.fs.dirname(match[1]) + end + return nil +end + function M.get_platformioRootDir() if vim.g.platformioRootDir == nil then - local path = vim.api.nvim_buf_get_name(0) - if path == '' then - path = vim.fn.getcwd() - end - local match = vim.fs.find({ 'platformio.ini' }, { upward = true, path = path }) - if #match > 0 then - vim.g.platformioRootDir = vim.fs.dirname(match[1]) - end + vim.g.platformioRootDir = M.find_platformioRootDir(0) if vim.g.platformioRootDir == nil then vim.notify('Could not find platformio.ini, run :Pioinit to create a new project', vim.log.levels.ERROR)