3.0.0 (2026-09-20)
⚠ BREAKING CHANGES
⚠️ Breaking changes
Neovim 0.10 is the minimum version
- Neovim 0.9 support is dropped. Stay on the
2.xbranch for 0.9. - All deprecated
nvim_buf_get_option,nvim_buf_set_option,nvim_win_get_optionandnvim_win_set_optioncalls now usenvim_get_option_value/nvim_set_option_value. - The
has_nvim9flag and its compatibility shims (api.set_buffer_option,api.set_window_option) are removed.
Integration keys are filetypes
An integrations key must now be the filetype of the integration window. The fileTypePattern field is gone. Rename your overrides:
| v2 key | v3 key |
|---|---|
NeoTree
| ["neo-tree"]
|
NvimDAPUI
| dap
|
NvimTree
| NvimTree (unchanged, the filetype matches)
|
Keys are lowercased internally, so NvimTree and nvimtree are the same entry.
integrations.*.reopen is removed
The plugin no longer closes and reopens an integration around enable. It accounts for the integration width instead. Remove reopen from your config.
constants.INTEGRATIONS and constants.DASHBOARDS are removed
Integration definitions come from _G.NoNeckPain.config.integrations only. A new integration needs no upstream change.
integrations.*.position is validated at setup time
position must be a string and one of left, right or none. Any other value raises an assertion in setup() instead of silently breaking the layout.
Default value changes
| Option | v2 | v3 | Why |
|---|---|---|---|
autocmds.reloadOnColorSchemeChange
| false
| true
| Side buffer colors follow the colorscheme. |
autocmds.skipEnteringNoNeckPainBuffer
| false
| true
| Side buffers are padding, not edit targets. The option is ignored while the scratchpad is enabled. |
integrations.dashboard.filetypes
| nil
| { "dashboard", "alpha", "starter", "snacks" }
| The list moved out of constants.DASHBOARDS into the config.
|
🔄 Behavior changes
integrations.dashboard.enabled = trueno longer forcesautocmds.enableOnVimEnter = "safe". Set"safe"yourself if you need it.- A new
FileTypeautocmd enables the plugin when a plugin resolves its filetype late. This makesenableOnVimEnter = "safe"reliable with dashboards. enable()runs synchronously from the public API. It is no longer debounced by 10ms.- Side windows are created with
nvim_create_buf+nvim_open_winundernoautocmdinstead oftopleft vnew/botright vnew. - Side windows are repositioned with
wincmdundernoautocmd, so user mappings and autocmds cannot hijack the move or steal focus. - A side buffer squeezed out by a split reinitializes the layout instead of disabling the plugin.
- Session restore (
:mksession/:source) keeps the plugin state. TheSessionLoadPosthandler gates ong:SessionLoad. - Floating windows are excluded from window and column counts, so popups no longer trigger a reposition or skew the padding width.
integrationswithposition = "none"(dap,oil) are counted in a dedicatednone_columnsstate field and subtracted from the padding width.- A deprecated
scratchPad.locationis only used as a directory when it is non-empty. OtherwisepathToFiledefaults to a relative filename, expanded at runtime. - Tests run per file (
make testruns eachtest-*target).luacheckis dropped frommake lint;lualsis the only linter. CI coversv0.10.4,v0.11.7andv0.12.0.
🚀 Features
- User-defined integrations. Any filetype is a valid
integrationskey, with apositionofleft,rightornone. No upstream change is needed to support a new sidebar plugin.integrations = { NvimTree = { position = "left" }, ["neo-tree"] = { position = "left" }, snacks_picker = { position = "left" }, my_custom_sidebar = { position = "right" }, }
- New built-in integrations:
oil(positionnone) andsnacks_picker(positionleft). - Column layout detection. The layout scanner walks
colnodes, not onlyleafnodes. This detects integrations that stack windows vertically, such assnacks_pickeranddap-ui. - Nested vertical splits are counted. The column count includes nested vsplits, so the padding stays correct in complex layouts.
WinEnterandWinClosedresize on window count change. Opening or closing a split now resizes the side buffers instead of leaving the layout drifted.log.warn(scope, str, ...)prints atWARNlevel regardless ofdebug.lua/no-neck-pain/util/helpers.luacentralizes config and state access (get_config_field,merge_config,get_state,is_filetype_integration, …). All direct_G.NoNeckPain.*reads go through it.
🐛 Fixes
buffers.setNameshad no effect: the UI read aset_nameskey that never existed (#227).- Side buffer width was wrong when only one side is enabled (#507).
- Side buffers drifted or disappeared after a
split/vsplit, and after closing a vsplit inside a split layout (#444, #514). - Toggling
dap-uirepeatedly drifted the side widths and could lose the side buffers (#470). snacks_picker/ snacks explorer was not detected, so the layout resized wrong (#511).- The main buffer was tagged as an integration during the layout scan, which inflated the column count.
oilas the main buffer is no longer counted as an integration. - A plain split of the main buffer no longer inflates the column count.
- The plugin no longer disables itself on an integration filetype (#297, #436).
skipEnteringNoNeckPainBuffercould close the wrong window or loop. The reroute is now synchronous and guarded against reentrancy, and is skipped while the scratchpad is active.api.is_relative_window()checked the wrong window config.state:get_side_id()and the other tab accessors (get_columns,get_none_columns,consume_redraw,get_integrations,set_side_id,get_scratch_pad, …) return a default instead of indexing aniltab.nvim_set_current_winandnvim_win_get_widthcalls are guarded bynvim_win_is_valid, which prevents errors when a window closes between the event and the handler.toggle_scratch_padvalidates the previously focused window before restoring focus.- The
QuitPre/BufDeletehandler validates thecurrwindow ID, and uses the right local variable. state:walk_layoutno longer mutates itsleafsargument, and counts a top-levelcolof leaves as one visual column.- A redraw is only requested when an integration window ID actually changed.
- A side buffer is created when the available padding equals
minSideBufferWidth(>=instead of>). - Side buffers are resized right after a
vsplitinstead of waiting for the next event. - Integration registration no longer writes a window
idinto the user config. api.debounce()reschedules through a flag instead of recreating a timer from inside its own callback.- Autocmd groups are created unconditionally in
setup(), which fixes missing autocmds when only a subset ofautocmds.*is enabled.
🧪 Tests
- New suites:
test_config_validation,test_constants,test_event,test_log,test_width_calculations,test_state_edge_cases,test_state_access_regression,test_regression_issues,test_helpers_coverage. test_integrations,test_autocmds,test_buffers,test_API,test_splitsandtest_tabsgained layout, focus and width invariant assertions.- New helpers:
child.wait_for_plugin_enabled(timeout),Helpers.assert_width_invariant(child),Helpers.generate_width_configs(min, max, count).
🏗 Internals
main.enable()is split into_on_skip_entering,_on_win_changeand_on_buf_delete. The function is now wiring only.- Layout decisions moved to
state:determine_layout_action(),validate_sides(),_scan_col_children(). state:is_side_enabled_and_valid()is nowstate:is_side_valid(), andstate:is_side_the_active_win()is nowstate:is_side_focused().state:resize_win(scope, side, width)takes a side name and resolves the window ID itself.session_restore_in_progresslives on the state object instead of a module-level global.state:set_tab()no longer deep-copies integrations.init_integrations()is the single source of truth.