New features:
- Neovim is now officially supported and tested in CI.
- Choice tabstops:
${1|one,two,three|}. Select items by index (1, 2, 3…). Commas can be escaped with\,, space works as a selection terminator. See |UltiSnips-tabstops|. - |CanExpandSnippet()|, |CanJumpForwards()|, |CanJumpBackwards()| — query snippet state from mappings or statusline. See |UltiSnips-functions|.
- |g:UltiSnipsJumpOrExpandTrigger| — expand if possible, otherwise jump forward. See |UltiSnips-trigger-key-mappings|.
- Trigger key is passed through as a normal keypress when no expansion or jump occurs, instead of being swallowed.
- Context conditions gained
match(the regex match object for regex-triggered snippets) andsnip.before(text on the line up to the trigger). See |UltiSnips-custom-context-snippets|. - |g:UltiSnipsAutoTrigger| disables/enables autotrigger at runtime. |UltiSnips#ToggleAutoTrigger()| toggles it. Autotrigger now only fires on character insertion, not cursor movement. See |UltiSnips-autotrigger|.
- |g:UltiSnipsSnippetStorageDirectoryForUltiSnipsEdit| controls where
:UltiSnipsEditstores new snippet files. - |g:UltiSnipsInsertTriggerOnFailure| controls whether the trigger key is re-inserted as buffer text when expansion fails. Default 1 preserves today's behavior; set to 0 to suppress garbage insertion for non-round-tripping special-key triggers like
<c-j>or<c-space>. See |UltiSnips-trigger-key-mappings|. - Remote debug server for Python snippets via |g:UltiSnipsDebugServerEnable| — connect with telnet/netcat to step through snippet code. See |UltiSnips-advanced.txt|.
- Spell checking enabled in snippet file comments.
Breaking changes:
- Python 2 support removed. Support for Python 3.5–3.9 was also dropped. Minimum Python version is now 3.11.
- Minimum Vim version is now 9.1 (was 7.4). Older versions down to 8.2 may still work but are not tested.
- |g:UltiSnipsSnippetsDir| removed. Snippet discovery was rewritten — see |UltiSnips-how-snippets-are-loaded|.
:UltiSnipsEditnow opens snippet files relative to your.vimdirectory using[gb]:UltiSnipsSnippetDirectories. Without bang it only looks in the private snippet directory; with bang it additionally shows all snippet files currently loaded from across the runtimepath. UltiSnips now also uses$MYVIMRCas a heuristic to locate~/.vim, fixing setups with non-standard paths. - snipMate snippets now expand with the
woption instead ofi, restoring correct behavior for triggers like.. See |UltiSnips-snipMate|. - snipMate parser now requires directives to be fully spelled out (e.g.
extends, not shorthand). Snippets using abbreviated directives will fail to load.
Performance:
- Runtimepath traversal now happens once (or when a snippet source needs refresh), not on every keypress. Combined with an internal caching layer this fixes long-standing insert mode lag, especially with autotrigger. (#1552)
- Python code in snippets is precompiled, which catches syntax errors early. (#1470)
- Edit detection rewritten to use Vim's
listener_add()and Neovim'son_bytesas real change signals, replacing theguess_edit()heuristic chain. On the test suite, expensivediff()fallback is hit ~96% less often on both Vim and Neovim. (#1613)
Bug fixes:
- Wildcards in
&runtimepathentries (e.g./bundle/*) are now expanded, fixing compatibility with many modern plugin managers. (#1416) - Wrong cursor position in nested snippets. (#1347)
- Backslash escaping in placeholder defaults. (#1346)
- Reading snippet files with BOM. (#1366)
- Floating windows no longer incorrectly trigger buffer-leave logic. (#1417)
- Null bytes in eval'd text are sanitized. (#1538)
- Creating an undo break no longer changes
undolevels. (#1534) - User errors reported without Python backtraces. (#1384)
- Out-of-bounds error in Neovim with |CanExpandSnippet()|. (#1562)
- Edit detection during a snippet no longer relies on cursor-movement heuristics, so visual/select-mode replacements, macros, and autocomplete/LSP insertions are handled correctly rather than silently falling through to the expensive full-buffer diff. (#1613)
- Pathological buffer changes (large undo, multi-line paste into a tabstop,
${VISUAL}capture of a huge selection) no longer hang Vim for tens of seconds indiff(); the snippet is dropped instead. (#1513, #1074, #155, #1617) - Nested snippet expansion triggered while a completion popup is visible (built-in pum, coc.nvim, deoplete, nvim-compe) no longer corrupts the placeholder structure. Queued buffer edits are drained before expansion runs. (#1380, #1327, #1620)
- Back-to-back
!pblocks at the same buffer position no longer produce empty output due to non-deterministic text-object ordering. (#1403) - Cursor placement is correct after
!pblocks whose output takes more than one iteration to converge. (#1402) - Quickfix or location-list opening mid-snippet (e.g. vimtex continuous compilation with
:copen) no longer tears down the active snippet or orphans buffer-local mappings. (#1527) - Failed expansion no longer inserts
<t_…>garbage for special-key triggers (<c-space>,<a-;>,<F2>, …) or a literal LF for<c-j>. Set |g:UltiSnipsInsertTriggerOnFailure| to 0 to opt out. (#1232, #1460, #1482, #1523) - Buffer switches (
:bd!and similar commands that fireCursorMovedon the new buffer beforeBufEnterteardown runs) no longer corrupt the destination buffer when leaving a buffer with an active snippet. (#1628)
Infrastructure:
- CI moved from Travis CI to GitHub Actions.
- Build tooling migrated from pipenv to uv.