retrovim 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/.conda/retrovim.yaml +259 -0
  2. package/.conda/terax.icns +0 -0
  3. package/.conda/terax.ico +0 -0
  4. package/.conda/terax.png +0 -0
  5. package/.github/workflows/release.yml +95 -0
  6. package/.gitmodules +3 -0
  7. package/.npm/kanata_touchcursor.js +28 -0
  8. package/.npm/nvim.js +26 -0
  9. package/.npm/yazi.js +26 -0
  10. package/.npm/zsh.js +17 -0
  11. package/.pip/kanata_touchcursor/__init__.py +28 -0
  12. package/.pip/nvim/__init__.py +31 -0
  13. package/.pip/yazi/__init__.py +31 -0
  14. package/.pip/zsh/__init__.py +22 -0
  15. package/CHANGELOG.md +26 -0
  16. package/LICENSE +674 -0
  17. package/README.md +336 -0
  18. package/kanata/touchcursor.kbd +164 -0
  19. package/nvim/init.lua +813 -0
  20. package/nvim/plugins/mini.nvim/.github/DISCUSSION_TEMPLATE/q-a.yml +72 -0
  21. package/nvim/plugins/mini.nvim/.github/ISSUE_TEMPLATE/bug-report.yml +136 -0
  22. package/nvim/plugins/mini.nvim/.github/ISSUE_TEMPLATE/config.yml +5 -0
  23. package/nvim/plugins/mini.nvim/.github/ISSUE_TEMPLATE/feature-request.yml +75 -0
  24. package/nvim/plugins/mini.nvim/.github/PULL_REQUEST_TEMPLATE.md +2 -0
  25. package/nvim/plugins/mini.nvim/.github/workflows/quality-control.yml +150 -0
  26. package/nvim/plugins/mini.nvim/.pre-commit-config.yaml +20 -0
  27. package/nvim/plugins/mini.nvim/.stylua.toml +7 -0
  28. package/nvim/plugins/mini.nvim/.styluaignore +1 -0
  29. package/nvim/plugins/mini.nvim/CHANGELOG.md +1894 -0
  30. package/nvim/plugins/mini.nvim/CODE_OF_CONDUCT.md +132 -0
  31. package/nvim/plugins/mini.nvim/CONTRIBUTING.md +331 -0
  32. package/nvim/plugins/mini.nvim/LICENSE +21 -0
  33. package/nvim/plugins/mini.nvim/MAINTAINING.md +330 -0
  34. package/nvim/plugins/mini.nvim/Makefile +51 -0
  35. package/nvim/plugins/mini.nvim/README.md +261 -0
  36. package/nvim/plugins/mini.nvim/TESTING.md +1011 -0
  37. package/nvim/plugins/mini.nvim/benchmarks/starter/README.md +35 -0
  38. package/nvim/plugins/mini.nvim/benchmarks/starter/benchmark.sh +56 -0
  39. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_dashboard-alpha.lua +21 -0
  40. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_dashboard-original.lua +17 -0
  41. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_dashboard-starter.lua +18 -0
  42. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_empty.lua +4 -0
  43. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_starter-default.lua +7 -0
  44. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_startify-alpha.lua +10 -0
  45. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_startify-original.lua +7 -0
  46. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_startify-starter.lua +20 -0
  47. package/nvim/plugins/mini.nvim/benchmarks/starter/install.sh +10 -0
  48. package/nvim/plugins/mini.nvim/benchmarks/starter/make_summary.py +68 -0
  49. package/nvim/plugins/mini.nvim/benchmarks/starter/startup-summary.md +10 -0
  50. package/nvim/plugins/mini.nvim/colors/miniautumn.lua +55 -0
  51. package/nvim/plugins/mini.nvim/colors/minicyan.lua +95 -0
  52. package/nvim/plugins/mini.nvim/colors/minischeme.lua +95 -0
  53. package/nvim/plugins/mini.nvim/colors/minispring.lua +53 -0
  54. package/nvim/plugins/mini.nvim/colors/minisummer.lua +49 -0
  55. package/nvim/plugins/mini.nvim/colors/miniwinter.lua +51 -0
  56. package/nvim/plugins/mini.nvim/colors/randomhue.lua +16 -0
  57. package/nvim/plugins/mini.nvim/doc/mini-ai.txt +889 -0
  58. package/nvim/plugins/mini.nvim/doc/mini-align.txt +923 -0
  59. package/nvim/plugins/mini.nvim/doc/mini-animate.txt +937 -0
  60. package/nvim/plugins/mini.nvim/doc/mini-base16.txt +285 -0
  61. package/nvim/plugins/mini.nvim/doc/mini-basics.txt +359 -0
  62. package/nvim/plugins/mini.nvim/doc/mini-bracketed.txt +622 -0
  63. package/nvim/plugins/mini.nvim/doc/mini-bufremove.txt +123 -0
  64. package/nvim/plugins/mini.nvim/doc/mini-clue.txt +760 -0
  65. package/nvim/plugins/mini.nvim/doc/mini-cmdline.txt +392 -0
  66. package/nvim/plugins/mini.nvim/doc/mini-colors.txt +880 -0
  67. package/nvim/plugins/mini.nvim/doc/mini-comment.txt +230 -0
  68. package/nvim/plugins/mini.nvim/doc/mini-completion.txt +477 -0
  69. package/nvim/plugins/mini.nvim/doc/mini-cursorword.txt +105 -0
  70. package/nvim/plugins/mini.nvim/doc/mini-deps.txt +590 -0
  71. package/nvim/plugins/mini.nvim/doc/mini-diff.txt +661 -0
  72. package/nvim/plugins/mini.nvim/doc/mini-doc.txt +434 -0
  73. package/nvim/plugins/mini.nvim/doc/mini-extra.txt +856 -0
  74. package/nvim/plugins/mini.nvim/doc/mini-files.txt +1018 -0
  75. package/nvim/plugins/mini.nvim/doc/mini-fuzzy.txt +160 -0
  76. package/nvim/plugins/mini.nvim/doc/mini-git.txt +425 -0
  77. package/nvim/plugins/mini.nvim/doc/mini-hipatterns.txt +489 -0
  78. package/nvim/plugins/mini.nvim/doc/mini-hues.txt +420 -0
  79. package/nvim/plugins/mini.nvim/doc/mini-icons.txt +508 -0
  80. package/nvim/plugins/mini.nvim/doc/mini-indentscope.txt +485 -0
  81. package/nvim/plugins/mini.nvim/doc/mini-input.txt +932 -0
  82. package/nvim/plugins/mini.nvim/doc/mini-jump.txt +202 -0
  83. package/nvim/plugins/mini.nvim/doc/mini-jump2d.txt +525 -0
  84. package/nvim/plugins/mini.nvim/doc/mini-keymap.txt +449 -0
  85. package/nvim/plugins/mini.nvim/doc/mini-map.txt +717 -0
  86. package/nvim/plugins/mini.nvim/doc/mini-misc.txt +431 -0
  87. package/nvim/plugins/mini.nvim/doc/mini-move.txt +181 -0
  88. package/nvim/plugins/mini.nvim/doc/mini-notify.txt +434 -0
  89. package/nvim/plugins/mini.nvim/doc/mini-nvim.txt +431 -0
  90. package/nvim/plugins/mini.nvim/doc/mini-operators.txt +472 -0
  91. package/nvim/plugins/mini.nvim/doc/mini-pairs.txt +346 -0
  92. package/nvim/plugins/mini.nvim/doc/mini-pick.txt +1535 -0
  93. package/nvim/plugins/mini.nvim/doc/mini-sessions.txt +239 -0
  94. package/nvim/plugins/mini.nvim/doc/mini-snippets.txt +1291 -0
  95. package/nvim/plugins/mini.nvim/doc/mini-splitjoin.txt +536 -0
  96. package/nvim/plugins/mini.nvim/doc/mini-starter.txt +618 -0
  97. package/nvim/plugins/mini.nvim/doc/mini-statusline.txt +361 -0
  98. package/nvim/plugins/mini.nvim/doc/mini-surround.txt +863 -0
  99. package/nvim/plugins/mini.nvim/doc/mini-tabline.txt +151 -0
  100. package/nvim/plugins/mini.nvim/doc/mini-test.txt +966 -0
  101. package/nvim/plugins/mini.nvim/doc/mini-trailspace.txt +91 -0
  102. package/nvim/plugins/mini.nvim/doc/mini-visits.txt +937 -0
  103. package/nvim/plugins/mini.nvim/logo.png +0 -0
  104. package/nvim/plugins/mini.nvim/lua/mini/ai.lua +2244 -0
  105. package/nvim/plugins/mini.nvim/lua/mini/align.lua +2076 -0
  106. package/nvim/plugins/mini.nvim/lua/mini/animate.lua +2127 -0
  107. package/nvim/plugins/mini.nvim/lua/mini/base16.lua +1752 -0
  108. package/nvim/plugins/mini.nvim/lua/mini/basics.lua +772 -0
  109. package/nvim/plugins/mini.nvim/lua/mini/bracketed.lua +2013 -0
  110. package/nvim/plugins/mini.nvim/lua/mini/bufremove.lua +283 -0
  111. package/nvim/plugins/mini.nvim/lua/mini/clue.lua +2096 -0
  112. package/nvim/plugins/mini.nvim/lua/mini/cmdline.lua +1182 -0
  113. package/nvim/plugins/mini.nvim/lua/mini/colors.lua +2483 -0
  114. package/nvim/plugins/mini.nvim/lua/mini/comment.lua +586 -0
  115. package/nvim/plugins/mini.nvim/lua/mini/completion.lua +2094 -0
  116. package/nvim/plugins/mini.nvim/lua/mini/cursorword.lua +311 -0
  117. package/nvim/plugins/mini.nvim/lua/mini/deps.lua +1660 -0
  118. package/nvim/plugins/mini.nvim/lua/mini/diff.lua +1938 -0
  119. package/nvim/plugins/mini.nvim/lua/mini/doc.lua +1371 -0
  120. package/nvim/plugins/mini.nvim/lua/mini/extra.lua +2290 -0
  121. package/nvim/plugins/mini.nvim/lua/mini/files.lua +3145 -0
  122. package/nvim/plugins/mini.nvim/lua/mini/fuzzy.lua +356 -0
  123. package/nvim/plugins/mini.nvim/lua/mini/git.lua +1737 -0
  124. package/nvim/plugins/mini.nvim/lua/mini/hipatterns.lua +1042 -0
  125. package/nvim/plugins/mini.nvim/lua/mini/hues.lua +2088 -0
  126. package/nvim/plugins/mini.nvim/lua/mini/icons.lua +2254 -0
  127. package/nvim/plugins/mini.nvim/lua/mini/indentscope.lua +1158 -0
  128. package/nvim/plugins/mini.nvim/lua/mini/init.lua +427 -0
  129. package/nvim/plugins/mini.nvim/lua/mini/input.lua +2183 -0
  130. package/nvim/plugins/mini.nvim/lua/mini/jump.lua +609 -0
  131. package/nvim/plugins/mini.nvim/lua/mini/jump2d.lua +1244 -0
  132. package/nvim/plugins/mini.nvim/lua/mini/keymap.lua +880 -0
  133. package/nvim/plugins/mini.nvim/lua/mini/map.lua +1709 -0
  134. package/nvim/plugins/mini.nvim/lua/mini/misc.lua +969 -0
  135. package/nvim/plugins/mini.nvim/lua/mini/move.lua +509 -0
  136. package/nvim/plugins/mini.nvim/lua/mini/notify.lua +938 -0
  137. package/nvim/plugins/mini.nvim/lua/mini/operators.lua +1349 -0
  138. package/nvim/plugins/mini.nvim/lua/mini/pairs.lua +657 -0
  139. package/nvim/plugins/mini.nvim/lua/mini/pick.lua +3876 -0
  140. package/nvim/plugins/mini.nvim/lua/mini/sessions.lua +689 -0
  141. package/nvim/plugins/mini.nvim/lua/mini/snippets.lua +2828 -0
  142. package/nvim/plugins/mini.nvim/lua/mini/splitjoin.lua +1132 -0
  143. package/nvim/plugins/mini.nvim/lua/mini/starter.lua +1599 -0
  144. package/nvim/plugins/mini.nvim/lua/mini/statusline.lua +686 -0
  145. package/nvim/plugins/mini.nvim/lua/mini/surround.lua +2388 -0
  146. package/nvim/plugins/mini.nvim/lua/mini/tabline.lua +564 -0
  147. package/nvim/plugins/mini.nvim/lua/mini/test.lua +2516 -0
  148. package/nvim/plugins/mini.nvim/lua/mini/trailspace.lua +215 -0
  149. package/nvim/plugins/mini.nvim/lua/mini/visits.lua +1577 -0
  150. package/nvim/plugins/mini.nvim/readmes/mini-ai.md +190 -0
  151. package/nvim/plugins/mini.nvim/readmes/mini-align.md +203 -0
  152. package/nvim/plugins/mini.nvim/readmes/mini-animate.md +214 -0
  153. package/nvim/plugins/mini.nvim/readmes/mini-base16.md +198 -0
  154. package/nvim/plugins/mini.nvim/readmes/mini-basics.md +184 -0
  155. package/nvim/plugins/mini.nvim/readmes/mini-bracketed.md +193 -0
  156. package/nvim/plugins/mini.nvim/readmes/mini-bufremove.md +139 -0
  157. package/nvim/plugins/mini.nvim/readmes/mini-clue.md +260 -0
  158. package/nvim/plugins/mini.nvim/readmes/mini-cmdline.md +183 -0
  159. package/nvim/plugins/mini.nvim/readmes/mini-colors.md +169 -0
  160. package/nvim/plugins/mini.nvim/readmes/mini-comment.md +183 -0
  161. package/nvim/plugins/mini.nvim/readmes/mini-completion.md +217 -0
  162. package/nvim/plugins/mini.nvim/readmes/mini-cursorword.md +137 -0
  163. package/nvim/plugins/mini.nvim/readmes/mini-deps.md +271 -0
  164. package/nvim/plugins/mini.nvim/readmes/mini-diff.md +289 -0
  165. package/nvim/plugins/mini.nvim/readmes/mini-doc.md +198 -0
  166. package/nvim/plugins/mini.nvim/readmes/mini-extra.md +158 -0
  167. package/nvim/plugins/mini.nvim/readmes/mini-files.md +247 -0
  168. package/nvim/plugins/mini.nvim/readmes/mini-fuzzy.md +134 -0
  169. package/nvim/plugins/mini.nvim/readmes/mini-git.md +168 -0
  170. package/nvim/plugins/mini.nvim/readmes/mini-hipatterns.md +176 -0
  171. package/nvim/plugins/mini.nvim/readmes/mini-hues.md +285 -0
  172. package/nvim/plugins/mini.nvim/readmes/mini-icons.md +183 -0
  173. package/nvim/plugins/mini.nvim/readmes/mini-indentscope.md +191 -0
  174. package/nvim/plugins/mini.nvim/readmes/mini-input.md +163 -0
  175. package/nvim/plugins/mini.nvim/readmes/mini-jump.md +161 -0
  176. package/nvim/plugins/mini.nvim/readmes/mini-jump2d.md +197 -0
  177. package/nvim/plugins/mini.nvim/readmes/mini-keymap.md +205 -0
  178. package/nvim/plugins/mini.nvim/readmes/mini-map.md +191 -0
  179. package/nvim/plugins/mini.nvim/readmes/mini-misc.md +145 -0
  180. package/nvim/plugins/mini.nvim/readmes/mini-move.md +163 -0
  181. package/nvim/plugins/mini.nvim/readmes/mini-notify.md +175 -0
  182. package/nvim/plugins/mini.nvim/readmes/mini-operators.md +193 -0
  183. package/nvim/plugins/mini.nvim/readmes/mini-pairs.md +158 -0
  184. package/nvim/plugins/mini.nvim/readmes/mini-pick.md +340 -0
  185. package/nvim/plugins/mini.nvim/readmes/mini-sessions.md +164 -0
  186. package/nvim/plugins/mini.nvim/readmes/mini-snippets.md +373 -0
  187. package/nvim/plugins/mini.nvim/readmes/mini-splitjoin.md +194 -0
  188. package/nvim/plugins/mini.nvim/readmes/mini-starter.md +176 -0
  189. package/nvim/plugins/mini.nvim/readmes/mini-statusline.md +155 -0
  190. package/nvim/plugins/mini.nvim/readmes/mini-surround.md +190 -0
  191. package/nvim/plugins/mini.nvim/readmes/mini-tabline.md +156 -0
  192. package/nvim/plugins/mini.nvim/readmes/mini-test.md +175 -0
  193. package/nvim/plugins/mini.nvim/readmes/mini-trailspace.md +137 -0
  194. package/nvim/plugins/mini.nvim/readmes/mini-visits.md +243 -0
  195. package/nvim/plugins/mini.nvim/scripts/dual_log.sh +13 -0
  196. package/nvim/plugins/mini.nvim/scripts/dual_push.sh +15 -0
  197. package/nvim/plugins/mini.nvim/scripts/dual_release.sh +102 -0
  198. package/nvim/plugins/mini.nvim/scripts/dual_sync.sh +115 -0
  199. package/nvim/plugins/mini.nvim/scripts/init-deps-example.lua +59 -0
  200. package/nvim/plugins/mini.nvim/scripts/lint-filename-length.sh +16 -0
  201. package/nvim/plugins/mini.nvim/scripts/lintcommit-ci.sh +18 -0
  202. package/nvim/plugins/mini.nvim/scripts/lintcommit.lua +393 -0
  203. package/nvim/plugins/mini.nvim/scripts/minidoc.lua +57 -0
  204. package/nvim/plugins/mini.nvim/scripts/minimal_init.lua +28 -0
  205. package/nvim/plugins/mini.nvim/scripts/minitest.lua +4 -0
  206. package/package.json +16 -0
  207. package/pyproject.toml +43 -0
  208. package/yazi/init.lua +12 -0
  209. package/yazi/keymap.toml +114 -0
  210. package/yazi/lesskey +5 -0
  211. package/yazi/plugins/bookmarks.yazi/main.lua +86 -0
  212. package/yazi/plugins/cd-git-root.yazi/main.lua +31 -0
  213. package/yazi/plugins/smart-enter.yazi/main.lua +11 -0
  214. package/yazi/theme.toml +26 -0
  215. package/yazi/yazi.toml +15 -0
  216. package/zsh/.zshrc +145 -0
  217. package/zsh/starship.toml +20 -0
  218. package/zsh/zsh-patina.toml +2 -0
