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.
- package/.conda/retrovim.yaml +259 -0
- package/.conda/terax.icns +0 -0
- package/.conda/terax.ico +0 -0
- package/.conda/terax.png +0 -0
- package/.github/workflows/release.yml +95 -0
- package/.gitmodules +3 -0
- package/.npm/kanata_touchcursor.js +28 -0
- package/.npm/nvim.js +26 -0
- package/.npm/yazi.js +26 -0
- package/.npm/zsh.js +17 -0
- package/.pip/kanata_touchcursor/__init__.py +28 -0
- package/.pip/nvim/__init__.py +31 -0
- package/.pip/yazi/__init__.py +31 -0
- package/.pip/zsh/__init__.py +22 -0
- package/CHANGELOG.md +26 -0
- package/LICENSE +674 -0
- package/README.md +336 -0
- package/kanata/touchcursor.kbd +164 -0
- package/nvim/init.lua +813 -0
- package/nvim/plugins/mini.nvim/.github/DISCUSSION_TEMPLATE/q-a.yml +72 -0
- package/nvim/plugins/mini.nvim/.github/ISSUE_TEMPLATE/bug-report.yml +136 -0
- package/nvim/plugins/mini.nvim/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/nvim/plugins/mini.nvim/.github/ISSUE_TEMPLATE/feature-request.yml +75 -0
- package/nvim/plugins/mini.nvim/.github/PULL_REQUEST_TEMPLATE.md +2 -0
- package/nvim/plugins/mini.nvim/.github/workflows/quality-control.yml +150 -0
- package/nvim/plugins/mini.nvim/.pre-commit-config.yaml +20 -0
- package/nvim/plugins/mini.nvim/.stylua.toml +7 -0
- package/nvim/plugins/mini.nvim/.styluaignore +1 -0
- package/nvim/plugins/mini.nvim/CHANGELOG.md +1894 -0
- package/nvim/plugins/mini.nvim/CODE_OF_CONDUCT.md +132 -0
- package/nvim/plugins/mini.nvim/CONTRIBUTING.md +331 -0
- package/nvim/plugins/mini.nvim/LICENSE +21 -0
- package/nvim/plugins/mini.nvim/MAINTAINING.md +330 -0
- package/nvim/plugins/mini.nvim/Makefile +51 -0
- package/nvim/plugins/mini.nvim/README.md +261 -0
- package/nvim/plugins/mini.nvim/TESTING.md +1011 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/README.md +35 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/benchmark.sh +56 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_dashboard-alpha.lua +21 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_dashboard-original.lua +17 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_dashboard-starter.lua +18 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_empty.lua +4 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_starter-default.lua +7 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_startify-alpha.lua +10 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_startify-original.lua +7 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_startify-starter.lua +20 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/install.sh +10 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/make_summary.py +68 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/startup-summary.md +10 -0
- package/nvim/plugins/mini.nvim/colors/miniautumn.lua +55 -0
- package/nvim/plugins/mini.nvim/colors/minicyan.lua +95 -0
- package/nvim/plugins/mini.nvim/colors/minischeme.lua +95 -0
- package/nvim/plugins/mini.nvim/colors/minispring.lua +53 -0
- package/nvim/plugins/mini.nvim/colors/minisummer.lua +49 -0
- package/nvim/plugins/mini.nvim/colors/miniwinter.lua +51 -0
- package/nvim/plugins/mini.nvim/colors/randomhue.lua +16 -0
- package/nvim/plugins/mini.nvim/doc/mini-ai.txt +889 -0
- package/nvim/plugins/mini.nvim/doc/mini-align.txt +923 -0
- package/nvim/plugins/mini.nvim/doc/mini-animate.txt +937 -0
- package/nvim/plugins/mini.nvim/doc/mini-base16.txt +285 -0
- package/nvim/plugins/mini.nvim/doc/mini-basics.txt +359 -0
- package/nvim/plugins/mini.nvim/doc/mini-bracketed.txt +622 -0
- package/nvim/plugins/mini.nvim/doc/mini-bufremove.txt +123 -0
- package/nvim/plugins/mini.nvim/doc/mini-clue.txt +760 -0
- package/nvim/plugins/mini.nvim/doc/mini-cmdline.txt +392 -0
- package/nvim/plugins/mini.nvim/doc/mini-colors.txt +880 -0
- package/nvim/plugins/mini.nvim/doc/mini-comment.txt +230 -0
- package/nvim/plugins/mini.nvim/doc/mini-completion.txt +477 -0
- package/nvim/plugins/mini.nvim/doc/mini-cursorword.txt +105 -0
- package/nvim/plugins/mini.nvim/doc/mini-deps.txt +590 -0
- package/nvim/plugins/mini.nvim/doc/mini-diff.txt +661 -0
- package/nvim/plugins/mini.nvim/doc/mini-doc.txt +434 -0
- package/nvim/plugins/mini.nvim/doc/mini-extra.txt +856 -0
- package/nvim/plugins/mini.nvim/doc/mini-files.txt +1018 -0
- package/nvim/plugins/mini.nvim/doc/mini-fuzzy.txt +160 -0
- package/nvim/plugins/mini.nvim/doc/mini-git.txt +425 -0
- package/nvim/plugins/mini.nvim/doc/mini-hipatterns.txt +489 -0
- package/nvim/plugins/mini.nvim/doc/mini-hues.txt +420 -0
- package/nvim/plugins/mini.nvim/doc/mini-icons.txt +508 -0
- package/nvim/plugins/mini.nvim/doc/mini-indentscope.txt +485 -0
- package/nvim/plugins/mini.nvim/doc/mini-input.txt +932 -0
- package/nvim/plugins/mini.nvim/doc/mini-jump.txt +202 -0
- package/nvim/plugins/mini.nvim/doc/mini-jump2d.txt +525 -0
- package/nvim/plugins/mini.nvim/doc/mini-keymap.txt +449 -0
- package/nvim/plugins/mini.nvim/doc/mini-map.txt +717 -0
- package/nvim/plugins/mini.nvim/doc/mini-misc.txt +431 -0
- package/nvim/plugins/mini.nvim/doc/mini-move.txt +181 -0
- package/nvim/plugins/mini.nvim/doc/mini-notify.txt +434 -0
- package/nvim/plugins/mini.nvim/doc/mini-nvim.txt +431 -0
- package/nvim/plugins/mini.nvim/doc/mini-operators.txt +472 -0
- package/nvim/plugins/mini.nvim/doc/mini-pairs.txt +346 -0
- package/nvim/plugins/mini.nvim/doc/mini-pick.txt +1535 -0
- package/nvim/plugins/mini.nvim/doc/mini-sessions.txt +239 -0
- package/nvim/plugins/mini.nvim/doc/mini-snippets.txt +1291 -0
- package/nvim/plugins/mini.nvim/doc/mini-splitjoin.txt +536 -0
- package/nvim/plugins/mini.nvim/doc/mini-starter.txt +618 -0
- package/nvim/plugins/mini.nvim/doc/mini-statusline.txt +361 -0
- package/nvim/plugins/mini.nvim/doc/mini-surround.txt +863 -0
- package/nvim/plugins/mini.nvim/doc/mini-tabline.txt +151 -0
- package/nvim/plugins/mini.nvim/doc/mini-test.txt +966 -0
- package/nvim/plugins/mini.nvim/doc/mini-trailspace.txt +91 -0
- package/nvim/plugins/mini.nvim/doc/mini-visits.txt +937 -0
- package/nvim/plugins/mini.nvim/logo.png +0 -0
- package/nvim/plugins/mini.nvim/lua/mini/ai.lua +2244 -0
- package/nvim/plugins/mini.nvim/lua/mini/align.lua +2076 -0
- package/nvim/plugins/mini.nvim/lua/mini/animate.lua +2127 -0
- package/nvim/plugins/mini.nvim/lua/mini/base16.lua +1752 -0
- package/nvim/plugins/mini.nvim/lua/mini/basics.lua +772 -0
- package/nvim/plugins/mini.nvim/lua/mini/bracketed.lua +2013 -0
- package/nvim/plugins/mini.nvim/lua/mini/bufremove.lua +283 -0
- package/nvim/plugins/mini.nvim/lua/mini/clue.lua +2096 -0
- package/nvim/plugins/mini.nvim/lua/mini/cmdline.lua +1182 -0
- package/nvim/plugins/mini.nvim/lua/mini/colors.lua +2483 -0
- package/nvim/plugins/mini.nvim/lua/mini/comment.lua +586 -0
- package/nvim/plugins/mini.nvim/lua/mini/completion.lua +2094 -0
- package/nvim/plugins/mini.nvim/lua/mini/cursorword.lua +311 -0
- package/nvim/plugins/mini.nvim/lua/mini/deps.lua +1660 -0
- package/nvim/plugins/mini.nvim/lua/mini/diff.lua +1938 -0
- package/nvim/plugins/mini.nvim/lua/mini/doc.lua +1371 -0
- package/nvim/plugins/mini.nvim/lua/mini/extra.lua +2290 -0
- package/nvim/plugins/mini.nvim/lua/mini/files.lua +3145 -0
- package/nvim/plugins/mini.nvim/lua/mini/fuzzy.lua +356 -0
- package/nvim/plugins/mini.nvim/lua/mini/git.lua +1737 -0
- package/nvim/plugins/mini.nvim/lua/mini/hipatterns.lua +1042 -0
- package/nvim/plugins/mini.nvim/lua/mini/hues.lua +2088 -0
- package/nvim/plugins/mini.nvim/lua/mini/icons.lua +2254 -0
- package/nvim/plugins/mini.nvim/lua/mini/indentscope.lua +1158 -0
- package/nvim/plugins/mini.nvim/lua/mini/init.lua +427 -0
- package/nvim/plugins/mini.nvim/lua/mini/input.lua +2183 -0
- package/nvim/plugins/mini.nvim/lua/mini/jump.lua +609 -0
- package/nvim/plugins/mini.nvim/lua/mini/jump2d.lua +1244 -0
- package/nvim/plugins/mini.nvim/lua/mini/keymap.lua +880 -0
- package/nvim/plugins/mini.nvim/lua/mini/map.lua +1709 -0
- package/nvim/plugins/mini.nvim/lua/mini/misc.lua +969 -0
- package/nvim/plugins/mini.nvim/lua/mini/move.lua +509 -0
- package/nvim/plugins/mini.nvim/lua/mini/notify.lua +938 -0
- package/nvim/plugins/mini.nvim/lua/mini/operators.lua +1349 -0
- package/nvim/plugins/mini.nvim/lua/mini/pairs.lua +657 -0
- package/nvim/plugins/mini.nvim/lua/mini/pick.lua +3876 -0
- package/nvim/plugins/mini.nvim/lua/mini/sessions.lua +689 -0
- package/nvim/plugins/mini.nvim/lua/mini/snippets.lua +2828 -0
- package/nvim/plugins/mini.nvim/lua/mini/splitjoin.lua +1132 -0
- package/nvim/plugins/mini.nvim/lua/mini/starter.lua +1599 -0
- package/nvim/plugins/mini.nvim/lua/mini/statusline.lua +686 -0
- package/nvim/plugins/mini.nvim/lua/mini/surround.lua +2388 -0
- package/nvim/plugins/mini.nvim/lua/mini/tabline.lua +564 -0
- package/nvim/plugins/mini.nvim/lua/mini/test.lua +2516 -0
- package/nvim/plugins/mini.nvim/lua/mini/trailspace.lua +215 -0
- package/nvim/plugins/mini.nvim/lua/mini/visits.lua +1577 -0
- package/nvim/plugins/mini.nvim/readmes/mini-ai.md +190 -0
- package/nvim/plugins/mini.nvim/readmes/mini-align.md +203 -0
- package/nvim/plugins/mini.nvim/readmes/mini-animate.md +214 -0
- package/nvim/plugins/mini.nvim/readmes/mini-base16.md +198 -0
- package/nvim/plugins/mini.nvim/readmes/mini-basics.md +184 -0
- package/nvim/plugins/mini.nvim/readmes/mini-bracketed.md +193 -0
- package/nvim/plugins/mini.nvim/readmes/mini-bufremove.md +139 -0
- package/nvim/plugins/mini.nvim/readmes/mini-clue.md +260 -0
- package/nvim/plugins/mini.nvim/readmes/mini-cmdline.md +183 -0
- package/nvim/plugins/mini.nvim/readmes/mini-colors.md +169 -0
- package/nvim/plugins/mini.nvim/readmes/mini-comment.md +183 -0
- package/nvim/plugins/mini.nvim/readmes/mini-completion.md +217 -0
- package/nvim/plugins/mini.nvim/readmes/mini-cursorword.md +137 -0
- package/nvim/plugins/mini.nvim/readmes/mini-deps.md +271 -0
- package/nvim/plugins/mini.nvim/readmes/mini-diff.md +289 -0
- package/nvim/plugins/mini.nvim/readmes/mini-doc.md +198 -0
- package/nvim/plugins/mini.nvim/readmes/mini-extra.md +158 -0
- package/nvim/plugins/mini.nvim/readmes/mini-files.md +247 -0
- package/nvim/plugins/mini.nvim/readmes/mini-fuzzy.md +134 -0
- package/nvim/plugins/mini.nvim/readmes/mini-git.md +168 -0
- package/nvim/plugins/mini.nvim/readmes/mini-hipatterns.md +176 -0
- package/nvim/plugins/mini.nvim/readmes/mini-hues.md +285 -0
- package/nvim/plugins/mini.nvim/readmes/mini-icons.md +183 -0
- package/nvim/plugins/mini.nvim/readmes/mini-indentscope.md +191 -0
- package/nvim/plugins/mini.nvim/readmes/mini-input.md +163 -0
- package/nvim/plugins/mini.nvim/readmes/mini-jump.md +161 -0
- package/nvim/plugins/mini.nvim/readmes/mini-jump2d.md +197 -0
- package/nvim/plugins/mini.nvim/readmes/mini-keymap.md +205 -0
- package/nvim/plugins/mini.nvim/readmes/mini-map.md +191 -0
- package/nvim/plugins/mini.nvim/readmes/mini-misc.md +145 -0
- package/nvim/plugins/mini.nvim/readmes/mini-move.md +163 -0
- package/nvim/plugins/mini.nvim/readmes/mini-notify.md +175 -0
- package/nvim/plugins/mini.nvim/readmes/mini-operators.md +193 -0
- package/nvim/plugins/mini.nvim/readmes/mini-pairs.md +158 -0
- package/nvim/plugins/mini.nvim/readmes/mini-pick.md +340 -0
- package/nvim/plugins/mini.nvim/readmes/mini-sessions.md +164 -0
- package/nvim/plugins/mini.nvim/readmes/mini-snippets.md +373 -0
- package/nvim/plugins/mini.nvim/readmes/mini-splitjoin.md +194 -0
- package/nvim/plugins/mini.nvim/readmes/mini-starter.md +176 -0
- package/nvim/plugins/mini.nvim/readmes/mini-statusline.md +155 -0
- package/nvim/plugins/mini.nvim/readmes/mini-surround.md +190 -0
- package/nvim/plugins/mini.nvim/readmes/mini-tabline.md +156 -0
- package/nvim/plugins/mini.nvim/readmes/mini-test.md +175 -0
- package/nvim/plugins/mini.nvim/readmes/mini-trailspace.md +137 -0
- package/nvim/plugins/mini.nvim/readmes/mini-visits.md +243 -0
- package/nvim/plugins/mini.nvim/scripts/dual_log.sh +13 -0
- package/nvim/plugins/mini.nvim/scripts/dual_push.sh +15 -0
- package/nvim/plugins/mini.nvim/scripts/dual_release.sh +102 -0
- package/nvim/plugins/mini.nvim/scripts/dual_sync.sh +115 -0
- package/nvim/plugins/mini.nvim/scripts/init-deps-example.lua +59 -0
- package/nvim/plugins/mini.nvim/scripts/lint-filename-length.sh +16 -0
- package/nvim/plugins/mini.nvim/scripts/lintcommit-ci.sh +18 -0
- package/nvim/plugins/mini.nvim/scripts/lintcommit.lua +393 -0
- package/nvim/plugins/mini.nvim/scripts/minidoc.lua +57 -0
- package/nvim/plugins/mini.nvim/scripts/minimal_init.lua +28 -0
- package/nvim/plugins/mini.nvim/scripts/minitest.lua +4 -0
- package/package.json +16 -0
- package/pyproject.toml +43 -0
- package/yazi/init.lua +12 -0
- package/yazi/keymap.toml +114 -0
- package/yazi/lesskey +5 -0
- package/yazi/plugins/bookmarks.yazi/main.lua +86 -0
- package/yazi/plugins/cd-git-root.yazi/main.lua +31 -0
- package/yazi/plugins/smart-enter.yazi/main.lua +11 -0
- package/yazi/theme.toml +26 -0
- package/yazi/yazi.toml +15 -0
- package/zsh/.zshrc +145 -0
- package/zsh/starship.toml +20 -0
- 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
|