@@ -0,0 +1,2828 @@
1
+ --- *mini.snippets* Manage and expand snippets
2
+ ---
3
+ --- MIT License Copyright (c) 2024 Evgeni Chasnovski
4
+
5
+ --- Snippet is a template for a frequently used text. Typical workflow is to type
6
+ --- snippet's (configurable) prefix and expand it into a snippet session.
7
+ ---
8
+ --- The template usually contains both pre-defined text and places (called
9
+ --- "tabstops") for user to interactively change/add text during snippet session.
10
+ ---
11
+ --- This module supports (only) snippet syntax defined in LSP specification (with
12
+ --- small deviations). See |MiniSnippets-syntax-specification|.
13
+ ---
14
+ --- Features:
15
+ --- - Manage snippet collection by adding it explicitly or with a flexible set of
16
+ --- performant built-in loaders. See |MiniSnippets.gen_loader|.
17
+ ---
18
+ --- - Configured snippets are efficiently resolved before every expand based on
19
+ --- current local context. This, for example, allows using different snippets
20
+ --- in different local tree-sitter languages (like in markdown code blocks).
21
+ --- See |MiniSnippets.default_prepare()|.
22
+ ---
23
+ --- - Match which snippet to insert based on the currently typed text.
24
+ --- Supports both exact and fuzzy matching. See |MiniSnippets.default_match()|.
25
+ ---
26
+ --- - Select from several matched snippets via `vim.ui.select()`.
27
+ --- See |MiniSnippets.default_select()|.
28
+ ---
29
+ --- - Start specialized in-process LSP server to show loaded snippets inside
30
+ --- (auto)completion engines (like |mini.completion|).
31
+ --- See |MiniSnippets.start_lsp_server()|.
32
+ ---
33
+ --- - Insert, jump, and edit during snippet session in a configurable manner:
34
+ --- - Configurable mappings for jumping and stopping.
35
+ --- - Jumping wraps around the tabstops for easier navigation.
36
+ --- - Easy to reason rules for when session automatically stops.
37
+ --- - Text synchronization of linked tabstops preserving relative indent.
38
+ --- - Dynamic tabstop state visualization (current/visited/unvisited, etc.)
39
+ --- - Inline visualization of empty tabstops (requires Neovim>=0.10).
40
+ --- - Works inside comments by preserving comment leader on new lines.
41
+ --- - Supports nested sessions (expand snippet while there is an active one).
42
+ --- See |MiniSnippets.default_insert()|.
43
+ ---
44
+ --- - Exported function to parse snippet body into easy-to-reason data structure.
45
+ --- See |MiniSnippets.parse()|.
46
+ ---
47
+ --- Notes:
48
+ --- - It does not set up any snippet collection by default. Explicitly populate
49
+ --- `config.snippets` to have snippets to match from.
50
+ --- - It does not come with a built-in snippet collection. It is expected from
51
+ --- users to add their own snippets, manually or with dedicated plugin(s).
52
+ --- - It does not support variable/tabstop transformations in default snippet
53
+ --- session. This requires ECMAScript Regular Expression parser which can not
54
+ --- be implemented concisely.
55
+ ---
56
+ --- Sources with more details:
57
+ --- - |MiniSnippets-glossary|
58
+ --- - |MiniSnippets-overview|
59
+ --- - |MiniSnippets-examples|
60
+ --- - |MiniSnippets-in-other-plugins| (for plugin authors)
61
+ ---
62
+ --- # Dependencies ~
63
+ ---
64
+ --- This module doesn't come with snippet collection. Either create it manually
65
+ --- or install a dedicated plugin. For example,
66
+ --- [rafamadriz/friendly-snippets](https://github.com/rafamadriz/friendly-snippets).
67
+ ---
68
+ --- # Setup ~
69
+ ---
70
+ --- This module needs a setup with `require('mini.snippets').setup({})` (replace `{}`
71
+ --- with your `config` table). It will create global Lua table `MiniSnippets` which
72
+ --- you can use for scripting or manually (with `:lua MiniSnippets.*`).
73
+ ---
74
+ --- See |MiniSnippets.config| for `config` structure and default values.
75
+ ---
76
+ --- You can override runtime config settings locally to buffer inside
77
+ --- `vim.b.minisnippets_config` which should have same structure as
78
+ --- `MiniSnippets.config`. See |mini.nvim-buffer-local-config| for more details.
79
+ ---
80
+ --- # Comparisons ~
81
+ ---
82
+ --- - [L3MON4D3/LuaSnip](https://github.com/L3MON4D3/LuaSnip):
83
+ --- - Both contain functionality to load snippets from file system.
84
+ --- This module provides several common loader generators while `LuaSnip`
85
+ --- contains a more elaborate loading setup.
86
+ --- Also both require explicit opt-in for which snippets to load.
87
+ --- - Both support LSP snippet format. `LuaSnip` also provides own more
88
+ --- elaborate snippet format which is out of scope for this module.
89
+ --- - `LuaSnip` can autoexpand snippets, while this module always requires
90
+ --- an explicit user action to expand (by design).
91
+ --- - Both contain snippet expand functionality which differs in some aspects:
92
+ --- - `LuaSnip` has an elaborate dynamic tabstop visualization config.
93
+ --- This module provides a handful of dedicated highlight groups.
94
+ --- - This module provides configurable visualization of empty tabstops.
95
+ --- - `LuaSnip` implements nested sessions by essentially merging them
96
+ --- into one. This module treats each nested session separately (to not
97
+ --- visually overload) while storing them in stack (first in last out).
98
+ --- - `LuaSnip` uses |Select-mode| to power replacing current tabstop,
99
+ --- while this module always stays in |Insert-mode|. This enables easier
100
+ --- mapping understanding and more targeted highlighting.
101
+ --- - This module implements jumping which wraps after final tabstop
102
+ --- for more flexible navigation (enhanced with by a more flexible
103
+ --- autostopping rules), while `LuaSnip` autostops session once
104
+ --- jumping reached the final tabstop.
105
+ ---
106
+ --- - Built-in |vim.snippet| (on Neovim>=0.10):
107
+ --- - Does not contain functionality to load or match snippets (by design),
108
+ --- while this module does.
109
+ --- - Both contain expand functionality based on LSP snippet format.
110
+ --- Differences in how snippet sessions are handled are similar to
111
+ --- comparison with `LuaSnip`.
112
+ ---
113
+ --- - [rafamadriz/friendly-snippets](https://github.com/rafamadriz/friendly-snippets):
114
+ --- - A snippet collection plugin without features to manage or expand them.
115
+ --- This module is designed with `friendly-snippets` compatibility in mind.
116
+ ---
117
+ --- - [abeldekat/cmp-mini-snippets](https://github.com/abeldekat/cmp-mini-snippets):
118
+ --- - A source for [hrsh7th/nvim-cmp](https://github.com/hrsh7th/nvim-cmp)
119
+ --- that integrates |mini.snippets|.
120
+ ---
121
+ --- # Highlight groups ~
122
+ --- *MiniSnippets-hl-groups*
123
+ ---
124
+ --- - `MiniSnippetsCurrent` - current tabstop.
125
+ --- - `MiniSnippetsCurrentReplace` - current tabstop, placeholder is to be replaced.
126
+ --- - `MiniSnippetsFinal` - special `$0` tabstop.
127
+ --- - `MiniSnippetsUnvisited` - not yet visited tabstop(s).
128
+ --- - `MiniSnippetsVisited` - visited tabstop(s).
129
+ ---
130
+ --- To change any highlight group, set it directly with |nvim_set_hl()|.
131
+ ---
132
+ --- # Disabling ~
133
+ ---
134
+ --- To disable core functionality, set `vim.g.minisnippets_disable` (globally) or
135
+ --- `vim.b.minisnippets_disable` (for a buffer) to `true`. Considering high number
136
+ --- of different scenarios and customization intentions, writing exact rules
137
+ --- for disabling module's functionality is left to user. See
138
+ --- |mini.nvim-disabling-recipes| for common recipes.
139
+ ---
140
+ --- # Using in other plugins ~
141
+ --- *MiniSnippets-in-other-plugins*
142
+ ---
143
+ --- - Perform a `_G.MiniSnippets ~= nil` check before using any feature. This
144
+ --- ensures that user explicitly set up the module.
145
+ ---
146
+ --- - To insert snippet given its body (like |vim.snippet.expand()|), use: >lua
147
+ ---
148
+ --- -- Use configured `insert` method with falling back to default
149
+ --- local insert = MiniSnippets.config.expand.insert
150
+ --- or MiniSnippets.default_insert
151
+ --- -- Insert at cursor
152
+ --- insert({ body = snippet })
153
+ --- <
154
+ --- - To get available snippets, use: >lua
155
+ ---
156
+ --- -- Get snippets matched at cursor
157
+ --- MiniSnippets.expand({ insert = false })
158
+ ---
159
+ --- -- Get all snippets available at cursor context
160
+ --- MiniSnippets.expand({ match = false, insert = false })
161
+ --- <
162
+ ---@tag MiniSnippets
163
+
164
+ --- POSITION ~
165
+ --- Table representing position in a buffer. Fields:
166
+ --- - <line> `(number)` - line number (starts at 1).
167
+ --- - <col> `(number)` - column number (starts at 1).
168
+ ---
169
+ --- REGION ~
170
+ --- Table representing region in a buffer.
171
+ --- Fields: <from> and <to> for inclusive start/end POSITIONs.
172
+ ---
173
+ --- SNIPPET ~
174
+ --- Data about template to insert. Should contain fields:
175
+ --- - <prefix> - string snippet identifier.
176
+ --- - <body> - string snippet content with appropriate syntax.
177
+ --- - <desc> - string snippet description in human readable form.
178
+ ---
179
+ --- Can also be used to mean snippet body if distinction is clear.
180
+ ---
181
+ --- SNIPPET SESSION ~
182
+ --- Interactive state for user to adjust inserted snippet.
183
+ ---
184
+ --- MATCHED SNIPPET ~
185
+ --- Snippet which contains <region> field with region that matched it.
186
+ --- Usually region needs to be removed.
187
+ ---
188
+ --- SNIPPET NODE ~
189
+ --- Unit of parsed snippet body. See |MiniSnippets.parse()|.
190
+ ---
191
+ --- TABSTOP ~
192
+ --- Dedicated places in snippet body for users to interactively adjust.
193
+ --- Specified in snippet body with `$` followed by digit(s).
194
+ ---
195
+ --- LINKED TABSTOPS ~
196
+ --- Different nodes assigned the same tabstop. Updated in sync.
197
+ ---
198
+ --- REFERENCE NODE ~
199
+ --- First (from left to right) node of linked tabstops. Used to determine
200
+ --- synced text and cursor placement after jump.
201
+ ---
202
+ --- EXPAND ~
203
+ --- Action to start snippet session based on currently typed text.
204
+ --- Always done in current buffer at cursor. Executed steps:
205
+ --- - `PREPARE` - resolve raw config snippets at context.
206
+ --- - `MATCH` - match resolved snippets at cursor position.
207
+ --- - `SELECT` - possibly choose among matched snippets.
208
+ --- - `INSERT` - insert selected snippet and start snippet session.
209
+ ---@tag MiniSnippets-glossary
210
+
211
+ --- Snippet is a template for a frequently used text. Typical workflow is to type
212
+ --- snippet's (configurable) prefix and expand it into a snippet session: add some
213
+ --- pre-defined text and allow user to interactively change/add at certain places.
214
+ ---
215
+ --- This overview assumes default config for mappings and expand.
216
+ --- See |MiniSnippets.config| and |MiniSnippets-examples| for more details.
217
+ ---
218
+ --- # Snippet structure ~
219
+ ---
220
+ --- Snippet consists from three parts:
221
+ --- - `Prefix` - identifier used to match against current text.
222
+ --- - `Body` - actually inserted content with appropriate syntax.
223
+ --- - `Desc` - description in human readable form.
224
+ ---
225
+ --- Example: `{ prefix = 'tis', body = 'This is snippet', desc = 'Snip' }`
226
+ --- Typing `tis` and pressing "expand" mapping (<C-j> by default) will remove "tis",
227
+ --- add "This is snippet", and place cursor at the end in Insert mode.
228
+ ---
229
+ --- # Syntax ~
230
+ --- *MiniSnippets-syntax-specification*
231
+ ---
232
+ --- Inserting just text after typing smaller prefix is already powerful enough.
233
+ --- For more flexibility, snippet body can be formatted in a special way to
234
+ --- provide extra features. This module implements support for syntax defined
235
+ --- in LSP specification (with small deviations). See this link for reference:
236
+ --- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#snippet_syntax
237
+ ---
238
+ --- A quick overview of basic syntax features:
239
+ ---
240
+ --- - Tabstops are snippet parts meant for interactive editing at their location.
241
+ --- They are denoted as `$1`, `$2`, etc.
242
+ --- Navigating between them is called "jumping" and is done in numerical order
243
+ --- of tabstop identifiers by pressing special keys: <C-l> and <C-h> to jump
244
+ --- to next and previous tabstop respectively.
245
+ --- Special tabstop `$0` is called "final tabstop": it is used to decide when
246
+ --- snippet session is automatically stopped and is visited last during jumping.
247
+ ---
248
+ --- Example: `T1=$1 T2=$2 T0=$0` is expanded as `T1= T2= T0=` with three tabstops.
249
+ ---
250
+ --- - Tabstop can have placeholder: a text used if tabstop is not yet edited.
251
+ --- Text is preserved if no editing is done. It follows this same syntax, which
252
+ --- means it can itself contain tabstops with placeholders (i.e. be nested).
253
+ --- Tabstop with placeholder is denoted as `${1:placeholder}` (`$1` is `${1:}`).
254
+ ---
255
+ --- Example: `T1=${1:text} T2=${2:<$1>}` is expanded as `T1=text T2=<text>`;
256
+ --- typing `x` at first placeholder results in `T1=x T2=<x>`;
257
+ --- jumping once and typing `y` results in `T1=x T2=y`.
258
+ ---
259
+ --- - There can be several tabstops with same identifier. They are linked and
260
+ --- updated in sync during text editing. Can also have different placeholders;
261
+ --- they are forced to be the same as in the first (from left to right) tabstop.
262
+ ---
263
+ --- Example: `T1=${1:text} T1=$1` is expanded as `T1=text T1=text`;
264
+ --- typing `x` at first placeholder results in `T1=x T1=x`.
265
+ ---
266
+ --- - Tabstop can also have choices: suggestions about tabstop text. It is denoted
267
+ --- as `${1|a,b,c|}`. First choice is used as placeholder.
268
+ ---
269
+ --- Example: `T1=${1|left,right|}` is expanded as `T1=left`.
270
+ ---
271
+ --- - Variables can be used to automatically insert text without user interaction.
272
+ --- As tabstops, each one can have a placeholder which is used if variable is
273
+ --- not defined. There is a special set of variables describing editor state.
274
+ ---
275
+ --- Example: `V1=$TM_FILENAME V2=${NOTDEFINED:placeholder}` is expanded as
276
+ --- `V1=current-file-basename V2=placeholder`.
277
+ ---
278
+ --- What's different from LSP specification:
279
+ --- - Special set of variables is wider and is taken from VSCode specification:
280
+ --- https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables
281
+ --- Exceptions are `BLOCK_COMMENT_START` and `BLOCK_COMMENT_END` as Neovim doesn't
282
+ --- provide this information.
283
+ --- - Variable `TM_SELECTED_TEXT` is resolved as contents of |quote_quote| register.
284
+ --- It assumes that text is put there prior to expanding. For example, visually
285
+ --- select, press |c|, type prefix, and expand.
286
+ --- See |MiniSnippets-examples| for how to adjust this.
287
+ --- - Environment variables are recognized and supported: `V1=$VIMRUNTIME` will
288
+ --- use an actual value of |$VIMRUNTIME|.
289
+ --- - Variable transformations are not supported during snippet session. It would
290
+ --- require interacting with ECMAScript-like regular expressions for which there
291
+ --- is no easy way in Neovim. It may change in the future.
292
+ --- Transformations are recognized during parsing, though, with some exceptions:
293
+ --- - The `}` inside `if` of `${1:?if:else}` needs escaping (for technical reasons).
294
+ ---
295
+ --- There is a |MiniSnippets.parse()| function for programmatically parsing
296
+ --- snippet body into a comprehensible data structure.
297
+ ---
298
+ --- # Expand ~
299
+ ---
300
+ --- Using snippets is done via what is called "expanding". It goes like this:
301
+ --- - Type snippet prefix or its recognizable part.
302
+ --- - Press <C-j> to expand. It will perform the following steps:
303
+ --- - Prepare available snippets in current context (buffer + local language).
304
+ --- This allows snippet setup to have general function loaders which return
305
+ --- different snippets in different contexts.
306
+ --- - Match text to the left of cursor with available prefixes. It first tries
307
+ --- to do exact match and falls back to fuzzy matching.
308
+ --- - If there are several matches, use `vim.ui.select()` to choose one.
309
+ --- - Insert single matching snippet. If snippet contains tabstops, start
310
+ --- snippet session.
311
+ ---
312
+ --- For more details about each step see:
313
+ --- - |MiniSnippets.default_prepare()|
314
+ --- - |MiniSnippets.default_match()|
315
+ --- - |MiniSnippets.default_select()|
316
+ --- - |MiniSnippets.default_insert()|
317
+ ---
318
+ --- Snippet session allows interactive editing at tabstop locations:
319
+ ---
320
+ --- - All tabstop locations are visualized depending on tabstop "state" (whether
321
+ --- it is current/visited/unvisited/final and whether it was already edited).
322
+ --- Empty tabstops are visualized with inline virtual text ("•"/"∎" for
323
+ --- regular/final tabstops). It is removed after session is stopped.
324
+ ---
325
+ --- - Start session at first tabstop. Type text to replace placeholder.
326
+ --- When finished with current tabstop, jump to next with <C-l>. Repeat.
327
+ --- If changed mind about some previous tabstop, jump back with <C-h>.
328
+ --- Jumping also wraps around the edge (first tabstop is next after final).
329
+ ---
330
+ --- - If tabstop has choices, use <C-n> / <C-p> to select next / previous item.
331
+ ---
332
+ --- - Starting another snippet session while there is an active one is allowed.
333
+ --- This creates nested sessions: suspend current, start the new one.
334
+ --- After newly created is stopped, resume the suspended one.
335
+ ---
336
+ --- - Stop session manually by pressing <C-c> or make it stop automatically:
337
+ --- if final tabstop is current either make a text edit or exit to Normal mode.
338
+ --- If snippet doesn't explicitly define final tabstop, it is added at the end
339
+ --- of the snippet.
340
+ ---
341
+ --- For more details about snippet session see |MiniSnippets-session|.
342
+ ---
343
+ --- To select and insert snippets via completion engine (that supports LSP
344
+ --- completion; like |mini.completion| or |lsp-autocompletion|),
345
+ --- call |MiniSnippets.start_lsp_server()| after |MiniSnippets.setup()|. This sets up
346
+ --- an LSP server that matches and provides snippets loaded with |mini.snippets|.
347
+ --- To match with completion engine, use `start_lsp_server({ match = false })`.
348
+ ---
349
+ --- # Management ~
350
+ ---
351
+ --- Out of the box |mini.snippets| doesn't load any snippets, it should be done
352
+ --- explicitly inside |MiniSnippets.setup()| following |MiniSnippets.config|.
353
+ ---
354
+ --- The suggested approach to snippet management is to create dedicated files with
355
+ --- snippet data and load them through function loaders in `config.snippets`.
356
+ --- See |MiniSnippets-examples| for basic (yet capable) snippet management config.
357
+ ---
358
+ --- ## File specification ~
359
+ --- *MiniSnippets-file-specification*
360
+ ---
361
+ --- General idea of supported files is to have at least out of the box experience
362
+ --- with common snippet collections. Namely "rafamadriz/friendly-snippets".
363
+ --- The following files are supported:
364
+ ---
365
+ --- - Extensions:
366
+ --- - Read/decoded as JSON object (|vim.json.decode()|): `*.json`, `*.code-snippets`
367
+ --- - Executed as Lua file (|dofile()|) and uses returned value: `*.lua`
368
+ ---
369
+ --- - Content:
370
+ --- - Dict-like: object in JSON; returned table in Lua; no order guarantees.
371
+ --- - Array-like: array in JSON; returned array table in Lua; preserves order.
372
+ ---
373
+ --- Example of file content with a single snippet:
374
+ --- - Lua dict-like: `return { name = { prefix = 't', body = 'Text' } }`
375
+ --- - Lua array-like: `return { { prefix = 't', body = 'Text', desc = 'name' } }`
376
+ --- - JSON dict-like: `{ "name": { "prefix": "t", "body": "Text" } }`
377
+ --- - JSON array-like: `[ { "prefix": "t", "body": "Text", "desc": "name" } ]`
378
+ ---
379
+ --- Notes:
380
+ --- - There is no built-in support for VSCode-like "package.json" files. Define
381
+ --- structure manually in |MiniSnippets.setup()| via built-in or custom loaders.
382
+ --- - There is no built-in support for `scope` field of snippet data. Snippets are
383
+ --- expected to be manually separated into smaller files and loaded on demand.
384
+ ---
385
+ --- For supported snippet syntax see |MiniSnippets-syntax-specification|.
386
+ ---
387
+ --- ## General advice ~
388
+ ---
389
+ --- - Put files in "snippets" subdirectory of any path in |'runtimepath'| (like
390
+ --- `$XDG_CONFIG_HOME/nvim/snippets/global.json`).
391
+ --- This is compatible with |MiniSnippets.gen_loader.from_runtime()| and
392
+ --- example from |MiniSnippets-examples|.
393
+ --- - Prefer `*.json` files with dict-like content if you want more cross platform
394
+ --- setup. Otherwise use `*.lua` files with array-like content.
395
+ --- - To implement "dynamic snippet" that changes data (usually <body>) depending
396
+ --- on the context, use `*.lua` file with function returning snippet data.
397
+ --- It should be an element in the output table (dict or array like).
398
+ ---
399
+ --- # Demo ~
400
+ ---
401
+ --- The best way to grasp the design of snippet management and expansion is to
402
+ --- try them out yourself. Here are steps for a basic demo:
403
+ --- - Create `snippets/global.json` file in the config directory with the content: >json
404
+ ---
405
+ --- {
406
+ --- "Basic": { "prefix": "ba", "body": "T1=$1 T2=$2 T0=$0" },
407
+ --- "Placeholders": { "prefix": "pl", "body": "T1=${1:aa}\nT2=${2:<$1>}" },
408
+ --- "Choices": { "prefix": "ch", "body": "T1=${1|a,b|} T2=${2|c,d|}" },
409
+ --- "Linked": { "prefix": "li", "body": "T1=$1\n\tT1=$1" },
410
+ --- "Variables": { "prefix": "va", "body": "Runtime: $VIMRUNTIME\n" },
411
+ --- "Complex": {
412
+ --- "prefix": "co",
413
+ --- "body": [ "T1=${1:$RANDOM}", "T3=${3:$1_${2:$1}}", "T2=$2" ]
414
+ --- }
415
+ --- }
416
+ --- <
417
+ --- - Set up |mini.snippets| as recommended in |MiniSnippets-examples|.
418
+ --- - Open Neovim. Type each snippet prefix and press <C-j> (even if there is
419
+ --- still active session). Explore from there.
420
+ ---@tag MiniSnippets-overview
421
+
422
+ --- # Basic snippet management config ~
423
+ ---
424
+ --- Example of snippet management setup that should cover most cases: >lua
425
+ ---
426
+ --- -- Setup
427
+ --- local gen_loader = require('mini.snippets').gen_loader
428
+ --- require('mini.snippets').setup({
429
+ --- snippets = {
430
+ --- -- Load custom file with global snippets first
431
+ --- gen_loader.from_file('~/.config/nvim/snippets/global.json'),
432
+ ---
433
+ --- -- Load snippets based on current language by reading files from
434
+ --- -- `snippets/` subdirectories from 'runtimepath' directories.
435
+ --- gen_loader.from_lang(),
436
+ --- },
437
+ --- })
438
+ --- <
439
+ --- This setup allows having single file with custom "global" snippets (will be
440
+ --- present in every buffer) and snippets which will be loaded based on the local
441
+ --- language (see |MiniSnippets.gen_loader.from_lang()|).
442
+ ---
443
+ --- Create language snippets manually (by creating and populating
444
+ --- `$XDG_CONFIG_HOME/nvim/snippets/lua.json` file) or by installing dedicated
445
+ --- snippet collection plugin (like `rafamadriz/friendly-snippets`).
446
+ ---
447
+ --- Note: all built-in loaders and |MiniSnippets.read_file()| cache their output
448
+ --- by default. It means that after a file is first read, changing it won't have
449
+ --- effect during current Neovim session. See |MiniSnippets.gen_loader| about how
450
+ --- to reset cache if necessary.
451
+ ---
452
+ --- # Select from all available snippets in current context ~
453
+ ---
454
+ --- With |MiniSnippets.default_match()|, expand snippets (<C-j> by default) at line
455
+ --- start or after whitespace. To be able to always select from all current
456
+ --- context snippets, make mapping similar to the following: >lua
457
+ ---
458
+ --- local rhs = function() MiniSnippets.expand({ match = false }) end
459
+ --- vim.keymap.set('i', '<C-g><C-j>', rhs, { desc = 'Expand all' })
460
+ --- <
461
+ --- # "Supertab"-like <Tab> / <S-Tab> mappings ~
462
+ ---
463
+ --- This module intentionally by default uses separate keys to expand and jump as
464
+ --- it enables cleaner use of nested sessions. Here is an example of setting up
465
+ --- custom <Tab> to "expand or jump" and <S-Tab> to "jump to previous": >lua
466
+ ---
467
+ --- local snippets = require('mini.snippets')
468
+ --- local match_strict = function(snips)
469
+ --- -- Do not match with whitespace to cursor's left
470
+ --- return snippets.default_match(snips, { pattern_fuzzy = '%S+' })
471
+ --- end
472
+ --- snippets.setup({
473
+ --- -- ... Set up snippets ...
474
+ --- mappings = { expand = '', jump_next = '', jump_prev = '' },
475
+ --- expand = { match = match_strict },
476
+ --- })
477
+ --- local expand_or_jump = function()
478
+ --- local can_expand = #MiniSnippets.expand({ insert = false }) > 0
479
+ --- if can_expand then vim.schedule(MiniSnippets.expand); return '' end
480
+ --- local is_active = MiniSnippets.session.get() ~= nil
481
+ --- if is_active then MiniSnippets.session.jump('next'); return '' end
482
+ --- return '\t'
483
+ --- end
484
+ --- local jump_prev = function() MiniSnippets.session.jump('prev') end
485
+ --- vim.keymap.set('i', '<Tab>', expand_or_jump, { expr = true })
486
+ --- vim.keymap.set('i', '<S-Tab>', jump_prev)
487
+ --- <
488
+ --- # Stop session immediately after jumping to final tabstop ~
489
+ ---
490
+ --- Utilize a dedicated |MiniSnippets-events|: >lua
491
+ ---
492
+ --- local fin_stop = function(args)
493
+ --- if args.data.tabstop_to == '0' then MiniSnippets.session.stop() end
494
+ --- end
495
+ --- local au_opts = { pattern = 'MiniSnippetsSessionJump', callback = fin_stop }
496
+ --- vim.api.nvim_create_autocmd('User', au_opts)
497
+ --- <
498
+ --- # Stop all sessions on Normal mode exit ~
499
+ ---
500
+ --- Use |ModeChanged| and |MiniSnippets-events| events: >lua
501
+ ---
502
+ --- local make_stop = function()
503
+ --- local au_opts = { pattern = '*:n', once = true }
504
+ --- au_opts.callback = function()
505
+ --- while MiniSnippets.session.get() do
506
+ --- MiniSnippets.session.stop()
507
+ --- end
508
+ --- end
509
+ --- vim.api.nvim_create_autocmd('ModeChanged', au_opts)
510
+ --- end
511
+ --- local opts = { pattern = 'MiniSnippetsSessionStart', callback = make_stop }
512
+ --- vim.api.nvim_create_autocmd('User', opts)
513
+ --- <
514
+ --- # Customize variable evaluation ~
515
+ ---
516
+ --- Create environment variables and `config.expand.insert` wrapper: >lua
517
+ ---
518
+ --- -- Use evnironment variables with value is same for all snippet sessions
519
+ --- vim.loop.os_setenv('USERNAME', 'user')
520
+ ---
521
+ --- -- Compute custom lookup for variables with dynamic values
522
+ --- local insert_with_lookup = function(snippet)
523
+ --- local lookup = {
524
+ --- TM_SELECTED_TEXT = table.concat(vim.fn.getreg('a', true, true), '\n'),
525
+ --- }
526
+ --- return MiniSnippets.default_insert(snippet, { lookup = lookup })
527
+ --- end
528
+ ---
529
+ --- require('mini.snippets').setup({
530
+ --- -- ... Set up snippets ...
531
+ --- expand = { insert = insert_with_lookup },
532
+ --- })
533
+ --- <
534
+ --- # Using Neovim's built-ins to insert snippet ~
535
+ ---
536
+ --- Define custom `expand.insert` in |MiniSnippets.config| and mappings: >lua
537
+ ---
538
+ --- require('mini.snippets').setup({
539
+ --- -- ... Set up snippets ...
540
+ --- expand = {
541
+ --- insert = function(snippet, _) vim.snippet.expand(snippet.body) end
542
+ --- }
543
+ --- })
544
+ --- -- Make jump mappings or skip to use built-in <Tab>/<S-Tab> in Neovim>=0.11
545
+ --- local jump_next = function()
546
+ --- if vim.snippet.active({direction = 1}) then return vim.snippet.jump(1) end
547
+ --- end
548
+ --- local jump_prev = function()
549
+ --- if vim.snippet.active({direction = -1}) then vim.snippet.jump(-1) end
550
+ --- end
551
+ --- vim.keymap.set({ 'i', 's' }, '<C-l>', jump_next)
552
+ --- vim.keymap.set({ 'i', 's' }, '<C-h>', jump_prev)
553
+ --- <
554
+ ---@tag MiniSnippets-examples
555
+
556
+ ---@alias __minisnippets_cache_opt <cache> `(boolean)` - whether to use cached output. Default: `true`.
557
+ ---@alias __minisnippets_silent_opt <silent> `(boolean)` - whether to hide non-error messages. Default: `false`.
558
+ ---@alias __minisnippets_loader_return function Snippet loader.
559
+
560
+ ---@diagnostic disable:undefined-field
561
+ ---@diagnostic disable:discard-returns
562
+ ---@diagnostic disable:unused-local
563
+
564
+ -- Module definition ==========================================================
565
+ local MiniSnippets = {}
566
+ local H = {}
567
+
568
+ --- Module setup
569
+ ---
570
+ ---@param config table|nil Module config table. See |MiniSnippets.config|.
571
+ ---
572
+ ---@usage >lua
573
+ --- require('mini.snippets').setup({}) -- replace {} with your config table
574
+ --- -- needs `snippets` field present
575
+ --- <
576
+ MiniSnippets.setup = function(config)
577
+ -- TODO: Remove after Neovim=0.9 support is dropped
578
+ if vim.fn.has('nvim-0.10') == 0 then
579
+ vim.notify(
580
+ '(mini.snippets) Neovim<0.10 is soft deprecated (module works but is not supported).'
581
+ .. " It will be deprecated after the next 'mini.nvim' release (module might not work)."
582
+ .. ' Please update your Neovim version.'
583
+ )
584
+ end
585
+
586
+ -- Export module
587
+ _G.MiniSnippets = MiniSnippets
588
+
589
+ -- Setup config
590
+ config = H.setup_config(config)
591
+
592
+ -- Apply config
593
+ H.apply_config(config)
594
+
595
+ -- Define behavior
596
+ H.create_autocommands()
597
+
598
+ -- Create default highlighting
599
+ H.create_default_hl()
600
+ end
601
+
602
+ --- Defaults ~
603
+ ---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
604
+ ---@text # Loaded snippets ~
605
+ ---
606
+ --- `config.snippets` is an array containing snippet data which can be: snippet
607
+ --- table, function loader, or (however deeply nested) array of snippet data.
608
+ ---
609
+ --- Snippet is a table with the following fields:
610
+ ---
611
+ --- - <prefix> `(string|table|nil)` - string used to match against current text.
612
+ --- If array, all strings should be used as separate prefixes.
613
+ --- - <body> `(string|table|nil)` - content of a snippet which should follow
614
+ --- the |MiniSnippets-syntax-specification|. Array is concatenated with `"\n"`.
615
+ --- - <desc> `(string|table|nil)` - description of snippet. Can be used to display
616
+ --- snippets in a more human readable form. Array is concatenated with `"\n"`.
617
+ ---
618
+ --- Function loaders are expected to be called with single `context` table argument
619
+ --- (containing any data about current context) and return same as `config.snippets`
620
+ --- data structure.
621
+ ---
622
+ --- `config.snippets` is resolved with `config.prepare` on every expand.
623
+ --- See |MiniSnippets.default_prepare()| for how it is done by default.
624
+ ---
625
+ --- For a practical example see |MiniSnippets-examples|.
626
+ --- Here is an illustration of `config.snippets` customization capabilities: >lua
627
+ ---
628
+ --- local gen_loader = require('mini.snippets').gen_loader
629
+ --- require('mini.snippets').setup({
630
+ --- snippets = {
631
+ --- -- Load custom file with global snippets first (order matters)
632
+ --- gen_loader.from_file('~/.config/nvim/snippets/global.json'),
633
+ ---
634
+ --- -- Or add them here explicitly
635
+ --- { prefix='cdate', body='$CURRENT_YEAR-$CURRENT_MONTH-$CURRENT_DATE' },
636
+ ---
637
+ --- -- Load snippets based on current language by reading files from
638
+ --- -- `snippets/` subdirectories from 'runtimepath' directories.
639
+ --- gen_loader.from_lang(),
640
+ ---
641
+ --- -- Load project-local snippets with `gen_loader.from_file()`
642
+ --- -- and relative path (file doesn't have to be present)
643
+ --- gen_loader.from_file('.vscode/project.code-snippets'),
644
+ ---
645
+ --- -- Custom loader for language-specific project-local snippets
646
+ --- function(context)
647
+ --- local rel_path = '.vscode/' .. context.lang .. '.code-snippets'
648
+ --- if vim.fn.filereadable(rel_path) == 0 then return end
649
+ --- return MiniSnippets.read_file(rel_path)
650
+ --- end,
651
+ ---
652
+ --- -- Ensure that some prefixes are not used (as there is no `body`)
653
+ --- { prefix = { 'bad', 'prefix' } },
654
+ --- }
655
+ --- })
656
+ --- <
657
+ --- # Mappings ~
658
+ ---
659
+ --- `config.mappings` describes which mappings are automatically created.
660
+ ---
661
+ --- `mappings.expand` is created globally in Insert mode and is used to expand
662
+ --- snippet at cursor. Use |MiniSnippets.expand()| for custom mappings.
663
+ ---
664
+ --- `mappings.jump_next`, `mappings.jump_prev`, and `mappings.stop` are created for
665
+ --- the duration of active snippet session(s) from |MiniSnippets.default_insert()|.
666
+ --- Used to jump to next/previous tabstop and stop active session respectively.
667
+ --- Use |MiniSnippets.session.jump()| and |MiniSnippets.session.stop()| for custom
668
+ --- Insert mode mappings.
669
+ --- Note: do not use `"<C-n>"` or `"<C-p>"` for any action as they conflict with
670
+ --- built-in completion: it forces them to mean "change focus to next/previous
671
+ --- completion item". This matters more frequently than when there is a tabstop
672
+ --- with choices due to how this module handles built-in completion during jumps.
673
+ ---
674
+ --- # Expand ~
675
+ ---
676
+ --- `config.expand` defines expand steps (see |MiniSnippets-glossary|), either after
677
+ --- pressing `mappings.expand` or starting manually via |MiniSnippets.expand()|.
678
+ ---
679
+ --- `expand.prepare` is a function that takes `raw_snippets` in the form of
680
+ --- `config.snippets` and should return a plain array of snippets (as described
681
+ --- in |MiniSnippets-glossary|). Will be called on every |MiniSnippets.expand()| call.
682
+ --- If returns second value, it will be used as context for warning messages.
683
+ --- Default: |MiniSnippets.default_prepare()|.
684
+ ---
685
+ --- `expand.match` is a function that takes `expand.prepare` output and returns
686
+ --- an array of matched snippets: one or several snippets user might intend to
687
+ --- eventually insert. Should sort matches in output from best to worst.
688
+ --- Entries can contain `region` field with current buffer region used to do
689
+ --- the match; usually it needs to be removed (similar to how |ins-completion|
690
+ --- and |abbreviations| work).
691
+ --- Default: |MiniSnippets.default_match()|
692
+ ---
693
+ --- `expand.select` is a function that takes output of `expand.match` and function
694
+ --- that inserts snippet (and also ensures Insert mode and removes snippet's match
695
+ --- region). Should allow user to perform interactive snippet selection and
696
+ --- insert the chosen one. Designed to be compatible with |vim.ui.select()|.
697
+ --- Called for any non-empty `expand.match` output (even with single entry).
698
+ --- Default: |MiniSnippets.default_select()|
699
+ ---
700
+ --- `expand.insert` is a function that takes single snippet table as input and
701
+ --- inserts snippet at cursor position. This is a main entry point for adding
702
+ --- text template to buffer and starting a snippet session.
703
+ --- If called inside |MiniSnippets.expand()| (which is a usual interactive case),
704
+ --- all it has to do is insert snippet at cursor position. Ensuring Insert mode
705
+ --- and removing matched snippet region is done beforehand.
706
+ --- Default: |MiniSnippets.default_insert()|
707
+ ---
708
+ --- Illustration of `config.expand` customization: >lua
709
+ ---
710
+ --- -- Supply extra data as context
711
+ --- local my_p = function(raw_snippets)
712
+ --- local _, cont = MiniSnippets.default_prepare({})
713
+ --- cont.cursor = vim.api.nvim_win_get_cursor()
714
+ --- return MiniSnippets.default_prepare(raw_snippets, { context = cont })
715
+ --- end
716
+ --- -- Perform fuzzy match based only on alphanumeric characters
717
+ --- local my_m = function(snippets)
718
+ --- return MiniSnippets.default_match(snippets, { pattern_fuzzy = '%w*' })
719
+ --- end
720
+ --- -- Always insert the best matched snippet
721
+ --- local my_s = function(snippets, insert) return insert(snippets[1]) end
722
+ --- -- Use different string to show empty tabstop as inline virtual text
723
+ --- local my_i = function(snippet)
724
+ --- return MiniSnippets.default_insert(snippet, { empty_tabstop = '$' })
725
+ --- end
726
+ ---
727
+ --- require('mini.snippets').setup({
728
+ --- -- ... Set up snippets ...
729
+ --- expand = { prepare = my_p, match = my_m, select = my_s, insert = my_i }
730
+ --- })
731
+ --- <
732
+ MiniSnippets.config = {
733
+ -- Array of snippets and loaders (see |MiniSnippets.config| for details).
734
+ -- Nothing is defined by default. Add manually to have snippets to match.
735
+ snippets = {},
736
+
737
+ -- Module mappings. Use `''` (empty string) to disable one.
738
+ mappings = {
739
+ -- Expand snippet at cursor position. Created globally in Insert mode.
740
+ expand = '<C-j>',
741
+
742
+ -- Interact with default `expand.insert` session.
743
+ -- Created for the duration of active session(s)
744
+ jump_next = '<C-l>',
745
+ jump_prev = '<C-h>',
746
+ stop = '<C-c>',
747
+ },
748
+
749
+ -- Functions describing snippet expansion. If `nil`, default values
750
+ -- are `MiniSnippets.default_<field>()`.
751
+ expand = {
752
+ -- Resolve raw config snippets at context
753
+ prepare = nil,
754
+ -- Match resolved snippets at cursor position
755
+ match = nil,
756
+ -- Possibly choose among matched snippets
757
+ select = nil,
758
+ -- Insert selected snippet
759
+ insert = nil,
760
+ },
761
+ }
762
+ --minidoc_afterlines_end
763
+
764
+ --- Expand snippet at cursor position
765
+ ---
766
+ --- Perform expand steps (see |MiniSnippets-glossary|).
767
+ --- Initial raw snippets are taken from `config.snippets` in current buffer.
768
+ --- Snippets from `vim.b.minisnippets_config` are appended to global snippet array.
769
+ ---
770
+ ---@param opts table|nil Options. Same structure as `expand` in |MiniSnippets.config|
771
+ --- and uses its values as default. There are differences in allowed values:
772
+ --- - Use `match = false` to have all buffer snippets as matches.
773
+ --- - Use `select = false` to always expand the best match (if any).
774
+ --- - Use `insert = false` to return all matches without inserting.
775
+ ---
776
+ --- Note: `opts.insert` is called after ensuring Insert mode, removing snippet's
777
+ --- match region, and positioning cursor.
778
+ ---
779
+ ---@return table|nil If `insert` is `false`, an array of matched snippets (`expand.match`
780
+ --- output). Otherwise `nil`.
781
+ ---
782
+ ---@usage >lua
783
+ --- -- Match, maybe select, and insert
784
+ --- MiniSnippets.expand()
785
+ ---
786
+ --- -- Match and force expand the best match (if any)
787
+ --- MiniSnippets.expand({ select = false })
788
+ ---
789
+ --- -- Use all current context snippets as matches
790
+ --- MiniSnippets.expand({ match = false })
791
+ ---
792
+ --- -- Get all matched snippets
793
+ --- local matches = MiniSnippets.expand({ insert = false })
794
+ ---
795
+ --- -- Get all current context snippets
796
+ --- local all = MiniSnippets.expand({ match = false, insert = false })
797
+ --- <
798
+ ---
799
+ ---@seealso |MiniSnippets.start_lsp_server()| to instead show loaded snippets
800
+ --- in (auto)completion engines (like |mini.completion|).
801
+ MiniSnippets.expand = function(opts)
802
+ if H.is_disabled() then return end
803
+ local config = H.get_config()
804
+ opts = vim.tbl_extend('force', config.expand, opts or {})
805
+
806
+ -- Validate
807
+ local prepare = opts.prepare or MiniSnippets.default_prepare
808
+ if not vim.is_callable(prepare) then H.error('`opts.prepare` should be callable') end
809
+
810
+ local match = false
811
+ if opts.match ~= false then match = opts.match or MiniSnippets.default_match end
812
+ if not (match == false or vim.is_callable(match)) then H.error('`opts.match` should be `false` or callable') end
813
+
814
+ local select = false
815
+ if opts.select ~= false then select = opts.select or MiniSnippets.default_select end
816
+ if not (select == false or vim.is_callable(select)) then H.error('`opts.select` should be `false` or callable') end
817
+
818
+ local insert = false
819
+ if opts.insert ~= false then insert = opts.insert or MiniSnippets.default_insert end
820
+ if not (insert == false or vim.is_callable(insert)) then H.error('`opts.insert` should be `false` or callable') end
821
+
822
+ -- Match
823
+ local all_snippets, context = prepare(config.snippets)
824
+ if not H.is_array_of(all_snippets, H.is_snippet) then H.error('`prepare` should return array of snippets') end
825
+ local matches = match == false and all_snippets or match(all_snippets)
826
+ if not H.is_array_of(matches, H.is_snippet) then H.error('`match` should return array of snippets') end
827
+
828
+ -- Act
829
+ if insert == false then return matches end
830
+ if #all_snippets == 0 then return H.notify('No snippets in context:\n' .. vim.inspect(context), 'WARN') end
831
+ if #matches == 0 then return H.notify('No matches in context:\n' .. vim.inspect(context), 'WARN') end
832
+
833
+ local insert_ext = H.make_extended_insert(insert)
834
+
835
+ if select == false then return insert_ext(matches[1]) end
836
+ select(matches, insert_ext)
837
+ end
838
+
839
+ --- Generate snippet loader
840
+ ---
841
+ --- This is a table with function elements. Call to actually get a loader.
842
+ ---
843
+ --- Common features for all produced loaders:
844
+ --- - Designed to work with |MiniSnippets-file-specification|.
845
+ --- - Cache output by default, i.e. second and later calls with same input value
846
+ --- don't read file system. Different loaders from same generator share cache.
847
+ --- Disable by setting `opts.cache` to `false`.
848
+ --- To clear all cache, call |MiniSnippets.setup()|. For example:
849
+ --- `MiniSnippets.setup(MiniSnippets.config)`
850
+ --- - Use |vim.notify()| to show problems during loading while trying to load as
851
+ --- much correctly defined snippet data as possible.
852
+ --- Disable by setting `opts.silent` to `true`.
853
+ MiniSnippets.gen_loader = {}
854
+
855
+ --- Generate language loader
856
+ ---
857
+ --- Output loads files from `snippets/` subdirectories of |'runtimepath'| matching
858
+ --- configured language patterns.
859
+ --- See |MiniSnippets.gen_loader.from_runtime()| for runtime loading details.
860
+ ---
861
+ --- Language is taken from <lang> field (if present with string value) of `context`
862
+ --- argument used in loader calls during "prepare" stage.
863
+ --- This is compatible with |MiniSnippets.default_prepare()| and most snippet
864
+ --- collection plugins.
865
+ ---
866
+ ---@param opts table|nil Options. Possible values:
867
+ --- - <lang_patterns> `(table)` - map from language to array of runtime patterns
868
+ --- used to find snippet files, as in |MiniSnippets.gen_loader.from_runtime()|.
869
+ --- Patterns will be processed in order. With |MiniSnippets.default_prepare()|
870
+ --- it means if snippets have same prefix, data from later patterns is used.
871
+ --- To interactively check the current language with default context, execute
872
+ --- `:=MiniSnippets.default_prepare({})` and see data in the second table.
873
+ ---
874
+ --- Default pattern array (for non-empty language) is constructed as to read
875
+ --- `*.json` and `*.lua` files that are:
876
+ --- - Inside `snippets/` subdirectory named as language (files can be however
877
+ --- deeply nested).
878
+ --- - Named as language and is in `snippets/` directory (however deep).
879
+ --- Example for "lua" language: >lua
880
+ --- { 'lua/**/*.json', 'lua/**/*.lua', '**/lua.json', '**/lua.lua' }
881
+ --- <
882
+ --- Add entry for `""` (empty string) as language to be sourced when `lang`
883
+ --- context is empty string (which is usually temporary scratch buffers).
884
+ ---
885
+ --- - __minisnippets_cache_opt
886
+ --- Note: caching is done per used runtime pattern, not `lang` value to allow
887
+ --- different `from_lang()` loaders to share cache.
888
+ --- - __minisnippets_silent_opt
889
+ ---
890
+ ---@return __minisnippets_loader_return
891
+ ---
892
+ ---@usage >lua
893
+ --- -- Adjust language patterns
894
+ --- local latex_patterns = { 'latex/**/*.json', '**/latex.json' }
895
+ --- local lang_patterns = {
896
+ --- tex = latex_patterns, plaintex = latex_patterns,
897
+ --- -- Recognize special injected language of markdown tree-sitter parser
898
+ --- markdown_inline = { 'markdown.json' },
899
+ --- }
900
+ --- local gen_loader = require('mini.snippets').gen_loader
901
+ --- require('mini.snippets').setup({
902
+ --- snippets = {
903
+ --- gen_loader.from_lang({ lang_patterns = lang_patterns }),
904
+ --- },
905
+ --- })
906
+ --- <
907
+ MiniSnippets.gen_loader.from_lang = function(opts)
908
+ opts = vim.tbl_extend('force', { lang_patterns = {}, cache = true, silent = false }, opts or {})
909
+ for lang, tbl in pairs(opts.lang_patterns) do
910
+ if type(lang) ~= 'string' then H.error('Keys of `opts.lang_patterns` should be string language names') end
911
+ if not H.is_array_of(tbl, H.is_string) then H.error('Values of `opts.lang_patterns` should be string arrays') end
912
+ end
913
+
914
+ local loaders, loader_opts = {}, { cache = opts.cache, silent = opts.silent }
915
+
916
+ return function(context)
917
+ local lang = (context or {}).lang
918
+ if type(lang) ~= 'string' then return {} end
919
+
920
+ local patterns = opts.lang_patterns[lang]
921
+ if patterns == nil and lang == '' then return {} end
922
+ -- NOTE: Don't use `{json,lua}` for better compatibility, as it seems that
923
+ -- its support might depend on the shell (and might not work on Windows).
924
+ -- Which is shame because fewer patterns used mean fewer calls to cache.
925
+ patterns = patterns
926
+ or { lang .. '/**/*.json', lang .. '/**/*.lua', '**/' .. lang .. '.json', '**/' .. lang .. '.lua' }
927
+
928
+ local res = {}
929
+ for _, pat in ipairs(patterns) do
930
+ local loader = loaders[pat] or MiniSnippets.gen_loader.from_runtime(pat, loader_opts)
931
+ loaders[pat] = loader
932
+ table.insert(res, loader(context))
933
+ end
934
+ return res
935
+ end
936
+ end
937
+
938
+ --- Generate runtime loader
939
+ ---
940
+ --- Output loads files which match `pattern` inside `snippets/` directories from
941
+ --- |'runtimepath'|. This is useful to simultaneously read several similarly
942
+ --- named files from different sources. Order from |'runtimepath'| is preserved.
943
+ ---
944
+ --- Typical case is loading snippets for a language from files like `xxx.{json,lua}`
945
+ --- but located in different `snippets/` directories inside |'runtimepath'|.
946
+ --- - `<config>`/snippets/lua.json - manually curated snippets in user config.
947
+ --- - `<path/to/installed/plugin>`/snippets/lua.json - from installed plugin.
948
+ --- - `<config>`/after/snippets/lua.json - used to adjust snippets from plugins.
949
+ --- For example, remove some snippets by using prefixes and no body.
950
+ ---
951
+ ---@param pattern string Pattern of files to read. Can have wildcards as described
952
+ --- in |nvim_get_runtime_file()|. Example for "lua" language: `'lua.{json,lua}'`.
953
+ ---@param opts table|nil Options. Possible fields:
954
+ --- - <all> `(boolean)` - whether to load from all matching runtime files.
955
+ --- Default: `true`.
956
+ --- - __minisnippets_cache_opt
957
+ --- Note: caching is done per `pattern` value, which assumes that both
958
+ --- |'runtimepath'| value and snippet files do not change during Neovim session.
959
+ --- Caching this way gives significant speed improvement by reducing the need
960
+ --- to traverse file system on every snippet expand.
961
+ --- - __minisnippets_silent_opt
962
+ ---
963
+ ---@return __minisnippets_loader_return
964
+ MiniSnippets.gen_loader.from_runtime = function(pattern, opts)
965
+ if type(pattern) ~= 'string' then H.error('`pattern` should be string') end
966
+ opts = vim.tbl_extend('force', { all = true, cache = true, silent = false }, opts or {})
967
+
968
+ pattern = 'snippets/' .. pattern
969
+ local cache, read_opts = opts.cache, { cache = opts.cache, silent = opts.silent }
970
+ local read = function(p) return MiniSnippets.read_file(p, read_opts) end
971
+ return function()
972
+ if cache and H.cache.runtime[pattern] ~= nil then return vim.deepcopy(H.cache.runtime[pattern]) end
973
+
974
+ local res = vim.tbl_map(read, vim.api.nvim_get_runtime_file(pattern, opts.all))
975
+ if cache then H.cache.runtime[pattern] = vim.deepcopy(res) end
976
+ return res
977
+ end
978
+ end
979
+
980
+ --- Generate single file loader
981
+ ---
982
+ --- Output is a thin wrapper around |MiniSnippets.read_file()| which will skip
983
+ --- warning if file is absent (other messages are still shown). Use it to load
984
+ --- file which is not guaranteed to exist (like project-local snippets).
985
+ ---
986
+ ---@param path string Same as in |MiniSnippets.read_file()|.
987
+ ---@param opts table|nil Same as in |MiniSnippets.read_file()|.
988
+ ---
989
+ ---@return __minisnippets_loader_return
990
+ MiniSnippets.gen_loader.from_file = function(path, opts)
991
+ if type(path) ~= 'string' then H.error('`path` should be string') end
992
+ opts = vim.tbl_extend('force', { cache = true, silent = false }, opts or {})
993
+
994
+ return function()
995
+ local full_path = vim.fn.fnamemodify(path, ':p')
996
+ if vim.fn.filereadable(full_path) ~= 1 then return {} end
997
+ return MiniSnippets.read_file(full_path, opts) or {}
998
+ end
999
+ end
1000
+
1001
+ --- Read file with snippet data
1002
+ ---
1003
+ ---@param path string Path to file with snippets. Can be relative.
1004
+ --- See |MiniSnippets-file-specification| for supported file formats.
1005
+ ---@param opts table|nil Options. Possible fields:
1006
+ --- - __minisnippets_cache_opt
1007
+ --- Note: Caching is done per full path only after successful reading.
1008
+ --- - __minisnippets_silent_opt
1009
+ ---
1010
+ ---@return table|nil Array of snippets or `nil` if failed (also warn with |vim.notify()|
1011
+ --- about the reason).
1012
+ MiniSnippets.read_file = function(path, opts)
1013
+ if type(path) ~= 'string' then H.error('`path` should be string') end
1014
+ opts = vim.tbl_extend('force', { cache = true, silent = false }, opts or {})
1015
+
1016
+ path = vim.fn.fnamemodify(path, ':p')
1017
+ local problem_prefix = 'There were problems reading file ' .. path .. ':\n'
1018
+ if opts.cache and H.cache.file[path] ~= nil then return vim.deepcopy(H.cache.file[path]) end
1019
+
1020
+ if vim.fn.filereadable(path) ~= 1 then
1021
+ return H.notify(problem_prefix .. 'File is absent or not readable', 'WARN', opts.silent)
1022
+ end
1023
+ local ext = path:match('%.([^%.]+)$')
1024
+ if ext == nil or not (ext == 'lua' or ext == 'json' or ext == 'code-snippets') then
1025
+ return H.notify(problem_prefix .. 'Extension is not supported', 'WARN', opts.silent)
1026
+ end
1027
+
1028
+ local res = H.file_readers[ext](path, opts.silent)
1029
+
1030
+ -- Notify about problems but still cache if there are read snippets
1031
+ local prob = table.concat(res.problems, '\n')
1032
+ if prob ~= '' then H.notify(problem_prefix .. prob, 'WARN', opts.silent) end
1033
+
1034
+ if res.snippets == nil then return nil end
1035
+ if opts.cache then H.cache.file[path] = vim.deepcopy(res.snippets) end
1036
+ return res.snippets
1037
+ end
1038
+
1039
+ --- Default prepare
1040
+ ---
1041
+ --- Normalize raw snippets (as in `snippets` from |MiniSnippets.config|) based on
1042
+ --- supplied context:
1043
+ --- - Traverse and flatten nested arrays. Function loaders are executed with
1044
+ --- `opts.context` as argument and output is processed recursively.
1045
+ --- - Ensure unique non-empty prefixes: later ones completely override earlier
1046
+ --- ones (similar to how |ftplugin| and similar runtime design behave).
1047
+ --- Empty string prefixes are all added (to allow inserting without matching).
1048
+ --- - Transform and infer fields:
1049
+ --- - Multiply array `prefix` into several snippets with same body/description.
1050
+ --- Infer absent `prefix` as empty string.
1051
+ --- - Concatenate array `body` with `"\n"`. Do not infer absent `body` to have
1052
+ --- it remove previously added snippet with the same prefix.
1053
+ --- - Concatenate array `desc` with `"\n"`. Infer `desc` field from `description`
1054
+ --- (for compatibility) or `body` fields, in that order.
1055
+ --- - Sort output by prefix.
1056
+ ---
1057
+ --- Unlike |MiniSnippets.gen_loader| entries, there is no output caching. This
1058
+ --- avoids duplicating data from `gen_loader` cache and reduces memory usage.
1059
+ --- It also means that every |MiniSnippets.expand()| call prepares snippets, which
1060
+ --- is usually fast enough. If not, consider manual caching: >lua
1061
+ ---
1062
+ --- local cache = {}
1063
+ --- local prepare_cached = function(raw_snippets)
1064
+ --- local _, cont = MiniSnippets.default_prepare({})
1065
+ --- local id = 'buf=' .. cont.buf_id .. ',lang=' .. cont.lang
1066
+ --- if cache[id] then return unpack(vim.deepcopy(cache[id])) end
1067
+ --- local snippets = MiniSnippets.default_prepare(raw_snippets)
1068
+ --- cache[id] = vim.deepcopy({ snippets, cont })
1069
+ --- return snippets, cont
1070
+ --- end
1071
+ --- <
1072
+ ---@param raw_snippets table Array of snippet data as from |MiniSnippets.config|.
1073
+ ---@param opts table|nil Options. Possible fields:
1074
+ --- - <context> `(any)` - Context used as an argument for callable snippet data.
1075
+ --- Default: table with <buf_id> (current buffer identifier) and <lang> (local
1076
+ --- language) fields. Language is computed from tree-sitter parser at cursor
1077
+ --- (allows different snippets in injected languages), |'filetype'| otherwise.
1078
+ ---
1079
+ ---@return ... Array of snippets and supplied context (default if none was supplied).
1080
+ MiniSnippets.default_prepare = function(raw_snippets, opts)
1081
+ if not H.islist(raw_snippets) then H.error('`raw_snippets` should be array') end
1082
+ opts = vim.tbl_extend('force', { context = nil }, opts or {})
1083
+ local context = opts.context
1084
+ if context == nil then context = H.get_default_context() end
1085
+
1086
+ -- Traverse snippets to have unique non-empty prefixes
1087
+ local res = {}
1088
+ H.traverse_raw_snippets(raw_snippets, res, context)
1089
+
1090
+ -- Convert to array ordered by prefix
1091
+ res = vim.tbl_values(res)
1092
+ table.sort(res, function(a, b) return a.prefix < b.prefix end)
1093
+ return res, context
1094
+ end
1095
+
1096
+ --- Default match
1097
+ ---
1098
+ --- Match snippets based on the line before cursor.
1099
+ ---
1100
+ --- Tries two matching approaches consecutively:
1101
+ --- - Find exact snippet prefix (if present and non-empty) to the left of cursor.
1102
+ --- It should also be preceded with a byte that matches `pattern_exact_boundary`.
1103
+ --- In case of any match, return the one with the longest prefix.
1104
+ --- - Match fuzzily snippet prefixes against the base (text to the left of cursor
1105
+ --- extracted via `opts.pattern_fuzzy`). Matching is done via |matchfuzzy()|.
1106
+ --- Empty base results in all snippets being matched. Return all fuzzy matches.
1107
+ ---
1108
+ ---@param snippets table Array of snippets which can be matched.
1109
+ ---@param opts table|nil Options. Possible fields:
1110
+ --- - <pattern_exact_boundary> `(string)` - Lua pattern for the byte to the left
1111
+ --- of exact match to accept it. Line start is matched against empty string;
1112
+ --- use `?` quantifier to allow it as boundary.
1113
+ --- Default: `[%s%p]?` (accept only whitespace and punctuation as boundary,
1114
+ --- allow match at line start).
1115
+ --- Example: prefix "l" matches in lines `l`, `_l`, `x l`; but not `1l`, `ll`.
1116
+ --- - <pattern_fuzzy> `(string)` - Lua pattern to extract base to the left of
1117
+ --- cursor for fuzzy matching. Supply empty string to skip this step.
1118
+ --- Default: `'%S*'` (as many as possible non-whitespace; allow empty string).
1119
+ ---
1120
+ ---@return table Array of matched snippets ordered from best to worst match.
1121
+ ---
1122
+ ---@usage >lua
1123
+ --- -- Accept any exact match
1124
+ --- MiniSnippets.default_match(snippets, { pattern_exact_boundary = '.?' })
1125
+ ---
1126
+ --- -- Perform fuzzy match based only on alphanumeric characters
1127
+ --- MiniSnippets.default_match(snippets, { pattern_fuzzy = '%w*' })
1128
+ --- <
1129
+ MiniSnippets.default_match = function(snippets, opts)
1130
+ if not H.is_array_of(snippets, H.is_snippet) then H.error('`snippets` should be array of snippets') end
1131
+ opts = vim.tbl_extend('force', { pattern_exact_boundary = '[%s%p]?', pattern_fuzzy = '%S*' }, opts or {})
1132
+ if not H.is_string(opts.pattern_exact_boundary) then H.error('`opts.pattern_exact_boundary` should be string') end
1133
+
1134
+ -- Compute line before cursor. Treat Insert mode as exclusive for right edge.
1135
+ local lnum, col = vim.fn.line('.'), vim.fn.col('.')
1136
+ local to = col - (vim.fn.mode() == 'i' and 1 or 0)
1137
+ local line = vim.fn.getline(lnum):sub(1, to)
1138
+
1139
+ -- Exact. Use 0 as initial best match width to not match empty prefixes.
1140
+ local best_id, best_match_width = nil, 0
1141
+ local pattern_boundary = '^' .. opts.pattern_exact_boundary .. '$'
1142
+ for i, s in pairs(snippets) do
1143
+ local w = (s.prefix or ''):len()
1144
+ if best_match_width < w and line:sub(-w) == s.prefix and line:sub(-w - 1, -w - 1):find(pattern_boundary) then
1145
+ best_id, best_match_width = i, w
1146
+ end
1147
+ end
1148
+ if best_id ~= nil then
1149
+ local res = vim.deepcopy(snippets[best_id])
1150
+ res.region = { from = { line = lnum, col = to - best_match_width + 1 }, to = { line = lnum, col = to } }
1151
+ return { res }
1152
+ end
1153
+
1154
+ -- Fuzzy
1155
+ if not H.is_string(opts.pattern_fuzzy) then H.error('`opts.pattern_fuzzy` should be string') end
1156
+ if opts.pattern_fuzzy == '' then return {} end
1157
+
1158
+ local base = string.match(line, opts.pattern_fuzzy .. '$')
1159
+ if base == nil then return {} end
1160
+ if base == '' then return vim.deepcopy(snippets) end
1161
+
1162
+ local snippets_with_prefix = vim.tbl_filter(function(s) return s.prefix ~= nil end, snippets)
1163
+ local fuzzy_matches = vim.fn.matchfuzzy(snippets_with_prefix, base, { key = 'prefix' })
1164
+ local from_col = to - base:len() + 1
1165
+ for _, s in ipairs(fuzzy_matches) do
1166
+ s.region = { from = { line = lnum, col = from_col }, to = { line = lnum, col = to } }
1167
+ end
1168
+
1169
+ return fuzzy_matches
1170
+ end
1171
+
1172
+ --- Default select
1173
+ ---
1174
+ --- Show snippets as |vim.ui.select()| items and insert the chosen one.
1175
+ --- For best interactive experience requires `vim.ui.select()` to work from Insert
1176
+ --- mode (be properly called and restore Insert mode after choice).
1177
+ --- This is the case for at least |MiniPick.ui_select()| and Neovim's default.
1178
+ ---
1179
+ ---@param snippets table Array of snippets (as an output of `config.expand.match`).
1180
+ ---@param insert function|nil Function to insert chosen snippet (passed as the only
1181
+ --- argument). Expected to remove snippet's match region (if present as a field)
1182
+ --- and ensure proper cursor position in Insert mode.
1183
+ --- Default: |MiniSnippets.default_insert()|.
1184
+ ---@param opts table|nil Options. Possible fields:
1185
+ --- - <insert_single> `(boolean)` - whether to skip |vim.ui.select()| for `snippets`
1186
+ --- with a single entry and insert it directly. Default: `true`.
1187
+ MiniSnippets.default_select = function(snippets, insert, opts)
1188
+ if not H.is_array_of(snippets, H.is_snippet) then H.error('`snippets` should be an array of snippets') end
1189
+ if #snippets == 0 then return H.notify('No snippets to select from', 'WARN') end
1190
+ insert = insert or MiniSnippets.default_insert
1191
+ if not vim.is_callable(insert) then H.error('`insert` should be callable') end
1192
+ opts = opts or {}
1193
+
1194
+ if #snippets == 1 and (opts.insert_single == nil or opts.insert_single == true) then
1195
+ insert(snippets[1])
1196
+ return
1197
+ end
1198
+
1199
+ -- Format
1200
+ local prefix_width = 0
1201
+ for i, s in ipairs(snippets) do
1202
+ local prefix = s.prefix or '<No prefix>'
1203
+ prefix_width = math.max(prefix_width, vim.fn.strdisplaywidth(prefix))
1204
+ end
1205
+ local format_item = function(s)
1206
+ local prefix, desc = s.prefix or '<No prefix>', s.desc or s.description or '<No description>'
1207
+ local pad = string.rep(' ', prefix_width - vim.fn.strdisplaywidth(prefix))
1208
+ return prefix .. pad .. ' │ ' .. desc
1209
+ end
1210
+
1211
+ -- Schedule insert to allow `vim.ui.select` override to restore window/cursor
1212
+ local on_choice = vim.schedule_wrap(function(item, _) insert(item) end)
1213
+ vim.ui.select(snippets, { prompt = 'Snippets', format_item = format_item }, on_choice)
1214
+ end
1215
+
1216
+ --- Default insert
1217
+ ---
1218
+ --- Prepare for snippet insert and do it:
1219
+ --- - Ensure Insert mode.
1220
+ --- - Delete snippet's match region (if present as <region> field). Ensure cursor.
1221
+ --- - Parse snippet body with |MiniSnippets.parse()| and enabled `normalize`.
1222
+ --- In particular, evaluate variables, ensure final node presence and same
1223
+ --- text for nodes with same tabstops. Stop if not able to.
1224
+ --- - Insert snippet at cursor:
1225
+ --- - Add snippet's text. Lines are split at "\n".
1226
+ --- Indent and left comment leaders (inferred from |'commentstring'| and
1227
+ --- |'comments'|) of current line are repeated on the next.
1228
+ --- Tabs ("\t") are expanded according to |'expandtab'| and |'shiftwidth'|.
1229
+ --- - If there is an actionable tabstop (not final), start snippet session.
1230
+ ---
1231
+ --- # Session life cycle ~
1232
+ --- *MiniSnippets-session*
1233
+ ---
1234
+ --- - Start with cursor at first tabstop. If there are linked tabstops, cursor
1235
+ --- is placed at start of reference node (see |MiniSnippets-glossary|).
1236
+ --- All tabstops are visualized with dedicated highlight groups (see "Highlight
1237
+ --- groups" section in |mini.snippets|).
1238
+ --- Empty tabstops are visualized with inline virtual text ("•"/"∎" for
1239
+ --- regular/final tabstops) meaning that it is not an actual text in the
1240
+ --- buffer and will be removed after session is stopped.
1241
+ ---
1242
+ --- - Decide whether you want to replace the placeholder. If not, jump to next or
1243
+ --- previous tabstop. If yes, edit it: add new and/or delete already added text.
1244
+ --- While doing so, several things happen in all linked tabstops (if any):
1245
+ ---
1246
+ --- - After first typed character the placeholder is removed and highlighting
1247
+ --- changes from `MiniSnippetsCurrentReplace` to `MiniSnippetsCurrent`.
1248
+ --- - Text in all tabstop nodes is synchronized with the reference one.
1249
+ --- Relative indent of reference tabstop's text is preserved: all but first
1250
+ --- lines in linked tabstops are reindented based on the first line indent.
1251
+ --- Note: text sync is forced only for current tabstop (for performance).
1252
+ ---
1253
+ --- - Jump with <C-l> / <C-h> to next / previous tabstop. Exact keys can be
1254
+ --- adjusted in |MiniSnippets.config| `mappings`.
1255
+ --- See |MiniSnippets.session.jump()| for jumping details.
1256
+ ---
1257
+ --- - If tabstop has choices, all of them are shown after each jump and deleting
1258
+ --- tabstop text. It is done with |complete()|, so use <C-n> / <C-p> to select
1259
+ --- next / previous choice. Type text to narrow down the list.
1260
+ --- Works best when |'completeopt'| option contains `menuone` and `noselect` flags.
1261
+ --- Note: deleting character hides the list due to how |complete()| works;
1262
+ --- delete whole tabstop text (for example with one or more |i_CTRL-W|) for
1263
+ --- full list to reappear.
1264
+ ---
1265
+ --- - Nest another session by expanding snippet in the same way as without
1266
+ --- active session (can be even done in another buffer). If snippet has no
1267
+ --- actionable tabstop, text is just inserted. Otherwise start nested session:
1268
+ ---
1269
+ --- - Suspend current session: hide highlights, keep text change tracking.
1270
+ --- - Start new session and act as if it is the only one (edit/jump/nest).
1271
+ --- - When ready (possibly after even more nested sessions), stop the session.
1272
+ --- This will resume previous one: sync text for its current tabstop and
1273
+ --- show highlighting.
1274
+ --- The experience of text synchronization only after resuming session is
1275
+ --- similar to how editing in |visual-block| mode works.
1276
+ --- Nothing else (like cursor/mode/buffer) is changed for a smoother
1277
+ --- automated session stop.
1278
+ ---
1279
+ --- Notes about the choice of the "session stack" approach to nesting over more
1280
+ --- common "merge into single session" approach:
1281
+ --- - Does not overload with highlighting.
1282
+ --- - Allows nested sessions in different buffers.
1283
+ --- - Doesn't need a complex logic of injecting one session into another.
1284
+ ---
1285
+ --- - Repeat edit/jump/nest steps any number of times.
1286
+ ---
1287
+ --- - Stop. It can be done in two ways:
1288
+ ---
1289
+ --- - Manually by pressing <C-c> or calling |MiniSnippets.session.stop()|.
1290
+ --- Exact key can be adjusted in |MiniSnippets.config| `mappings`.
1291
+ --- - Automatically: any text edit or switching to Normal mode stops session
1292
+ --- if final tabstop (`$0`) is current. Its presence is ensured after insert.
1293
+ --- Not stopping session right away after jumping to final mode (as most
1294
+ --- other snippet plugins do) allows going back to other tabstops in case
1295
+ --- of a late missed typo. Wrapping around the edge during jumping also
1296
+ --- helps with that.
1297
+ --- If current tabstop is not final, exiting into Normal mode for quick edit
1298
+ --- outside of snippets range (or carefully inside) is fine. Later get back
1299
+ --- into Insert mode and jump to next tabstop or manually stop session.
1300
+ --- See |MiniSnippets-examples| for how to set up custom stopping rules.
1301
+ ---
1302
+ --- Use |MiniSnippets.session.get()| to get data about active/nested session(s).
1303
+ --- Use |MiniSnippets.session.jump()| / |MiniSnippets.session.stop()| in mappings.
1304
+ ---
1305
+ --- What is allowed but not officially supported/recommended:
1306
+ ---
1307
+ --- - Editing text within snippet range but outside of session life cycle. Mostly
1308
+ --- behaves as expected, but may harm tracking metadata (|extmarks|).
1309
+ --- In general anything but deleting tabstop range should be OK.
1310
+ --- Text synchronization of current tabstop would still be active.
1311
+ ---
1312
+ --- # Events ~
1313
+ --- *MiniSnippets-events*
1314
+ ---
1315
+ --- General session activity (autocommand data contains <session> field):
1316
+ --- - `MiniSnippetsSessionStart` - after a session is started.
1317
+ --- - `MiniSnippetsSessionStop` - before a session is stopped.
1318
+ ---
1319
+ --- Nesting session activity (autocommand data contains <session> field):
1320
+ --- - `MiniSnippetsSessionSuspend` - before a session is suspended.
1321
+ --- - `MiniSnippetsSessionResume` - after a session is resumed.
1322
+ ---
1323
+ --- Jumping between tabstops (autocommand data contains <tabstop_from> and
1324
+ --- <tabstop_new> fields):
1325
+ --- - `MiniSnippetsSessionJumpPre` - before jumping to a new tabstop.
1326
+ --- - `MiniSnippetsSessionJump` - after jumping to a new tabstop.
1327
+ ---
1328
+ ---@param snippet table Snippet table. Field <body> is mandatory.
1329
+ ---@param opts table|nil Options. Possible fields:
1330
+ --- - <empty_tabstop> `(string)` - used to visualize empty regular tabstops.
1331
+ --- Default: "•".
1332
+ --- - <empty_tabstop_final> `(string)` - used to visualize empty final tabstop(s).
1333
+ --- Default: "∎".
1334
+ --- - <lookup> `(table)` - passed to |MiniSnippets.parse()|. Use it to adjust
1335
+ --- how variables are evaluated. Default: `{}`.
1336
+ MiniSnippets.default_insert = function(snippet, opts)
1337
+ if not H.is_snippet(snippet) then H.error('`snippet` should be a snippet table') end
1338
+
1339
+ local default_opts = { empty_tabstop = '•', empty_tabstop_final = '∎', lookup = {} }
1340
+ opts = vim.tbl_deep_extend('force', default_opts, opts or {})
1341
+ if not H.is_string(opts.empty_tabstop) then H.error('`empty_tabstop` should be string') end
1342
+ if not H.is_string(opts.empty_tabstop_final) then H.error('`empty_tabstop_final` should be string') end
1343
+ if type(opts.lookup) ~= 'table' then H.error('`lookup` should be table') end
1344
+
1345
+ local nodes = MiniSnippets.parse(snippet.body, { normalize = true, lookup = opts.lookup })
1346
+
1347
+ -- Ensure insert in Insert mode (for proper cursor positioning at EOL)
1348
+ H.call_in_insert_mode(function()
1349
+ H.delete_region(snippet.region)
1350
+ H.session_init(H.session_new(nodes, snippet, opts), true)
1351
+ end)
1352
+ end
1353
+
1354
+ --- Work with snippet session from |MiniSnippets.default_insert()|
1355
+ MiniSnippets.session = {}
1356
+
1357
+ --- Get data about active session
1358
+ ---
1359
+ ---@param all boolean|nil Whether to return array with the whole session stack.
1360
+ --- Default: `false`.
1361
+ ---
1362
+ ---@return table Single table with session data (if `all` is `false`) or array of them.
1363
+ --- Session data contains the following fields:
1364
+ --- - <buf_id> `(number)` - identifier of session's buffer.
1365
+ --- - <cur_tabstop> `(string)` - identifier of session's current tabstop.
1366
+ --- - <extmark_id> `(number)` - |extmark| identifier which track session range.
1367
+ --- - <insert_args> `(table)` - |MiniSnippets.default_insert()| arguments used to
1368
+ --- create the session. A table with <snippet> and <opts> fields.
1369
+ --- - <nodes> `(table)` - parsed array of snippet nodes which is kept up to date
1370
+ --- during session. Has the structure of a normalized |MiniSnippets.parse()|
1371
+ --- output, plus every node contains `extmark_id` field with |extmark| identifier
1372
+ --- which can be used to get data about the current node state.
1373
+ --- - <ns_id> `(number)` - |namespace| identifier for all session's extmarks.
1374
+ --- - <tabstops> `(table)` - data about session's tabstops. Fields are string
1375
+ --- tabstop identifiers and values are tables with the following fields:
1376
+ --- - <is_visited> `(boolean)` - whether tabstop was visited.
1377
+ --- - <next> `(string)` - identifier of the next tabstop.
1378
+ --- - <prev> `(string)` - identifier of the previous tabstop.
1379
+ MiniSnippets.session.get = function(all) return vim.deepcopy(all and H.sessions or H.get_active_session()) end
1380
+
1381
+ --- Jump to next/previous tabstop
1382
+ ---
1383
+ --- Make next/previous tabstop be current. Executes the following steps:
1384
+ --- - Mark current tabstop as visited.
1385
+ --- - Find the next/previous tabstop id assuming they are sorted as numbers.
1386
+ --- Tabstop "0" is always last. Search is wrapped around the edges: first and
1387
+ --- final tabstops are next/previous for one another.
1388
+ --- - Focus on target tabstop:
1389
+ --- - Ensure session's buffer is current.
1390
+ --- - Adjust highlighting of affected nodes.
1391
+ --- - Set cursor at tabstop's reference node (first node among linked).
1392
+ --- Cursor is placed on left edge if tabstop has not been edited yet (so
1393
+ --- typing text replaces placeholder), on right edge otherwise (to update
1394
+ --- already edited text).
1395
+ --- - Show all choices for tabstop with choices. Navigating through choices
1396
+ --- will update tabstop's text.
1397
+ ---
1398
+ ---@param direction string One of "next" or "prev".
1399
+ MiniSnippets.session.jump = function(direction)
1400
+ if not (direction == 'prev' or direction == 'next') then H.error('`direction` should be one of "prev", "next"') end
1401
+ H.call_in_insert_mode(function() H.session_jump(H.get_active_session(), direction) end)
1402
+ end
1403
+
1404
+ --- Stop (only) active session
1405
+ ---
1406
+ --- To stop all nested sessions use the following code: >lua
1407
+ ---
1408
+ --- while MiniSnippets.session.get() do
1409
+ --- MiniSnippets.session.stop()
1410
+ --- end
1411
+ --- <
1412
+ MiniSnippets.session.stop = function()
1413
+ local cur_session = H.get_active_session()
1414
+ if cur_session == nil then return end
1415
+ H.session_deinit(cur_session, true)
1416
+ H.sessions[#H.sessions] = nil
1417
+ if #H.sessions == 0 then
1418
+ vim.api.nvim_del_augroup_by_name('MiniSnippetsTrack')
1419
+ H.unmap_in_sessions()
1420
+ end
1421
+ H.clean_sessions()
1422
+ H.session_init(H.get_active_session(), false)
1423
+ end
1424
+
1425
+ --- Parse snippet
1426
+ ---
1427
+ ---@param snippet_body string|table Snippet body as string or array of strings.
1428
+ --- Should follow |MiniSnippets-syntax-specification|.
1429
+ ---@param opts table|nil Options. Possible fields:
1430
+ --- - <normalize> `(boolean)` - whether to normalize nodes:
1431
+ --- - Evaluate variable nodes and add output as a `text` field.
1432
+ --- If variable is not set, `text` field is `nil`.
1433
+ --- Values from `opts.lookup` are preferred over evaluation output.
1434
+ --- See |MiniSnippets-syntax-specification| for more info about variables.
1435
+ --- - Add `text` field for tabstops present in `opts.lookup`.
1436
+ --- - Ensure every node contains exactly one of `text` or `placeholder` fields.
1437
+ --- If there are none, add default `placeholder` (one text node with first
1438
+ --- choice or empty string). If there are both, remove `placeholder` field.
1439
+ --- - Ensure present final tabstop: append to end if absent.
1440
+ --- - Ensure that nodes for same tabstop have same placeholder. Use the one
1441
+ --- from the first node.
1442
+ --- Default: `false`.
1443
+ --- - <lookup> `(table)` - map from variable/tabstop (string) name to its value.
1444
+ --- Default: `{}`.
1445
+ ---
1446
+ ---@return table Array of nodes. Node is a table with fields depending on node type:
1447
+ --- - Text node:
1448
+ --- - <text> `(string)` - node's text.
1449
+ --- - Tabstop node:
1450
+ --- - <tabstop> `(string)` - tabstop identifier.
1451
+ --- - <text> `(string|nil)` - tabstop value (if present in <lookup>).
1452
+ --- - <placeholder> `(table|nil)` - array of nodes to be used as placeholder.
1453
+ --- - <choices> `(table|nil)` - array of string choices.
1454
+ --- - <transform> `(table|nil)` - array of transformation string parts.
1455
+ --- - Variable node:
1456
+ --- - <var> `(string)` - variable name.
1457
+ --- - <text> `(string|nil)` - variable value.
1458
+ --- - <placeholder> `(table|nil)` - array of nodes to be used as placeholder.
1459
+ --- - <transform> `(table|nil)` - array of transformation string parts.
1460
+ MiniSnippets.parse = function(snippet_body, opts)
1461
+ if H.is_array_of(snippet_body, H.is_string) then snippet_body = table.concat(snippet_body, '\n') end
1462
+ if type(snippet_body) ~= 'string' then H.error('Snippet body should be string or array of strings') end
1463
+
1464
+ opts = vim.tbl_extend('force', { normalize = false, lookup = {} }, opts or {})
1465
+
1466
+ -- Overall idea: implement a state machine which updates on every character.
1467
+ -- This leads to a bit spaghetti code, but doesn't require `vim.lpeg` DSL
1468
+ -- knowledge and can provide more information in error messages.
1469
+ -- Output is array of nodes representing the snippet body.
1470
+ -- Format is mostly based on grammar in LSP spec 3.18 with small differences.
1471
+
1472
+ -- State table. Each future string is tracked as array and merged later.
1473
+ --stylua: ignore
1474
+ local state = {
1475
+ name = 'text',
1476
+ -- Node array for depths of currently processed nested placeholders.
1477
+ -- Depth 1 is the original snippet.
1478
+ depth_arrays = { { { text = {} } } },
1479
+ set_name = function(self, name) self.name = name; return self end,
1480
+ add_node = function(self, node) table.insert(self.depth_arrays[#self.depth_arrays], node); return self end,
1481
+ set_in = function(self, node, field, value) node[field] = value; return self end,
1482
+ is_not_top_level = function(self) return #self.depth_arrays > 1 end,
1483
+ }
1484
+
1485
+ for i = 0, vim.fn.strchars(snippet_body) - 1 do
1486
+ -- Infer helper data (for more concise manipulations inside processor)
1487
+ local depth = #state.depth_arrays
1488
+ local arr = state.depth_arrays[depth]
1489
+ local processor, node = H.parse_processors[state.name], arr[#arr]
1490
+ processor(vim.fn.strcharpart(snippet_body, i, 1), state, node)
1491
+ end
1492
+
1493
+ -- Verify, post-process, normalize
1494
+ H.parse_verify(state)
1495
+ local nodes = H.parse_post_process(state.depth_arrays[1], state.name)
1496
+ return opts.normalize and H.parse_normalize(nodes, opts) or nodes
1497
+ end
1498
+
1499
+ --- Start completion LSP server
1500
+ ---
1501
+ --- This starts (|vim.lsp.start()|) an LSP server with the purpose of displaying
1502
+ --- snippets in (auto)completion engines (|mini.completion| in particular).
1503
+ --- The server:
1504
+ --- - Only implements `textDocument/completion` method which prepares and matches
1505
+ --- snippets at cursor (via |MiniSnippets.expand()|).
1506
+ --- - Auto-attaches to all loaded buffers by default.
1507
+ ---
1508
+ ---@param opts table|nil Options. Possible fields:
1509
+ --- - <before_attach> `(function)` - function executed before every attach to
1510
+ --- the buffer. Takes buffer id as input and can return `false` (not `nil`) to
1511
+ --- cancel attaching to the buffer. Default: attach to loaded normal buffers.
1512
+ --- - <match> `(false|function)` - value of `opts.match` forwarded to
1513
+ --- the |MiniSnippets.expand()| when computing completion candidates.
1514
+ --- Supply `false` to not do matching at cursor, return all available snippets
1515
+ --- in cursor context, and rely on completion engine to match and sort items.
1516
+ --- Default: `nil` (equivalent to |MiniSnippets.default_match()|).
1517
+ --- - <server_config> `(table)` - server config to be used as basis for first
1518
+ --- argument to |vim.lsp.start()| (`cmd` will be overridden). Default: `{}`.
1519
+ --- - <triggers> `(table)` - array of trigger characters to be used as
1520
+ --- `completionProvider.triggerCharacters` server capability. Default: `{}`.
1521
+ ---
1522
+ ---@return integer|nil Identifier of started LSP server.
1523
+ MiniSnippets.start_lsp_server = function(opts)
1524
+ local default_opts = { before_attach = H.lsp_default_before_attach, match = nil, server_config = {}, triggers = {} }
1525
+ opts = vim.tbl_extend('force', default_opts, opts or {})
1526
+ H.check_type('opts.before_attach', opts.before_attach, 'callable')
1527
+ H.check_type('opts.server_config', opts.server_config, 'table')
1528
+ H.check_type('opts.triggers', opts.triggers, 'table')
1529
+
1530
+ local config = vim.deepcopy(opts.server_config)
1531
+ -- NOTE: set `root_dir` for a working `reuse_client` on Neovim<0.11
1532
+ config.name, config.root_dir = config.name or 'mini.snippets', config.root_dir or vim.fn.getcwd()
1533
+ config.cmd = H.lsp_make_cmd(opts)
1534
+ local ok, client_id = pcall(vim.lsp.start, config, { attach = false })
1535
+ if not (ok and type(client_id) == 'number') then H.error("Could not start 'mini.snippets' in-process LSP server") end
1536
+ if vim.fn.has('nvim-0.11') == 0 then pcall(vim.lsp.buf_detach_client, 0, client_id) end
1537
+
1538
+ local attach = function(buf_id)
1539
+ if not vim.api.nvim_buf_is_valid(buf_id) or opts.before_attach(buf_id) == false then return end
1540
+ vim.lsp.buf_attach_client(buf_id, client_id)
1541
+ end
1542
+ for _, buf_id in ipairs(vim.api.nvim_list_bufs()) do
1543
+ attach(buf_id)
1544
+ end
1545
+
1546
+ local gr = vim.api.nvim_create_augroup('MiniSnippetsLsp', {})
1547
+ -- NOTE: schedule to auto-attach only on explicit buffer ente (not temporary
1548
+ -- from script) and have buffer properties (like 'filetype') set up.
1549
+ local auto_attach = vim.schedule_wrap(function(ev)
1550
+ if ev.buf ~= vim.api.nvim_get_current_buf() then return end
1551
+ attach(ev.buf)
1552
+ end)
1553
+ vim.api.nvim_create_autocmd('BufEnter', { callback = auto_attach, desc = "Auto attach 'mini.snippets' LSP server" })
1554
+
1555
+ return client_id
1556
+ end
1557
+
1558
+ -- Helper data ================================================================
1559
+ -- Module default config
1560
+ H.default_config = vim.deepcopy(MiniSnippets.config)
1561
+
1562
+ -- Namespaces for extmarks
1563
+ H.ns_id = {
1564
+ nodes = vim.api.nvim_create_namespace('MiniSnippetsNodes'),
1565
+ }
1566
+
1567
+ -- Array of current (nested) snippet sessions from `default_insert`
1568
+ H.sessions = {}
1569
+
1570
+ -- Various cache
1571
+ H.cache = {
1572
+ -- Loaders output
1573
+ runtime = {},
1574
+ file = {},
1575
+ -- Data for possibly overridden session mappings
1576
+ mappings = {},
1577
+ }
1578
+
1579
+ -- Capabilities of current Neovim version
1580
+ H.nvim_supports_inline_extmarks = vim.fn.has('nvim-0.10') == 1
1581
+
1582
+ -- Helper functionality =======================================================
1583
+ -- Settings -------------------------------------------------------------------
1584
+ H.setup_config = function(config)
1585
+ H.check_type('config', config, 'table', true)
1586
+ config = vim.tbl_deep_extend('force', vim.deepcopy(H.default_config), config or {})
1587
+
1588
+ H.check_type('snippets', config.snippets, 'table')
1589
+
1590
+ H.check_type('mappings', config.mappings, 'table')
1591
+ H.check_type('mappings.expand', config.mappings.expand, 'string')
1592
+ H.check_type('mappings.jump_next', config.mappings.jump_next, 'string')
1593
+ H.check_type('mappings.jump_prev', config.mappings.jump_prev, 'string')
1594
+ H.check_type('mappings.stop', config.mappings.stop, 'string')
1595
+
1596
+ H.check_type('expand', config.expand, 'table')
1597
+ H.check_type('expand.prepare', config.expand.prepare, 'function', true)
1598
+ H.check_type('expand.match', config.expand.match, 'function', true)
1599
+ H.check_type('expand.select', config.expand.select, 'function', true)
1600
+ H.check_type('expand.insert', config.expand.insert, 'function', true)
1601
+
1602
+ return config
1603
+ end
1604
+
1605
+ H.apply_config = function(config)
1606
+ MiniSnippets.config = config
1607
+
1608
+ -- Reset loader cache
1609
+ H.cache = { runtime = {}, file = {}, mappings = {} }
1610
+
1611
+ -- Make mappings
1612
+ local mappings = config.mappings
1613
+ local map = function(lhs, rhs, desc)
1614
+ if lhs == '' then return end
1615
+ vim.keymap.set('i', lhs, rhs, { desc = desc })
1616
+ end
1617
+ map(mappings.expand, '<Cmd>lua MiniSnippets.expand()<CR>', 'Expand snippet')
1618
+
1619
+ -- Register 'code-snippets' extension as JSON (helps with highlighting)
1620
+ vim.schedule(function() vim.filetype.add({ extension = { ['code-snippets'] = 'json' } }) end)
1621
+ end
1622
+
1623
+ H.create_autocommands = function()
1624
+ local gr = vim.api.nvim_create_augroup('MiniSnippets', {})
1625
+
1626
+ local au = function(event, pattern, callback, desc)
1627
+ vim.api.nvim_create_autocmd(event, { group = gr, pattern = pattern, callback = callback, desc = desc })
1628
+ end
1629
+
1630
+ au('ColorScheme', '*', H.create_default_hl, 'Ensure colors')
1631
+
1632
+ -- Clean up invalid sessions (i.e. which have outdated or corrupted data)
1633
+ -- - Use `vim.schedule_wrap` to make it work with `:edit` command
1634
+ au('BufUnload', '*', vim.schedule_wrap(H.clean_sessions), 'Clean sessions stack')
1635
+ end
1636
+
1637
+ H.create_default_hl = function()
1638
+ local hi_link_underdouble = function(to, from)
1639
+ local data = vim.api.nvim_get_hl(0, { name = from, link = false })
1640
+ vim.api.nvim_set_hl(0, to, { default = true, sp = data.sp, underdouble = true })
1641
+ end
1642
+ hi_link_underdouble('MiniSnippetsCurrent', 'DiagnosticUnderlineWarn')
1643
+ hi_link_underdouble('MiniSnippetsCurrentReplace', 'DiagnosticUnderlineError')
1644
+ hi_link_underdouble('MiniSnippetsFinal', 'DiagnosticUnderlineOk')
1645
+ hi_link_underdouble('MiniSnippetsUnvisited', 'DiagnosticUnderlineHint')
1646
+ hi_link_underdouble('MiniSnippetsVisited', 'DiagnosticUnderlineInfo')
1647
+ end
1648
+
1649
+ H.is_disabled = function() return vim.g.minisnippets_disable == true or vim.b.minisnippets_disable == true end
1650
+
1651
+ H.get_config = function()
1652
+ local global, buf = MiniSnippets.config, vim.b.minisnippets_config
1653
+ -- Fast path for most common case
1654
+ if buf == nil then return vim.deepcopy(global) end
1655
+ -- Manually reconstruct to allow snippet array to be concatenated
1656
+ buf = buf or {}
1657
+ return {
1658
+ snippets = vim.list_extend(vim.deepcopy(global.snippets), buf.snippets or {}),
1659
+ mappings = vim.tbl_extend('force', global.mappings, buf.mappings or {}),
1660
+ expand = vim.tbl_extend('force', global.expand, buf.expand or {}),
1661
+ }
1662
+ end
1663
+
1664
+ -- Read -----------------------------------------------------------------------
1665
+ H.file_readers = {}
1666
+
1667
+ H.file_readers.lua = function(path, silent)
1668
+ local ok, contents = pcall(dofile, path)
1669
+ if not ok then return { problems = { 'Could not execute Lua file' } } end
1670
+ if type(contents) ~= 'table' then return { problems = { 'Returned object is not a table' } } end
1671
+ return H.read_snippet_data(contents)
1672
+ end
1673
+
1674
+ H.file_readers.json = function(path, silent)
1675
+ local file = io.open(path)
1676
+ if file == nil then return { problems = { 'Could not open file' } } end
1677
+ local raw = file:read('*all')
1678
+ file:close()
1679
+
1680
+ local ok, contents = pcall(vim.json.decode, raw)
1681
+ if not (ok and type(contents) == 'table') then
1682
+ local msg = ok and 'Object is not a dictionary or array' or contents
1683
+ return { problems = { 'File does not contain a valid JSON object. Reason: ' .. msg } }
1684
+ end
1685
+
1686
+ return H.read_snippet_data(contents)
1687
+ end
1688
+
1689
+ H.file_readers['code-snippets'] = H.file_readers.json
1690
+
1691
+ H.read_snippet_data = function(contents)
1692
+ local res, problems = {}, {}
1693
+ for name, t in pairs(contents) do
1694
+ if H.is_snippet(t) then
1695
+ -- Try inferring description from dict's field (if appropriate)
1696
+ if type(name) == 'string' and (t.desc == nil and t.description == nil) then t.desc = name end
1697
+ table.insert(res, t)
1698
+ elseif vim.is_callable(t) then
1699
+ -- Allow entries to be functions (relevant for Lua files)
1700
+ table.insert(res, t)
1701
+ else
1702
+ table.insert(problems, 'The following is not a valid snippet data:\n' .. vim.inspect(t))
1703
+ end
1704
+ end
1705
+ return { snippets = res, problems = problems }
1706
+ end
1707
+
1708
+ -- Context snippets -----------------------------------------------------------
1709
+ H.get_default_context = function()
1710
+ local buf_id = vim.api.nvim_get_current_buf()
1711
+
1712
+ -- TODO: Remove `opts.error` after compatibility with Neovim=0.11 is dropped
1713
+ local has_parser, parser = pcall(vim.treesitter.get_parser, buf_id, nil, { error = false })
1714
+ if not has_parser or parser == nil then return { buf_id = buf_id, lang = vim.bo[buf_id].filetype } end
1715
+
1716
+ -- Compute local (at cursor) TS language
1717
+ local pos = vim.api.nvim_win_get_cursor(0)
1718
+ local lang_tree = parser:language_for_range({ pos[1] - 1, pos[2], pos[1] - 1, pos[2] })
1719
+ local lang = lang_tree:lang() or vim.bo[buf_id].filetype
1720
+ return { buf_id = buf_id, lang = lang }
1721
+ end
1722
+
1723
+ H.traverse_raw_snippets = function(x, target, context)
1724
+ if H.is_snippet(x) then
1725
+ local body
1726
+ if x.body ~= nil then body = type(x.body) == 'string' and x.body or table.concat(x.body, '\n') end
1727
+
1728
+ local desc = x.desc or x.description or body
1729
+ if desc ~= nil then desc = type(desc) == 'string' and desc or table.concat(desc, '\n') end
1730
+
1731
+ local prefix = x.prefix or ''
1732
+ prefix = type(prefix) == 'string' and { prefix } or prefix
1733
+
1734
+ for _, pr in ipairs(prefix) do
1735
+ -- Add snippets with empty prefixes separately
1736
+ local index = pr == '' and (#target + 1) or pr
1737
+ -- Allow absent `body` to result in completely removing prefix(es)
1738
+ target[index] = body ~= nil and { prefix = pr, body = body, desc = desc } or nil
1739
+ end
1740
+ end
1741
+
1742
+ if H.islist(x) then
1743
+ for _, v in ipairs(x) do
1744
+ H.traverse_raw_snippets(v, target, context)
1745
+ end
1746
+ end
1747
+
1748
+ if vim.is_callable(x) then H.traverse_raw_snippets(x(context), target, context) end
1749
+ end
1750
+
1751
+ -- Expand ---------------------------------------------------------------------
1752
+ H.make_extended_insert = function(insert)
1753
+ return function(snippet)
1754
+ if snippet == nil then return end
1755
+
1756
+ -- Ensure Insert mode. This helps to properly position cursor at EOL.
1757
+ H.call_in_insert_mode(function()
1758
+ -- Delete snippet's region and remove the data from the snippet (as it
1759
+ -- wouldn't need to be removed and will represent outdated information)
1760
+ H.delete_region(snippet.region)
1761
+ snippet = vim.deepcopy(snippet)
1762
+ snippet.region = nil
1763
+
1764
+ -- Insert snippet at cursor
1765
+ insert(snippet)
1766
+ end)
1767
+ end
1768
+ end
1769
+
1770
+ -- Parse ----------------------------------------------------------------------
1771
+ H.parse_verify = function(state)
1772
+ if state.name == 'dollar_lbrace' then H.error('"${" should be closed with "}"') end
1773
+ if state.name == 'choice' then H.error('Tabstop with choices should be closed with "|}"') end
1774
+ if vim.startswith(state.name, 'transform_') then
1775
+ H.error('Transform should contain 3 "/" outside of `${...}` and be closed with "}"')
1776
+ end
1777
+ if #state.depth_arrays > 1 then H.error('Placeholder should be closed with "}"') end
1778
+ end
1779
+
1780
+ H.parse_post_process = function(node_arr, state_name)
1781
+ -- Allow "$" at the end of the snippet
1782
+ if state_name == 'dollar' then table.insert(node_arr, { text = { '$' } }) end
1783
+
1784
+ -- Process
1785
+ local traverse
1786
+ traverse = function(arr)
1787
+ for _, node in ipairs(arr) do
1788
+ -- Clean up trailing `\`
1789
+ if node.after_slash and node.text ~= nil then table.insert(node.text, '\\') end
1790
+ node.after_slash = nil
1791
+
1792
+ -- Convert arrays to strings
1793
+ if node.text then node.text = table.concat(node.text) end
1794
+ if node.tabstop then node.tabstop = table.concat(node.tabstop) end
1795
+ if node.choices then node.choices = vim.tbl_map(table.concat, node.choices) end
1796
+ if node.var then node.var = table.concat(node.var) end
1797
+ if node.transform then node.transform = vim.tbl_map(table.concat, node.transform) end
1798
+
1799
+ -- Recursively post-process placeholders
1800
+ if node.placeholder ~= nil then node.placeholder = traverse(node.placeholder) end
1801
+ end
1802
+ arr = vim.tbl_filter(function(n) return n.text == nil or (n.text ~= nil and n.text:len() > 0) end, arr)
1803
+ if #arr == 0 then return { { text = '' } } end
1804
+ return arr
1805
+ end
1806
+
1807
+ return traverse(node_arr)
1808
+ end
1809
+
1810
+ H.parse_normalize = function(node_arr, opts)
1811
+ local lookup = {}
1812
+ for key, val in pairs(opts.lookup) do
1813
+ if type(key) == 'string' then lookup[key] = tostring(val) end
1814
+ end
1815
+
1816
+ local has_final_tabstop = false
1817
+ local normalize = function(n)
1818
+ -- Evaluate variable
1819
+ local var_value
1820
+ if n.var ~= nil then var_value = H.parse_eval_var(n.var, lookup) end
1821
+ if type(var_value) == 'string' then n.text = var_value end
1822
+
1823
+ -- Look up tabstop
1824
+ if n.tabstop ~= nil then n.text = lookup[n.tabstop] end
1825
+
1826
+ -- Ensure text-or-placeholder (use first choice for choice node)
1827
+ if n.text == nil and n.placeholder == nil then n.placeholder = { { text = (n.choices or {})[1] or '' } } end
1828
+ if n.text ~= nil and n.placeholder ~= nil then n.placeholder = nil end
1829
+
1830
+ -- Track presence of final tabstop
1831
+ has_final_tabstop = has_final_tabstop or n.tabstop == '0'
1832
+ end
1833
+ -- - Ensure proper random random variables
1834
+ math.randomseed(vim.loop.hrtime())
1835
+ H.nodes_traverse(node_arr, normalize)
1836
+
1837
+ -- Possibly append final tabstop as a regular normalized tabstop
1838
+ if not has_final_tabstop then table.insert(node_arr, { tabstop = '0', placeholder = { { text = '' } } }) end
1839
+
1840
+ -- Ensure same resolved text in linked tabstops
1841
+ local tabstop_ref = {}
1842
+ local sync_linked_tabstops = function(n)
1843
+ if n.tabstop == nil then return end
1844
+ local ref = tabstop_ref[n.tabstop]
1845
+ if ref ~= nil then
1846
+ -- Set data for repeated tabstops. Do not sync transforms (for future).
1847
+ n.text, n.placeholder, n.choices = ref.text, vim.deepcopy(ref.placeholder), vim.deepcopy(ref.choices)
1848
+ return
1849
+ end
1850
+ -- Compute reference data for repeated tabstops
1851
+ if n.placeholder ~= nil and H.parse_nodes_contain_tabstop(n.placeholder, n.tabstop) then
1852
+ H.error('Placeholder can not contain its tabstop')
1853
+ end
1854
+ tabstop_ref[n.tabstop] = { text = n.text, placeholder = n.placeholder, choices = n.choices }
1855
+ end
1856
+ H.nodes_traverse(node_arr, sync_linked_tabstops)
1857
+
1858
+ return node_arr
1859
+ end
1860
+
1861
+ H.parse_nodes_contain_tabstop = function(node_arr, tabstop)
1862
+ for _, n in ipairs(node_arr) do
1863
+ if n.tabstop == tabstop then return true end
1864
+ if n.placeholder ~= nil and H.parse_nodes_contain_tabstop(n.placeholder, tabstop) then return true end
1865
+ end
1866
+ return false
1867
+ end
1868
+
1869
+ H.parse_get_text = function(node_arr)
1870
+ local parts = {}
1871
+ for _, n in ipairs(node_arr) do
1872
+ table.insert(parts, n.text or H.parse_get_text(n.placeholder))
1873
+ end
1874
+ return table.concat(parts, '')
1875
+ end
1876
+
1877
+ H.parse_rise_depth = function(state)
1878
+ -- Set the deepest array as a placeholder of the last node in previous layer.
1879
+ -- This can happen only after `}` which does not close current node.
1880
+ local depth = #state.depth_arrays
1881
+ local cur_layer, prev_layer = state.depth_arrays[depth], state.depth_arrays[depth - 1]
1882
+ prev_layer[#prev_layer].placeholder = vim.deepcopy(cur_layer)
1883
+ state.depth_arrays[depth] = nil
1884
+ state:add_node({ text = {} }):set_name('text')
1885
+ end
1886
+
1887
+ -- Each method processes single character based on the character (`c`),
1888
+ -- state (`s`), and current node (`n`).
1889
+ H.parse_processors = {}
1890
+
1891
+ H.parse_processors.text = function(c, s, n)
1892
+ if n.after_slash then
1893
+ -- Escape `$}\` and allow unescaped '\\' to precede any character
1894
+ if not (c == '$' or c == '}' or c == '\\') then table.insert(n.text, '\\') end
1895
+ n.text[#n.text + 1], n.after_slash = c, nil
1896
+ return
1897
+ end
1898
+ if c == '}' and s:is_not_top_level() then return H.parse_rise_depth(s) end
1899
+ if c == '\\' then return s:set_in(n, 'after_slash', true) end
1900
+ if c == '$' then return s:set_name('dollar') end
1901
+ table.insert(n.text, c)
1902
+ end
1903
+
1904
+ H.parse_processors.dollar = function(c, s, n)
1905
+ if c == '}' and s:is_not_top_level() then
1906
+ if n.text ~= nil then table.insert(n.text, '$') end
1907
+ if n.text == nil then s:add_node({ text = { '$' } }) end
1908
+ s:set_name('text')
1909
+ H.parse_rise_depth(s)
1910
+ return
1911
+ end
1912
+
1913
+ if c:find('^[0-9]$') then return s:add_node({ tabstop = { c } }):set_name('dollar_tabstop') end -- Tabstops
1914
+ if c:find('^[_a-zA-Z]$') then return s:add_node({ var = { c } }):set_name('dollar_var') end -- Variables
1915
+ if c == '{' then return s:set_name('dollar_lbrace') end -- Cases of `${...}`
1916
+ table.insert(n.text, '$') -- Case of unescaped `$`
1917
+ if c == '$' then return end -- Case of `$$1` and `$${1}`
1918
+ table.insert(n.text, c)
1919
+ s:set_name('text')
1920
+ end
1921
+
1922
+ H.parse_processors.dollar_tabstop = function(c, s, n)
1923
+ if c:find('^[0-9]$') then return table.insert(n.tabstop, c) end
1924
+ if c == '}' and s:is_not_top_level() then return H.parse_rise_depth(s) end
1925
+ local new_node = { text = {} }
1926
+ s:add_node(new_node)
1927
+ if c == '$' then return s:set_name('dollar') end -- Case of `$1$2` and `$1$a`
1928
+ s:set_name('text')
1929
+ if c == '\\' then return s:set_in(new_node, 'after_slash', true) end -- Case of `${1:{$2\}}`
1930
+ table.insert(new_node.text, c) -- Case of `$1a`
1931
+ end
1932
+
1933
+ H.parse_processors.dollar_var = function(c, s, n)
1934
+ if c:find('^[_a-zA-Z0-9]$') then return table.insert(n.var, c) end
1935
+ if c == '}' and s:is_not_top_level() then return H.parse_rise_depth(s) end
1936
+ local new_node = { text = {} }
1937
+ s:add_node(new_node)
1938
+ if c == '$' then return s:set_name('dollar') end -- Case of `$a$b` and `$a$1`
1939
+ s:set_name('text')
1940
+ if c == '\\' then return s:set_in(new_node, 'after_slash', true) end -- Case of `${AAA:{$1\}}`
1941
+ table.insert(new_node.text, c) -- Case of `$a-`
1942
+ end
1943
+
1944
+ H.parse_processors.dollar_lbrace = function(c, s, n)
1945
+ if n.tabstop == nil and n.var == nil then -- Detect the type of `${...}`
1946
+ if c:find('^[0-9]$') then return s:add_node({ tabstop = { c } }) end
1947
+ if c:find('^[_a-zA-Z]$') then return s:add_node({ var = { c } }) end
1948
+ H.error('`${` should be followed by digit (in tabstop) or letter/underscore (in variable), not ' .. vim.inspect(c))
1949
+ end
1950
+ if c == '}' then return s:add_node({ text = {} }):set_name('text') end -- Cases of `${1}` and `${a}`
1951
+ if c == ':' then -- Placeholder
1952
+ table.insert(s.depth_arrays, { { text = {} } })
1953
+ return s:set_name('text')
1954
+ end
1955
+ if c == '/' then return s:set_in(n, 'transform', { {}, {}, {} }):set_name('transform_regex') end -- Transform
1956
+ if n.var ~= nil then -- Variable
1957
+ if c:find('^[_a-zA-Z0-9]$') then return table.insert(n.var, c) end
1958
+ H.error('Variable name should be followed by "}", ":" or "/", not ' .. vim.inspect(c))
1959
+ else -- Tabstop
1960
+ if c:find('^[0-9]$') then return table.insert(n.tabstop, c) end
1961
+ if c == '|' then return s:set_name('choice') end
1962
+ H.error('Tabstop id should be followed by "}", ":", "|", or "/" not ' .. vim.inspect(c))
1963
+ end
1964
+ end
1965
+
1966
+ H.parse_processors.choice = function(c, s, n)
1967
+ n.choices = n.choices or { {} }
1968
+ if n.after_bar then
1969
+ if c ~= '}' then H.error('Tabstop with choices should be closed with "|}"') end
1970
+ return s:set_in(n, 'after_bar', nil):add_node({ text = {} }):set_name('text')
1971
+ end
1972
+
1973
+ local cur = n.choices[#n.choices]
1974
+ if n.after_slash then
1975
+ -- Escape `$}\` and allow unescaped '\\' to precede any character
1976
+ if not (c == ',' or c == '|' or c == '\\') then table.insert(cur, '\\') end
1977
+ cur[#cur + 1], n.after_slash = c, nil
1978
+ return
1979
+ end
1980
+ if c == ',' then return table.insert(n.choices, {}) end
1981
+ if c == '\\' then return s:set_in(n, 'after_slash', true) end
1982
+ if c == '|' then return s:set_in(n, 'after_bar', true) end
1983
+ table.insert(cur, c)
1984
+ end
1985
+
1986
+ -- Silently gather all the transform data and wait until proper `}`
1987
+ H.parse_processors.transform_regex = function(c, s, n)
1988
+ table.insert(n.transform[1], c)
1989
+ if n.after_slash then return s:set_in(n, 'after_slash', nil) end
1990
+ if c == '\\' then return s:set_in(n, 'after_slash', true) end
1991
+ if c == '/' then return s:set_in(n.transform[1], #n.transform[1], nil):set_name('transform_format') end -- Assumes any `/` is escaped in regex
1992
+ end
1993
+
1994
+ H.parse_processors.transform_format = function(c, s, n)
1995
+ table.insert(n.transform[2], c)
1996
+ if n.after_slash then return s:set_in(n, 'after_slash', nil) end
1997
+ if n.after_dollar then
1998
+ n.after_dollar = nil
1999
+ -- Inside `${}` wait until the first (unescaped) `}`. Technically, this
2000
+ -- breaks LSP spec in `${1:?if:else}` (`if` doesn't have to escape `}`).
2001
+ -- Accept this as known limitation and ask to escape `}` in such cases.
2002
+ if c == '{' and not n.inside_braces then return s:set_in(n, 'inside_braces', true) end
2003
+ end
2004
+ if c == '\\' then return s:set_in(n, 'after_slash', true) end
2005
+ if c == '$' then return s:set_in(n, 'after_dollar', true) end
2006
+ if c == '}' and n.inside_braces then return s:set_in(n, 'inside_braces', nil) end
2007
+ if c == '/' and not n.inside_braces then
2008
+ return s:set_in(n.transform[2], #n.transform[2], nil):set_name('transform_options')
2009
+ end
2010
+ end
2011
+
2012
+ H.parse_processors.transform_options = function(c, s, n)
2013
+ table.insert(n.transform[3], c)
2014
+ if n.after_slash then return s:set_in(n, 'after_slash', nil) end
2015
+ if c == '\\' then return s:set_in(n, 'after_slash', true) end
2016
+ if c == '}' then return s:set_in(n.transform[3], #n.transform[3], nil):add_node({ text = {} }):set_name('text') end
2017
+ end
2018
+
2019
+ --stylua: ignore
2020
+ H.parse_eval_var = function(var, lookup)
2021
+ -- Always prefer using lookup
2022
+ if lookup[var] ~= nil then return lookup[var] end
2023
+
2024
+ -- Evaluate variable
2025
+ local value
2026
+ if H.var_evaluators[var] ~= nil then value = H.var_evaluators[var]() end
2027
+ -- - Fall back to environment variable or `-1` to not evaluate twice
2028
+ if value == nil then value = vim.loop.os_getenv(var) or -1 end
2029
+
2030
+ -- Skip caching random variables (to allow several different in one snippet)
2031
+ if not (var == 'RANDOM' or var == 'RANDOM_HEX' or var == 'UUID') then lookup[var] = value end
2032
+ return value
2033
+ end
2034
+
2035
+ --stylua: ignore
2036
+ H.var_evaluators = {
2037
+ -- LSP
2038
+ TM_SELECTED_TEXT = function() return table.concat(vim.fn.getreg('"', true, true), '\n') end,
2039
+ TM_CURRENT_LINE = function() return vim.api.nvim_get_current_line() end,
2040
+ TM_CURRENT_WORD = function() return vim.fn.expand('<cword>') end,
2041
+ TM_LINE_INDEX = function() return tostring(vim.fn.line('.') - 1) end,
2042
+ TM_LINE_NUMBER = function() return tostring(vim.fn.line('.')) end,
2043
+ TM_FILENAME = function() return vim.fn.expand('%:t') end,
2044
+ TM_FILENAME_BASE = function() return vim.fn.expand('%:t:r') end,
2045
+ TM_DIRECTORY = function() return vim.fn.expand('%:p:h') end,
2046
+ TM_FILEPATH = function() return vim.fn.expand('%:p') end,
2047
+
2048
+ -- VS Code
2049
+ CLIPBOARD = function() return vim.fn.getreg('+') end,
2050
+ CURSOR_INDEX = function() return tostring(vim.fn.col('.') - 1) end,
2051
+ CURSOR_NUMBER = function() return tostring(vim.fn.col('.')) end,
2052
+ RELATIVE_FILEPATH = function() return vim.fn.expand('%:.') end,
2053
+ WORKSPACE_FOLDER = function() return vim.fn.getcwd() end,
2054
+
2055
+ LINE_COMMENT = function() return vim.bo.commentstring:gsub('%s*%%s.*$', '') end,
2056
+ -- No BLOCK_COMMENT_{START,END} as there is no built-in way to get them
2057
+
2058
+ CURRENT_YEAR = function() return vim.fn.strftime('%Y') end,
2059
+ CURRENT_YEAR_SHORT = function() return vim.fn.strftime('%y') end,
2060
+ CURRENT_MONTH = function() return vim.fn.strftime('%m') end,
2061
+ CURRENT_MONTH_NAME = function() return vim.fn.strftime('%B') end,
2062
+ CURRENT_MONTH_NAME_SHORT = function() return vim.fn.strftime('%b') end,
2063
+ CURRENT_DATE = function() return vim.fn.strftime('%d') end,
2064
+ CURRENT_DAY_NAME = function() return vim.fn.strftime('%A') end,
2065
+ CURRENT_DAY_NAME_SHORT = function() return vim.fn.strftime('%a') end,
2066
+ CURRENT_HOUR = function() return vim.fn.strftime('%H') end,
2067
+ CURRENT_MINUTE = function() return vim.fn.strftime('%M') end,
2068
+ CURRENT_SECOND = function() return vim.fn.strftime('%S') end,
2069
+ CURRENT_TIMEZONE_OFFSET = function() return vim.fn.strftime('%z') end,
2070
+
2071
+ CURRENT_SECONDS_UNIX = function() return tostring(os.time()) end,
2072
+
2073
+ -- Random
2074
+ RANDOM = function() return string.format('%06d', math.random(0, 999999)) end,
2075
+ RANDOM_HEX = function() return string.format('%06x', math.random(0, 16777216 - 1)) end,
2076
+ UUID = function()
2077
+ -- Source: https://gist.github.com/jrus/3197011
2078
+ local template ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
2079
+ return string.gsub(template, '[xy]', function (c)
2080
+ local v = c == 'x' and math.random(0, 0xf) or math.random(8, 0xb)
2081
+ return string.format('%x', v)
2082
+ end)
2083
+ end
2084
+ }
2085
+
2086
+ -- Session --------------------------------------------------------------------
2087
+ H.get_active_session = function() return H.sessions[#H.sessions] end
2088
+
2089
+ H.session_new = function(nodes, snippet, opts)
2090
+ -- Compute all present tabstops in session traverse order
2091
+ local taborder = H.compute_tabstop_order(nodes)
2092
+ local tabstops = {}
2093
+ for i, id in ipairs(taborder) do
2094
+ tabstops[id] =
2095
+ { prev = taborder[i - 1] or taborder[#taborder], next = taborder[i + 1] or taborder[1], is_visited = false }
2096
+ end
2097
+
2098
+ return {
2099
+ buf_id = vim.api.nvim_get_current_buf(),
2100
+ cur_tabstop = taborder[1],
2101
+ extmark_id = H.extmark_new(0, vim.fn.line('.') - 1, vim.fn.col('.') - 1),
2102
+ insert_args = vim.deepcopy({ snippet = snippet, opts = opts }),
2103
+ nodes = nodes,
2104
+ ns_id = H.ns_id.nodes,
2105
+ tabstops = tabstops,
2106
+ }
2107
+ end
2108
+
2109
+ H.session_init = function(session, full)
2110
+ if session == nil then return end
2111
+ local buf_id = session.buf_id
2112
+
2113
+ -- Prepare
2114
+ if full then
2115
+ -- Set buffer text preserving snippet text relative indent
2116
+ local indent = H.get_indent(vim.fn.getline('.'):sub(1, vim.fn.col('.') - 1))
2117
+ H.nodes_set_text(buf_id, session.nodes, session.extmark_id, indent)
2118
+
2119
+ -- No session if no input needed: single final tabstop without placeholder
2120
+ if session.cur_tabstop == '0' then
2121
+ local ref_node = H.session_get_ref_node(session)
2122
+ local row, col, opts = H.extmark_get(buf_id, ref_node.extmark_id)
2123
+ local is_empty = row == opts.end_row and col == opts.end_col
2124
+ if is_empty then
2125
+ -- Clean up
2126
+ H.nodes_traverse(session.nodes, function(n) H.extmark_del(buf_id, n.extmark_id) end)
2127
+ H.extmark_del(buf_id, session.extmark_id)
2128
+ return H.set_cursor({ row + 1, col })
2129
+ end
2130
+ end
2131
+
2132
+ -- Register new session
2133
+ local cur_session = H.get_active_session()
2134
+ if cur_session ~= nil then
2135
+ -- Sync before deinit to allow removing current placeholder
2136
+ H.session_sync_current_tabstop(cur_session)
2137
+ H.session_deinit(cur_session, false)
2138
+ end
2139
+ table.insert(H.sessions, session)
2140
+
2141
+ -- Focus on the current tabstop
2142
+ H.session_tabstop_focus(session, session.cur_tabstop)
2143
+
2144
+ -- Possibly set behavior for all sessions
2145
+ H.track_sessions()
2146
+ H.map_in_sessions()
2147
+ else
2148
+ -- Sync current tabstop for resumed session. This is useful when nested
2149
+ -- session was done inside reference tabstop node (most common case).
2150
+ -- On purpose don't change cursor/buffer/focus to allow smoother typing.
2151
+ H.session_sync_current_tabstop(session)
2152
+ H.session_update_hl(session)
2153
+ H.session_ensure_gravity(session)
2154
+ end
2155
+
2156
+ -- Trigger proper event
2157
+ H.trigger_event('MiniSnippetsSession' .. (full and 'Start' or 'Resume'), { session = vim.deepcopy(session) })
2158
+ end
2159
+
2160
+ H.track_sessions = function()
2161
+ -- Create tracking autocommands only once for all nested sessions
2162
+ if #H.sessions > 1 then return end
2163
+ local gr = vim.api.nvim_create_augroup('MiniSnippetsTrack', { clear = true })
2164
+
2165
+ -- React to text changes. NOTE: Use 'TextChangedP' to update linked tabstops
2166
+ -- with visible popup. It has downsides though:
2167
+ -- - Placeholder is removed after selecting first choice. Together with
2168
+ -- showing choices in empty tabstops, feels like a good compromise.
2169
+ -- - Tabstop sync runs more frequently (especially with 'mini.completion'),
2170
+ -- because of how built-in completion constantly 'delete-add' completion
2171
+ -- leader text (which is treated as text change).
2172
+ local on_textchanged = function(args)
2173
+ local session, buf_id = H.get_active_session(), args.buf
2174
+ -- React only to text changes in session's buffer for performance
2175
+ if session.buf_id ~= buf_id then return end
2176
+ -- Ensure that session is valid, like no extmarks got corrupted
2177
+ if not H.session_is_valid(session) then
2178
+ H.notify('Session contains corrupted data (deleted or out of range extmarks). It is stopped.', 'WARN')
2179
+ return MiniSnippets.session.stop()
2180
+ end
2181
+ H.session_sync_current_tabstop(session)
2182
+ end
2183
+ local text_events = { 'TextChanged', 'TextChangedI', 'TextChangedP' }
2184
+ vim.api.nvim_create_autocmd(text_events, { group = gr, callback = on_textchanged, desc = 'React to text change' })
2185
+
2186
+ -- Stop if final tabstop is current: exit to Normal mode or *any* text change
2187
+ local latest_changedtick = vim.b.changedtick
2188
+ local stop_if_final = function(args)
2189
+ -- *Actual* text change check is a workaround for `TextChangedI` sometimes
2190
+ -- getting triggered unnecessarily and too late with built-in completion
2191
+ if vim.b.changedtick == latest_changedtick and args.event ~= 'ModeChanged' then return end
2192
+ latest_changedtick = vim.b.changedtick
2193
+
2194
+ -- React only on text changes in session's buffer
2195
+ local session, buf_id = H.get_active_session(), args.buf
2196
+ if not ((session or {}).buf_id == buf_id and session.cur_tabstop == '0') then return end
2197
+
2198
+ -- Stop without forcing to hide completion
2199
+ H.cache.stop_is_auto = true
2200
+ MiniSnippets.session.stop()
2201
+ H.cache.stop_is_auto = nil
2202
+ end
2203
+ local modechanged_opts = { group = gr, pattern = '*:n', callback = stop_if_final, desc = 'Stop on final tabstop' }
2204
+ vim.api.nvim_create_autocmd('ModeChanged', modechanged_opts)
2205
+ vim.api.nvim_create_autocmd(text_events, { group = gr, callback = stop_if_final, desc = 'Stop on final tabstop' })
2206
+ end
2207
+
2208
+ H.clean_sessions = function()
2209
+ for i = #H.sessions - 1, 1, -1 do
2210
+ if not H.session_is_valid(H.sessions[i]) then
2211
+ H.session_deinit(H.sessions[i], true)
2212
+ table.remove(H.sessions, i)
2213
+ end
2214
+ end
2215
+ if #H.sessions > 0 and not H.session_is_valid(H.get_active_session()) then MiniSnippets.session.stop() end
2216
+ end
2217
+
2218
+ H.map_in_sessions = function()
2219
+ -- Create mapping only once for all nested sessions
2220
+ if #H.sessions > 1 then return end
2221
+ local mappings = H.get_config().mappings
2222
+ local map_with_cache = function(lhs, call, desc)
2223
+ if lhs == '' then return end
2224
+ H.cache.mappings[lhs] = vim.fn.maparg(lhs, 'i', false, true)
2225
+ -- NOTE: Map globally to work in nested sessions in different buffers
2226
+ vim.keymap.set('i', lhs, '<Cmd>lua MiniSnippets.session.' .. call .. '<CR>', { desc = desc })
2227
+ end
2228
+ map_with_cache(mappings.jump_next, 'jump("next")', 'Jump to next snippet tabstop')
2229
+ map_with_cache(mappings.jump_prev, 'jump("prev")', 'Jump to previous snippet tabstop')
2230
+ map_with_cache(mappings.stop, 'stop()', 'Stop active snippet session')
2231
+ end
2232
+
2233
+ H.unmap_in_sessions = function()
2234
+ for lhs, data in pairs(H.cache.mappings) do
2235
+ local needs_restore = vim.tbl_count(data) > 0
2236
+ if needs_restore then vim.fn.mapset('i', false, data) end
2237
+ if not needs_restore then vim.keymap.del('i', lhs) end
2238
+ end
2239
+ H.cache.mappings = {}
2240
+ end
2241
+
2242
+ H.session_tabstop_focus = function(session, tabstop_id)
2243
+ session.cur_tabstop = tabstop_id
2244
+ session.tabstops[tabstop_id].is_visited = true
2245
+
2246
+ -- Ensure target buffer is current
2247
+ H.ensure_cur_buf(session.buf_id)
2248
+
2249
+ -- Update highlighting
2250
+ H.session_update_hl(session)
2251
+
2252
+ -- Ensure proper gravity as reference node has changed
2253
+ H.session_ensure_gravity(session)
2254
+
2255
+ -- Set cursor based on reference node: left side if there is placeholder (and
2256
+ -- will be replaced), right side otherwise (to append).
2257
+ local ref_node = H.session_get_ref_node(session)
2258
+ local row, col, end_row, end_col = H.extmark_get_range(session.buf_id, ref_node.extmark_id)
2259
+ local pos = ref_node.placeholder ~= nil and { row + 1, col } or { end_row + 1, end_col }
2260
+ H.set_cursor(pos)
2261
+
2262
+ -- Show choices: if present and match node text (or all if still placeholder)
2263
+ H.show_completion(ref_node.choices, col + 1)
2264
+ end
2265
+
2266
+ H.session_ensure_gravity = function(session)
2267
+ -- Ensure proper gravity relative to reference node (first node with current
2268
+ -- tabstop): "left" before, "expand" at and all its parents, "right" after.
2269
+ -- This accounts for typing in snippets like `$1$2$1$2$1` (in both 1 and 2)
2270
+ -- and correct tracking of $2 in `${2:$1}` (should expand if typing in 1).
2271
+ local buf_id, cur_tabstop, base_gravity = session.buf_id, session.cur_tabstop, 'left'
2272
+ local parent_extmarks = {}
2273
+ local ensure = function(n)
2274
+ local is_ref_node = n.tabstop == cur_tabstop and base_gravity == 'left'
2275
+ if is_ref_node then
2276
+ for _, extmark_id in ipairs(parent_extmarks) do
2277
+ H.extmark_set_gravity(buf_id, extmark_id, 'expand')
2278
+ end
2279
+ -- Disable parent stack tracking, as reference node is accounted for
2280
+ parent_extmarks = nil
2281
+ end
2282
+ H.extmark_set_gravity(buf_id, n.extmark_id, is_ref_node and 'expand' or base_gravity)
2283
+ base_gravity = (is_ref_node or base_gravity == 'right') and 'right' or 'left'
2284
+ end
2285
+
2286
+ local ensure_in_nodes
2287
+ ensure_in_nodes = function(nodes)
2288
+ for _, n in ipairs(nodes) do
2289
+ -- NOTE: apply first to the node and only later to placeholder nodes,
2290
+ -- which makes them have "right" gravity and thus being removable during
2291
+ -- replacing placeholder (as they will not cover newly inserted text).
2292
+ ensure(n)
2293
+ if n.placeholder ~= nil then
2294
+ if parent_extmarks ~= nil then table.insert(parent_extmarks, n.extmark_id) end
2295
+ ensure_in_nodes(n.placeholder)
2296
+ if parent_extmarks ~= nil then parent_extmarks[#parent_extmarks] = nil end
2297
+ end
2298
+ end
2299
+ end
2300
+ ensure_in_nodes(session.nodes)
2301
+ end
2302
+
2303
+ H.session_get_ref_node = function(session)
2304
+ local res, cur_tabstop = nil, session.cur_tabstop
2305
+ local find = function(n) res = res or (n.tabstop == cur_tabstop and n or nil) end
2306
+ H.nodes_traverse(session.nodes, find)
2307
+ return res
2308
+ end
2309
+
2310
+ H.session_is_valid = function(session)
2311
+ local buf_id = session.buf_id
2312
+ if not H.is_loaded_buf(buf_id) then return false end
2313
+ local res, f, n_lines = true, nil, vim.api.nvim_buf_line_count(buf_id)
2314
+ f = function(n)
2315
+ -- NOTE: Invalid extmark tracking (via `invalidate=true`) should be doable,
2316
+ -- but comes with constraints: manually making tabstop empty should be
2317
+ -- allowed; deleting placeholder also deletes extmark's range. Both make
2318
+ -- extmark invalid, so delegate to users to see that extmarks are broken.
2319
+ local ok, row, _, _ = pcall(H.extmark_get, buf_id, n.extmark_id)
2320
+ res = res and (ok and row < n_lines)
2321
+ end
2322
+ H.nodes_traverse(session.nodes, f)
2323
+ return res
2324
+ end
2325
+
2326
+ H.session_sync_current_tabstop = function(session)
2327
+ if session._no_sync then return end
2328
+
2329
+ local buf_id, ref_node = session.buf_id, H.session_get_ref_node(session)
2330
+ local ref_extmark_id = ref_node.extmark_id
2331
+
2332
+ -- With present placeholder, decide whether there was a valid change (then
2333
+ -- remove placeholder) or not (then no sync)
2334
+ -- NOTE: Only current tabstop is synced *and* only after its first edit is
2335
+ -- mostly done to limit code complexity. This is a reasonable compromise
2336
+ -- together with `parse()` syncing all tabstops in its normalization. Doing
2337
+ -- more is better for cases which are outside of suggested workflow (like
2338
+ -- editing text outside of "jump-edit-jump-edit-stop" loop).
2339
+ if ref_node.placeholder ~= nil then
2340
+ local ref_row, ref_col = H.extmark_get_range(buf_id, ref_extmark_id)
2341
+ local phd_row, phd_col = H.extmark_get_range(buf_id, ref_node.placeholder[1].extmark_id)
2342
+ if ref_row == phd_row and ref_col == phd_col then return end
2343
+
2344
+ -- Remove placeholder to get extmark representing newly added text
2345
+ H.nodes_del(buf_id, ref_node.placeholder)
2346
+ ref_node.placeholder = nil
2347
+ end
2348
+
2349
+ -- Compute reference text: dedented version of reference node's text to later
2350
+ -- reindent linked tabstops so that they preserve relative indent
2351
+ local row, col, end_row, end_col = H.extmark_get_range(buf_id, ref_extmark_id)
2352
+ local ref_text = vim.api.nvim_buf_get_text(0, row, col, end_row, end_col, {})
2353
+ ref_node.text = table.concat(ref_text, '\n')
2354
+
2355
+ ref_text = H.dedent(ref_text, row, col)
2356
+
2357
+ -- Sync nodes with current tabstop to have text from reference node
2358
+ local cur_tabstop = session.cur_tabstop
2359
+ local sync = function(n)
2360
+ -- Make expanding extmark for all nodes because current tabstop might be
2361
+ -- placed inside any placeholder. This allows proper extmark tracking.
2362
+ H.extmark_set_gravity(buf_id, n.extmark_id, 'expand')
2363
+ if not (n.tabstop == cur_tabstop and n.extmark_id ~= ref_extmark_id) then return end
2364
+
2365
+ -- Ensure no placeholder because reference doesn't have one
2366
+ if n.placeholder ~= nil then H.nodes_del(buf_id, n.placeholder) end
2367
+
2368
+ -- Set reference text reindented based on the start line's indent
2369
+ local cur_row, cur_col, cur_end_row, cur_end_col = H.extmark_get_range(buf_id, n.extmark_id)
2370
+ local cur_text = H.reindent(vim.deepcopy(ref_text), cur_row, cur_col)
2371
+ vim.api.nvim_buf_set_text(buf_id, cur_row, cur_col, cur_end_row, cur_end_col, cur_text)
2372
+ n.placeholder, n.text = nil, table.concat(cur_text, '\n')
2373
+ end
2374
+ local sync_cleanup = function(n)
2375
+ -- Make sure node's extmark doesn't move when setting later text
2376
+ -- Set this *after* traversing placeholder to have proper tracking in
2377
+ -- cases like `$1 ${2:$1}` - $2 extmark should be still expanding to track
2378
+ -- setting new text in $1.
2379
+ H.extmark_set_gravity(buf_id, n.extmark_id, 'left')
2380
+ end
2381
+ -- - Temporarily disable running this function (as autocommands will trigger)
2382
+ session._no_sync = true
2383
+ H.nodes_traverse(session.nodes, sync, sync_cleanup)
2384
+ session._no_sync = nil
2385
+ H.session_ensure_gravity(session)
2386
+
2387
+ -- Maybe show choices for empty tabstop at cursor
2388
+ local cur_pos = vim.api.nvim_win_get_cursor(0)
2389
+ if ref_node.text == '' and cur_pos[1] == (row + 1) and cur_pos[2] == col then H.show_completion(ref_node.choices) end
2390
+
2391
+ -- Make highlighting up to date
2392
+ H.session_update_hl(session)
2393
+ end
2394
+
2395
+ H.session_jump = vim.schedule_wrap(function(session, direction)
2396
+ -- NOTE: Use `schedule_wrap` to workaround some edge cases when used inside
2397
+ -- expression mapping (as recommended for `<Tab>`)
2398
+ if session == nil then return end
2399
+
2400
+ -- Compute target tabstop accounting for possibly missing ones.
2401
+ -- Example why needed: `${1:$2}$3`, setting text in $1 removes $2 tabstop
2402
+ -- and jumping should be done from 1 to 3.
2403
+ local present_tabstops, all_tabstops = {}, session.tabstops
2404
+ H.nodes_traverse(session.nodes, function(n) present_tabstops[n.tabstop or true] = true end)
2405
+ local cur_tabstop, new_tabstop = session.cur_tabstop, nil
2406
+ -- - NOTE: This can't be infinite as `prev`/`next` traverse all tabstops
2407
+ if not present_tabstops[cur_tabstop] then return end
2408
+ while not present_tabstops[new_tabstop] do
2409
+ new_tabstop = all_tabstops[new_tabstop or cur_tabstop][direction]
2410
+ end
2411
+
2412
+ local event_data = { tabstop_from = cur_tabstop, tabstop_to = new_tabstop }
2413
+ H.trigger_event('MiniSnippetsSessionJumpPre', event_data)
2414
+ H.session_tabstop_focus(session, new_tabstop)
2415
+ H.trigger_event('MiniSnippetsSessionJump', event_data)
2416
+ end)
2417
+
2418
+ H.session_update_hl = function(session)
2419
+ local buf_id, insert_opts = session.buf_id, session.insert_args.opts
2420
+ local empty_tabstop, empty_tabstop_final = insert_opts.empty_tabstop, insert_opts.empty_tabstop_final
2421
+ local cur_tabstop, tabstops = session.cur_tabstop, session.tabstops
2422
+ local is_replace = H.session_get_ref_node(session).placeholder ~= nil
2423
+ local current_hl = 'MiniSnippetsCurrent' .. (is_replace and 'Replace' or '')
2424
+ local priority = 101
2425
+
2426
+ local update_hl = function(n, is_in_cur_tabstop)
2427
+ if n.tabstop == nil then return end
2428
+
2429
+ -- Compute tabstop's features
2430
+ local row, col, opts = H.extmark_get(buf_id, n.extmark_id)
2431
+ local is_empty = row == opts.end_row and col == opts.end_col
2432
+ local is_final = n.tabstop == '0'
2433
+ local is_visited = tabstops[n.tabstop].is_visited
2434
+ local hl_group = (n.tabstop == cur_tabstop or is_in_cur_tabstop) and current_hl
2435
+ or (is_final and 'MiniSnippetsFinal' or (is_visited and 'MiniSnippetsVisited' or 'MiniSnippetsUnvisited'))
2436
+
2437
+ -- Ensure up to date highlighting
2438
+ opts.hl_group, opts.virt_text_pos, opts.virt_text = nil, nil, nil
2439
+
2440
+ if is_empty then
2441
+ if H.nvim_supports_inline_extmarks then
2442
+ opts.virt_text_pos = 'inline'
2443
+ opts.virt_text = { { is_final and empty_tabstop_final or empty_tabstop, hl_group } }
2444
+ end
2445
+ else
2446
+ opts.hl_group = hl_group
2447
+ end
2448
+
2449
+ -- Make inline extmarks preserve order if placed at same position
2450
+ priority = priority + 1
2451
+ opts.priority = priority
2452
+
2453
+ -- Update extmark
2454
+ vim.api.nvim_buf_set_extmark(buf_id, H.ns_id.nodes, row, col, opts)
2455
+ end
2456
+
2457
+ -- Use custom traversing to ensure that nested tabstops inside current
2458
+ -- tabstop's placeholder are highlighted the same, even inline virtual text.
2459
+ local update_hl_in_nodes
2460
+ update_hl_in_nodes = function(nodes, is_in_cur_tabstop)
2461
+ for _, n in ipairs(nodes) do
2462
+ update_hl(n, is_in_cur_tabstop)
2463
+ if n.placeholder ~= nil then update_hl_in_nodes(n.placeholder, is_in_cur_tabstop or n.tabstop == cur_tabstop) end
2464
+ end
2465
+ end
2466
+ update_hl_in_nodes(session.nodes, false)
2467
+ end
2468
+
2469
+ H.session_deinit = function(session, full)
2470
+ if session == nil then return end
2471
+
2472
+ -- Trigger proper event
2473
+ H.trigger_event('MiniSnippetsSession' .. (full and 'Stop' or 'Suspend'), { session = vim.deepcopy(session) })
2474
+ if not H.is_loaded_buf(session.buf_id) then return end
2475
+
2476
+ -- Delete or hide (make invisible) extmarks
2477
+ local extmark_fun = full and H.extmark_del or H.extmark_hide
2478
+ extmark_fun(session.buf_id, session.extmark_id)
2479
+ H.nodes_traverse(session.nodes, function(n) extmark_fun(session.buf_id, n.extmark_id) end)
2480
+
2481
+ -- Hide completion if stopping was done manually
2482
+ if not H.cache.stop_is_auto then H.hide_completion() end
2483
+ end
2484
+
2485
+ H.nodes_set_text = function(buf_id, nodes, tracking_extmark_id, indent, cur_body_line)
2486
+ local sw = vim.bo.shiftwidth
2487
+ local tab_text = vim.bo.expandtab and string.rep(' ', sw == 0 and vim.bo.tabstop or sw) or '\t'
2488
+
2489
+ cur_body_line = cur_body_line or ''
2490
+ for _, n in ipairs(nodes) do
2491
+ -- Add tracking extmark
2492
+ local _, _, row, col = H.extmark_get_range(buf_id, tracking_extmark_id)
2493
+ n.extmark_id = H.extmark_new(buf_id, row, col)
2494
+
2495
+ -- Adjust node's text and append it to currently set text
2496
+ if n.text ~= nil then
2497
+ -- Make variable/tabstop lines preserve relative indent
2498
+ local body_indent = (n.var == nil and n.tabstop == nil) and '' or H.get_indent(cur_body_line)
2499
+ local new_text = n.text:gsub('\n', '\n' .. indent .. body_indent):gsub('\t', tab_text)
2500
+ H.extmark_set_text(buf_id, tracking_extmark_id, 'right', new_text)
2501
+
2502
+ -- NOTE: Compute current body line *before* setting node's actual text
2503
+ cur_body_line = (cur_body_line .. n.text):match('[^\n]*$')
2504
+ n.text = new_text
2505
+ end
2506
+
2507
+ -- Process (possibly nested) placeholder nodes
2508
+ if n.placeholder ~= nil then H.nodes_set_text(buf_id, n.placeholder, tracking_extmark_id, indent, cur_body_line) end
2509
+
2510
+ -- Make sure that node's extmark doesn't move when adding next node text
2511
+ H.extmark_set_gravity(buf_id, n.extmark_id, 'left')
2512
+ end
2513
+ end
2514
+
2515
+ H.nodes_del = function(buf_id, nodes)
2516
+ local del = function(n)
2517
+ H.extmark_set_text(buf_id, n.extmark_id, 'inside', {})
2518
+ H.extmark_del(buf_id, n.extmark_id)
2519
+ end
2520
+ H.nodes_traverse(nodes, del)
2521
+ end
2522
+
2523
+ H.nodes_traverse = function(nodes, f, f_post)
2524
+ for i, n in ipairs(nodes) do
2525
+ -- Prefer visiting whole node first to allow `f` to modify placeholder.
2526
+ -- It is also important to ensure proper gravity inside placeholder nodes.
2527
+ n = f(n) or n
2528
+ if n.placeholder ~= nil then n.placeholder = H.nodes_traverse(n.placeholder, f, f_post) end
2529
+ if f_post then n = f_post(n) or n end
2530
+ nodes[i] = n
2531
+ end
2532
+ return nodes
2533
+ end
2534
+
2535
+ H.compute_tabstop_order = function(nodes)
2536
+ local tabstops_map = {}
2537
+ H.nodes_traverse(nodes, function(n) tabstops_map[n.tabstop or true] = true end)
2538
+ tabstops_map[true] = nil
2539
+
2540
+ -- Order as numbers while allowing leading zeros. Put special `$0` last.
2541
+ local tabstops = vim.tbl_map(function(x) return { tonumber(x), x } end, vim.tbl_keys(tabstops_map))
2542
+ table.sort(tabstops, function(a, b)
2543
+ if a[2] == '0' then return false end
2544
+ if b[2] == '0' then return true end
2545
+ return a[1] < b[1] or (a[1] == b[1] and a[2] < b[2])
2546
+ end)
2547
+ return vim.tbl_map(function(x) return x[2] end, tabstops)
2548
+ end
2549
+
2550
+ -- Extmarks -------------------------------------------------------------------
2551
+ -- All extmark functions work in current buffer with same global namespace.
2552
+ -- This is because interaction with snippets eventually requires buffer to be
2553
+ -- current, so instead rely on it becoming such as soon as possible.
2554
+ H.extmark_new = function(buf_id, row, col)
2555
+ -- Create expanding extmark by default
2556
+ local opts = { end_row = row, end_col = col, right_gravity = false, end_right_gravity = true }
2557
+ return vim.api.nvim_buf_set_extmark(buf_id, H.ns_id.nodes, row, col, opts)
2558
+ end
2559
+
2560
+ H.extmark_get = function(buf_id, ext_id)
2561
+ local data = vim.api.nvim_buf_get_extmark_by_id(buf_id, H.ns_id.nodes, ext_id, { details = true })
2562
+ data[3].id, data[3].ns_id = ext_id, nil
2563
+ return data[1], data[2], data[3]
2564
+ end
2565
+
2566
+ H.extmark_get_range = function(buf_id, ext_id)
2567
+ local row, col, opts = H.extmark_get(buf_id, ext_id)
2568
+ return row, col, opts.end_row, opts.end_col
2569
+ end
2570
+
2571
+ H.extmark_del = function(buf_id, ext_id) vim.api.nvim_buf_del_extmark(buf_id, H.ns_id.nodes, ext_id or -1) end
2572
+
2573
+ H.extmark_hide = function(buf_id, ext_id)
2574
+ local row, col, opts = H.extmark_get(buf_id, ext_id)
2575
+ opts.hl_group, opts.virt_text, opts.virt_text_pos = nil, nil, nil
2576
+ vim.api.nvim_buf_set_extmark(buf_id, H.ns_id.nodes, row, col, opts)
2577
+ end
2578
+
2579
+ H.extmark_set_gravity = function(buf_id, ext_id, gravity)
2580
+ local row, col, opts = H.extmark_get(buf_id, ext_id)
2581
+ opts.right_gravity, opts.end_right_gravity = gravity == 'right', gravity ~= 'left'
2582
+ vim.api.nvim_buf_set_extmark(buf_id, H.ns_id.nodes, row, col, opts)
2583
+ end
2584
+
2585
+ --stylua: ignore
2586
+ H.extmark_set_text = function(buf_id, ext_id, side, text)
2587
+ local row, col, end_row, end_col = H.extmark_get_range(buf_id, ext_id)
2588
+ if side == 'left' then end_row, end_col = row, col end
2589
+ if side == 'right' then row, col = end_row, end_col end
2590
+ text = type(text) == 'string' and vim.split(text, '\n') or text
2591
+ vim.api.nvim_buf_set_text(buf_id, row, col, end_row, end_col, text)
2592
+ end
2593
+
2594
+ -- Indent ---------------------------------------------------------------------
2595
+ H.get_indent = function(line)
2596
+ line = line or vim.fn.getline('.')
2597
+ local comment_indent = ''
2598
+ -- Treat comment leaders as part of indent
2599
+ for _, leader in ipairs(H.get_comment_leaders()) do
2600
+ local cur_match = line:match('^%s*' .. vim.pesc(leader) .. '%s*')
2601
+ -- Use biggest match in case of several matches. Allows respecting "nested"
2602
+ -- comment leaders like "---" and "--".
2603
+ if type(cur_match) == 'string' and comment_indent:len() < cur_match:len() then comment_indent = cur_match end
2604
+ end
2605
+ return comment_indent ~= '' and comment_indent or line:match('^%s*')
2606
+ end
2607
+
2608
+ H.get_comment_leaders = function()
2609
+ local res = {}
2610
+
2611
+ -- From 'commentstring'
2612
+ local main_leader = vim.split(vim.bo.commentstring, '%%s')[1]
2613
+ table.insert(res, vim.trim(main_leader))
2614
+
2615
+ -- From 'comments'
2616
+ for _, comment_part in ipairs(vim.opt_local.comments:get()) do
2617
+ local prefix, suffix = comment_part:match('^(.*):(.*)$')
2618
+ suffix = vim.trim(suffix)
2619
+ if prefix:find('b') then
2620
+ -- Respect `b` flag (for blank) requiring space, tab or EOL after it
2621
+ table.insert(res, suffix .. ' ')
2622
+ table.insert(res, suffix .. '\t')
2623
+ elseif prefix:find('f') == nil then
2624
+ -- Add otherwise ignoring `f` flag (only first line should have it)
2625
+ table.insert(res, suffix)
2626
+ end
2627
+ end
2628
+
2629
+ return res
2630
+ end
2631
+
2632
+ H.dedent = function(lines, row, col)
2633
+ if #lines <= 1 then return lines end
2634
+ -- Compute common (smallest) indent width. Not accounting for actual indent
2635
+ -- characters is easier and works for common cases but breaks for weird ones,
2636
+ -- like `# a\n\t# b`.
2637
+ local init_line_at_pos = vim.fn.getline(row + 1):sub(1, col)
2638
+ local indent_width = H.get_indent(init_line_at_pos):len()
2639
+ for i = 2, #lines do
2640
+ -- Don't count "only indent" lines (i.e. blank with/without comment leader)
2641
+ local cur_indent = H.get_indent(lines[i])
2642
+ if cur_indent:len() < indent_width and cur_indent ~= lines[i] then indent_width = cur_indent:len() end
2643
+ end
2644
+
2645
+ for i = 2, #lines do
2646
+ lines[i] = lines[i]:sub(indent_width + 1)
2647
+ end
2648
+
2649
+ return lines
2650
+ end
2651
+
2652
+ H.reindent = function(lines, row, col)
2653
+ if #lines <= 1 then return lines end
2654
+ local init_line_at_pos = vim.fn.getline(row + 1):sub(1, col)
2655
+ local indent = H.get_indent(init_line_at_pos)
2656
+ for i = 2, #lines do
2657
+ -- NOTE: reindent even "pure indent" lines, as it seems more natural
2658
+ lines[i] = indent .. lines[i]
2659
+ end
2660
+ return lines
2661
+ end
2662
+
2663
+ -- LSP server -----------------------------------------------------------------
2664
+ H.lsp_make_cmd = function(opts)
2665
+ local capabilities = {
2666
+ capabilities = { completionProvider = { triggerCharacters = opts.triggers, resolveProvider = false } },
2667
+ }
2668
+ local textdocument_completion = H.lsp_make_textdocument_completion(opts)
2669
+
2670
+ return function(dispatchers)
2671
+ -- Loose adaptation of https://github.com/neovim/neovim/pull/24338
2672
+ local is_closing, request_id = false, 0
2673
+ return {
2674
+ request = function(method, params, callback, notify_reply_callback)
2675
+ if method == 'initialize' then callback(nil, capabilities) end
2676
+ if method == 'textDocument/completion' then textdocument_completion(params, callback) end
2677
+ if method == 'shutdown' then callback(nil, nil) end
2678
+ request_id = request_id + 1
2679
+ -- NOTE: This is needed to not accumulated "pending" `Client.requests`
2680
+ if notify_reply_callback then vim.schedule(function() pcall(notify_reply_callback, request_id) end) end
2681
+ return true, request_id
2682
+ end,
2683
+ notify = function(method, params)
2684
+ if method == 'exit' then dispatchers.on_exit(0, 15) end
2685
+ return false
2686
+ end,
2687
+ is_closing = function() return is_closing end,
2688
+ terminate = function() is_closing = true end,
2689
+ }
2690
+ end
2691
+ end
2692
+
2693
+ H.lsp_make_textdocument_completion = function(opts)
2694
+ local expand_opts = { match = opts.match, insert = false }
2695
+ local insert_text_format_snippet = vim.lsp.protocol.InsertTextFormat.Snippet
2696
+ local kind_snippet = vim.lsp.protocol.CompletionItemKind.Snippet
2697
+
2698
+ return vim.schedule_wrap(function(params, callback)
2699
+ local res = {}
2700
+ for _, s in ipairs(MiniSnippets.expand(expand_opts)) do
2701
+ local candidate = { label = s.prefix, insertText = s.body, documentation = s.desc }
2702
+ -- NOTE: set `detail` along with `documentation` if it provides new info
2703
+ candidate.detail = s.body ~= s.desc and s.body or nil
2704
+ candidate.insertTextFormat, candidate.kind = insert_text_format_snippet, kind_snippet
2705
+ if s.region ~= nil then
2706
+ local from, to = s.region.from, s.region.to
2707
+ local range_start = { line = from.line - 1, character = from.col - 1 }
2708
+ local range_end = { line = to.line - 1, character = to.col }
2709
+ candidate.textEdit = { newText = s.body, range = { start = range_start, ['end'] = range_end } }
2710
+ candidate.insertText = nil
2711
+ end
2712
+ table.insert(res, candidate)
2713
+ end
2714
+
2715
+ callback(nil, res)
2716
+ end)
2717
+ end
2718
+
2719
+ H.lsp_default_before_attach = function(buf_id)
2720
+ return vim.api.nvim_buf_is_loaded(buf_id) and vim.bo[buf_id].buftype == ''
2721
+ end
2722
+
2723
+ -- Validators -----------------------------------------------------------------
2724
+ H.is_string = function(x) return type(x) == 'string' end
2725
+
2726
+ H.is_maybe_string_or_arr = function(x) return x == nil or H.is_string(x) or H.is_array_of(x, H.is_string) end
2727
+
2728
+ H.is_snippet = function(x)
2729
+ return type(x) == 'table'
2730
+ -- Allow nil `prefix`: inferred as empty string
2731
+ and H.is_maybe_string_or_arr(x.prefix)
2732
+ -- Allow nil `body` to remove snippet with `prefix`
2733
+ and H.is_maybe_string_or_arr(x.body)
2734
+ -- Allow nil `desc` / `description`, in which case "prefix" is used
2735
+ and H.is_maybe_string_or_arr(x.desc)
2736
+ and H.is_maybe_string_or_arr(x.description)
2737
+ -- Allow nil `region` because it is not mandatory
2738
+ and (x.region == nil or H.is_region(x.region))
2739
+ end
2740
+
2741
+ H.is_position = function(x) return type(x) == 'table' and type(x.line) == 'number' and type(x.col) == 'number' end
2742
+
2743
+ H.is_region = function(x) return type(x) == 'table' and H.is_position(x.from) and H.is_position(x.to) end
2744
+
2745
+ -- Utilities ------------------------------------------------------------------
2746
+ H.error = function(msg) error('(mini.snippets) ' .. msg, 0) end
2747
+
2748
+ H.check_type = function(name, val, ref, allow_nil)
2749
+ if type(val) == ref or (ref == 'callable' and vim.is_callable(val)) or (allow_nil and val == nil) then return end
2750
+ H.error(string.format('`%s` should be %s, not %s', name, ref, type(val)))
2751
+ end
2752
+
2753
+ H.notify = function(msg, level_name, silent)
2754
+ if not silent then vim.notify('(mini.snippets) ' .. msg, vim.log.levels[level_name]) end
2755
+ end
2756
+
2757
+ H.trigger_event = function(event_name, data) vim.api.nvim_exec_autocmds('User', { pattern = event_name, data = data }) end
2758
+
2759
+ H.is_array_of = function(x, predicate)
2760
+ if not H.islist(x) then return false end
2761
+ for i = 1, #x do
2762
+ if not predicate(x[i]) then return false end
2763
+ end
2764
+ return true
2765
+ end
2766
+
2767
+ H.is_loaded_buf = function(buf_id) return type(buf_id) == 'number' and vim.api.nvim_buf_is_loaded(buf_id) end
2768
+
2769
+ H.ensure_cur_buf = function(buf_id)
2770
+ if buf_id == 0 or buf_id == vim.api.nvim_get_current_buf() or not H.is_loaded_buf(buf_id) then return end
2771
+ local win_id = vim.fn.win_findbuf(buf_id)[1]
2772
+ if win_id == nil then return vim.api.nvim_win_set_buf(0, buf_id) end
2773
+ vim.api.nvim_set_current_win(win_id)
2774
+ end
2775
+
2776
+ H.set_cursor = function(pos)
2777
+ -- NOTE: This won't put cursor past enf of line (for cursor in Insert mode to
2778
+ -- append text to the line). Ensure that Insert mode is active prior.
2779
+ vim.api.nvim_win_set_cursor(0, pos)
2780
+
2781
+ -- Ensure no built-in completion window
2782
+ -- HACK: Always clearing (and not *only* when pumvisible) accounts for weird
2783
+ -- edge case when it is not visible (i.e. candidates *just* got exhausted)
2784
+ -- but will still "clear and restore" text leading to squashing of extmarks.
2785
+ H.hide_completion()
2786
+ end
2787
+
2788
+ H.call_in_insert_mode = function(f)
2789
+ if vim.fn.mode() == 'i' then return f() end
2790
+
2791
+ -- This is seemingly the only "good" way to ensure Insert mode.
2792
+ -- Mostly because it works with `vim.snippet.expand()` as its implementation
2793
+ -- uses `vim.api.nvim_feedkeys(k, 'n', true)` to select text in Select mode.
2794
+ vim.api.nvim_feedkeys('\28\14i', 'n', false)
2795
+
2796
+ -- NOTE: mode changing is not immediate, only on some next tick. So schedule
2797
+ -- to execute `f` precisely when Insert mode is active.
2798
+ local cb = function() f() end
2799
+ vim.api.nvim_create_autocmd('ModeChanged', { pattern = '*:i*', once = true, callback = cb, desc = 'Call in Insert' })
2800
+ end
2801
+
2802
+ H.delete_region = function(region)
2803
+ if not H.is_region(region) then return end
2804
+ vim.api.nvim_buf_set_text(0, region.from.line - 1, region.from.col - 1, region.to.line - 1, region.to.col, {})
2805
+ H.set_cursor({ region.from.line, region.from.col - 1 })
2806
+ end
2807
+
2808
+ H.show_completion = function(items, startcol)
2809
+ if items == nil or #items == 0 or vim.fn.mode() ~= 'i' then return end
2810
+ vim.fn.complete(startcol or vim.fn.col('.'), items)
2811
+ end
2812
+
2813
+ H.hide_completion = function()
2814
+ -- NOTE: `complete()` instead of emulating <C-y> has immediate effect
2815
+ -- (without the need to `vim.schedule()`). The downsides are that `fn.mode(1)`
2816
+ -- returns 'ic' (i.e. not "i" for clean Insert mode) and <C-n>/<C-p> act as if
2817
+ -- there is completion active (thus not allowing them as custom mappings).
2818
+ -- Appending ` | call feedkeys("\\<C-y>", "n")` removes that, but still would
2819
+ -- require workarounds to work in edge cases.
2820
+ -- NOTE: Use `silent` to not show "Pattern not found" messages. It also hides
2821
+ -- '--INSERT--' temporarily when 'showmode' is active, but seems acceptable.
2822
+ if vim.fn.mode() == 'i' then vim.cmd('silent noautocmd call complete(col("."), [])') end
2823
+ end
2824
+
2825
+ -- TODO: Remove after compatibility with Neovim=0.9 is dropped
2826
+ H.islist = vim.fn.has('nvim-0.10') == 1 and vim.islist or vim.tbl_islist
2827
+
2828
+ return MiniSnippets