retrovim 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/.conda/retrovim.yaml +259 -0
  2. package/.conda/terax.icns +0 -0
  3. package/.conda/terax.ico +0 -0
  4. package/.conda/terax.png +0 -0
  5. package/.github/workflows/release.yml +95 -0
  6. package/.gitmodules +3 -0
  7. package/.npm/kanata_touchcursor.js +28 -0
  8. package/.npm/nvim.js +26 -0
  9. package/.npm/yazi.js +26 -0
  10. package/.npm/zsh.js +17 -0
  11. package/.pip/kanata_touchcursor/__init__.py +28 -0
  12. package/.pip/nvim/__init__.py +31 -0
  13. package/.pip/yazi/__init__.py +31 -0
  14. package/.pip/zsh/__init__.py +22 -0
  15. package/CHANGELOG.md +26 -0
  16. package/LICENSE +674 -0
  17. package/README.md +336 -0
  18. package/kanata/touchcursor.kbd +164 -0
  19. package/nvim/init.lua +813 -0
  20. package/nvim/plugins/mini.nvim/.github/DISCUSSION_TEMPLATE/q-a.yml +72 -0
  21. package/nvim/plugins/mini.nvim/.github/ISSUE_TEMPLATE/bug-report.yml +136 -0
  22. package/nvim/plugins/mini.nvim/.github/ISSUE_TEMPLATE/config.yml +5 -0
  23. package/nvim/plugins/mini.nvim/.github/ISSUE_TEMPLATE/feature-request.yml +75 -0
  24. package/nvim/plugins/mini.nvim/.github/PULL_REQUEST_TEMPLATE.md +2 -0
  25. package/nvim/plugins/mini.nvim/.github/workflows/quality-control.yml +150 -0
  26. package/nvim/plugins/mini.nvim/.pre-commit-config.yaml +20 -0
  27. package/nvim/plugins/mini.nvim/.stylua.toml +7 -0
  28. package/nvim/plugins/mini.nvim/.styluaignore +1 -0
  29. package/nvim/plugins/mini.nvim/CHANGELOG.md +1894 -0
  30. package/nvim/plugins/mini.nvim/CODE_OF_CONDUCT.md +132 -0
  31. package/nvim/plugins/mini.nvim/CONTRIBUTING.md +331 -0
  32. package/nvim/plugins/mini.nvim/LICENSE +21 -0
  33. package/nvim/plugins/mini.nvim/MAINTAINING.md +330 -0
  34. package/nvim/plugins/mini.nvim/Makefile +51 -0
  35. package/nvim/plugins/mini.nvim/README.md +261 -0
  36. package/nvim/plugins/mini.nvim/TESTING.md +1011 -0
  37. package/nvim/plugins/mini.nvim/benchmarks/starter/README.md +35 -0
  38. package/nvim/plugins/mini.nvim/benchmarks/starter/benchmark.sh +56 -0
  39. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_dashboard-alpha.lua +21 -0
  40. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_dashboard-original.lua +17 -0
  41. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_dashboard-starter.lua +18 -0
  42. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_empty.lua +4 -0
  43. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_starter-default.lua +7 -0
  44. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_startify-alpha.lua +10 -0
  45. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_startify-original.lua +7 -0
  46. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_startify-starter.lua +20 -0
  47. package/nvim/plugins/mini.nvim/benchmarks/starter/install.sh +10 -0
  48. package/nvim/plugins/mini.nvim/benchmarks/starter/make_summary.py +68 -0
  49. package/nvim/plugins/mini.nvim/benchmarks/starter/startup-summary.md +10 -0
  50. package/nvim/plugins/mini.nvim/colors/miniautumn.lua +55 -0
  51. package/nvim/plugins/mini.nvim/colors/minicyan.lua +95 -0
  52. package/nvim/plugins/mini.nvim/colors/minischeme.lua +95 -0
  53. package/nvim/plugins/mini.nvim/colors/minispring.lua +53 -0
  54. package/nvim/plugins/mini.nvim/colors/minisummer.lua +49 -0
  55. package/nvim/plugins/mini.nvim/colors/miniwinter.lua +51 -0
  56. package/nvim/plugins/mini.nvim/colors/randomhue.lua +16 -0
  57. package/nvim/plugins/mini.nvim/doc/mini-ai.txt +889 -0
  58. package/nvim/plugins/mini.nvim/doc/mini-align.txt +923 -0
  59. package/nvim/plugins/mini.nvim/doc/mini-animate.txt +937 -0
  60. package/nvim/plugins/mini.nvim/doc/mini-base16.txt +285 -0
  61. package/nvim/plugins/mini.nvim/doc/mini-basics.txt +359 -0
  62. package/nvim/plugins/mini.nvim/doc/mini-bracketed.txt +622 -0
  63. package/nvim/plugins/mini.nvim/doc/mini-bufremove.txt +123 -0
  64. package/nvim/plugins/mini.nvim/doc/mini-clue.txt +760 -0
  65. package/nvim/plugins/mini.nvim/doc/mini-cmdline.txt +392 -0
  66. package/nvim/plugins/mini.nvim/doc/mini-colors.txt +880 -0
  67. package/nvim/plugins/mini.nvim/doc/mini-comment.txt +230 -0
  68. package/nvim/plugins/mini.nvim/doc/mini-completion.txt +477 -0
  69. package/nvim/plugins/mini.nvim/doc/mini-cursorword.txt +105 -0
  70. package/nvim/plugins/mini.nvim/doc/mini-deps.txt +590 -0
  71. package/nvim/plugins/mini.nvim/doc/mini-diff.txt +661 -0
  72. package/nvim/plugins/mini.nvim/doc/mini-doc.txt +434 -0
  73. package/nvim/plugins/mini.nvim/doc/mini-extra.txt +856 -0
  74. package/nvim/plugins/mini.nvim/doc/mini-files.txt +1018 -0
  75. package/nvim/plugins/mini.nvim/doc/mini-fuzzy.txt +160 -0
  76. package/nvim/plugins/mini.nvim/doc/mini-git.txt +425 -0
  77. package/nvim/plugins/mini.nvim/doc/mini-hipatterns.txt +489 -0
  78. package/nvim/plugins/mini.nvim/doc/mini-hues.txt +420 -0
  79. package/nvim/plugins/mini.nvim/doc/mini-icons.txt +508 -0
  80. package/nvim/plugins/mini.nvim/doc/mini-indentscope.txt +485 -0
  81. package/nvim/plugins/mini.nvim/doc/mini-input.txt +932 -0
  82. package/nvim/plugins/mini.nvim/doc/mini-jump.txt +202 -0
  83. package/nvim/plugins/mini.nvim/doc/mini-jump2d.txt +525 -0
  84. package/nvim/plugins/mini.nvim/doc/mini-keymap.txt +449 -0
  85. package/nvim/plugins/mini.nvim/doc/mini-map.txt +717 -0
  86. package/nvim/plugins/mini.nvim/doc/mini-misc.txt +431 -0
  87. package/nvim/plugins/mini.nvim/doc/mini-move.txt +181 -0
  88. package/nvim/plugins/mini.nvim/doc/mini-notify.txt +434 -0
  89. package/nvim/plugins/mini.nvim/doc/mini-nvim.txt +431 -0
  90. package/nvim/plugins/mini.nvim/doc/mini-operators.txt +472 -0
  91. package/nvim/plugins/mini.nvim/doc/mini-pairs.txt +346 -0
  92. package/nvim/plugins/mini.nvim/doc/mini-pick.txt +1535 -0
  93. package/nvim/plugins/mini.nvim/doc/mini-sessions.txt +239 -0
  94. package/nvim/plugins/mini.nvim/doc/mini-snippets.txt +1291 -0
  95. package/nvim/plugins/mini.nvim/doc/mini-splitjoin.txt +536 -0
  96. package/nvim/plugins/mini.nvim/doc/mini-starter.txt +618 -0
  97. package/nvim/plugins/mini.nvim/doc/mini-statusline.txt +361 -0
  98. package/nvim/plugins/mini.nvim/doc/mini-surround.txt +863 -0
  99. package/nvim/plugins/mini.nvim/doc/mini-tabline.txt +151 -0
  100. package/nvim/plugins/mini.nvim/doc/mini-test.txt +966 -0
  101. package/nvim/plugins/mini.nvim/doc/mini-trailspace.txt +91 -0
  102. package/nvim/plugins/mini.nvim/doc/mini-visits.txt +937 -0
  103. package/nvim/plugins/mini.nvim/logo.png +0 -0
  104. package/nvim/plugins/mini.nvim/lua/mini/ai.lua +2244 -0
  105. package/nvim/plugins/mini.nvim/lua/mini/align.lua +2076 -0
  106. package/nvim/plugins/mini.nvim/lua/mini/animate.lua +2127 -0
  107. package/nvim/plugins/mini.nvim/lua/mini/base16.lua +1752 -0
  108. package/nvim/plugins/mini.nvim/lua/mini/basics.lua +772 -0
  109. package/nvim/plugins/mini.nvim/lua/mini/bracketed.lua +2013 -0
  110. package/nvim/plugins/mini.nvim/lua/mini/bufremove.lua +283 -0
  111. package/nvim/plugins/mini.nvim/lua/mini/clue.lua +2096 -0
  112. package/nvim/plugins/mini.nvim/lua/mini/cmdline.lua +1182 -0
  113. package/nvim/plugins/mini.nvim/lua/mini/colors.lua +2483 -0
  114. package/nvim/plugins/mini.nvim/lua/mini/comment.lua +586 -0
  115. package/nvim/plugins/mini.nvim/lua/mini/completion.lua +2094 -0
  116. package/nvim/plugins/mini.nvim/lua/mini/cursorword.lua +311 -0
  117. package/nvim/plugins/mini.nvim/lua/mini/deps.lua +1660 -0
  118. package/nvim/plugins/mini.nvim/lua/mini/diff.lua +1938 -0
  119. package/nvim/plugins/mini.nvim/lua/mini/doc.lua +1371 -0
  120. package/nvim/plugins/mini.nvim/lua/mini/extra.lua +2290 -0
  121. package/nvim/plugins/mini.nvim/lua/mini/files.lua +3145 -0
  122. package/nvim/plugins/mini.nvim/lua/mini/fuzzy.lua +356 -0
  123. package/nvim/plugins/mini.nvim/lua/mini/git.lua +1737 -0
  124. package/nvim/plugins/mini.nvim/lua/mini/hipatterns.lua +1042 -0
  125. package/nvim/plugins/mini.nvim/lua/mini/hues.lua +2088 -0
  126. package/nvim/plugins/mini.nvim/lua/mini/icons.lua +2254 -0
  127. package/nvim/plugins/mini.nvim/lua/mini/indentscope.lua +1158 -0
  128. package/nvim/plugins/mini.nvim/lua/mini/init.lua +427 -0
  129. package/nvim/plugins/mini.nvim/lua/mini/input.lua +2183 -0
  130. package/nvim/plugins/mini.nvim/lua/mini/jump.lua +609 -0
  131. package/nvim/plugins/mini.nvim/lua/mini/jump2d.lua +1244 -0
  132. package/nvim/plugins/mini.nvim/lua/mini/keymap.lua +880 -0
  133. package/nvim/plugins/mini.nvim/lua/mini/map.lua +1709 -0
  134. package/nvim/plugins/mini.nvim/lua/mini/misc.lua +969 -0
  135. package/nvim/plugins/mini.nvim/lua/mini/move.lua +509 -0
  136. package/nvim/plugins/mini.nvim/lua/mini/notify.lua +938 -0
  137. package/nvim/plugins/mini.nvim/lua/mini/operators.lua +1349 -0
  138. package/nvim/plugins/mini.nvim/lua/mini/pairs.lua +657 -0
  139. package/nvim/plugins/mini.nvim/lua/mini/pick.lua +3876 -0
  140. package/nvim/plugins/mini.nvim/lua/mini/sessions.lua +689 -0
  141. package/nvim/plugins/mini.nvim/lua/mini/snippets.lua +2828 -0
  142. package/nvim/plugins/mini.nvim/lua/mini/splitjoin.lua +1132 -0
  143. package/nvim/plugins/mini.nvim/lua/mini/starter.lua +1599 -0
  144. package/nvim/plugins/mini.nvim/lua/mini/statusline.lua +686 -0
  145. package/nvim/plugins/mini.nvim/lua/mini/surround.lua +2388 -0
  146. package/nvim/plugins/mini.nvim/lua/mini/tabline.lua +564 -0
  147. package/nvim/plugins/mini.nvim/lua/mini/test.lua +2516 -0
  148. package/nvim/plugins/mini.nvim/lua/mini/trailspace.lua +215 -0
  149. package/nvim/plugins/mini.nvim/lua/mini/visits.lua +1577 -0
  150. package/nvim/plugins/mini.nvim/readmes/mini-ai.md +190 -0
  151. package/nvim/plugins/mini.nvim/readmes/mini-align.md +203 -0
  152. package/nvim/plugins/mini.nvim/readmes/mini-animate.md +214 -0
  153. package/nvim/plugins/mini.nvim/readmes/mini-base16.md +198 -0
  154. package/nvim/plugins/mini.nvim/readmes/mini-basics.md +184 -0
  155. package/nvim/plugins/mini.nvim/readmes/mini-bracketed.md +193 -0
  156. package/nvim/plugins/mini.nvim/readmes/mini-bufremove.md +139 -0
  157. package/nvim/plugins/mini.nvim/readmes/mini-clue.md +260 -0
  158. package/nvim/plugins/mini.nvim/readmes/mini-cmdline.md +183 -0
  159. package/nvim/plugins/mini.nvim/readmes/mini-colors.md +169 -0
  160. package/nvim/plugins/mini.nvim/readmes/mini-comment.md +183 -0
  161. package/nvim/plugins/mini.nvim/readmes/mini-completion.md +217 -0
  162. package/nvim/plugins/mini.nvim/readmes/mini-cursorword.md +137 -0
  163. package/nvim/plugins/mini.nvim/readmes/mini-deps.md +271 -0
  164. package/nvim/plugins/mini.nvim/readmes/mini-diff.md +289 -0
  165. package/nvim/plugins/mini.nvim/readmes/mini-doc.md +198 -0
  166. package/nvim/plugins/mini.nvim/readmes/mini-extra.md +158 -0
  167. package/nvim/plugins/mini.nvim/readmes/mini-files.md +247 -0
  168. package/nvim/plugins/mini.nvim/readmes/mini-fuzzy.md +134 -0
  169. package/nvim/plugins/mini.nvim/readmes/mini-git.md +168 -0
  170. package/nvim/plugins/mini.nvim/readmes/mini-hipatterns.md +176 -0
  171. package/nvim/plugins/mini.nvim/readmes/mini-hues.md +285 -0
  172. package/nvim/plugins/mini.nvim/readmes/mini-icons.md +183 -0
  173. package/nvim/plugins/mini.nvim/readmes/mini-indentscope.md +191 -0
  174. package/nvim/plugins/mini.nvim/readmes/mini-input.md +163 -0
  175. package/nvim/plugins/mini.nvim/readmes/mini-jump.md +161 -0
  176. package/nvim/plugins/mini.nvim/readmes/mini-jump2d.md +197 -0
  177. package/nvim/plugins/mini.nvim/readmes/mini-keymap.md +205 -0
  178. package/nvim/plugins/mini.nvim/readmes/mini-map.md +191 -0
  179. package/nvim/plugins/mini.nvim/readmes/mini-misc.md +145 -0
  180. package/nvim/plugins/mini.nvim/readmes/mini-move.md +163 -0
  181. package/nvim/plugins/mini.nvim/readmes/mini-notify.md +175 -0
  182. package/nvim/plugins/mini.nvim/readmes/mini-operators.md +193 -0
  183. package/nvim/plugins/mini.nvim/readmes/mini-pairs.md +158 -0
  184. package/nvim/plugins/mini.nvim/readmes/mini-pick.md +340 -0
  185. package/nvim/plugins/mini.nvim/readmes/mini-sessions.md +164 -0
  186. package/nvim/plugins/mini.nvim/readmes/mini-snippets.md +373 -0
  187. package/nvim/plugins/mini.nvim/readmes/mini-splitjoin.md +194 -0
  188. package/nvim/plugins/mini.nvim/readmes/mini-starter.md +176 -0
  189. package/nvim/plugins/mini.nvim/readmes/mini-statusline.md +155 -0
  190. package/nvim/plugins/mini.nvim/readmes/mini-surround.md +190 -0
  191. package/nvim/plugins/mini.nvim/readmes/mini-tabline.md +156 -0
  192. package/nvim/plugins/mini.nvim/readmes/mini-test.md +175 -0
  193. package/nvim/plugins/mini.nvim/readmes/mini-trailspace.md +137 -0
  194. package/nvim/plugins/mini.nvim/readmes/mini-visits.md +243 -0
  195. package/nvim/plugins/mini.nvim/scripts/dual_log.sh +13 -0
  196. package/nvim/plugins/mini.nvim/scripts/dual_push.sh +15 -0
  197. package/nvim/plugins/mini.nvim/scripts/dual_release.sh +102 -0
  198. package/nvim/plugins/mini.nvim/scripts/dual_sync.sh +115 -0
  199. package/nvim/plugins/mini.nvim/scripts/init-deps-example.lua +59 -0
  200. package/nvim/plugins/mini.nvim/scripts/lint-filename-length.sh +16 -0
  201. package/nvim/plugins/mini.nvim/scripts/lintcommit-ci.sh +18 -0
  202. package/nvim/plugins/mini.nvim/scripts/lintcommit.lua +393 -0
  203. package/nvim/plugins/mini.nvim/scripts/minidoc.lua +57 -0
  204. package/nvim/plugins/mini.nvim/scripts/minimal_init.lua +28 -0
  205. package/nvim/plugins/mini.nvim/scripts/minitest.lua +4 -0
  206. package/package.json +16 -0
  207. package/pyproject.toml +43 -0
  208. package/yazi/init.lua +12 -0
  209. package/yazi/keymap.toml +114 -0
  210. package/yazi/lesskey +5 -0
  211. package/yazi/plugins/bookmarks.yazi/main.lua +86 -0
  212. package/yazi/plugins/cd-git-root.yazi/main.lua +31 -0
  213. package/yazi/plugins/smart-enter.yazi/main.lua +11 -0
  214. package/yazi/theme.toml +26 -0
  215. package/yazi/yazi.toml +15 -0
  216. package/zsh/.zshrc +145 -0
  217. package/zsh/starship.toml +20 -0
  218. package/zsh/zsh-patina.toml +2 -0
@@ -0,0 +1,3876 @@
1
+ --- *mini.pick* Pick anything
2
+ ---
3
+ --- MIT License Copyright (c) 2023 Evgeni Chasnovski
4
+
5
+ --- Features:
6
+ ---
7
+ --- - Single window general purpose interface for picking element from any array.
8
+ ---
9
+ --- - On demand toggleable preview and info views.
10
+ ---
11
+ --- - Interactive query matching (filter+sort) with fast non-blocking default
12
+ --- which does fuzzy matching and allows other modes (|MiniPick.default_match()|).
13
+ ---
14
+ --- - Built-in pickers (see |MiniPick.builtin|):
15
+ --- - Files.
16
+ --- - Pattern match (for fixed pattern or with live feedback; both allow
17
+ --- file filtering via glob patterns).
18
+ --- - Buffers.
19
+ --- - Help tags.
20
+ --- - CLI output.
21
+ --- - Resume latest picker.
22
+ ---
23
+ --- - |:Pick| command to work with extensible |MiniPick.registry|.
24
+ ---
25
+ --- - |vim.ui.select()| implementation. To adjust, use |MiniPick.ui_select()|
26
+ --- or save-restore `vim.ui.select` manually after calling |MiniPick.setup()|.
27
+ ---
28
+ --- - Rich and customizable built-in |MiniPick-actions| when picker is active:
29
+ --- - Manually change currently focused item.
30
+ --- - Scroll vertically and horizontally.
31
+ --- - Toggle preview or info view.
32
+ --- - Mark/unmark items to choose later.
33
+ --- - Refine current matches (make them part of a new picker).
34
+ --- - And many more.
35
+ ---
36
+ --- - Minimal yet flexible |MiniPick-source| specification with:
37
+ --- - Items (array, callable, or manually set later).
38
+ --- - Source name.
39
+ --- - Working directory.
40
+ --- - Matching algorithm.
41
+ --- - Way matches are shown in main window.
42
+ --- - Item preview.
43
+ --- - "On choice" action for current and marked items.
44
+ ---
45
+ --- - Custom actions/keys can be configured globally, per buffer, or per picker.
46
+ ---
47
+ --- - Out of the box support for |'ignorecase'| and |'smartcase'|.
48
+ ---
49
+ --- - Match caching to increase responsiveness on repeated prompts.
50
+ ---
51
+ --- Notes:
52
+ --- - Works on all supported versions but Neovim>=0.10 will give more visual
53
+ --- feedback in floating window footer.
54
+ ---
55
+ --- - For more pickers see |MiniExtra.pickers|.
56
+ ---
57
+ --- Sources with more details:
58
+ --- - |MiniPick-overview|
59
+ --- - |MiniPick-source|
60
+ --- - |MiniPick-actions|
61
+ --- - |MiniPick-examples|
62
+ --- - |MiniPick.builtin|
63
+ --- - |MiniPick-in-other-plugins| (for plugin authors)
64
+ ---
65
+ --- # Dependencies ~
66
+ ---
67
+ --- Suggested dependencies (provide extra functionality, will work without them):
68
+ ---
69
+ --- - Enabled |mini.icons| module to show icons near the items for actual paths.
70
+ --- Falls back to `nvim-tree/nvim-web-devicons` plugin or no icons will be used.
71
+ ---
72
+ --- - *MiniPick-cli-tools* CLI tool(s) to power |MiniPick.builtin.files()|,
73
+ --- |MiniPick.builtin.grep()|, and |MiniPick.builtin.grep_live()| built-in pickers:
74
+ --- - `rg` (github.com/BurntSushi/ripgrep; enough for all three; recommended).
75
+ --- - `fd` (github.com/sharkdp/fd; for `files` only).
76
+ --- - `git` (github.com/git/git; enough for all three).
77
+ ---
78
+ --- Note: CLI tools are called only with basic arguments needed to get items.
79
+ --- To customize the output, use their respective configuration approaches.
80
+ --- Here are some examples of where to start:
81
+ --- - github.com/BurntSushi/ripgrep/blob/master/GUIDE.md#configuration-file
82
+ --- - github.com/sharkdp/fd#excluding-specific-files-or-directories
83
+ --- - git-scm.com/docs/gitignore
84
+ ---
85
+ --- # Setup ~
86
+ ---
87
+ --- This module needs a setup with `require('mini.pick').setup({})` (replace
88
+ --- `{}` with your `config` table). It will create global Lua table `MiniPick`
89
+ --- which you can use for scripting or manually (with `:lua MiniPick.*`).
90
+ ---
91
+ --- See |MiniPick.config| for available config settings.
92
+ ---
93
+ --- You can override runtime config settings locally to buffer inside
94
+ --- `vim.b.minipick_config` which should have same structure as `MiniPick.config`.
95
+ --- See |mini.nvim-buffer-local-config| for more details.
96
+ ---
97
+ --- # Comparisons ~
98
+ ---
99
+ --- - [nvim-telescope/telescope.nvim](https://github.com/nvim-telescope/telescope.nvim):
100
+ --- - The main inspiration for this module, so there is significant overlap.
101
+ --- - Has three (or two) window UI (prompt, matches, preview), while this
102
+ --- module combines everything in one window. It allows more straightforward
103
+ --- customization for unusual scenarios.
104
+ --- - Default match algorithm is somewhat slow, while this module should
105
+ --- match relatively lag-free for at least 100K+ items.
106
+ --- - Has many built-in pickers, while this module has handful at its core
107
+ --- relying on other |mini.nvim| modules to provide more (see |mini.extra|).
108
+ ---
109
+ --- - [ibhagwan/fzf-lua](https://github.com/ibhagwan/fzf-lua):
110
+ --- - Mostly same comparison as with `nvim-telescope/telescope.nvim`.
111
+ --- - Requires [junegunn/fzf](https://github.com/junegunn/fzf) installed to
112
+ --- power fuzzy matching, while this module provides built-in Lua matching.
113
+ ---
114
+ --- # Highlight groups ~
115
+ --- *MiniPick-hl-groups*
116
+ ---
117
+ --- - `MiniPickBorder` - window border.
118
+ --- - `MiniPickBorderBusy` - window border while picker is busy processing.
119
+ --- - `MiniPickBorderText` - non-prompt on border.
120
+ --- - `MiniPickCursor` - cursor during active picker (hidden by default).
121
+ --- - `MiniPickIconDirectory` - default icon for directory.
122
+ --- - `MiniPickIconFile` - default icon for file.
123
+ --- - `MiniPickHeader` - headers in info buffer and previews.
124
+ --- - `MiniPickMatchCurrent` - current matched item.
125
+ --- - `MiniPickMatchMarked` - marked matched items.
126
+ --- - `MiniPickMatchRanges` - ranges matching query elements.
127
+ --- - `MiniPickNormal` - basic foreground/background highlighting.
128
+ --- - `MiniPickPreviewLine` - target line in preview.
129
+ --- - `MiniPickPreviewRegion` - target region in preview.
130
+ --- - `MiniPickPrompt` - prompt.
131
+ --- - `MiniPickPromptCaret` - caret in prompt.
132
+ --- - `MiniPickPromptPrefix` - prefix of the prompt.
133
+ ---
134
+ --- To change any highlight group, set it directly with |nvim_set_hl()|.
135
+ ---
136
+ --- # Using in other plugins ~
137
+ --- *MiniPick-in-other-plugins*
138
+ ---
139
+ --- - Prefer using |vim.ui.select()| for more user coverage. Use |MiniPick.start()|
140
+ --- when synchronous select or its extra capabilities is absolutely necessary.
141
+ ---
142
+ --- - Perform a `_G.MiniPick ~= nil` check before using any feature. This ensures
143
+ --- that user explicitly set up the module.
144
+ ---@tag MiniPick
145
+
146
+ --- To allow user customization and integration of external tools, certain |User|
147
+ --- autocommand events are triggered under common circumstances:
148
+ ---
149
+ --- - `MiniPickMatch` - just after updating query matches or setting items.
150
+ --- - `MiniPickStart` - just after picker has started.
151
+ --- - `MiniPickStop` - just before picker is stopped.
152
+ ---@tag MiniPick-events
153
+
154
+ --- General idea is to take array of objects, display them with interactive
155
+ --- filter/sort/navigate/preview, and allow to choose one or more items.
156
+ ---
157
+ --- # How to start a picker ~
158
+ ---
159
+ --- - Use |MiniPick.start()| with `opts.source` defining |MiniPick-source|.
160
+ --- Example: `MiniPick.start({ source = { items = vim.fn.readdir('.') } })`
161
+ ---
162
+ --- - Use any of |MiniPick.builtin| pickers directly.
163
+ --- Example: `MiniPick.builtin.files({ tool = 'git' })`
164
+ ---
165
+ --- - Use |:Pick| command which uses customizable pickers from |MiniPick.registry|.
166
+ --- Example: `:Pick files tool='git'`
167
+ ---
168
+ --- # User interface ~
169
+ ---
170
+ --- UI consists from a single window capable of displaying three different views:
171
+ --- - "Main" - where current query matches are shown.
172
+ --- - "Preview" - preview of current item (toggle with `<Tab>`).
173
+ --- - "Info" - general info about picker and its state (toggle with `<S-Tab>`).
174
+ ---
175
+ --- Current prompt is displayed at the top left of the window border with vertical
176
+ --- line indicating caret (current input position).
177
+ ---
178
+ --- Bottom part of window border displays (in Neovim>=0.10) extra visual feedback:
179
+ --- - Left part is a picker name.
180
+ --- - Right part contains information in the format >
181
+ ---
182
+ --- <current index in matches> | <match count> | <marked count> / <total count>
183
+ --- <
184
+ --- When picker is busy (like if there are no items yet set or matching is active)
185
+ --- window border changes color to be `MiniPickBorderBusy` after `config.delay.busy`
186
+ --- milliseconds of idle time.
187
+ ---
188
+ --- # Life cycle ~
189
+ ---
190
+ --- - Type characters to filter and sort matches. It uses |MiniPick.default_match()|
191
+ --- with `query` being an array of pressed characters.
192
+ --- Overview of how it matches:
193
+ --- - If query starts with `'`, the match is exact.
194
+ --- - If query starts with `^`, the match is exact at start.
195
+ --- - If query ends with `$`, the match is exact at end.
196
+ --- - If query starts with `*`, the match is forced to be fuzzy.
197
+ --- - Otherwise match is fuzzy.
198
+ --- - Sorting is done to first minimize match width and then match start.
199
+ --- Nothing more: no favoring certain places in string, etc.
200
+ ---
201
+ --- - Type special keys to perform |MiniPick-actions|. Here are some basic ones:
202
+ --- - `<C-n>` / `<Down>` moves down; `<C-p>` / `<Up>` moves up.
203
+ --- - `<Left>` / `<Right>` moves prompt caret left / right.
204
+ --- - `<S-Tab>` toggles information window with all available mappings.
205
+ --- - `<Tab>` toggles preview.
206
+ --- - `<C-x>` / `<C-a>` toggles current / all item(s) as (un)marked.
207
+ --- - `<C-Space>` / `<M-Space>` makes all matches or marked items as new picker.
208
+ --- - `<CR>` / `<M-CR>` chooses current/marked item(s).
209
+ --- - `<Esc>` / `<C-c>` stops picker.
210
+ ---
211
+ --- # Implementation details ~
212
+ ---
213
+ --- - Processing key typing is done via a dedicated key query process for more
214
+ --- control over their side effects. As a result, regular mappings don't work
215
+ --- here and picker's window needs to be current as long as it is shown.
216
+ --- Changing window focus leads to automatic picker stop (after small delay).
217
+ --- Not picker related screen changes require explicit |:redraw|.
218
+ --- - Any picker is non-blocking but waits to return the chosen item. Example:
219
+ --- `file = MiniPick.builtin.files()` allows other actions to be executed when
220
+ --- picker is shown while still assigning `file` with value of the chosen item.
221
+ ---@tag MiniPick-overview
222
+
223
+ --- Source is defined as a `source` field inside one of (in increasing priority):
224
+ --- - |MiniPick.config| - has global effect.
225
+ --- - `vim.b.minipick_config` - has buffer-local effect.
226
+ --- - `opts.source` in picker call - has effect for that particular call.
227
+ ---
228
+ --- Example of source to choose from |arglist|: >lua
229
+ ---
230
+ --- { items = vim.fn.argv, name = 'Arglist' }
231
+ --- <
232
+ --- Note: this is mostly useful for writing pickers. Can safely skip if you
233
+ --- want to just use provided pickers.
234
+ ---
235
+ --- # Items ~
236
+ --- *MiniPick-source.items*
237
+ ---
238
+ --- `source.items` defines items to choose from. It should be one of the following:
239
+ --- - Array of objects which can have different types. Any type is allowed.
240
+ --- - `nil`. Picker waits for explicit |MiniPick.set_picker_items()| call.
241
+ --- - Callable returning any of the previous types. Will be called once on start
242
+ --- with source's `cwd` set as |current-directory|.
243
+ ---
244
+ --- *MiniPick-source.items-stritems*
245
+ --- Matching is done for items array based on the string representation of its
246
+ --- elements (here called "stritems"). For single item it is computed as follows:
247
+ --- - Callable is called once with output used in next steps.
248
+ --- - String item is used as is.
249
+ --- - String <text> field of table item is used (if present).
250
+ --- - Use output of |vim.inspect()|.
251
+ ---
252
+ --- Example: >lua
253
+ ---
254
+ --- items = { 'aaa.txt', { text = 'bbb' }, function() return 'ccc' end }
255
+ --- -- corresponding stritems are { 'aaa.txt', 'bbb', 'ccc' }
256
+ --- <
257
+ --- Default value is `nil`, assuming it always be supplied by the caller.
258
+ ---
259
+ --- *MiniPick-source.items-common*
260
+ --- There are some recommendations for common item types in order for them to work
261
+ --- out of the box with |MiniPick.default_show()|, |MiniPick.default_preview()|,
262
+ --- |MiniPick.default_choose()|, |MiniPick.default_choose_marked()|:
263
+ ---
264
+ --- - Path (file or directory). Use string or `path` field of a table. Path can
265
+ --- be either absolute, relative to the `source.cwd`, or have a general URI format
266
+ --- (only if supplied as table field).
267
+ --- Examples: `'aaa.txt'`, `{ path = 'aaa.txt' }`
268
+ ---
269
+ --- - Buffer. Use buffer id as number, string, or `bufnr` / `buf_id` / `buf`
270
+ --- field of a table (any name is allowed).
271
+ --- Examples: `1`, `'1'`, `{ bufnr = 1 }`, `{ buf_id = 1 }`, `{ buf = 1 }`
272
+ ---
273
+ --- - Line in file or buffer. Use table representation with `lnum` field with line
274
+ --- number (starting from 1) or string in "<path>\0<line>" format (`\0` is
275
+ --- an actual null character; don't escape the slash; may need to be `\000`).
276
+ --- Examples: >lua
277
+ ---
278
+ --- { path = 'aaa.txt', lnum = 2 }, 'aaa.txt\0002', { bufnr = 1, lnum = 3 }
279
+ --- <
280
+ --- - Position in file or buffer. Use table representation with `lnum` and `col`
281
+ --- fields with line and column numbers (starting from 1) or string in
282
+ --- "<path>\0<line>\0<col>" format (`\0` is an actual null character, don't
283
+ --- escape the slash; may need to be `\000`).
284
+ --- Examples: >lua
285
+ ---
286
+ --- { path = 'aaa.txt', lnum = 2, col = 3 }, 'aaa.txt\0' .. '2\0003',
287
+ --- { bufnr = 1, lnum = 3, col = 4 }
288
+ --- <
289
+ --- - Region in file or buffer. Use table representation with `lnum`, `col`,
290
+ --- `end_lnum`, `end_col` fields for start and end line/column. All numbers
291
+ --- start from 1, end line is inclusive, end column is exclusive.
292
+ --- This naming is similar to |getqflist()| and |diagnostic-structure|.
293
+ --- Examples: >lua
294
+ ---
295
+ --- { path = 'aaa.txt', lnum = 2, col = 3, end_lnum = 4, end_col = 5 },
296
+ --- { bufnr = 1, lnum = 3, col = 4, end_lnum = 5, end_col = 6 }
297
+ --- <
298
+ --- Note: all table items will benefit from having `text` field for better matching.
299
+ ---
300
+ --- # Name ~
301
+ --- *MiniPick-source.name*
302
+ ---
303
+ --- `source.name` defines the name of the picker to be used for visual feedback.
304
+ ---
305
+ --- Default value is "<No name>".
306
+ ---
307
+ --- # Current working directory ~
308
+ --- *MiniPick-source.cwd*
309
+ ---
310
+ --- `source.cwd` is a string defining the current working directory in which
311
+ --- picker operates. It should point to a valid actually present directory path.
312
+ --- This is a part of source to allow persistent way to use relative paths,
313
+ --- i.e. not depend on current directory being constant after picker start.
314
+ --- It also makes the |MiniPick.builtin.resume()| picker more robust.
315
+ ---
316
+ --- It will be set as local |current-directory| (|:lcd|) of picker's main window
317
+ --- to allow simpler code for "in window" functions (choose/preview/custom/etc.).
318
+ ---
319
+ --- Default value is |current-directory|.
320
+ ---
321
+ --- # Match ~
322
+ --- *MiniPick-source.match*
323
+ ---
324
+ --- `source.match` is a callable defining how stritems
325
+ --- (see |MiniPick-source.items-stritems|) are matched (filtered and sorted) based
326
+ --- on the query.
327
+ ---
328
+ --- It will be called with the following arguments:
329
+ --- - `stritems` - all available stritems for current picker.
330
+ --- - `inds` - array of `stritems` indexes usually pointing at current matches.
331
+ --- It does point to current matches in the case of interactively appending
332
+ --- character at the end of the query. It assumes that matches for such bigger
333
+ --- query is a subset of previous matches (implementation can ignore it).
334
+ --- This can be utilized to increase performance by checking fewer stritems.
335
+ --- - `query` - array of strings. Usually (like is common case of user interactively
336
+ --- typing query) each string represents one character. However, any strings are
337
+ --- allowed, as query can be set with |MiniPick.set_picker_query()|.
338
+ ---
339
+ --- It should either return array of match indexes for stritems elements matching
340
+ --- the query (synchronous) or explicitly use |MiniPick.set_picker_match_inds()|
341
+ --- to set them (may be asynchronous).
342
+ ---
343
+ --- Notes:
344
+ --- - The result can be any array of `stritems` indexes, i.e. not necessarily
345
+ --- a subset of input `inds`.
346
+ ---
347
+ --- - Both `stritems` and `query` depend on values of |'ignorecase'| and |'smartcase'|.
348
+ --- If query shows "ignore case" properties (only |'ignorecase'| is set or both
349
+ --- |'ignorecase'| / |'smartcase'| are set and query has only lowercase characters),
350
+ --- then `stritems` and `query` will have only lowercase characters.
351
+ --- This allows automatic support for case insensitive matching while being
352
+ --- faster and having simpler match function implementation.
353
+ ---
354
+ --- - Writing custom `source.match` usually means also changing |MiniPick-source.show|
355
+ --- because it is used to highlight stritems parts actually matching the query.
356
+ ---
357
+ --- Example of simple "exact" `match()` preserving initial order: >lua
358
+ ---
359
+ --- local match_exact = function(stritems, inds, query)
360
+ --- local prompt_pattern = vim.pesc(table.concat(query))
361
+ --- local f = function(i) return stritems[i]:find(prompt_pattern) ~= nil end
362
+ --- return vim.tbl_filter(f, inds)
363
+ --- end
364
+ --- -- For non-blocking version see `:h MiniPick.poke_is_picker_active()`
365
+ --- <
366
+ --- Default value is |MiniPick.default_match()|.
367
+ ---
368
+ --- # Show ~
369
+ --- *MiniPick-source.show*
370
+ ---
371
+ --- `source.show` is a callable defining how matched items are shown in the window.
372
+ ---
373
+ --- It will be called with the following arguments:
374
+ --- - `buf_id` - identifier of the target buffer.
375
+ --- - `items_to_show` - array of actual items to be shown in `buf_id`. This is
376
+ --- a subset of currently matched items computed to fit in current window view.
377
+ --- - `query` - array of strings. Same as in `source.match`.
378
+ ---
379
+ --- It should update buffer `buf_id` to visually represent `items_to_show`
380
+ --- __one item per line starting from line one__ (it shouldn't depend on
381
+ --- `options.content_from_bottom`). This also includes possible visualization
382
+ --- of which parts of stritem actually matched query.
383
+ ---
384
+ --- Example (assuming string items; without highlighting): >lua
385
+ ---
386
+ --- local show_prepend = function(buf_id, items_arr, query)
387
+ --- local lines = vim.tbl_map(function(x) return 'Item: ' .. x end, items_arr)
388
+ --- vim.api.nvim_buf_set_lines(buf_id, 0, -1, false, lines)
389
+ --- end
390
+ --- <
391
+ --- Default value is |MiniPick.default_show()|.
392
+ ---
393
+ --- # Preview ~
394
+ --- *MiniPick-source.preview*
395
+ ---
396
+ --- `source.preview` is a callable defining how item preview is done.
397
+ ---
398
+ --- It will be called with the following arguments:
399
+ --- - `buf_id` - identifier of the target buffer. Note: for every separate instance
400
+ --- of item previewing new scratch buffer is be created.
401
+ --- - `item` - item to preview.
402
+ ---
403
+ --- It should update buffer `buf_id` to visually represent `item`. It can also
404
+ --- directly set another buffer in picker's main window, but usually it is more
405
+ --- robust to update given `buf_id` directly.
406
+ ---
407
+ --- Example: >lua
408
+ ---
409
+ --- local preview_inspect = function(buf_id, item)
410
+ --- local lines = vim.split(vim.inspect(item), '\n')
411
+ --- vim.api.nvim_buf_set_lines(buf_id, 0, -1, false, lines)
412
+ --- end
413
+ --- <
414
+ --- Default value is |MiniPick.default_preview()|.
415
+ ---
416
+ --- # Choose an item ~
417
+ --- *MiniPick-source.choose*
418
+ ---
419
+ --- `source.choose` is a callable defining what to do when an item is chosen.
420
+ ---
421
+ --- It will be called with the following arguments:
422
+ --- - `item` - chosen item. Always non-`nil`.
423
+ ---
424
+ --- It should perform any intended "choose" action for an item and return
425
+ --- a value indicating whether picker should continue (i.e. not stop):
426
+ --- `nil` and `false` will stop picker, other values will continue.
427
+ ---
428
+ --- Notes:
429
+ --- - It is called when picker window is still current. Use `windows.target` value
430
+ --- from |MiniPick.get_picker_state()| output to do something with target window.
431
+ ---
432
+ --- Example: >lua
433
+ ---
434
+ --- local choose_file_continue = function(item)
435
+ --- if vim.fn.filereadable(item) == 0 then return end
436
+ --- vim.api.nvim_win_call(
437
+ --- MiniPick.get_picker_state().windows.target,
438
+ --- function() vim.cmd('edit ' .. item) end
439
+ --- )
440
+ --- return true
441
+ --- end
442
+ --- <
443
+ --- Default value is |MiniPick.default_choose()|.
444
+ ---
445
+ --- # Choose marked items ~
446
+ --- *MiniPick-source.choose_marked*
447
+ ---
448
+ --- `source.choose_marked` is a callable defining what to do when marked items
449
+ --- (see |MiniPick-actions-mark|) are chosen. Serves as a companion to
450
+ --- `source.choose` which can choose several items.
451
+ ---
452
+ --- It will be called with the following arguments:
453
+ --- - `items_marked` - array of marked items. Can be empty.
454
+ ---
455
+ --- It should perform any intended "choose" action for several items and return
456
+ --- a value indicating whether picker should continue (i.e. not stop):
457
+ --- `nil` and `false` will stop picker, other values will continue.
458
+ ---
459
+ --- Notes:
460
+ --- - It is called when picker window is still current. Use `windows.target` value
461
+ --- from |MiniPick.get_picker_state()| output to do something with target window.
462
+ ---
463
+ --- Example: >lua
464
+ ---
465
+ --- local choose_marked_print = function(items) print(vim.inspect(items)) end
466
+ --- <
467
+ --- Default value is |MiniPick.default_choose_marked()|.
468
+ ---@tag MiniPick-source
469
+
470
+ --- When picker is active, `mappings` table defines a set of special keys which when
471
+ --- pressed will execute certain actions. Those can be of two types:
472
+ --- - Built-in: actions present in default `config.mappings`. Can be only overridden
473
+ --- with a different key.
474
+ --- - Custom: user defined actions. Should be a table with `char` and `func` fields.
475
+ ---
476
+ ---
477
+ --- # Built-in ~
478
+ ---
479
+ --- ## Caret ~
480
+ --- *MiniPick-actions-caret*
481
+ ---
482
+ --- User can add character not only at query end, but more generally at caret.
483
+ ---
484
+ --- - `mappings.caret_left` - move caret to left.
485
+ --- - `mappings.caret_right` - move caret to right.
486
+ ---
487
+ --- ## Choose ~
488
+ --- *MiniPick-actions-choose*
489
+ ---
490
+ --- Choose is a fundamental action that actually implements the intent of
491
+ --- calling a picker, i.e. pick an item.
492
+ ---
493
+ --- - `mappings.choose` - choose as is, i.e. apply `source.choose` for current item.
494
+ --- - `mappings.choose_in_split` - make horizontal split at target window, update
495
+ --- target window to the new split, and choose.
496
+ --- - `mappings.choose_in_tabpage` - same as `choose_in_split`, but create tabpage.
497
+ --- - `mappings.choose_in_vsplit` - same as `choose_in_split`, but split vertically.
498
+ --- - `mappings.choose_marked` - choose marked items as is, i.e.
499
+ --- apply `source.choose_marked` at current marked items.
500
+ ---
501
+ --- ## Delete ~
502
+ --- *MiniPick-actions-delete*
503
+ ---
504
+ --- Delete actions are for deleting elements from query.
505
+ ---
506
+ --- - `mappings.delete_char` - delete one character to the left.
507
+ --- - `mappings.delete_char_right` - delete one character to the right.
508
+ --- - `mappings.delete_left` - delete everything to the left (like |i_CTRL-U|).
509
+ --- - `mappings.delete_word` - delete word to the left (like |i_CTRL-W|).
510
+ ---
511
+ --- ## Mark ~
512
+ --- *MiniPick-actions-mark*
513
+ ---
514
+ --- Marking is an action of adding certain items to a separate list which then can
515
+ --- be chosen with `mappings.choose_marked` (for example, sent to quickfix list).
516
+ --- This is a companion to a regular choosing which can pick only one item.
517
+ ---
518
+ --- - `mappings.mark` - toggle marked/unmarked state of current item.
519
+ --- - `mappings.mark_all` - toggle marked/unmarked state (mark all if not all
520
+ --- marked; unmark all otherwise) of all currently matched items.
521
+ ---
522
+ --- Notes:
523
+ --- - Marks persist across queries and matches. For example, user can make a query
524
+ --- with marking all matches several times and marked items from all queries
525
+ --- will be preserved.
526
+ ---
527
+ --- ## Move ~
528
+ --- *MiniPick-actions-move*
529
+ ---
530
+ --- Move is a fundamental action of changing which item is current.
531
+ ---
532
+ --- - `mappings.move_down` - change focus to the item below.
533
+ --- - `mappings.move_start` change focus to the first currently matched item.
534
+ --- - `mappings.move_up` - change focus to the item above.
535
+ ---
536
+ --- Notes:
537
+ --- - Up and down wrap around edges: `move_down` on last item moves to first,
538
+ --- `move_up` on first moves to last.
539
+ --- - Moving when preview or info view is shown updates the view with new item.
540
+ --- - There are also hard-coded alternative keys (can be used for other actions):
541
+ --- - `<Down>` moves down.
542
+ --- - `<Home>` moves to first currently matched item.
543
+ --- - `<Up>` moves up.
544
+ ---
545
+ --- ## Paste ~
546
+ --- *MiniPick-actions-paste*
547
+ ---
548
+ --- Paste is an action to paste content of |registers| at caret.
549
+ ---
550
+ --- - `mappings.paste` - paste from register defined by the next key press.
551
+ ---
552
+ --- Notes:
553
+ --- - Does not support expression register `=`.
554
+ --- - Supports special cases of register: <C-f> (as |c_CTRL-R_CTRL-F|),
555
+ --- <C-w> (as |c_CTRL-R_CTRL-W|), <C-a> (as |c_CTRL-R_CTRL-A|),
556
+ --- <C-l> (as |c_CTRL-R_CTRL-L|).
557
+ --- - Pasting from system |clipboard| is supported for "non-streaming" paste (as
558
+ --- described in |vim.paste()|). Use terminal's key combo instead of this action.
559
+ ---
560
+ --- ## Refine ~
561
+ --- *MiniPick-actions-refine*
562
+ ---
563
+ --- Refine is an action that primarily executes the following:
564
+ --- - Takes certain items and makes them be all items (in order they are present).
565
+ --- - Resets query.
566
+ --- - Updates `source.match` to be the one from config.
567
+ ---
568
+ --- - `mappings.refine` - refine currently matched items.
569
+ --- - `mappings.refine_marked` - refine currently marked items.
570
+ ---
571
+ --- This action is useful in at least two cases:
572
+ --- - Perform consecutive "narrowing" queries. Example: to get items that contain
573
+ --- both `hello` and `world` exact matches (in no particular order) with default
574
+ --- matching, type `'hello` (notice `'` at the start) followed by <C-Space> and
575
+ --- another `'world`.
576
+ --- - Reset `match` to default. Particularly useful in |MiniPick.builtin.grep_live()|
577
+ --- and |MiniExtra.pickers.lsp()| with "workspace_symbol_live" scope.
578
+ ---
579
+ --- ## Scroll ~
580
+ --- *MiniPick-actions-scroll*
581
+ ---
582
+ --- Scroll is an action to either move current item focus further than to the
583
+ --- neighbor item or adjust window view to see more information.
584
+ ---
585
+ --- - `mappings.scroll_down` - when matches are shown, go down by the amount of
586
+ --- visible matches. In preview and info view - scroll down as with |CTRL-F|.
587
+ --- - `mappings.scroll_left` - scroll left as with |zH|.
588
+ --- - `mappings.scroll_right` - scroll right as with |zL|.
589
+ --- - `mappings.scroll_up` - when matches are shown, go up by the amount of
590
+ --- visible matches. In preview and info view - scroll up as with |CTRL-B|.
591
+ ---
592
+ --- ## Stop ~
593
+ --- *MiniPick-actions-stop*
594
+ ---
595
+ --- `mappings.stop` stops the picker. <C-c> also always stops the picker.
596
+ ---
597
+ ---
598
+ --- ## Toggle ~
599
+ --- *MiniPick-actions-toggle*
600
+ ---
601
+ --- Toggle action is a way to change view: show if target is not shown, reset to
602
+ --- main view otherwise.
603
+ ---
604
+ --- - `mappings.toggle_info` - toggle info view.
605
+ --- - `mappings.toggle_preview` - toggle preview.
606
+ ---
607
+ --- Note:
608
+ --- - Updating query in any way resets window view to show matches.
609
+ --- - Moving current item focus keeps preview or info view with updated item.
610
+ ---
611
+ --- # Custom ~
612
+ --- *MiniPick-actions-custom*
613
+ ---
614
+ --- Along with built-in actions, users can define custom actions. This can be
615
+ --- done by supplying custom elements to `mappings` table. The field defines action
616
+ --- name (used to infer an action description in info view). The value is a table
617
+ --- with the following fields:
618
+ --- - <char> `(string)` - single character acting as action trigger.
619
+ --- - <func> `(function)` - callable to be executed without arguments after
620
+ --- user presses <char>. Its return value is treated as "should stop picker
621
+ --- after execution", i.e. returning nothing, `nil`, or `false` continues
622
+ --- picker while everything else (prefer `true`) stops it.
623
+ ---
624
+ --- Example of `execute` custom mapping: >lua
625
+ ---
626
+ --- execute = {
627
+ --- char = '<C-e>',
628
+ --- func = function() vim.cmd(vim.fn.input('Execute: ')) end,
629
+ --- }
630
+ --- <
631
+ ---@tag MiniPick-actions
632
+
633
+ --- # Disable icons ~
634
+ ---
635
+ --- Disable icons in |MiniPick.builtin| pickers related to paths: >lua
636
+ ---
637
+ --- local pick = require('mini.pick')
638
+ --- pick.setup({ source = { show = pick.default_show } })
639
+ --- <
640
+ --- # Switch toggle and move keys ~
641
+ --- >lua
642
+ --- require('mini.pick').setup({
643
+ --- mappings = {
644
+ --- toggle_info = '<C-k>',
645
+ --- toggle_preview = '<C-p>',
646
+ --- move_down = '<Tab>',
647
+ --- move_up = '<S-Tab>',
648
+ --- }
649
+ --- })
650
+ --- <
651
+ --- # Different window styles ~
652
+ --- >lua
653
+ --- -- Different border
654
+ --- { window = { config = { border = 'double' } } }
655
+ ---
656
+ --- -- "Cursor tooltip"
657
+ --- {
658
+ --- window = {
659
+ --- config = {
660
+ --- relative = 'cursor', anchor = 'NW',
661
+ --- row = 0, col = 0, width = 40, height = 20,
662
+ --- },
663
+ --- },
664
+ --- }
665
+ ---
666
+ --- -- Centered on screen
667
+ --- local win_config = function()
668
+ --- local height = math.floor(0.618 * vim.o.lines)
669
+ --- local width = math.floor(0.618 * vim.o.columns)
670
+ --- return {
671
+ --- anchor = 'NW', height = height, width = width,
672
+ --- row = math.floor(0.5 * (vim.o.lines - height)),
673
+ --- col = math.floor(0.5 * (vim.o.columns - width)),
674
+ --- }
675
+ --- end
676
+ --- { window = { config = win_config } }
677
+ --- <
678
+ ---@tag MiniPick-examples
679
+
680
+ ---@alias __pick_builtin_opts table|nil Options forwarded to |MiniPick.start()|.
681
+ ---@alias __pick_builtin_local_opts table|nil Options defining behavior of this particular picker.
682
+ ---@alias __pick_builtin_grep_globs <globs> `(table)` - array of string glob patterns to restrict search to
683
+ --- matching files. Supported only by "rg" and "git" tools, respects their
684
+ --- specific glob syntax and effects. Default: `{}` (no restriction).
685
+ --- Example: `{ '*.lua', 'lua/**' }` for Lua files and files in "lua" directory.
686
+ ---@alias __pick_builtin_grep_method <method> `(string)` - pattern matching method: `"regex"` (default) or `"plain"`.
687
+
688
+ ---@diagnostic disable:undefined-field
689
+ ---@diagnostic disable:discard-returns
690
+ ---@diagnostic disable:unused-local
691
+ ---@diagnostic disable:cast-local-type
692
+
693
+ -- Module definition ==========================================================
694
+ local MiniPick = {}
695
+ local H = {}
696
+
697
+ --- Module setup
698
+ ---
699
+ --- # :Pick ~
700
+ --- *:Pick*
701
+ ---
702
+ --- Calling this function creates a `:Pick` user command. It takes picker name
703
+ --- from |MiniPick.registry| as mandatory first argument and executes it with
704
+ --- following (expanded, |expandcmd()|) |<f-args>| combined in a single table.
705
+ --- To add custom pickers, update |MiniPick.registry|.
706
+ ---
707
+ --- Example: >vim
708
+ ---
709
+ --- :Pick files tool='git'
710
+ --- :Pick grep pattern='<cword>'
711
+ --- <
712
+ ---
713
+ --- It also sets custom |vim.ui.select()| implementation to use the module.
714
+ --- See |MiniPick.ui_select()|.
715
+ ---
716
+ ---@param config table|nil Module config table. See |MiniPick.config|.
717
+ ---
718
+ ---@usage >lua
719
+ --- require('mini.pick').setup() -- use default config
720
+ --- -- OR
721
+ --- require('mini.pick').setup({}) -- replace {} with your config table
722
+ --- <
723
+ MiniPick.setup = function(config)
724
+ -- TODO: Remove after Neovim=0.9 support is dropped
725
+ if vim.fn.has('nvim-0.10') == 0 then
726
+ vim.notify(
727
+ '(mini.pick) Neovim<0.10 is soft deprecated (module works but is not supported).'
728
+ .. " It will be deprecated after the next 'mini.nvim' release (module might not work)."
729
+ .. ' Please update your Neovim version.'
730
+ )
731
+ end
732
+
733
+ -- Export module
734
+ _G.MiniPick = MiniPick
735
+
736
+ -- Setup config
737
+ config = H.setup_config(config)
738
+
739
+ -- Apply config
740
+ H.apply_config(config)
741
+
742
+ -- Define behavior
743
+ H.create_autocommands()
744
+
745
+ -- Create default highlighting
746
+ H.create_default_hl()
747
+
748
+ -- Create user commands
749
+ H.create_user_commands()
750
+
751
+ -- Adjust terminal emulator's pasting with active picker
752
+ H.adjust_vim_paste()
753
+
754
+ -- Set custom implementation
755
+ vim.ui.select = MiniPick.ui_select
756
+ end
757
+
758
+ --stylua: ignore
759
+ --- Defaults ~
760
+ ---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
761
+ ---@text # Delays ~
762
+ ---
763
+ --- `config.delay` defines plugin delays (in ms). All should be strictly positive.
764
+ ---
765
+ --- `delay.async` is a delay between forcing asynchronous behavior. This usually
766
+ --- means forcing |:redraw| in preview (several but limited number of times to
767
+ --- ensure visible async highlighting) and using |MiniPick.poke_is_picker_active()|
768
+ --- (for example, to stop current matching if query has updated).
769
+ --- Smaller values give smoother user experience at the cost of more computations.
770
+ ---
771
+ --- `delay.busy` is a delay between when some computation starts and showing
772
+ --- visual feedback about it by making window border to have `MiniPickBorderBusy`
773
+ --- highlight group.
774
+ --- Smaller values will give feedback faster at the cost of feeling like flicker.
775
+ ---
776
+ --- # Mappings ~
777
+ ---
778
+ --- `config.mappings` defines keys for special actions to be triggered after certain
779
+ --- keys. See |MiniPick-actions| for more information.
780
+ ---
781
+ --- # Options ~
782
+ ---
783
+ --- `config.options` contains some general purpose options.
784
+ ---
785
+ --- `options.content_from_bottom` is a boolean indicating whether content should be
786
+ --- shown from bottom to top. That means that best matches will be shown at
787
+ --- the bottom. Note: for better experience use Neovim>=0.10, which has floating
788
+ --- window footer capability. Default: `false`.
789
+ ---
790
+ --- `options.use_cache` is a boolean indicating whether match results should be
791
+ --- cached per prompt (i.e. concatenated query). This results into faster response
792
+ --- on repeated prompts (like when deleting query entries) at the cost of using
793
+ --- more memory. Default: `false`.
794
+ ---
795
+ --- # Source ~
796
+ ---
797
+ --- `config.source` defines fallbacks for source specification. For example, this
798
+ --- can be used to change default `match` to use different implementation or `show`
799
+ --- to not show icons for some |MiniPick.builtin| pickers (see |MiniPick-examples|).
800
+ --- See |MiniPick-source| for more information.
801
+ ---
802
+ --- # Window ~
803
+ ---
804
+ --- `config.window` contains window specific configurations.
805
+ ---
806
+ --- `window.config` defines a (parts of) default floating window config for the main
807
+ --- picker window. This can be either a table overriding some parts or a callable
808
+ --- returning such table. See |MiniPick-examples| for some examples.
809
+ ---
810
+ --- `window.prompt_caret` defines how caret is displayed in window's prompt.
811
+ --- Default: `'▏'`.
812
+ ---
813
+ --- `window.prompt_prefix` defines what prefix is used in window's prompt.
814
+ --- Default: `'> '`.
815
+ MiniPick.config = {
816
+ -- Delays (in ms; should be at least 1)
817
+ delay = {
818
+ -- Delay between forcing asynchronous behavior
819
+ async = 10,
820
+
821
+ -- Delay between computation start and visual feedback about it
822
+ busy = 50,
823
+ },
824
+
825
+ -- Keys for performing actions. See `:h MiniPick-actions`.
826
+ mappings = {
827
+ caret_left = '<Left>',
828
+ caret_right = '<Right>',
829
+
830
+ choose = '<CR>',
831
+ choose_in_split = '<C-s>',
832
+ choose_in_tabpage = '<C-t>',
833
+ choose_in_vsplit = '<C-v>',
834
+ choose_marked = '<M-CR>',
835
+
836
+ delete_char = '<BS>',
837
+ delete_char_right = '<Del>',
838
+ delete_left = '<C-u>',
839
+ delete_word = '<C-w>',
840
+
841
+ mark = '<C-x>',
842
+ mark_all = '<C-a>',
843
+
844
+ move_down = '<C-n>',
845
+ move_start = '<C-g>',
846
+ move_up = '<C-p>',
847
+
848
+ paste = '<C-r>',
849
+
850
+ refine = '<C-Space>',
851
+ refine_marked = '<M-Space>',
852
+
853
+ scroll_down = '<C-f>',
854
+ scroll_left = '<C-h>',
855
+ scroll_right = '<C-l>',
856
+ scroll_up = '<C-b>',
857
+
858
+ stop = '<Esc>',
859
+
860
+ toggle_info = '<S-Tab>',
861
+ toggle_preview = '<Tab>',
862
+ },
863
+
864
+ -- General options
865
+ options = {
866
+ -- Whether to show content from bottom to top
867
+ content_from_bottom = false,
868
+
869
+ -- Whether to cache matches (more speed and memory on repeated prompts)
870
+ use_cache = false,
871
+ },
872
+
873
+ -- Source definition. See `:h MiniPick-source`.
874
+ source = {
875
+ items = nil,
876
+ name = nil,
877
+ cwd = nil,
878
+
879
+ match = nil,
880
+ show = nil,
881
+ preview = nil,
882
+
883
+ choose = nil,
884
+ choose_marked = nil,
885
+ },
886
+
887
+ -- Window related options
888
+ window = {
889
+ -- Float window config (table or callable returning it)
890
+ config = nil,
891
+
892
+ -- String to use as caret in prompt
893
+ prompt_caret = '▏',
894
+
895
+ -- String to use as prefix in prompt
896
+ prompt_prefix = '> ',
897
+ },
898
+ }
899
+ --minidoc_afterlines_end
900
+
901
+ --- Start picker
902
+ ---
903
+ --- Notes:
904
+ --- - If there is currently an active picker, it is properly stopped and new one
905
+ --- is started "soon" in the main event-loop (see |vim.schedule()|).
906
+ --- - Current window at the moment of this function call is treated as "target".
907
+ --- It will be set back as current after |MiniPick.stop()|.
908
+ --- See |MiniPick.get_picker_state()| and |MiniPick.set_picker_target_window()|.
909
+ ---
910
+ ---@param opts table|nil Options. Should have same structure as |MiniPick.config|.
911
+ --- Default values are inferred from there.
912
+ --- Usually should have proper |MiniPick-source.items| defined.
913
+ ---
914
+ ---@return any Item which was current when picker is stopped; `nil` if aborted.
915
+ MiniPick.start = function(opts)
916
+ if MiniPick.is_picker_active() then
917
+ -- Try proper 'key query process' stop
918
+ MiniPick.stop()
919
+ -- NOTE: Needs `defer_fn()` for `stop()` to properly finish code flow and
920
+ -- not be executed before it
921
+ return vim.defer_fn(function()
922
+ -- NOTE: if `MiniPick.stop()` still didn't stop, force abort
923
+ if MiniPick.is_picker_active() then H.picker_stop(H.pickers.active, true) end
924
+ MiniPick.start(opts)
925
+ end, 0.5)
926
+ end
927
+
928
+ if vim.fn.getcmdwintype() ~= '' then return H.notify('Can not open a picker with active command window', 'WARN') end
929
+
930
+ H.cache = {}
931
+ opts = H.validate_picker_opts(opts)
932
+ local picker = H.picker_new(opts)
933
+ H.pickers.active = picker
934
+
935
+ H.picker_set_busy(picker, true)
936
+ local items = H.expand_callable(opts.source.items)
937
+ -- - Set items on next event loop to not block when computing stritems
938
+ if H.islist(items) then vim.schedule(function() MiniPick.set_picker_items(items) end) end
939
+
940
+ H.picker_track_lost_focus(picker)
941
+ return H.picker_advance(picker)
942
+ end
943
+
944
+ --- Stop active picker
945
+ MiniPick.stop = function()
946
+ if not MiniPick.is_picker_active() then return end
947
+ H.cache.is_force_stop_advance = true
948
+ if H.cache.is_in_getcharstr then vim.api.nvim_feedkeys('\3', 't', true) end
949
+ end
950
+
951
+ --- Refresh active picker
952
+ MiniPick.refresh = function()
953
+ if not MiniPick.is_picker_active() then return end
954
+ H.picker_update(H.pickers.active, false, true)
955
+ -- Needed for something like `VimResized` (as `getcharstr` blocks redraw)
956
+ H.ensure_redraw(H.pickers.active)
957
+ end
958
+
959
+ --- Default match
960
+ ---
961
+ --- Filter target stritems to contain query and sort from best to worst matches.
962
+ ---
963
+ --- Implements default value for |MiniPick-source.match|.
964
+ ---
965
+ --- By default (if no special modes apply) it does the following fuzzy matching:
966
+ ---
967
+ --- - Stritem contains query if it contains all its elements verbatim in the same
968
+ --- order (possibly with gaps, i.e. not strictly one after another).
969
+ --- Note: empty query and empty string element is contained in any string.
970
+ ---
971
+ --- - Sorting is done with the following ordering (same as in |mini.fuzzy|):
972
+ --- - The smaller the match width (end column minus start column) the better.
973
+ --- - Among same match width, the smaller start column the better.
974
+ --- - Among same match width and start column, preserve original order.
975
+ ---
976
+ --- Notes:
977
+ --- - Most common interactive usage results into `query` containing one typed
978
+ --- character per element.
979
+ ---
980
+ --- # Special modes ~
981
+ ---
982
+ --- - Forced modes:
983
+ --- - Query starts with `*`: match the rest fuzzy (without other modes).
984
+ --- - Query starts with `'`: match the rest exactly (without gaps).
985
+ ---
986
+ --- - Place modes:
987
+ --- - Query starts with `^`: match the rest exactly at start.
988
+ --- - Query ends with `$`: match the rest exactly at end.
989
+ --- - Both modes can be used simultaneously.
990
+ ---
991
+ --- - Grouped: query contains at least one whitespace element. Output is computed
992
+ --- as if query is split at whitespace indexes with concatenation between them.
993
+ ---
994
+ --- Precedence of modes:
995
+ --- "forced exact" = "forced fuzzy" > "place start/end" > "grouped" > "default"
996
+ ---
997
+ --- # Examples ~
998
+ ---
999
+ --- Assuming `stritems` are `{ '_abc', 'a_bc', 'ab_c', 'abc_' }`, here are some
1000
+ --- example matches based on prompt (concatenated query): >
1001
+ ---
1002
+ --- | Prompt | Matches |
1003
+ --- |--------|------------------------|
1004
+ --- | abc | All |
1005
+ --- | *abc | All |
1006
+ --- | | |
1007
+ --- | 'abc | abc_, _abc |
1008
+ --- | *'abc | None (no "'" in items) |
1009
+ --- | | |
1010
+ --- | ^abc | abc_ |
1011
+ --- | *^abc | None (no "^" in items) |
1012
+ --- | | |
1013
+ --- | abc$ | _abc |
1014
+ --- | *abc$ | None (no "$" in items) |
1015
+ --- | | |
1016
+ --- | ab c | abc_, _abc, ab_c |
1017
+ --- | *ab c | None (no " " in items) |
1018
+ --- <
1019
+ --- Having query `{ 'ab', 'c' }` is the same as "ab c" prompt.
1020
+ ---
1021
+ --- You can have a feel of how this works with this command: >lua
1022
+ ---
1023
+ --- MiniPick.start({ source = { items = { '_abc', 'a_bc', 'ab_c', 'abc_' } } })
1024
+ --- <
1025
+ ---@param stritems table Array of all stritems.
1026
+ ---@param inds table Array of `stritems` indexes to match. All of them should point
1027
+ --- at string elements of `stritems`. No check is done for performance reasons.
1028
+ ---@param query table Array of strings.
1029
+ ---@param opts table|nil Options. Possible fields:
1030
+ --- - <sync> `(boolean)` - Whether to match synchronously. Default: `false`.
1031
+ --- - <preserve_order> `(boolean)` - Whether to skip sort step. Default: `false`.
1032
+ ---
1033
+ ---@return table|nil Depending on whether computation is synchronous (either `opts.sync`
1034
+ --- is `true` or there is an active picker):
1035
+ --- - If yes, array of `stritems` indexes matching the `query` (from best to worst).
1036
+ --- - If no, `nil` is returned with |MiniPick.set_picker_match_inds()| used later.
1037
+ MiniPick.default_match = function(stritems, inds, query, opts)
1038
+ opts = opts or {}
1039
+ local is_sync = opts.sync or not MiniPick.is_picker_active()
1040
+ local set_match_inds = is_sync and function(x) return x end or MiniPick.set_picker_match_inds
1041
+ local f = function()
1042
+ if #query == 0 then return set_match_inds(H.seq_along(stritems)) end
1043
+ local match_data, match_type = H.match_filter(inds, stritems, query)
1044
+ if match_data == nil then return end
1045
+ if match_type == 'useall' then return set_match_inds(H.seq_along(stritems)) end
1046
+ if opts.preserve_order then return set_match_inds(H.match_no_sort(match_data)) end
1047
+ local match_inds = H.match_sort(match_data)
1048
+ if match_inds == nil then return end
1049
+ return set_match_inds(match_inds)
1050
+ end
1051
+
1052
+ if is_sync then return f() end
1053
+ coroutine.resume(coroutine.create(f))
1054
+ end
1055
+
1056
+ --- Default show
1057
+ ---
1058
+ --- Show items in a buffer and highlight parts that actually match query (assuming
1059
+ --- match is done with |MiniPick.default_match()|). Lines are computed based on
1060
+ --- the |MiniPick-source.items-stritems|.
1061
+ ---
1062
+ --- Implements default value for |MiniPick-source.show|.
1063
+ ---
1064
+ --- Uses the following highlight groups (see |mini.pick| for their description):
1065
+ ---
1066
+ --- - `MiniPickIconDirectory`
1067
+ --- - `MiniPickIconFile`
1068
+ --- - `MiniPickMatchCurrent`
1069
+ --- - `MiniPickMatchMarked`
1070
+ --- - `MiniPickMatchRanges`
1071
+ ---
1072
+ ---@param buf_id number Identifier of target buffer.
1073
+ ---@param items table Array of items to show.
1074
+ ---@param query table Array of strings representing query.
1075
+ ---@param opts table|nil Options. Possible fields:
1076
+ --- - <show_icons> `(boolean)` - whether to show icons for entries recognized as
1077
+ --- valid actually present paths on disk (see |MiniPick-source.items-common|),
1078
+ --- empty space otherwise. Tries to use `text` field as fallback for path.
1079
+ --- Default: `false`. Note: |MiniPick.builtin| pickers showing file/directory
1080
+ --- paths use `true` by default.
1081
+ --- - <icons> `(table)` - table with fallback icons used if icon provider
1082
+ --- does not itself supply default icons for category. Can have fields:
1083
+ --- - <directory> `(string)` - icon for directory. Default: " ".
1084
+ --- - <file> `(string)` - icon for file. Default: " ".
1085
+ --- - <none> `(string)` - icon for non-valid path. Default: " ".
1086
+ MiniPick.default_show = function(buf_id, items, query, opts)
1087
+ local default_icons = { directory = ' ', file = ' ', none = ' ' }
1088
+ opts = vim.tbl_deep_extend('force', { show_icons = false, icons = default_icons }, opts or {})
1089
+
1090
+ -- Compute and set lines. Compute prefix based on the whole items to allow
1091
+ -- separate `text` and `path` table fields (preferring second one).
1092
+ local get_prefix_data = opts.show_icons and function(item) return H.get_icon(item, opts.icons) end
1093
+ or function() return { text = '' } end
1094
+ local prefix_data = vim.tbl_map(get_prefix_data, items)
1095
+
1096
+ local lines = vim.tbl_map(H.item_to_string, items)
1097
+ local tab_spaces = string.rep(' ', vim.o.tabstop)
1098
+ lines = vim.tbl_map(function(l) return l:gsub('%z', '│'):gsub('[\r\n]', ' '):gsub('\t', tab_spaces) end, lines)
1099
+
1100
+ local lines_to_show = {}
1101
+ for i, l in ipairs(lines) do
1102
+ lines_to_show[i] = prefix_data[i].text .. l
1103
+ end
1104
+
1105
+ H.set_buflines(buf_id, lines_to_show)
1106
+
1107
+ -- Extract match ranges
1108
+ local ns_id = H.ns_id.ranges
1109
+ H.clear_namespace(buf_id, ns_id)
1110
+
1111
+ if H.query_is_ignorecase(query) then
1112
+ lines, query = vim.tbl_map(H.tolower, lines), vim.tbl_map(H.tolower, query)
1113
+ end
1114
+ local match_data, match_type, query_adjusted = H.match_filter(H.seq_along(lines), lines, query)
1115
+ if match_data == nil then return end
1116
+
1117
+ local match_ranges_fun = match_type == 'fuzzy' and H.match_ranges_fuzzy or H.match_ranges_exact
1118
+ local match_ranges = match_ranges_fun(match_data, query_adjusted, lines)
1119
+
1120
+ -- Place range highlights accounting for possible shift due to prefixes
1121
+ local extmark_opts = { hl_group = 'MiniPickMatchRanges', hl_mode = 'combine', priority = 200 }
1122
+ for i = 1, #match_data do
1123
+ local row, ranges = match_data[i][3], match_ranges[i]
1124
+ local start_offset = prefix_data[row].text:len()
1125
+ for _, range in ipairs(ranges) do
1126
+ extmark_opts.end_row, extmark_opts.end_col = row - 1, start_offset + range[2]
1127
+ H.set_extmark(buf_id, ns_id, row - 1, start_offset + range[1] - 1, extmark_opts)
1128
+ end
1129
+ end
1130
+
1131
+ -- Highlight prefixes
1132
+ if not opts.show_icons then return end
1133
+ local icon_extmark_opts = { hl_mode = 'combine', priority = 200 }
1134
+ for i = 1, #prefix_data do
1135
+ icon_extmark_opts.hl_group = prefix_data[i].hl
1136
+ icon_extmark_opts.end_row, icon_extmark_opts.end_col = i - 1, prefix_data[i].text:len()
1137
+ H.set_extmark(buf_id, ns_id, i - 1, 0, icon_extmark_opts)
1138
+ end
1139
+ end
1140
+
1141
+ --- Default preview
1142
+ ---
1143
+ --- Preview item. Logic follows the rules in |MiniPick-source.items-common|:
1144
+ --- - File and buffer are shown at the start.
1145
+ --- - Directory has its content listed.
1146
+ --- - Line/position/region in file or buffer is shown at start.
1147
+ --- - Others are shown directly with |vim.inspect()|.
1148
+ ---
1149
+ --- Implements default value for |MiniPick-source.preview|.
1150
+ ---
1151
+ --- Uses the following highlight groups (see |mini.pick| for their description):
1152
+ ---
1153
+ --- - `MiniPickPreviewLine`
1154
+ --- - `MiniPickPreviewRegion`
1155
+ ---
1156
+ ---@param buf_id number Identifier of target buffer.
1157
+ ---@param item any Item to preview.
1158
+ ---@param opts table|nil Options. Possible values:
1159
+ --- - <n_context_lines> `(number)` - number of lines to load past target position
1160
+ --- when reading from disk. Useful to explore context. Default: |'lines'| twice.
1161
+ --- - <line_position> `(string)` - where in the window to show item position.
1162
+ --- One of "top", "center", "bottom". Default: "top".
1163
+ MiniPick.default_preview = function(buf_id, item, opts)
1164
+ opts = vim.tbl_deep_extend('force', { n_context_lines = 2 * vim.o.lines, line_position = 'top' }, opts or {})
1165
+ local item_data = H.parse_item(item)
1166
+ if item_data.type == 'file' then return H.preview_file(buf_id, item_data, opts) end
1167
+ if item_data.type == 'directory' then return H.preview_directory(buf_id, item_data) end
1168
+ if item_data.type == 'buffer' then return H.preview_buffer(buf_id, item_data, opts) end
1169
+ if item_data.type == 'uri' then return H.preview_uri(buf_id, item_data, opts) end
1170
+ H.preview_inspect(buf_id, item)
1171
+ end
1172
+
1173
+ --- Default choose
1174
+ ---
1175
+ --- Choose item. Logic follows the rules in |MiniPick-source.items-common|:
1176
+ --- - File uses |bufadd()| and sets cursor at the start of line/position/region.
1177
+ --- - Buffer is set as current in target window and sets cursor similarly.
1178
+ --- - Directory is called with |:edit| in the target window.
1179
+ --- - Others have the output of |vim.inspect()| printed in Command line.
1180
+ ---
1181
+ --- Implements default value for |MiniPick-source.choose|.
1182
+ ---
1183
+ ---@param item any Item to choose.
1184
+ MiniPick.default_choose = function(item)
1185
+ if item == nil then return end
1186
+ local picker_state = MiniPick.get_picker_state()
1187
+ local win_target = picker_state ~= nil and picker_state.windows.target or vim.api.nvim_get_current_win()
1188
+ if not H.is_valid_win(win_target) then win_target = H.get_first_valid_normal_window() end
1189
+
1190
+ local item_data = H.parse_item(item)
1191
+ if item_data.type == 'file' or item_data.type == 'directory' or item_data.type == 'uri' then
1192
+ return H.choose_path(win_target, item_data)
1193
+ end
1194
+ if item_data.type == 'buffer' then return H.choose_buffer(win_target, item_data) end
1195
+ H.choose_print(item)
1196
+ end
1197
+
1198
+ --- Default choose marked items
1199
+ ---
1200
+ --- Choose marked items. Logic follows the rules in |MiniPick-source.items-common|:
1201
+ --- - If among items there is at least one file or buffer, quickfix list is opened
1202
+ --- with all file or buffer lines/positions/regions.
1203
+ --- - Otherwise, picker's `source.choose` is called on the first item.
1204
+ ---
1205
+ --- Implements default value for |MiniPick-source.choose_marked|.
1206
+ ---
1207
+ ---@param items table Array of items to choose.
1208
+ ---@param opts table|nil Options. Possible fields:
1209
+ --- - <list_type> `(string)` - which type of list to open. One of "quickfix"
1210
+ --- or "location". Default: "quickfix".
1211
+ MiniPick.default_choose_marked = function(items, opts)
1212
+ if not H.islist(items) then H.error('`items` should be an array') end
1213
+ if #items == 0 then return end
1214
+ opts = vim.tbl_deep_extend('force', { list_type = 'quickfix' }, opts or {})
1215
+
1216
+ -- Construct a potential quickfix/location list
1217
+ local list = {}
1218
+ for _, item in ipairs(items) do
1219
+ local item_data = H.parse_item(item)
1220
+ if item_data.type == 'file' or item_data.type == 'buffer' or item_data.type == 'uri' then
1221
+ local entry = { bufnr = item_data.buf_id, filename = H.parse_uri(item_data.path) or item_data.path }
1222
+ entry.lnum, entry.col = item_data.lnum or 1, item_data.col or 1
1223
+ entry.text = (item_data.text or ''):gsub('%z', '│')
1224
+ entry.end_lnum, entry.end_col = item_data.end_lnum, item_data.end_col
1225
+ table.insert(list, entry)
1226
+ end
1227
+ end
1228
+
1229
+ -- Fall back to choosing first item if no quickfix list was constructed
1230
+ local is_active = MiniPick.is_picker_active()
1231
+ if #list == 0 then
1232
+ if not is_active then return end
1233
+ local choose = MiniPick.get_picker_opts().source.choose
1234
+ return choose(items[1])
1235
+ end
1236
+
1237
+ -- Set as quickfix or location list
1238
+ local title = '<No picker>'
1239
+ if is_active then
1240
+ ---@diagnostic disable:param-type-mismatch
1241
+ local source_name, prompt = MiniPick.get_picker_opts().source.name, table.concat(MiniPick.get_picker_query())
1242
+ title = source_name .. (prompt == '' and '' or (' : ' .. prompt))
1243
+ end
1244
+ local list_data = { items = list, title = title, nr = '$' }
1245
+
1246
+ if opts.list_type == 'location' then
1247
+ local win_target = MiniPick.get_picker_state().windows.target
1248
+ if not H.is_valid_win(win_target) then win_target = H.get_first_valid_normal_window() end
1249
+ vim.fn.setloclist(win_target, {}, ' ', list_data)
1250
+ vim.schedule(function() vim.cmd('lopen') end)
1251
+ else
1252
+ vim.fn.setqflist({}, ' ', list_data)
1253
+ vim.schedule(function() vim.cmd('copen') end)
1254
+ end
1255
+ end
1256
+
1257
+ --- Select rewrite
1258
+ ---
1259
+ --- Function which can be used to directly override |vim.ui.select()| to use
1260
+ --- |mini.pick| for any "select" type of tasks.
1261
+ --- Set automatically in |MiniPick.setup()|.
1262
+ ---
1263
+ --- Implements required by `vim.ui.select()` signature, with some differences:
1264
+ --- - Allows `opts.preview_item` that returns one of:
1265
+ --- - Array of lines for item preview.
1266
+ --- - Preview data described in |vim.ui.select()| (on Neovim>=0.12.3).
1267
+ --- - Allows fourth `start_opts` argument to customize |MiniPick.start()| call.
1268
+ ---
1269
+ --- Notes:
1270
+ --- - `on_choice` with selected item is called when target window is current.
1271
+ --- - If a picker is active, start a new one after the current one is stopped.
1272
+ ---
1273
+ ---@usage >lua
1274
+ --- -- Customize with fourth argument inside a function wrapper
1275
+ --- vim.ui.select = function(items, opts, on_choice)
1276
+ --- local start_opts = { window = { config = { width = vim.o.columns } } }
1277
+ --- return MiniPick.ui_select(items, opts, on_choice, start_opts)
1278
+ --- end
1279
+ --- <
1280
+ --- To preserve original `vim.ui.select()`: >lua
1281
+ ---
1282
+ --- local ui_select_orig = vim.ui.select
1283
+ --- require('mini.pick').setup()
1284
+ --- vim.ui.select = ui_select_orig
1285
+ --- <
1286
+ MiniPick.ui_select = function(items, opts, on_choice, start_opts)
1287
+ if MiniPick.is_picker_active() then
1288
+ local cb = vim.schedule_wrap(function() MiniPick.ui_select(items, opts, on_choice, start_opts) end)
1289
+ vim.api.nvim_create_autocmd('User', { pattern = 'MiniPickStop', once = true, callback = cb })
1290
+ return
1291
+ end
1292
+
1293
+ local format_item = opts.format_item or H.item_to_string
1294
+ local items_ext = {}
1295
+ for i = 1, #items do
1296
+ table.insert(items_ext, { text = format_item(items[i]), item = items[i], index = i })
1297
+ end
1298
+
1299
+ local preview_item = vim.is_callable(opts.preview_item) and opts.preview_item
1300
+ or function(x) return vim.split(vim.inspect(x), '\n') end
1301
+ local preview = function(buf_id, item)
1302
+ local data = preview_item(item.item)
1303
+ if H.islist(data) then return H.set_buflines(buf_id, data) end
1304
+ if not (type(data) == 'table' and H.is_valid_buf(data.buf)) then return end
1305
+ vim.api.nvim_buf_call(buf_id, function()
1306
+ vim.api.nvim_set_current_buf(data.buf)
1307
+ pcall(vim.api.nvim_win_set_cursor, 0, data.pos)
1308
+ vim.cmd('normal! zt')
1309
+ end)
1310
+ end
1311
+
1312
+ local choose = function(item)
1313
+ local win_target = MiniPick.get_picker_state().windows.target
1314
+ if not H.is_valid_win(win_target) then win_target = H.get_first_valid_normal_window() end
1315
+ vim.api.nvim_win_call(win_target, function()
1316
+ on_choice(items[item.index], item.index)
1317
+ MiniPick.set_picker_target_window(vim.api.nvim_get_current_win())
1318
+ end)
1319
+ end
1320
+
1321
+ local source = { items = items_ext, name = opts.prompt or opts.kind, preview = preview, choose = choose }
1322
+ start_opts = vim.tbl_deep_extend('force', start_opts or {}, { source = source })
1323
+ local item = MiniPick.start(start_opts)
1324
+ if item == nil then on_choice(nil) end
1325
+ end
1326
+
1327
+ --- Table with built-in pickers
1328
+ MiniPick.builtin = {}
1329
+
1330
+ --- Pick from files
1331
+ ---
1332
+ --- Lists all files recursively in all subdirectories. Tries to use one of the
1333
+ --- CLI tools to create items (see |MiniPick-cli-tools|): `rg`, `fd`, `git`.
1334
+ --- If none is present, uses fallback which utilizes |vim.fs.dir()|.
1335
+ ---
1336
+ --- To customize CLI tool search, either use tool's global configuration approach
1337
+ --- or directly |MiniPick.builtin.cli()| with specific command.
1338
+ ---
1339
+ ---@param local_opts __pick_builtin_local_opts
1340
+ --- Possible fields:
1341
+ --- - <tool> `(string)` - which tool to use. One of "rg", "fd", "git", "fallback".
1342
+ --- Default: whichever tool is present, trying in that same order.
1343
+ ---@param opts __pick_builtin_opts
1344
+ MiniPick.builtin.files = function(local_opts, opts)
1345
+ local_opts = vim.tbl_deep_extend('force', { tool = nil }, local_opts or {})
1346
+ local tool = local_opts.tool or H.files_get_tool()
1347
+ local show = H.get_config().source.show or H.show_with_icons
1348
+ local default_opts = { source = { name = string.format('Files (%s)', tool), show = show } }
1349
+ opts = vim.tbl_deep_extend('force', default_opts, opts or {})
1350
+
1351
+ if tool == 'fallback' then
1352
+ local cwd = H.full_path(opts.source.cwd or vim.fn.getcwd())
1353
+ opts.source.items = function() H.files_fallback_items(cwd) end
1354
+ return MiniPick.start(opts)
1355
+ end
1356
+
1357
+ return MiniPick.builtin.cli({ command = H.files_get_command(tool) }, opts)
1358
+ end
1359
+
1360
+ --- Pick from pattern matches
1361
+ ---
1362
+ --- Lists all pattern matches recursively in all subdirectories.
1363
+ --- Tries to use one of the CLI tools to create items (see |MiniPick-cli-tools|):
1364
+ --- `rg`, `git`. If none is present, uses fallback which utilizes |vim.fs.dir()| and
1365
+ --- Lua pattern matches (NOT recommended in large directories).
1366
+ ---
1367
+ --- To customize CLI tool search, either use tool's global configuration approach
1368
+ --- or directly |MiniPick.builtin.cli()| with specific command.
1369
+ --- Options |'ignorecase'| and |'smartcase'| are respected via forcing appropriate
1370
+ --- flags to CLI tool (i.e. overriding tool's global config).
1371
+ ---
1372
+ ---@param local_opts __pick_builtin_local_opts
1373
+ --- Possible fields:
1374
+ --- - <tool> `(string)` - which tool to use. One of "rg", "git", "fallback".
1375
+ --- Default: whichever tool is present, trying in that same order.
1376
+ --- - <pattern> `(string)` - string pattern to search. If not given, asks user
1377
+ --- interactively with |MiniInput.get()| (if enabled) or |input()|.
1378
+ --- - __pick_builtin_grep_globs
1379
+ --- - __pick_builtin_grep_method
1380
+ ---@param opts __pick_builtin_opts
1381
+ MiniPick.builtin.grep = function(local_opts, opts)
1382
+ local_opts = vim.tbl_extend('force', { tool = nil, pattern = nil, globs = {}, method = 'regex' }, local_opts or {})
1383
+ local tool = local_opts.tool or H.grep_get_tool()
1384
+ local globs = H.is_array_of(local_opts.globs, 'string') and local_opts.globs or {}
1385
+ local globs_suffix = #globs == 0 and '' or (' | ' .. table.concat(globs, ', '))
1386
+ local name = string.format('Grep (%s %s%s)', tool, local_opts.method, globs_suffix)
1387
+ local default_opts = { source = { name = name, show = H.get_config().source.show or H.show_with_icons } }
1388
+ opts = vim.tbl_deep_extend('force', default_opts, opts or {})
1389
+
1390
+ local pattern = local_opts.pattern
1391
+ if type(local_opts.pattern) ~= 'string' then pattern = H.user_input('Grep pattern', nil, 'file', 'editor') end
1392
+ if pattern == nil then return end
1393
+ if tool == 'fallback' then
1394
+ local cwd = H.full_path(opts.source.cwd or vim.fn.getcwd())
1395
+ opts.source.items = function() H.grep_fallback_items(pattern, cwd) end
1396
+ return MiniPick.start(opts)
1397
+ end
1398
+
1399
+ return MiniPick.builtin.cli({ command = H.grep_get_command(tool, pattern, globs, local_opts.method) }, opts)
1400
+ end
1401
+
1402
+ --- Pick from pattern matches with live feedback
1403
+ ---
1404
+ --- Perform pattern matching treating prompt as pattern. Gives live feedback on
1405
+ --- which matches are found. Use |MiniPick-actions-refine| to revert to regular
1406
+ --- matching. Use <C-o> to restrict search to files matching glob patterns.
1407
+ --- Use <C-e> to change pattern matching method. Tries to use one of the CLI
1408
+ --- tools to create items (see |MiniPick-cli-tools|): `rg`, `git`. If none is
1409
+ --- present, error is thrown (for performance reasons).
1410
+ ---
1411
+ --- To customize search, use tool's global configuration approach.
1412
+ --- Options |'ignorecase'| and |'smartcase'| are respected via forcing appropriate
1413
+ --- flags to CLI tool (i.e. overriding tool's global config).
1414
+ ---
1415
+ ---@param local_opts __pick_builtin_local_opts
1416
+ --- Possible fields:
1417
+ --- - <tool> `(string)` - which tool to use. One of "rg", "git".
1418
+ --- Default: whichever tool is present, trying in that same order.
1419
+ --- - __pick_builtin_grep_globs
1420
+ --- Use <C-o> custom mapping to add glob to the array. It uses |MiniInput.get()|
1421
+ --- (if enabled) to show input in |'winbar'| or |'statusline'| (depending of
1422
+ --- whether content is from bottom).
1423
+ --- - __pick_builtin_grep_method
1424
+ --- Use <C-e> custom mapping to add glob to the array.
1425
+ ---@param opts __pick_builtin_opts
1426
+ MiniPick.builtin.grep_live = function(local_opts, opts)
1427
+ local_opts = vim.tbl_extend('force', { tool = nil, globs = {}, method = 'regex' }, local_opts or {})
1428
+ local tool = local_opts.tool or H.grep_get_tool()
1429
+ if tool == 'fallback' or not H.is_executable(tool) then H.error('`grep_live` needs non-fallback executable tool.') end
1430
+
1431
+ local globs = H.is_array_of(local_opts.globs, 'string') and local_opts.globs or {}
1432
+ local globs_suffix = #globs == 0 and '' or (' | ' .. table.concat(globs, ', '))
1433
+ local method = local_opts.method
1434
+ local get_name = function() return string.format('Grep live (%s %s%s)', tool, method, globs_suffix) end
1435
+ local default_source = { name = get_name(), show = H.get_config().source.show or H.show_with_icons }
1436
+ opts = vim.tbl_deep_extend('force', { source = default_source }, opts or {})
1437
+
1438
+ local cwd = H.full_path(opts.source.cwd or vim.fn.getcwd())
1439
+ local set_items_opts, spawn_opts = { do_match = false, querytick = H.querytick }, { cwd = cwd }
1440
+ local sys = { kill = function() end }
1441
+ local match = function(_, _, query)
1442
+ sys:kill()
1443
+ if H.querytick == set_items_opts.querytick then return end
1444
+ if #query == 0 then
1445
+ sys = { kill = function() end }
1446
+ return MiniPick.set_picker_items({}, set_items_opts)
1447
+ end
1448
+
1449
+ set_items_opts.querytick = H.querytick
1450
+ local command = H.grep_get_command(tool, table.concat(query), globs, method)
1451
+ sys = MiniPick.set_picker_items_from_cli(command, { set_items_opts = set_items_opts, spawn_opts = spawn_opts })
1452
+ end
1453
+
1454
+ local ag = function()
1455
+ table.insert(globs, H.user_input('Glob pattern', nil, 'file', 'window'))
1456
+ globs_suffix = #globs == 0 and '' or (' | ' .. table.concat(globs, ', '))
1457
+ MiniPick.set_picker_opts({ source = { name = get_name() } })
1458
+ MiniPick.set_picker_query(MiniPick.get_picker_query())
1459
+ end
1460
+ local sm = function()
1461
+ method = method == 'regex' and 'plain' or 'regex'
1462
+ MiniPick.set_picker_opts({ source = { name = get_name() } })
1463
+ MiniPick.set_picker_query(MiniPick.get_picker_query())
1464
+ end
1465
+ local mappings = { add_glob = { char = '<C-o>', func = ag }, switch_method = { char = '<C-e>', func = sm } }
1466
+
1467
+ opts = vim.tbl_deep_extend('force', opts or {}, { source = { items = {}, match = match }, mappings = mappings })
1468
+ return MiniPick.start(opts)
1469
+ end
1470
+
1471
+ --- Pick from help tags
1472
+ ---
1473
+ --- Notes:
1474
+ --- - On choose directly executes |:help| command with appropriate modifier
1475
+ --- (none, |:vertical|, |:tab|). This is done through custom mappings named
1476
+ --- `show_help_in_{split,vsplit,tab}`. Not `choose_in_{split,vsplit,tab}` because
1477
+ --- there is no split guarantee (like if there is already help window opened).
1478
+ ---
1479
+ ---@param local_opts __pick_builtin_local_opts
1480
+ --- Possible fields:
1481
+ --- - <default_split> `(string)` - direction of a split for `choose` action.
1482
+ --- One of "horizontal", "vertical", "tab". Default: "horizontal".
1483
+ ---@param opts __pick_builtin_opts
1484
+ MiniPick.builtin.help = function(local_opts, opts)
1485
+ local_opts = vim.tbl_deep_extend('force', { default_split = 'horizontal' }, local_opts or {})
1486
+ local default_modifier = ({ horizontal = '', vertical = 'vert ', tab = 'tab ' })[local_opts.default_split]
1487
+ if default_modifier == nil then H.error('`opts.default_split` should be one of "horizontal", "vertical", "tab"') end
1488
+
1489
+ -- Get all tags
1490
+ local help_buf = vim.api.nvim_create_buf(false, true)
1491
+ vim.bo[help_buf].buftype = 'help'
1492
+ -- - NOTE: no dedicated buffer name because it is immediately wiped out
1493
+ local tags = vim.api.nvim_buf_call(help_buf, function() return vim.fn.taglist('.*') end)
1494
+ vim.api.nvim_buf_delete(help_buf, { force = true })
1495
+ vim.tbl_map(function(t) t.text = t.name end, tags)
1496
+
1497
+ -- NOTE: Choosing is done on next event loop to properly overcome special
1498
+ -- nature of `:help {subject}` command. For example, it didn't quite work
1499
+ -- when choosing tags in same file consecutively.
1500
+ local choose = function(item, modifier)
1501
+ if item == nil then return end
1502
+ vim.schedule(function() vim.cmd((modifier or default_modifier) .. 'help ' .. (item.name or '')) end)
1503
+ end
1504
+ local preview = function(buf_id, item)
1505
+ -- Take advantage of `taglist` output on how to open tag
1506
+ vim.api.nvim_buf_call(buf_id, function()
1507
+ vim.cmd('noautocmd edit ' .. vim.fn.fnameescape(item.filename))
1508
+ vim.bo.buftype, vim.bo.buflisted, vim.bo.bufhidden = 'nofile', false, 'wipe'
1509
+ local has_ts = pcall(vim.treesitter.start, 0)
1510
+ if not has_ts then vim.bo.syntax = 'help' end
1511
+
1512
+ local cache_hlsearch = vim.v.hlsearch
1513
+ -- Make a "very nomagic" search to account for special characters in tag
1514
+ local search_cmd = string.gsub(item.cmd, '^/', '/\\V')
1515
+ vim.cmd('silent keeppatterns ' .. search_cmd)
1516
+ -- Here `vim.v` doesn't work: https://github.com/neovim/neovim/issues/25294
1517
+ vim.cmd('let v:hlsearch=' .. cache_hlsearch)
1518
+ vim.cmd('normal! zt')
1519
+ end)
1520
+ end
1521
+
1522
+ -- Modify default mappings to work with special `:help` command
1523
+ local map_custom = function(char, modifier)
1524
+ local f = function()
1525
+ choose(MiniPick.get_picker_matches().current, modifier)
1526
+ return true
1527
+ end
1528
+ return { char = char, func = f }
1529
+ end
1530
+
1531
+ local config_mappings = H.get_config().mappings
1532
+ --stylua: ignore
1533
+ local mappings = {
1534
+ choose_in_split = '', show_help_in_split = map_custom(config_mappings.choose_in_split, ''),
1535
+ choose_in_vsplit = '', show_help_in_vsplit = map_custom(config_mappings.choose_in_vsplit, 'vertical '),
1536
+ choose_in_tabpage = '', show_help_in_tabpage = map_custom(config_mappings.choose_in_tabpage, 'tab '),
1537
+ }
1538
+
1539
+ local source = { items = tags, name = 'Help', choose = choose, preview = preview }
1540
+ opts = vim.tbl_deep_extend('force', { source = source, mappings = mappings }, opts or {})
1541
+ return MiniPick.start(opts)
1542
+ end
1543
+
1544
+ --- Pick from buffers
1545
+ ---
1546
+ --- Notes:
1547
+ --- - There are not built-in mappings for buffer manipulation. Here is an example
1548
+ --- of how to call this function with mapping to wipeout the current item: >lua
1549
+ ---
1550
+ --- local wipeout_cur = function()
1551
+ --- vim.api.nvim_buf_delete(MiniPick.get_picker_matches().current.bufnr, {})
1552
+ --- end
1553
+ --- local buffer_mappings = { wipeout = { char = '<C-d>', func = wipeout_cur } }
1554
+ --- MiniPick.builtin.buffers(local_opts, { mappings = buffer_mappings })
1555
+ --- <
1556
+ ---@param local_opts __pick_builtin_local_opts
1557
+ --- Possible fields:
1558
+ --- - <include_current> `(boolean)` - whether to include current buffer in
1559
+ --- the output. Default: `true`.
1560
+ --- - <include_unlisted> `(boolean)` - whether to include |unlisted-buffer|s in
1561
+ --- the output. Default: `false`.
1562
+ ---@param opts __pick_builtin_opts
1563
+ MiniPick.builtin.buffers = function(local_opts, opts)
1564
+ local_opts = vim.tbl_deep_extend('force', { include_current = true, include_unlisted = false }, local_opts or {})
1565
+
1566
+ local buffers_output = vim.api.nvim_exec('buffers' .. (local_opts.include_unlisted and '!' or ''), true)
1567
+ local cur_buf_id, include_current = vim.api.nvim_get_current_buf(), local_opts.include_current
1568
+ local items = {}
1569
+ for _, l in ipairs(vim.split(buffers_output, '\n')) do
1570
+ local buf_str, name = l:match('^%s*%d+'), l:match('"(.*)"')
1571
+ local buf_id = tonumber(buf_str)
1572
+ local item = { text = name, bufnr = buf_id }
1573
+ if buf_id ~= cur_buf_id or include_current then table.insert(items, item) end
1574
+ end
1575
+
1576
+ local show = H.get_config().source.show or H.show_with_icons
1577
+ local default_opts = { source = { name = 'Buffers', show = show } }
1578
+ opts = vim.tbl_deep_extend('force', default_opts, opts or {}, { source = { items = items } })
1579
+ return MiniPick.start(opts)
1580
+ end
1581
+
1582
+ --- Pick from CLI output
1583
+ ---
1584
+ --- Executes command line tool and constructs items based on its output.
1585
+ --- Uses |MiniPick.set_picker_items_from_cli()|.
1586
+ ---
1587
+ --- Example: `MiniPick.builtin.cli({ command = { 'echo', 'a\nb\nc' } })`
1588
+ ---
1589
+ ---@param local_opts __pick_builtin_local_opts
1590
+ --- Possible fields:
1591
+ --- - <command> `(table)` - forwarded to `set_picker_items_from_cli()`.
1592
+ --- - <postprocess> `(function)` - forwarded to `set_picker_items_from_cli()`.
1593
+ --- - <spawn_opts> `(table)` - forwarded to `set_picker_items_from_cli()`.
1594
+ --- Note: if `cwd` field is absent, it is inferred from |MiniPick-source.cwd|.
1595
+ ---@param opts __pick_builtin_opts
1596
+ MiniPick.builtin.cli = function(local_opts, opts)
1597
+ local_opts = vim.tbl_deep_extend('force', { command = {}, postprocess = nil, spawn_opts = {} }, local_opts or {})
1598
+ local name = string.format('CLI (%s)', tostring(local_opts.command[1] or ''))
1599
+ opts = vim.tbl_deep_extend('force', { source = { name = name } }, opts or {})
1600
+ -- Explicitly use full path to not conflict with `set_picker_items_from_cli`
1601
+ -- behavior of treating `spawn_opts.cwd` relative to source's cwd
1602
+ local_opts.spawn_opts.cwd = H.full_path(local_opts.spawn_opts.cwd or opts.source.cwd or vim.fn.getcwd())
1603
+
1604
+ local command = local_opts.command
1605
+ local set_from_cli_opts = { postprocess = local_opts.postprocess, spawn_opts = local_opts.spawn_opts }
1606
+ opts.source.items = vim.schedule_wrap(function() MiniPick.set_picker_items_from_cli(command, set_from_cli_opts) end)
1607
+ return MiniPick.start(opts)
1608
+ end
1609
+
1610
+ --- Resume latest picker
1611
+ MiniPick.builtin.resume = function()
1612
+ local picker = H.pickers.latest
1613
+ if picker == nil then H.error('There is no picker to resume.') end
1614
+
1615
+ H.cache = {}
1616
+ local buf_id = H.picker_new_buf()
1617
+ local win_target = vim.api.nvim_get_current_win()
1618
+ local win_id = H.picker_new_win(buf_id, picker.opts.window.config, picker.opts.source.cwd)
1619
+ picker.buffers = { main = buf_id }
1620
+ picker.windows = { main = win_id, target = win_target }
1621
+ picker.view_state = 'main'
1622
+ H.pickers.active = picker
1623
+
1624
+ return H.picker_advance(picker)
1625
+ end
1626
+
1627
+ --- Picker registry
1628
+ ---
1629
+ --- Place for users and extensions to manage pickers with their commonly used
1630
+ --- local options. By default contains all |MiniPick.builtin| pickers.
1631
+ --- All entries should accept only a single `local_opts` table argument.
1632
+ ---
1633
+ --- Serves as a source for |:Pick| command.
1634
+ ---
1635
+ --- Customization examples: >lua
1636
+ ---
1637
+ --- -- Adding custom picker to pick `register` entries
1638
+ --- MiniPick.registry.registry = function()
1639
+ --- local items = vim.tbl_keys(MiniPick.registry)
1640
+ --- table.sort(items)
1641
+ --- local source = {items = items, name = 'Registry', choose = function() end}
1642
+ --- local chosen_picker_name = MiniPick.start({ source = source })
1643
+ --- if chosen_picker_name == nil then return end
1644
+ --- return MiniPick.registry[chosen_picker_name]()
1645
+ --- end
1646
+ ---
1647
+ --- -- Make `:Pick files` accept `cwd`
1648
+ --- MiniPick.registry.files = function(local_opts)
1649
+ --- local opts = { source = { cwd = local_opts.cwd } }
1650
+ --- local_opts.cwd = nil
1651
+ --- return MiniPick.builtin.files(local_opts, opts)
1652
+ --- end
1653
+ --- <
1654
+ MiniPick.registry = {}
1655
+
1656
+ for name, f in pairs(MiniPick.builtin) do
1657
+ MiniPick.registry[name] = function(local_opts) return f(local_opts) end
1658
+ end
1659
+
1660
+ --- Get items of active picker
1661
+ ---
1662
+ ---@return table|nil Picker items or `nil` if no active picker.
1663
+ ---
1664
+ ---@seealso |MiniPick.set_picker_items()| and |MiniPick.set_picker_items_from_cli()|
1665
+ MiniPick.get_picker_items = function() return H.copy_tables((H.pickers.active or {}).items) end
1666
+
1667
+ --- Get stritems of active picker
1668
+ ---
1669
+ ---@return table|nil Picker stritems (|MiniPick-source.items-stritems|) or `nil` if
1670
+ --- no active picker.
1671
+ ---
1672
+ ---@seealso |MiniPick.set_picker_items()| and |MiniPick.set_picker_items_from_cli()|
1673
+ MiniPick.get_picker_stritems = function() return vim.deepcopy((H.pickers.active or {}).stritems) end
1674
+
1675
+ --- Get matches of active picker
1676
+ ---
1677
+ ---@return table|nil Picker matches or `nil` if no active picker. Matches is a table
1678
+ --- with the following fields:
1679
+ --- - <all> `(table|nil)` - all currently matched items.
1680
+ --- - <all_inds> `(table|nil)` - indexes of all currently matched items.
1681
+ --- - <current> `(any)` - current matched item.
1682
+ --- - <current_ind> `(number|nil)` - index of current matched item.
1683
+ --- - <marked> `(table|nil)` - marked items.
1684
+ --- - <marked_inds> `(table|nil)` - indexes of marked items.
1685
+ --- - <shown> `(table|nil)` - shown items (from top to bottom).
1686
+ --- - <shown_inds> `(table|nil)` - indexes of shown items (from top to bottom).
1687
+ ---
1688
+ ---@seealso |MiniPick.set_picker_match_inds()|
1689
+ MiniPick.get_picker_matches = function()
1690
+ if not MiniPick.is_picker_active() then return end
1691
+ local picker = H.pickers.active
1692
+ local items = picker.items
1693
+ if items == nil or #items == 0 then return {} end
1694
+
1695
+ local match_inds = vim.deepcopy(picker.match_inds)
1696
+ local res = { all_inds = match_inds, current_ind = match_inds[picker.current_ind] }
1697
+ res.all = vim.tbl_map(function(ind) return items[ind] end, match_inds)
1698
+ res.current = picker.items[res.current_ind]
1699
+ local marked_inds = vim.tbl_keys(picker.marked_inds_map)
1700
+ table.sort(marked_inds)
1701
+ res.marked_inds, res.marked = marked_inds, vim.tbl_map(function(ind) return items[ind] end, marked_inds)
1702
+ res.shown_inds = vim.tbl_map(function(ind) return match_inds[ind] end, picker.shown_inds)
1703
+ res.shown = vim.tbl_map(function(ind) return items[ind] end, res.shown_inds)
1704
+ return res
1705
+ end
1706
+
1707
+ --- Get config of active picker
1708
+ ---
1709
+ ---@return table|nil Picker config (`opts` table of |MiniPick.start()|) or `nil` if
1710
+ --- no active picker.
1711
+ ---
1712
+ ---@seealso |MiniPick.set_picker_opts()|
1713
+ MiniPick.get_picker_opts = function() return vim.deepcopy((H.pickers.active or {}).opts) end
1714
+
1715
+ --- Get state data of active picker
1716
+ ---
1717
+ ---@return table|nil Table with picker state data or `nil` if no active picker.
1718
+ --- State data is a table with the following fields:
1719
+ --- - <buffers> `(table)` - table with `main`, `preview`, `info` fields representing
1720
+ --- buffer identifier (or `nil`) for corresponding view.
1721
+ --- - <windows> `(table)` - table with `main` and `target` fields representing
1722
+ --- window identifiers for main and target windows.
1723
+ --- - <caret> `(number)` - caret column.
1724
+ --- - <is_busy> `(boolean)` - whether picker is busy with computations.
1725
+ ---
1726
+ ---@seealso |MiniPick.set_picker_target_window()|
1727
+ MiniPick.get_picker_state = function()
1728
+ if not MiniPick.is_picker_active() then return end
1729
+ local picker = H.pickers.active
1730
+ --stylua: ignore
1731
+ return vim.deepcopy({
1732
+ buffers = picker.buffers, windows = picker.windows, caret = picker.caret, is_busy = picker.is_busy
1733
+ })
1734
+ end
1735
+
1736
+ --- Get query of active picker
1737
+ ---
1738
+ ---@return table|nil Array of picker query or `nil` if no active picker.
1739
+ ---
1740
+ ---@seealso |MiniPick.set_picker_query()|
1741
+ MiniPick.get_picker_query = function() return vim.deepcopy((H.pickers.active or {}).query) end
1742
+
1743
+ --- Set items for active picker
1744
+ ---
1745
+ --- Note: sets items asynchronously in non-blocking fashion.
1746
+ ---
1747
+ ---@param items table Array of items.
1748
+ ---@param opts table|nil Options. Possible fields:
1749
+ --- - <do_match> `(boolean)` - whether to perform match after setting items.
1750
+ --- Default: `true`.
1751
+ --- - <querytick> `(number|nil)` - value of querytick (|MiniPick.get_querytick()|)
1752
+ --- to periodically check against when setting items. If checked querytick
1753
+ --- differs from supplied, no items are set.
1754
+ ---
1755
+ ---@seealso |MiniPick.get_picker_items()| and |MiniPick.get_picker_stritems()|
1756
+ MiniPick.set_picker_items = function(items, opts)
1757
+ if not H.islist(items) then H.error('`items` should be an array.') end
1758
+ opts = vim.tbl_deep_extend('force', { do_match = true, querytick = nil }, opts or {})
1759
+ if not MiniPick.is_picker_active() then return end
1760
+
1761
+ -- Set items in async because computing lower `stritems` can block much time
1762
+ coroutine.wrap(H.picker_set_items)(H.pickers.active, items, opts)
1763
+ end
1764
+
1765
+ --- Set items for active picker based on CLI output
1766
+ ---
1767
+ --- Asynchronously executes `command` and sets items to its postprocessed output.
1768
+ ---
1769
+ --- Example: >lua
1770
+ ---
1771
+ --- local items = vim.schedule_wrap(function()
1772
+ --- MiniPick.set_picker_items_from_cli({ 'echo', 'a\nb\nc' })
1773
+ --- end)
1774
+ --- MiniPick.start({ source = { items = items, name = 'Echo abc' } })
1775
+ --- <
1776
+ ---@param command table Array with (at least one) string command parts.
1777
+ ---@param opts table|nil Options. Possible fields:
1778
+ --- - <postprocess> `(function)` - callable performing postprocessing of output.
1779
+ --- Will be called with array of lines as input, should return array of items.
1780
+ --- Default: removes trailing empty lines and uses rest as string items.
1781
+ --- - <spawn_opts> `(table)` - `options` for |uv.spawn()|, except `args` and `stdio`.
1782
+ --- Note: relative `cwd` path is resolved against active picker's `cwd`.
1783
+ --- - <set_items_opts> `(table)` - table forwarded to |MiniPick.set_picker_items()|.
1784
+ ---
1785
+ ---@seealso |MiniPick.get_picker_items()| and |MiniPick.get_picker_stritems()|
1786
+ MiniPick.set_picker_items_from_cli = function(command, opts)
1787
+ local is_valid_command = H.is_array_of(command, 'string') and #command >= 1
1788
+ if not is_valid_command then H.error('`command` should be an array of strings.') end
1789
+ local default_opts = { postprocess = H.cli_postprocess, set_items_opts = {}, spawn_opts = {} }
1790
+ opts = vim.tbl_deep_extend('force', default_opts, opts or {})
1791
+ if not MiniPick.is_picker_active() then return end
1792
+
1793
+ local executable, args = command[1], vim.list_slice(command, 2, #command)
1794
+ local stdout, data_feed = vim.loop.new_pipe(), {}
1795
+ local spawn_opts = vim.tbl_deep_extend('force', opts.spawn_opts, { args = args, stdio = { nil, stdout, nil } })
1796
+ if type(spawn_opts.cwd) == 'string' then spawn_opts.cwd = H.full_path(spawn_opts.cwd) end
1797
+
1798
+ local process, pid
1799
+ process, pid = vim.loop.spawn(executable, spawn_opts, function()
1800
+ if not process:is_closing() then process:close() end
1801
+ end)
1802
+ -- NOTE: `cancel` is better name, but go with `vim.system():kill` for future
1803
+ local kill = function()
1804
+ if stdout:is_active() then stdout:read_stop() end
1805
+ data_feed = nil
1806
+ if not stdout:is_closing() then stdout:close() end
1807
+ if process:is_active() then process:kill() end
1808
+ end
1809
+
1810
+ -- Make sure to fully stop the process if picker is stopped
1811
+ vim.api.nvim_create_autocmd('User', { pattern = 'MiniPickStop', once = true, callback = kill })
1812
+
1813
+ stdout:read_start(function(err, data)
1814
+ assert(not err, err)
1815
+ if data ~= nil then return table.insert(data_feed, data) end
1816
+ stdout:close()
1817
+ coroutine.wrap(H.set_picker_items_from_feed)(data_feed, '\r?\n', opts)
1818
+ end)
1819
+
1820
+ return { pid = pid, kill = kill }
1821
+ end
1822
+
1823
+ --- Set match indexes for active picker
1824
+ ---
1825
+ --- There are two intended use cases:
1826
+ --- - Inside custom asynchronous |MiniPick-source.match| function to set which of
1827
+ --- picker's stritems match the query. See |MiniPick.poke_is_picker_active()|.
1828
+ --- - To programmatically set current match and marked items.
1829
+ --- See |MiniPick.get_picker_matches()|.
1830
+ ---
1831
+ ---@param match_inds table Array of numbers with picker's items indexes.
1832
+ ---@param match_type string|nil Type of match indexes to set. One of:
1833
+ --- - `"all"` (default) - indexes of items that match query.
1834
+ --- - `"current"` - index of current match. Only first element is used and should
1835
+ --- also be present among query matches.
1836
+ --- - `"marked"` - indexes of marked items. Values can be not among query matches.
1837
+ --- Will make only input indexes be marked, i.e. current marks are reset.
1838
+ --- Note: no `"shown"` match type as those indexes are computed automatically.
1839
+ ---
1840
+ ---@seealso |MiniPick.get_picker_matches()|
1841
+ MiniPick.set_picker_match_inds = function(match_inds, match_type)
1842
+ if not H.is_array_of(match_inds, 'number') then H.error('`match_inds` should be an array of numbers.') end
1843
+ local set = H.picker_set_inds[match_type or 'all']
1844
+ if set == nil then H.error('`match_type` should be one of "all", "marked", "current"') end
1845
+ if not MiniPick.is_picker_active() then return end
1846
+
1847
+ set(H.pickers.active, match_inds)
1848
+ H.picker_update(H.pickers.active, false)
1849
+ end
1850
+
1851
+ --- Set config for active picker
1852
+ ---
1853
+ ---@param opts table Table overriding initial `opts` input of |MiniPick.start()|.
1854
+ ---
1855
+ ---@seealso |MiniPick.get_picker_opts()|
1856
+ MiniPick.set_picker_opts = function(opts)
1857
+ if not MiniPick.is_picker_active() then return end
1858
+ local picker, cur_cwd = H.pickers.active, H.pickers.active.opts.source.cwd
1859
+ picker.opts = vim.tbl_deep_extend('force', picker.opts, opts or {})
1860
+ picker.action_keys = H.normalize_mappings(picker.opts.mappings)
1861
+ if cur_cwd ~= picker.opts.source.cwd then H.win_set_cwd(picker.windows.main, picker.opts.source.cwd) end
1862
+ local do_match = ((opts or {}).source or {}).match ~= nil
1863
+ H.picker_update(picker, do_match, true)
1864
+ end
1865
+
1866
+ --- Set target window for active picker
1867
+ ---
1868
+ ---@param win_id number Valid window identifier to be used as the new target window.
1869
+ ---
1870
+ ---@seealso |MiniPick.get_picker_state()|
1871
+ MiniPick.set_picker_target_window = function(win_id)
1872
+ if not H.is_valid_win(win_id) then H.error('`win_id` is not a valid window identifier.') end
1873
+ if not MiniPick.is_picker_active() then return end
1874
+ H.pickers.active.windows.target = win_id
1875
+ end
1876
+
1877
+ --- Set query for active picker
1878
+ ---
1879
+ ---@param query table Array of strings to be set as the new picker query.
1880
+ ---
1881
+ ---@seealso |MiniPick.get_picker_query()|
1882
+ MiniPick.set_picker_query = function(query)
1883
+ if not H.is_array_of(query, 'string') then H.error('`query` should be an array of strings.') end
1884
+ if not MiniPick.is_picker_active() then return end
1885
+
1886
+ H.pickers.active.query, H.pickers.active.caret = vim.deepcopy(query), #query + 1
1887
+ H.querytick = H.querytick + 1
1888
+ H.pickers.active.match_inds = H.seq_along(MiniPick.get_picker_items())
1889
+ H.picker_update(H.pickers.active, true)
1890
+ end
1891
+
1892
+ --- Get query tick
1893
+ ---
1894
+ --- Query tick is a unique query identifier. Intended to be used to detect user
1895
+ --- activity during and between |MiniPick.start()| calls for efficient non-blocking
1896
+ --- functionality. Updates after any query change, picker start and stop.
1897
+ ---
1898
+ --- See |MiniPick.poke_is_picker_active()| for usage example.
1899
+ ---
1900
+ ---@return number Query tick.
1901
+ MiniPick.get_querytick = function() return H.querytick end
1902
+
1903
+ --- Check if there is an active picker
1904
+ ---
1905
+ ---@return boolean Whether there is currently an active picker.
1906
+ ---
1907
+ ---@seealso |MiniPick.poke_is_picker_active()|
1908
+ MiniPick.is_picker_active = function() return H.pickers.active ~= nil end
1909
+
1910
+ --- Poke if picker is active
1911
+ ---
1912
+ --- Intended to be used for non-blocking implementation of source methods.
1913
+ --- Returns an output of |MiniPick.is_picker_active()|, but depending on
1914
+ --- whether there is a coroutine running:
1915
+ --- - If no, return it immediately.
1916
+ --- - If yes, return it after `coroutine.yield()` with `coroutine.resume()`
1917
+ --- called "soon" by the main event-loop (see |vim.schedule()|).
1918
+ ---
1919
+ --- Example of non-blocking exact `match` (as demo; can be optimized further): >lua
1920
+ ---
1921
+ --- local match_nonblock = function(match_inds, stritems, query)
1922
+ --- local prompt, querytick = table.concat(query), MiniPick.get_querytick()
1923
+ --- local f = function()
1924
+ --- local res = {}
1925
+ --- for _, ind in ipairs(match_inds) do
1926
+ --- local should_stop = not MiniPick.poke_is_picker_active() or
1927
+ --- MiniPick.get_querytick() ~= querytick
1928
+ --- if should_stop then return end
1929
+ ---
1930
+ --- if stritems[ind]:find(prompt) ~= nil then table.insert(res, ind) end
1931
+ --- end
1932
+ ---
1933
+ --- MiniPick.set_picker_match_inds(res)
1934
+ --- end
1935
+ ---
1936
+ --- coroutine.resume(coroutine.create(f))
1937
+ --- end
1938
+ --- <
1939
+ ---@return boolean Whether there is an active picker.
1940
+ ---
1941
+ ---@seealso |MiniPick.is_picker_active()|
1942
+ MiniPick.poke_is_picker_active = function()
1943
+ local co = coroutine.running()
1944
+ if co == nil then return MiniPick.is_picker_active() end
1945
+ H.schedule_resume_is_active(co)
1946
+ return coroutine.yield()
1947
+ end
1948
+
1949
+ -- Helper data ================================================================
1950
+ -- Module default config
1951
+ H.default_config = vim.deepcopy(MiniPick.config)
1952
+
1953
+ -- Namespaces
1954
+ H.ns_id = {
1955
+ matches = vim.api.nvim_create_namespace('MiniPickMatches'),
1956
+ headers = vim.api.nvim_create_namespace('MiniPickHeaders'),
1957
+ preview = vim.api.nvim_create_namespace('MiniPickPreview'),
1958
+ ranges = vim.api.nvim_create_namespace('MiniPickRanges'),
1959
+ input = vim.api.nvim_create_namespace('MiniPickInput'),
1960
+ }
1961
+
1962
+ -- Timers
1963
+ H.timers = {
1964
+ busy = vim.loop.new_timer(),
1965
+ focus = vim.loop.new_timer(),
1966
+ redraw = vim.loop.new_timer(),
1967
+ }
1968
+
1969
+ -- Pickers
1970
+ H.pickers = { active = nil, latest = nil }
1971
+
1972
+ -- Picker-independent counter of query updates
1973
+ H.querytick = 0
1974
+
1975
+ -- General purpose cache
1976
+ H.cache = {}
1977
+
1978
+ -- File system information
1979
+ H.is_windows = vim.loop.os_uname().sysname == 'Windows_NT'
1980
+
1981
+ -- Helper functionality =======================================================
1982
+ -- Settings -------------------------------------------------------------------
1983
+ H.setup_config = function(config)
1984
+ H.check_type('config', config, 'table', true)
1985
+ config = vim.tbl_deep_extend('force', vim.deepcopy(H.default_config), config or {})
1986
+
1987
+ H.check_type('delay', config.delay, 'table')
1988
+ H.check_type('delay.async', config.delay.async, 'number')
1989
+ H.check_type('delay.busy', config.delay.busy, 'number')
1990
+
1991
+ H.check_type('mappings', config.mappings, 'table')
1992
+ H.check_type('mappings.caret_left', config.mappings.caret_left, 'string')
1993
+ H.check_type('mappings.caret_right', config.mappings.caret_right, 'string')
1994
+ H.check_type('mappings.choose', config.mappings.choose, 'string')
1995
+ H.check_type('mappings.choose_in_split', config.mappings.choose_in_split, 'string')
1996
+ H.check_type('mappings.choose_in_tabpage', config.mappings.choose_in_tabpage, 'string')
1997
+ H.check_type('mappings.choose_in_vsplit', config.mappings.choose_in_vsplit, 'string')
1998
+ H.check_type('mappings.choose_marked', config.mappings.choose_marked, 'string')
1999
+ H.check_type('mappings.delete_char', config.mappings.delete_char, 'string')
2000
+ H.check_type('mappings.delete_char_right', config.mappings.delete_char_right, 'string')
2001
+ H.check_type('mappings.delete_left', config.mappings.delete_left, 'string')
2002
+ H.check_type('mappings.delete_word', config.mappings.delete_word, 'string')
2003
+ H.check_type('mappings.mark', config.mappings.mark, 'string')
2004
+ H.check_type('mappings.mark_all', config.mappings.mark_all, 'string')
2005
+ H.check_type('mappings.move_down', config.mappings.move_down, 'string')
2006
+ H.check_type('mappings.move_start', config.mappings.move_start, 'string')
2007
+ H.check_type('mappings.move_up', config.mappings.move_up, 'string')
2008
+ H.check_type('mappings.paste', config.mappings.paste, 'string')
2009
+ H.check_type('mappings.refine', config.mappings.refine, 'string')
2010
+ H.check_type('mappings.refine_marked', config.mappings.refine_marked, 'string')
2011
+ H.check_type('mappings.scroll_down', config.mappings.scroll_down, 'string')
2012
+ H.check_type('mappings.scroll_up', config.mappings.scroll_up, 'string')
2013
+ H.check_type('mappings.scroll_left', config.mappings.scroll_left, 'string')
2014
+ H.check_type('mappings.scroll_right', config.mappings.scroll_right, 'string')
2015
+ H.check_type('mappings.stop', config.mappings.stop, 'string')
2016
+ H.check_type('mappings.toggle_info', config.mappings.toggle_info, 'string')
2017
+ H.check_type('mappings.toggle_preview', config.mappings.toggle_preview, 'string')
2018
+
2019
+ H.check_type('options', config.options, 'table')
2020
+ H.check_type('options.content_from_bottom', config.options.content_from_bottom, 'boolean')
2021
+ H.check_type('options.use_cache', config.options.use_cache, 'boolean')
2022
+
2023
+ H.check_type('source', config.source, 'table')
2024
+ H.check_type('source.items', config.source.items, 'table', true)
2025
+ H.check_type('source.name', config.source.name, 'string', true)
2026
+ H.check_type('source.cwd', config.source.cwd, 'string', true)
2027
+ H.check_type('source.match', config.source.match, 'function', true)
2028
+ H.check_type('source.show', config.source.show, 'function', true)
2029
+ H.check_type('source.preview', config.source.preview, 'function', true)
2030
+ H.check_type('source.choose', config.source.choose, 'function', true)
2031
+ H.check_type('source.choose_marked', config.source.choose_marked, 'function', true)
2032
+
2033
+ H.check_type('window', config.window, 'table')
2034
+ local is_table_or_callable = function(x) return x == nil or type(x) == 'table' or vim.is_callable(x) end
2035
+ if not is_table_or_callable(config.window.config) then
2036
+ H.error('`window.config` should be table or callable, not ' .. type(config.window.config))
2037
+ end
2038
+ -- TODO: Remove after releasing 'mini.nvim' 0.16.0
2039
+ if config.window.prompt_cursor ~= nil then
2040
+ local msg = '`prompt_cursor` in `config.window` is renamed to `prompt_caret` for better naming consistency.'
2041
+ .. ' It works for now, but will stop in the next release. Sorry for the inconvenience.'
2042
+ H.notify(msg, 'WARN')
2043
+ config.window.prompt_caret = config.window.prompt_cursor
2044
+ config.window.prompt_cursor = nil
2045
+ end
2046
+ H.check_type('window.prompt_caret', config.window.prompt_caret, 'string')
2047
+ H.check_type('window.prompt_prefix', config.window.prompt_prefix, 'string')
2048
+
2049
+ return config
2050
+ end
2051
+
2052
+ H.apply_config = function(config)
2053
+ MiniPick.config = config
2054
+
2055
+ -- Register 'mini.extra' pickers
2056
+ if type(_G.MiniExtra) == 'table' then
2057
+ for name, f in pairs(_G.MiniExtra.pickers) do
2058
+ MiniPick.registry[name] = MiniPick.registry[name] or function(local_opts) return f(local_opts) end
2059
+ end
2060
+ end
2061
+ end
2062
+
2063
+ H.get_config = function(config)
2064
+ return vim.tbl_deep_extend('force', MiniPick.config, vim.b.minipick_config or {}, config or {})
2065
+ end
2066
+
2067
+ H.create_autocommands = function()
2068
+ local gr = vim.api.nvim_create_augroup('MiniPick', {})
2069
+
2070
+ local au = function(event, pattern, callback, desc)
2071
+ vim.api.nvim_create_autocmd(event, { group = gr, pattern = pattern, callback = callback, desc = desc })
2072
+ end
2073
+
2074
+ au('VimResized', '*', MiniPick.refresh, 'Refresh on resize')
2075
+ au('ColorScheme', '*', H.create_default_hl, 'Ensure colors')
2076
+ end
2077
+
2078
+ --stylua: ignore
2079
+ H.create_default_hl = function()
2080
+ local hi = function(name, opts)
2081
+ opts.default = true
2082
+ vim.api.nvim_set_hl(0, name, opts)
2083
+ end
2084
+
2085
+ hi('MiniPickBorder', { link = 'FloatBorder' })
2086
+ hi('MiniPickBorderBusy', { link = 'DiagnosticFloatingWarn' })
2087
+ hi('MiniPickBorderText', { link = 'FloatTitle' })
2088
+ hi('MiniPickCursor', { blend = 100, nocombine = true })
2089
+ hi('MiniPickIconDirectory', { link = 'Directory' })
2090
+ hi('MiniPickIconFile', { link = 'MiniPickNormal' })
2091
+ hi('MiniPickHeader', { link = 'DiagnosticFloatingHint' })
2092
+ hi('MiniPickMatchCurrent', { link = 'CursorLine' })
2093
+ hi('MiniPickMatchMarked', { link = 'Visual' })
2094
+ hi('MiniPickMatchRanges', { link = 'DiagnosticFloatingHint' })
2095
+ hi('MiniPickNormal', { link = 'NormalFloat' })
2096
+ hi('MiniPickPreviewLine', { link = 'CursorLine' })
2097
+ hi('MiniPickPreviewRegion', { link = 'IncSearch' })
2098
+ hi('MiniPickPrompt', { link = 'DiagnosticFloatingInfo' })
2099
+ hi('MiniPickPromptCaret', { link = 'MiniPickPrompt' })
2100
+ hi('MiniPickPromptPrefix', { link = 'MiniPickPrompt' })
2101
+ end
2102
+
2103
+ H.create_user_commands = function()
2104
+ local callback = function(input)
2105
+ local name, local_opts = H.command_parse_fargs(input.fargs)
2106
+ local f = MiniPick.registry[name]
2107
+ if f == nil then H.error(string.format('There is no picker named "%s" in registry.', name)) end
2108
+ f(local_opts)
2109
+ end
2110
+ local opts = { nargs = '+', complete = H.command_complete, desc = "Pick from 'mini.pick' registry" }
2111
+ vim.api.nvim_create_user_command('Pick', callback, opts)
2112
+ end
2113
+
2114
+ H.adjust_vim_paste = function()
2115
+ local paste_orig = vim.paste
2116
+ vim.paste = function(lines, phase)
2117
+ local picker = H.pickers.active
2118
+ if picker == nil then return paste_orig(lines, phase) end
2119
+ if phase ~= -1 then
2120
+ return H.notify('There is no streaming paste support. Use `mappings.paste` with "*" or "+" register.', 'WARN')
2121
+ end
2122
+ local text = table.concat(lines, ' '):gsub('[\n\t]', ' ')
2123
+ for i = 1, vim.fn.strchars(text) do
2124
+ H.picker_query_add(picker, vim.fn.strcharpart(text, i - 1, 1))
2125
+ end
2126
+ H.picker_update(picker, true, true)
2127
+ end
2128
+ end
2129
+
2130
+ -- Command --------------------------------------------------------------------
2131
+ H.command_parse_fargs = function(fargs)
2132
+ local name, opts_parts = fargs[1], vim.tbl_map(H.expandcmd, vim.list_slice(fargs, 2, #fargs))
2133
+ local tbl_string = string.format('{ %s }', table.concat(opts_parts, ', '))
2134
+ local lua_load = loadstring('return ' .. tbl_string)
2135
+ if lua_load == nil then H.error('Could not convert extra command arguments to table: ' .. tbl_string) end
2136
+ return name, lua_load()
2137
+ end
2138
+
2139
+ H.command_complete = function(_, line, col)
2140
+ local prefix_from, prefix_to, prefix = string.find(line, '^%S+%s+(%S*)')
2141
+ if col < prefix_from or prefix_to < col then return {} end
2142
+ local candidates = vim.tbl_filter(
2143
+ function(x) return tostring(x):find(prefix, 1, true) ~= nil end,
2144
+ vim.tbl_keys(MiniPick.registry)
2145
+ )
2146
+ table.sort(candidates)
2147
+ return candidates
2148
+ end
2149
+
2150
+ -- Picker object --------------------------------------------------------------
2151
+ H.validate_picker_opts = function(opts)
2152
+ opts = opts or {}
2153
+ if type(opts) ~= 'table' then H.error('Picker options should be table.') end
2154
+
2155
+ opts = H.copy_tables(H.get_config(opts))
2156
+
2157
+ local validate_callable = function(x, x_name)
2158
+ if not vim.is_callable(x) then H.error(string.format('`%s` should be callable.', x_name)) end
2159
+ end
2160
+
2161
+ -- Source
2162
+ local source = opts.source
2163
+
2164
+ local items = source.items or {}
2165
+ local is_valid_items = H.islist(items) or vim.is_callable(items)
2166
+ if not is_valid_items then H.error('`source.items` should be array or callable.') end
2167
+
2168
+ source.name = tostring(source.name or '<No name>')
2169
+
2170
+ if type(source.cwd) == 'string' then source.cwd = H.full_path(source.cwd) end
2171
+ if source.cwd == nil then source.cwd = vim.fn.getcwd() end
2172
+ if vim.fn.isdirectory(source.cwd) == 0 then H.error('`source.cwd` should be a valid directory path.') end
2173
+
2174
+ source.match = source.match or MiniPick.default_match
2175
+ validate_callable(source.match, 'source.match')
2176
+
2177
+ source.show = source.show or MiniPick.default_show
2178
+ validate_callable(source.show, 'source.show')
2179
+
2180
+ source.preview = source.preview or MiniPick.default_preview
2181
+ validate_callable(source.preview, 'source.preview')
2182
+
2183
+ source.choose = source.choose or MiniPick.default_choose
2184
+ validate_callable(source.choose, 'source.choose')
2185
+
2186
+ source.choose_marked = source.choose_marked or MiniPick.default_choose_marked
2187
+ validate_callable(source.choose_marked, 'source.choose_marked')
2188
+
2189
+ -- Delay
2190
+ for key, value in pairs(opts.delay) do
2191
+ local is_valid_value = type(value) == 'number' and value > 0
2192
+ if not is_valid_value then H.error(string.format('`delay.%s` should be a positive number.', key)) end
2193
+ end
2194
+
2195
+ -- Mappings
2196
+ local default_mappings = H.default_config.mappings
2197
+ for field, x in pairs(opts.mappings) do
2198
+ if type(field) ~= 'string' then H.error('`mappings` should have only string fields.') end
2199
+ local is_builtin_action = default_mappings[field] ~= nil
2200
+ if is_builtin_action and type(x) ~= 'string' then
2201
+ H.error(string.format('Mapping for built-in action "%s" should be string.', field))
2202
+ end
2203
+ if not is_builtin_action and not (type(x) == 'table' and type(x.char) == 'string' and vim.is_callable(x.func)) then
2204
+ H.error(string.format('Mapping for custom action "%s" should be table with `char` and `func`.', field))
2205
+ end
2206
+ end
2207
+
2208
+ -- Options
2209
+ local options = opts.options
2210
+ if type(options.content_from_bottom) ~= 'boolean' then H.error('`options.content_from_bottom` should be boolean.') end
2211
+ if type(options.use_cache) ~= 'boolean' then H.error('`options.use_cache` should be boolean.') end
2212
+
2213
+ -- Window
2214
+ local win_config = opts.window.config
2215
+ local is_valid_winconfig = win_config == nil or type(win_config) == 'table' or vim.is_callable(win_config)
2216
+ if not is_valid_winconfig then H.error('`window.config` should be table or callable.') end
2217
+
2218
+ return opts
2219
+ end
2220
+
2221
+ H.picker_new = function(opts)
2222
+ -- Create buffer
2223
+ local buf_id = H.picker_new_buf()
2224
+
2225
+ -- Create window
2226
+ local win_target = vim.api.nvim_get_current_win()
2227
+ local win_id = H.picker_new_win(buf_id, opts.window.config, opts.source.cwd)
2228
+
2229
+ -- Construct and return object
2230
+ local picker = {
2231
+ -- Permanent data about picker (should not change)
2232
+ opts = opts,
2233
+
2234
+ -- Items to pick from
2235
+ items = nil,
2236
+ stritems = nil,
2237
+ stritems_ignorecase = nil,
2238
+
2239
+ -- Associated Neovim objects
2240
+ buffers = { main = buf_id, preview = nil, info = nil },
2241
+ windows = { main = win_id, target = win_target },
2242
+
2243
+ -- Query data
2244
+ query = {},
2245
+ -- - Query index at which new entry will be inserted
2246
+ caret = 1,
2247
+ -- - Array of `stritems` indexes matching current query
2248
+ match_inds = nil,
2249
+ -- - Map of of currently marked `stritems` indexes (as keys)
2250
+ marked_inds_map = {},
2251
+ -- - Action keys which should be processed as described in mappings
2252
+ action_keys = H.normalize_mappings(opts.mappings),
2253
+
2254
+ -- Whether picker is currently busy processing data
2255
+ is_busy = false,
2256
+
2257
+ -- Cache for `matches` per prompt for more performant querying
2258
+ cache = {},
2259
+
2260
+ -- View data
2261
+ -- - Which buffer should currently be shown
2262
+ view_state = 'main',
2263
+
2264
+ -- - Index range of `match_inds` currently visible. Present for significant
2265
+ -- performance increase to render only what is visible.
2266
+ visible_range = { from = nil, to = nil },
2267
+
2268
+ -- - Index of `match_inds` pointing at current item
2269
+ current_ind = nil,
2270
+ -- - Array of indexes of `match_inds` pointing at currently shown items
2271
+ shown_inds = {},
2272
+ }
2273
+
2274
+ H.querytick = H.querytick + 1
2275
+
2276
+ return picker
2277
+ end
2278
+
2279
+ H.picker_advance = function(picker)
2280
+ vim.schedule(function() vim.api.nvim_exec_autocmds('User', { pattern = 'MiniPickStart' }) end)
2281
+
2282
+ local do_match, is_aborted = false, false
2283
+ local lmap = H.get_lmap()
2284
+ for _ = 1, 1000000 do
2285
+ if H.cache.is_force_stop_advance then break end
2286
+ H.picker_update(picker, do_match)
2287
+
2288
+ local char = H.getcharstr(lmap)
2289
+ if H.cache.is_force_stop_advance then break end
2290
+
2291
+ is_aborted = char == nil
2292
+ if is_aborted then break end
2293
+
2294
+ local cur_action = picker.action_keys[char] or {}
2295
+ do_match = cur_action.name == nil or vim.startswith(cur_action.name, 'delete') or cur_action.name == 'paste'
2296
+ is_aborted = cur_action.name == 'stop'
2297
+
2298
+ local should_stop
2299
+ if cur_action.is_custom then
2300
+ should_stop = cur_action.func()
2301
+ else
2302
+ should_stop = (cur_action.func or H.picker_query_add)(picker, char)
2303
+ end
2304
+ if should_stop then break end
2305
+ end
2306
+
2307
+ local item
2308
+ if not is_aborted then item = H.picker_get_current_item(picker) end
2309
+ H.cache.is_force_stop_advance = nil
2310
+ H.picker_stop(picker)
2311
+ return item
2312
+ end
2313
+
2314
+ H.picker_update = function(picker, do_match, update_window)
2315
+ if do_match then H.picker_match(picker) end
2316
+ if update_window then
2317
+ local config = H.picker_compute_win_config(picker.opts.window.config)
2318
+ vim.api.nvim_win_set_config(picker.windows.main, config)
2319
+ H.picker_set_current_ind(picker, picker.current_ind, true)
2320
+ end
2321
+ H.picker_set_bordertext(picker)
2322
+ H.picker_set_lines(picker)
2323
+ vim.cmd('redraw')
2324
+ end
2325
+
2326
+ H.picker_new_buf = function()
2327
+ local buf_id = H.create_scratch_buf('main')
2328
+ vim.bo[buf_id].filetype = 'minipick'
2329
+ return buf_id
2330
+ end
2331
+
2332
+ H.picker_new_win = function(buf_id, win_config, cwd)
2333
+ -- Hide cursor while picker is active (to not be visible in the window)
2334
+ -- This mostly follows a hack from 'folke/noice.nvim'
2335
+ H.cache.guicursor = vim.o.guicursor
2336
+ vim.o.guicursor = 'a:MiniPickCursor'
2337
+
2338
+ -- Create window and focus on it
2339
+ local win_id = vim.api.nvim_open_win(buf_id, true, H.picker_compute_win_config(win_config, true))
2340
+
2341
+ -- Set window-local data
2342
+ local opts_scope = { scope = 'local', win = win_id }
2343
+ vim.wo[win_id].foldenable = false
2344
+ vim.wo[win_id].foldmethod = 'manual'
2345
+ -- TODO: Use vim.wo[win_id][0] after compatibility with Neovim=0.9 is dropped
2346
+ vim.api.nvim_set_option_value('list', true, opts_scope)
2347
+ vim.api.nvim_set_option_value('listchars', 'extends:…,precedes:…', opts_scope)
2348
+ vim.wo[win_id].scrolloff = 0
2349
+ vim.wo[win_id].wrap = false
2350
+ H.win_update_hl(win_id, 'NormalFloat', 'MiniPickNormal')
2351
+ H.win_update_hl(win_id, 'FloatBorder', 'MiniPickBorder')
2352
+ vim.fn.clearmatches(win_id)
2353
+
2354
+ -- Set window's local "current directory" for easier choose/preview/etc.
2355
+ H.win_set_cwd(nil, cwd)
2356
+
2357
+ return win_id
2358
+ end
2359
+
2360
+ H.picker_compute_win_config = function(win_config, is_for_open)
2361
+ local has_tabline = vim.o.showtabline == 2 or (vim.o.showtabline == 1 and #vim.api.nvim_list_tabpages() > 1)
2362
+ local has_statusline = vim.o.laststatus > 0
2363
+ local max_height = vim.o.lines - vim.o.cmdheight - (has_tabline and 1 or 0) - (has_statusline and 1 or 0)
2364
+ local max_width = vim.o.columns
2365
+
2366
+ local winborder = vim.fn.exists('+winborder') == 0 and '' or vim.o.winborder
2367
+ local default_config = {
2368
+ relative = 'editor',
2369
+ anchor = 'SW',
2370
+ width = math.floor(0.618 * max_width),
2371
+ height = math.floor(0.618 * max_height),
2372
+ col = 0,
2373
+ row = max_height + (has_tabline and 1 or 0),
2374
+ border = winborder == '' and 'single' or nil,
2375
+ style = 'minimal',
2376
+ noautocmd = is_for_open,
2377
+ -- Use high enough value to be on top of built-in windows (pmenu, etc.)
2378
+ zindex = 251,
2379
+ }
2380
+ local config = vim.tbl_deep_extend('force', default_config, H.expand_callable(win_config) or {})
2381
+
2382
+ -- Tweak config values to ensure they are proper
2383
+ if (config.border or winborder) == 'none' then config.border = { '', ' ', '', '', '', ' ', '', '' } end
2384
+ -- - Adjust dimensions accounting for actually present border parts
2385
+ local bor, n = config.border, type(config.border) == 'table' and #config.border or 0
2386
+ local height_offset = n == 0 and 2 or ((bor[1 % n + 1] == '' and 0 or 1) + (bor[5 % n + 1] == '' and 0 or 1))
2387
+ local width_offset = n == 0 and 2 or ((bor[3 % n + 1] == '' and 0 or 1) + (bor[7 % n + 1] == '' and 0 or 1))
2388
+ config.height = math.max(math.min(config.height, max_height - height_offset), 1)
2389
+ config.width = math.max(math.min(config.width, max_width - width_offset), 1)
2390
+
2391
+ return config
2392
+ end
2393
+
2394
+ H.picker_track_lost_focus = function(picker)
2395
+ local track = vim.schedule_wrap(function()
2396
+ local is_cur_win = vim.api.nvim_get_current_win() == picker.windows.main
2397
+ local is_proper_focus = is_cur_win and (H.cache.is_in_getcharstr or vim.fn.mode() ~= 'n')
2398
+ if is_proper_focus then return end
2399
+ if _G.MiniInput ~= nil and _G.MiniInput.get_state() ~= nil then return end
2400
+ if H.cache.is_in_getcharstr then return vim.api.nvim_feedkeys('\3', 't', true) end
2401
+ H.picker_stop(picker, true)
2402
+ end)
2403
+ H.timers.focus:start(1000, 1000, track)
2404
+ end
2405
+
2406
+ H.picker_set_items = function(picker, items, opts)
2407
+ -- Compute string items to work with (along with their lower variants)
2408
+ local stritems, stritems_ignorecase, tolower = {}, {}, H.tolower
2409
+ local poke_picker = H.poke_picker_throttle(opts.querytick)
2410
+ for i, x in ipairs(items) do
2411
+ if not poke_picker() then return end
2412
+ local to_add = H.item_to_string(x)
2413
+ table.insert(stritems, to_add)
2414
+ table.insert(stritems_ignorecase, tolower(to_add))
2415
+ end
2416
+
2417
+ picker.items, picker.stritems, picker.stritems_ignorecase = items, stritems, stritems_ignorecase
2418
+ picker.cache, picker.marked_inds_map = {}, {}
2419
+ H.picker_set_busy(picker, false)
2420
+
2421
+ H.picker_set_match_inds(picker, H.seq_along(items))
2422
+ -- Force update visible range for correct "show" lines computation
2423
+ H.picker_set_current_ind(picker, picker.current_ind, true)
2424
+ H.picker_update(picker, opts.do_match)
2425
+ end
2426
+
2427
+ H.item_to_string = function(item)
2428
+ item = H.expand_callable(item)
2429
+ if type(item) == 'string' then return item end
2430
+ if type(item) == 'table' and type(item.text) == 'string' then return item.text end
2431
+ return vim.inspect(item, { newline = ' ', indent = '' })
2432
+ end
2433
+
2434
+ H.picker_set_busy = function(picker, value)
2435
+ picker.is_busy = value
2436
+
2437
+ -- NOTE: Don't precompute highlight group to always set a valid one
2438
+ local update_border_hl = function()
2439
+ H.timers.busy:stop()
2440
+ H.win_update_hl(picker.windows.main, 'FloatBorder', picker.is_busy and 'MiniPickBorderBusy' or 'MiniPickBorder')
2441
+ end
2442
+
2443
+ if value then return H.timers.busy:start(picker.opts.delay.busy, 0, vim.schedule_wrap(update_border_hl)) end
2444
+ update_border_hl()
2445
+ end
2446
+
2447
+ H.picker_set_match_inds = function(picker, inds)
2448
+ if inds == nil then return end
2449
+ H.picker_set_busy(picker, false)
2450
+
2451
+ picker.match_inds = inds
2452
+
2453
+ local cache_prompt = table.concat(picker.query)
2454
+ if picker.opts.options.use_cache then picker.cache[cache_prompt] = { inds = inds } end
2455
+
2456
+ -- Always show result of updated matches
2457
+ H.picker_show_main(picker)
2458
+
2459
+ -- Reset current index if match indexes are updated
2460
+ H.picker_set_current_ind(picker, 1)
2461
+
2462
+ -- Trigger relevant event if not already inside it
2463
+ if not H.inside_minipickmatch then
2464
+ H.inside_minipickmatch = true
2465
+ vim.api.nvim_exec_autocmds('User', { pattern = 'MiniPickMatch' })
2466
+ H.inside_minipickmatch = nil
2467
+ end
2468
+ end
2469
+
2470
+ H.picker_set_current_ind = function(picker, ind, force_update)
2471
+ if picker.items == nil or #picker.match_inds == 0 then
2472
+ picker.current_ind, picker.visible_range = nil, {}
2473
+ return
2474
+ end
2475
+
2476
+ -- Wrap index around edges
2477
+ local n_matches = #picker.match_inds
2478
+ ind = (ind - 1) % n_matches + 1
2479
+
2480
+ -- (Re)Compute visible range (centers current index if it is currently outside)
2481
+ local from, to, querytick = picker.visible_range.from, picker.visible_range.to, picker.visible_range.querytick
2482
+ local needs_update = H.querytick ~= querytick or from == nil or to == nil or not (from <= ind and ind <= to)
2483
+ if (force_update or needs_update) and H.is_valid_win(picker.windows.main) then
2484
+ local win_height = vim.api.nvim_win_get_height(picker.windows.main)
2485
+ to = math.min(n_matches, math.floor(ind + 0.5 * win_height))
2486
+ from = math.max(1, to - win_height + 1)
2487
+ to = from + math.min(win_height, n_matches) - 1
2488
+ end
2489
+
2490
+ -- Set data
2491
+ picker.current_ind = ind
2492
+ picker.visible_range = { from = from, to = to, querytick = H.querytick }
2493
+ end
2494
+
2495
+ H.picker_set_inds = {
2496
+ all = function(picker, inds) H.picker_set_match_inds(H.pickers.active, inds) end,
2497
+ current = function(picker, inds)
2498
+ if inds[1] == nil or picker.match_inds == nil then return end
2499
+ local current_match_ind, current_abs_ind = nil, inds[1]
2500
+ for i, match_abs_ind in ipairs(picker.match_inds) do
2501
+ if match_abs_ind == current_abs_ind then current_match_ind = i end
2502
+ end
2503
+ if current_match_ind == nil then H.error('Current match index should be present among all current matches') end
2504
+ H.picker_set_current_ind(picker, current_match_ind, true)
2505
+ end,
2506
+ marked = function(picker, inds)
2507
+ if picker.items == nil then return end
2508
+ local marked_inds_map, n_items = {}, #picker.items
2509
+ for _, ind in ipairs(inds) do
2510
+ if not (1 <= ind and ind <= n_items) then H.error('Marked indexes should be from 1 to number of items') end
2511
+ marked_inds_map[ind] = true
2512
+ end
2513
+ picker.marked_inds_map = marked_inds_map
2514
+ end,
2515
+ }
2516
+
2517
+ H.picker_set_lines = function(picker)
2518
+ local buf_id, win_id = picker.buffers.main, picker.windows.main
2519
+ if not (H.is_valid_buf(buf_id) and H.is_valid_win(win_id)) then return end
2520
+
2521
+ if picker.is_busy then return end
2522
+
2523
+ local visible_range, query = picker.visible_range, picker.query
2524
+ if picker.items == nil or visible_range.from == nil or visible_range.to == nil then
2525
+ picker.shown_inds = {}
2526
+ picker.opts.source.show(buf_id, {}, query)
2527
+ H.clear_namespace(buf_id, H.ns_id.matches)
2528
+ return
2529
+ end
2530
+
2531
+ -- Construct target items
2532
+ local items_to_show, items, shown_inds, match_inds = {}, picker.items, {}, picker.match_inds
2533
+ local cur_ind, cur_line = picker.current_ind, nil
2534
+ local marked_inds_map, marked_lnums = picker.marked_inds_map, {}
2535
+ local is_from_bottom = picker.opts.options.content_from_bottom
2536
+ local from = is_from_bottom and visible_range.to or visible_range.from
2537
+ local to = is_from_bottom and visible_range.from or visible_range.to
2538
+ for i = from, to, (from <= to and 1 or -1) do
2539
+ table.insert(shown_inds, i)
2540
+ table.insert(items_to_show, items[match_inds[i]])
2541
+ if i == cur_ind then cur_line = #items_to_show end
2542
+ if marked_inds_map[match_inds[i]] then table.insert(marked_lnums, #items_to_show) end
2543
+ end
2544
+
2545
+ local n_empty_top_lines = is_from_bottom and (vim.api.nvim_win_get_height(win_id) - #items_to_show) or 0
2546
+ cur_line = cur_line + n_empty_top_lines
2547
+ marked_lnums = vim.tbl_map(function(x) return x + n_empty_top_lines end, marked_lnums)
2548
+
2549
+ -- Update visible lines accounting for "from_bottom" direction
2550
+ picker.shown_inds = shown_inds
2551
+ picker.opts.source.show(buf_id, items_to_show, query)
2552
+ if n_empty_top_lines > 0 then
2553
+ local empty_lines = vim.fn['repeat']({ '' }, n_empty_top_lines)
2554
+ vim.api.nvim_buf_set_lines(buf_id, 0, 0, true, empty_lines)
2555
+ end
2556
+
2557
+ local ns_id = H.ns_id.matches
2558
+ H.clear_namespace(buf_id, ns_id)
2559
+
2560
+ -- Add highlighting for marked lines
2561
+ local marked_opts = { end_col = 0, hl_group = 'MiniPickMatchMarked', priority = 202 }
2562
+ for _, lnum in ipairs(marked_lnums) do
2563
+ marked_opts.end_row = lnum
2564
+ H.set_extmark(buf_id, ns_id, lnum - 1, 0, marked_opts)
2565
+ end
2566
+
2567
+ -- Update current item
2568
+ if cur_line > vim.api.nvim_buf_line_count(buf_id) then return end
2569
+
2570
+ local cur_opts = { end_row = cur_line, end_col = 0, hl_eol = true, hl_group = 'MiniPickMatchCurrent', priority = 201 }
2571
+ H.set_extmark(buf_id, ns_id, cur_line - 1, 0, cur_opts)
2572
+
2573
+ -- - Update cursor if showing item matches (needed for 'scroll_{left,right}')
2574
+ local cursor = vim.api.nvim_win_get_cursor(win_id)
2575
+ if picker.view_state == 'main' and cursor[1] ~= cur_line then H.set_cursor(win_id, cur_line, cursor[2] + 1) end
2576
+ end
2577
+
2578
+ H.picker_match = function(picker)
2579
+ if picker.items == nil then return end
2580
+
2581
+ -- Try to use cache first
2582
+ local prompt_cache
2583
+ if picker.opts.options.use_cache then prompt_cache = picker.cache[table.concat(picker.query)] end
2584
+ if prompt_cache ~= nil then return H.picker_set_match_inds(picker, prompt_cache.inds) end
2585
+
2586
+ local is_ignorecase = H.query_is_ignorecase(picker.query)
2587
+ local stritems = is_ignorecase and picker.stritems_ignorecase or picker.stritems
2588
+ local query = is_ignorecase and vim.tbl_map(H.tolower, picker.query) or picker.query
2589
+
2590
+ H.picker_set_busy(picker, true)
2591
+ local new_inds = picker.opts.source.match(stritems, picker.match_inds, query)
2592
+ H.picker_set_match_inds(picker, new_inds)
2593
+ end
2594
+
2595
+ H.query_is_ignorecase = function(query)
2596
+ if not vim.o.ignorecase then return false end
2597
+ if not vim.o.smartcase then return true end
2598
+ local prompt = table.concat(query)
2599
+ return prompt == vim.fn.tolower(prompt)
2600
+ end
2601
+
2602
+ H.normalize_mappings = function(mappings)
2603
+ local make_data = function(char, name, func, is_custom)
2604
+ return { char = char, name = name, func = is_custom and func or H.actions[name], is_custom = is_custom }
2605
+ end
2606
+
2607
+ local res = {}
2608
+ for name, rhs in pairs(mappings) do
2609
+ local is_custom = type(rhs) == 'table'
2610
+ local char = is_custom and rhs.char or rhs
2611
+ local key = H.replace_termcodes(char)
2612
+ -- Omit disabled keys and prefer custom actions over built-ins
2613
+ if key ~= '' and (res[key] == nil or not res[key].is_custom) then
2614
+ if res[key] ~= nil then H.notify('Duplicating mapping keys: ' .. name .. ' and ' .. res[key].name, 'WARN') end
2615
+ res[key] = make_data(char, name, rhs.func, is_custom)
2616
+ end
2617
+ end
2618
+
2619
+ -- Populate alternative keys, but not force them
2620
+ local home, up, down = H.replace_termcodes('<Home>'), H.replace_termcodes('<Up>'), H.replace_termcodes('<Down>')
2621
+ res[home] = res[home] or make_data('<Home>', 'move_start', nil, false)
2622
+ res[up] = res[up] or make_data('<Up>', 'move_up', nil, false)
2623
+ res[down] = res[down] or make_data('<Down>', 'move_down', nil, false)
2624
+
2625
+ return res
2626
+ end
2627
+
2628
+ H.picker_set_bordertext = function(picker)
2629
+ local opts = picker.opts
2630
+ local win_id = picker.windows.main
2631
+ if not H.is_valid_win(win_id) then return end
2632
+
2633
+ -- Compute main text managing views separately and truncating from left
2634
+ local view_state, win_width = picker.view_state, vim.api.nvim_win_get_width(win_id)
2635
+ local config
2636
+ if view_state == 'main' then
2637
+ local caret, query = picker.caret, picker.query
2638
+ local prompt_prefix, prompt_caret = opts.window.prompt_prefix, opts.window.prompt_caret
2639
+ local max_width = math.max(1, win_width - vim.fn.strchars(prompt_prefix) - vim.fn.strchars(prompt_caret))
2640
+
2641
+ -- Try to put caret in the center if there is not enough room to show the
2642
+ -- whole query (as in 'mini.tabline'). Do that after concatenating query
2643
+ -- parts as (after `set_picker_query()`) they can have multiple characters.
2644
+ local before_caret = table.concat(vim.list_slice(query, 1, caret - 1))
2645
+ local after_caret = table.concat(vim.list_slice(query, caret, #query))
2646
+ local w_before, w_after = vim.fn.strchars(before_caret), vim.fn.strchars(after_caret)
2647
+
2648
+ local w_right = math.min(math.floor(0.5 * max_width), w_after)
2649
+ local w_left = math.min(math.max(max_width - w_right, 0), w_before)
2650
+ w_right = math.min(math.max(max_width - w_left, 0), w_after)
2651
+
2652
+ -- Show standard "there is more" padding symbols if needed
2653
+ local pad_left, pad_right = w_left == w_before and '' or '…', w_right == w_after and '' or '…'
2654
+ w_left, w_right = w_left - (pad_left == '' and 0 or 1), w_right - (pad_right == '' and 0 or 1)
2655
+
2656
+ before_caret = vim.fn.strcharpart(before_caret, w_before - w_left, w_left)
2657
+ after_caret = vim.fn.strcharpart(after_caret, 0, w_right)
2658
+
2659
+ local prompt = { { prompt_prefix, 'MiniPickPromptPrefix' }, { prompt_caret, 'MiniPickPromptCaret' } }
2660
+ if after_caret ~= '' then table.insert(prompt, 3, { after_caret .. pad_right, 'MiniPickPrompt' }) end
2661
+ if before_caret ~= '' then table.insert(prompt, 2, { pad_left .. before_caret, 'MiniPickPrompt' }) end
2662
+ config = { title = prompt }
2663
+ end
2664
+
2665
+ local has_items = picker.items ~= nil
2666
+ if view_state == 'preview' and has_items then
2667
+ local stritem_cur = picker.stritems[picker.match_inds[picker.current_ind]] or ''
2668
+ -- Sanitize title
2669
+ stritem_cur = stritem_cur:gsub('%z', '│'):gsub('%s', ' ')
2670
+ config = { title = { { H.fit_to_width(' ' .. stritem_cur .. ' ', win_width), 'MiniPickBorderText' } } }
2671
+ end
2672
+
2673
+ if view_state == 'info' then
2674
+ config = { title = { { H.fit_to_width(' Info ', win_width), 'MiniPickBorderText' } } }
2675
+ end
2676
+
2677
+ -- Compute helper footer only if Neovim version permits and not in busy
2678
+ -- picker (otherwise it will flicker number of matches data on char delete)
2679
+ local nvim_has_window_footer = vim.fn.has('nvim-0.10') == 1
2680
+ if nvim_has_window_footer and not picker.is_busy then
2681
+ config.footer, config.footer_pos = H.picker_compute_footer(picker, win_id), 'left'
2682
+ end
2683
+
2684
+ -- Respect `options.content_from_bottom`
2685
+ if nvim_has_window_footer and opts.options.content_from_bottom then
2686
+ config.title, config.footer = config.footer, config.title
2687
+ end
2688
+
2689
+ local win_list_option = vim.api.nvim_get_option_value('list', { scope = 'local', win = win_id })
2690
+ vim.api.nvim_win_set_config(win_id, config)
2691
+ vim.api.nvim_set_option_value('list', win_list_option, { scope = 'local', win = win_id })
2692
+ end
2693
+
2694
+ H.picker_compute_footer = function(picker, win_id)
2695
+ local info = H.picker_get_general_info(picker)
2696
+ local source_name = string.format(' %s ', info.source_name):gsub('[%z%s]', ' ')
2697
+ local n_marked_text = info.n_marked == 0 and '' or (info.n_marked .. '/')
2698
+ local inds = string.format(' %s|%s|%s%s ', info.relative_current_ind, info.n_matched, n_marked_text, info.n_total)
2699
+ local win_width, source_width, inds_width =
2700
+ vim.api.nvim_win_get_width(win_id), vim.fn.strchars(source_name), vim.fn.strchars(inds)
2701
+
2702
+ local footer = { { H.fit_to_width(source_name, win_width), 'MiniPickBorderText' } }
2703
+ local n_spaces_between = win_width - (source_width + inds_width)
2704
+ if n_spaces_between > 0 then
2705
+ local border_hl = picker.is_busy and 'MiniPickBorderBusy' or 'MiniPickBorder'
2706
+ local part_id = picker.opts.options.content_from_bottom and 2 or 6
2707
+ footer[2] = { H.win_get_bottom_border(win_id, part_id):rep(n_spaces_between), border_hl }
2708
+ footer[3] = { inds, 'MiniPickBorderText' }
2709
+ end
2710
+ return footer
2711
+ end
2712
+
2713
+ H.picker_stop = function(picker, abort)
2714
+ vim.tbl_map(function(timer) pcall(vim.loop.timer_stop, timer) end, H.timers)
2715
+
2716
+ -- Show cursor (work around `guicursor=''` actually leaving cursor hidden)
2717
+ if H.cache.guicursor == '' then vim.cmd('set guicursor=a: | redraw') end
2718
+ pcall(function() vim.o.guicursor = H.cache.guicursor end)
2719
+
2720
+ if picker == nil then return end
2721
+
2722
+ vim.api.nvim_exec_autocmds('User', { pattern = 'MiniPickStop' })
2723
+
2724
+ if abort then
2725
+ H.pickers = {}
2726
+ else
2727
+ local new_latest = H.copy_tables(picker)
2728
+ H.picker_free(H.pickers.latest)
2729
+ H.pickers = { active = nil, latest = new_latest }
2730
+ end
2731
+
2732
+ H.set_curwin(picker.windows.target)
2733
+ pcall(vim.api.nvim_win_close, picker.windows.main, true)
2734
+ pcall(vim.api.nvim_buf_delete, picker.buffers.main, { force = true })
2735
+ pcall(vim.api.nvim_buf_delete, picker.buffers.info, { force = true })
2736
+ picker.windows, picker.buffers = {}, {}
2737
+
2738
+ H.querytick = H.querytick + 1
2739
+
2740
+ if H.cache.getcharstr_errmsg ~= nil then error(H.cache.getcharstr_errmsg) end
2741
+ end
2742
+
2743
+ H.picker_free = function(picker)
2744
+ if picker == nil then return end
2745
+ picker.match_inds = nil
2746
+ picker.shown_inds = {}
2747
+ picker.cache = nil
2748
+ picker.stritems, picker.stritems_ignorecase, picker.marked_inds_map = nil, nil, nil
2749
+ picker.items = nil
2750
+ picker = nil
2751
+ vim.schedule(function() collectgarbage('collect') end)
2752
+ end
2753
+
2754
+ --stylua: ignore
2755
+ H.actions = {
2756
+ caret_left = function(picker, _) H.picker_move_caret(picker, -1) end,
2757
+ caret_right = function(picker, _) H.picker_move_caret(picker, 1) end,
2758
+
2759
+ choose = function(picker, _) return H.picker_choose(picker, nil) end,
2760
+ choose_in_split = function(picker, _) return H.picker_choose(picker, 'split') end,
2761
+ choose_in_tabpage = function(picker, _) return H.picker_choose(picker, 'tab split') end,
2762
+ choose_in_vsplit = function(picker, _) return H.picker_choose(picker, 'vsplit') end,
2763
+ choose_marked = function(picker, _)
2764
+ local ok, res = pcall(picker.opts.source.choose_marked, MiniPick.get_picker_matches().marked)
2765
+ if not ok then vim.schedule(function() H.error('Error during choose marked:\n' .. res) end) end
2766
+ return not (ok and res)
2767
+ end,
2768
+
2769
+ delete_char = function(picker, _) H.picker_query_delete(picker, 1) end,
2770
+ delete_char_right = function(picker, _) H.picker_query_delete(picker, 0) end,
2771
+ delete_left = function(picker, _) H.picker_query_delete(picker, picker.caret - 1) end,
2772
+ delete_word = function(picker, _)
2773
+ local init, n_del = picker.caret - 1, 0
2774
+ if init == 0 then return end
2775
+ local ref_is_keyword = vim.fn.match(picker.query[init], '[[:keyword:]]') >= 0
2776
+ for i = init, 1, -1 do
2777
+ local cur_is_keyword = vim.fn.match(picker.query[i], '[[:keyword:]]') >= 0
2778
+ if (ref_is_keyword and not cur_is_keyword) or (not ref_is_keyword and cur_is_keyword) then break end
2779
+ n_del = n_del + 1
2780
+ end
2781
+ H.picker_query_delete(picker, n_del)
2782
+ end,
2783
+
2784
+ mark = function(picker, _) H.picker_mark_indexes(picker, 'current') end,
2785
+ mark_all = function(picker, _) H.picker_mark_indexes(picker, 'all') end,
2786
+
2787
+ move_down = function(picker, _) H.picker_move_current(picker, 1) end,
2788
+ move_start = function(picker, _) H.picker_move_current(picker, nil, 1) end,
2789
+ move_up = function(picker, _) H.picker_move_current(picker, -1) end,
2790
+
2791
+ paste = function(picker, _)
2792
+ local reg_contents = H.picker_get_register_contents(picker):gsub('[\n\t]', ' ')
2793
+ for i = 1, vim.fn.strchars(reg_contents) do
2794
+ H.picker_query_add(picker, vim.fn.strcharpart(reg_contents, i - 1, 1))
2795
+ end
2796
+ end,
2797
+
2798
+ refine = function(picker, _) H.picker_refine(picker, 'all') end,
2799
+ refine_marked = function(picker, _) H.picker_refine(picker, 'marked') end,
2800
+
2801
+ scroll_down = function(picker, _) H.picker_scroll(picker, 'down') end,
2802
+ scroll_up = function(picker, _) H.picker_scroll(picker, 'up') end,
2803
+ scroll_left = function(picker, _) H.picker_scroll(picker, 'left') end,
2804
+ scroll_right = function(picker, _) H.picker_scroll(picker, 'right') end,
2805
+
2806
+ toggle_info = function(picker, _)
2807
+ if picker.view_state == 'info' then return H.picker_show_main(picker) end
2808
+ H.picker_show_info(picker)
2809
+ end,
2810
+
2811
+ toggle_preview = function(picker, _)
2812
+ if picker.view_state == 'preview' then return H.picker_show_main(picker) end
2813
+ H.picker_show_preview(picker)
2814
+ end,
2815
+
2816
+ stop = function(_, _) return true end,
2817
+ }
2818
+
2819
+ H.picker_query_add = function(picker, char)
2820
+ -- Determine if it **is** proper single character
2821
+ if not H.is_query_char(char) then return end
2822
+ table.insert(picker.query, picker.caret, char)
2823
+ picker.caret = picker.caret + 1
2824
+ H.querytick = H.querytick + 1
2825
+
2826
+ -- Adding character inside query might not result into narrowing matches, so
2827
+ -- reset match indexes. Use cache to speed this up.
2828
+ local should_reset = picker.items ~= nil and picker.caret <= #picker.query
2829
+ if should_reset then picker.match_inds = H.seq_along(picker.items) end
2830
+ end
2831
+
2832
+ H.is_query_char = function(char) return vim.fn.strchars(char) == 1 and vim.fn.char2nr(char) > 31 end
2833
+
2834
+ H.picker_query_delete = function(picker, n)
2835
+ local delete_to_left = n > 0
2836
+ local left = delete_to_left and math.max(picker.caret - n, 1) or picker.caret
2837
+ local right = delete_to_left and picker.caret - 1 or math.min(picker.caret + n, #picker.query)
2838
+ for i = right, left, -1 do
2839
+ table.remove(picker.query, i)
2840
+ end
2841
+ picker.caret = left
2842
+ H.querytick = H.querytick + 1
2843
+
2844
+ -- Deleting query character increases number of possible matches, so need to
2845
+ -- reset already matched indexes prior deleting. Use cache to speed this up.
2846
+ if picker.items ~= nil then picker.match_inds = H.seq_along(picker.items) end
2847
+ end
2848
+
2849
+ H.picker_choose = function(picker, pre_command)
2850
+ local cur_item = H.picker_get_current_item(picker)
2851
+ if cur_item == nil then return true end
2852
+
2853
+ local win_id_target = picker.windows.target
2854
+ if pre_command ~= nil and H.is_valid_win(win_id_target) then
2855
+ -- Work around Neovim not preserving cwd during `nvim_win_call`
2856
+ -- See: https://github.com/neovim/neovim/issues/32203
2857
+ local picker_cwd, global_cwd = vim.fn.getcwd(0), vim.fn.getcwd(-1, -1)
2858
+ vim.fn.chdir(global_cwd)
2859
+ vim.api.nvim_win_call(win_id_target, function()
2860
+ vim.cmd(pre_command)
2861
+ picker.windows.target = vim.api.nvim_get_current_win()
2862
+ end)
2863
+ vim.fn.chdir(picker_cwd)
2864
+ end
2865
+
2866
+ local ok, res = pcall(picker.opts.source.choose, cur_item)
2867
+ -- Delay error to have time to hide picker window
2868
+ if not ok then vim.schedule(function() H.error('Error during choose:\n' .. res) end) end
2869
+ -- Error or returning nothing, `nil`, or `false` should lead to picker stop
2870
+ return not (ok and res)
2871
+ end
2872
+
2873
+ H.picker_mark_indexes = function(picker, range_type)
2874
+ if picker.items == nil then return end
2875
+ local test_inds = range_type == 'current' and { picker.match_inds[picker.current_ind] } or picker.match_inds
2876
+
2877
+ -- Mark if not all marked, unmark otherwise
2878
+ local marked_inds_map, is_all_marked = picker.marked_inds_map, true
2879
+ for _, ind in ipairs(test_inds) do
2880
+ is_all_marked = is_all_marked and marked_inds_map[ind]
2881
+ end
2882
+
2883
+ -- NOTE: Set to `nil` and not `false` for easier counting of present values
2884
+ local new_val
2885
+ if not is_all_marked then new_val = true end
2886
+ for _, ind in ipairs(test_inds) do
2887
+ marked_inds_map[ind] = new_val
2888
+ end
2889
+
2890
+ if picker.view_state == 'info' then H.picker_show_info(picker) end
2891
+ end
2892
+
2893
+ H.picker_move_caret = function(picker, n) picker.caret = math.min(math.max(picker.caret + n, 1), #picker.query + 1) end
2894
+
2895
+ H.picker_move_current = function(picker, by, to)
2896
+ if picker.items == nil then return end
2897
+ local n_matches = #picker.match_inds
2898
+ if n_matches == 0 then return end
2899
+
2900
+ if to == nil then
2901
+ -- Account for content direction
2902
+ by = (picker.opts.options.content_from_bottom and -1 or 1) * by
2903
+
2904
+ -- Wrap around edges only if current index is at edge
2905
+ to = picker.current_ind
2906
+ if to == 1 and by < 0 then
2907
+ to = n_matches
2908
+ elseif to == n_matches and by > 0 then
2909
+ to = 1
2910
+ else
2911
+ to = to + by
2912
+ end
2913
+ to = math.min(math.max(to, 1), n_matches)
2914
+ end
2915
+
2916
+ H.picker_set_current_ind(picker, to)
2917
+
2918
+ -- Update not main buffer(s)
2919
+ if picker.view_state == 'info' then H.picker_show_info(picker) end
2920
+ if picker.view_state == 'preview' then H.picker_show_preview(picker) end
2921
+ end
2922
+
2923
+ H.picker_refine = function(picker, refine_type)
2924
+ if picker.items == nil then return end
2925
+
2926
+ -- Make current matches be new items to be matched with default match
2927
+ picker.opts.source.match = H.get_config().source.match or MiniPick.default_match
2928
+ picker.query, picker.caret = {}, 1
2929
+ MiniPick.set_picker_items(MiniPick.get_picker_matches()[refine_type] or {})
2930
+
2931
+ picker._refine = picker._refine or { orig_name = picker.opts.source.name, count = 0 }
2932
+ picker._refine.count = picker._refine.count + 1
2933
+ local count_suffix = picker._refine.count == 1 and '' or (' ' .. picker._refine.count)
2934
+ picker.opts.source.name = string.format('%s (Refine%s)', picker._refine.orig_name, count_suffix)
2935
+ end
2936
+
2937
+ H.picker_scroll = function(picker, direction)
2938
+ local win_id = picker.windows.main
2939
+ if picker.view_state == 'main' and (direction == 'down' or direction == 'up') then
2940
+ local n = (direction == 'down' and 1 or -1) * vim.api.nvim_win_get_height(win_id)
2941
+ H.picker_move_current(picker, n)
2942
+ else
2943
+ local keys = ({ down = '<C-f>', up = '<C-b>', left = 'zH', right = 'zL' })[direction]
2944
+ vim.api.nvim_win_call(win_id, function() vim.cmd('normal! ' .. H.replace_termcodes(keys)) end)
2945
+ end
2946
+ end
2947
+
2948
+ H.picker_get_current_item = function(picker)
2949
+ if picker.items == nil then return nil end
2950
+ return picker.items[picker.match_inds[picker.current_ind]]
2951
+ end
2952
+
2953
+ H.picker_get_register_contents = function(picker)
2954
+ local register = H.getcharstr({})
2955
+ -- Mimic some "insert object under cursor" behavior of Command-line mode
2956
+ local expand_var = ({ ['\1'] = '<cWORD>', ['\6'] = '<cfile>', ['\23'] = '<cword>' })[register]
2957
+ if expand_var then
2958
+ return vim.api.nvim_win_call(picker.windows.target, function() return vim.fn.expand(expand_var) end)
2959
+ end
2960
+ if register == '\f' then
2961
+ return vim.api.nvim_win_call(picker.windows.target, function() return vim.fn.getline('.') end)
2962
+ end
2963
+ local has_register, res = pcall(vim.fn.getreg, register)
2964
+ return has_register and res or ''
2965
+ end
2966
+
2967
+ H.picker_show_main = function(picker)
2968
+ H.set_winbuf(picker.windows.main, picker.buffers.main)
2969
+ vim.api.nvim_set_option_value('list', true, { scope = 'local', win = picker.windows.main })
2970
+ picker.view_state = 'main'
2971
+ end
2972
+
2973
+ H.picker_show_info = function(picker)
2974
+ -- General information
2975
+ local info = H.picker_get_general_info(picker)
2976
+ local lines = {
2977
+ 'General',
2978
+ 'Source name │ ' .. info.source_name,
2979
+ 'Source cwd │ ' .. info.source_cwd,
2980
+ 'Total items │ ' .. info.n_total,
2981
+ 'Matched items │ ' .. info.n_matched,
2982
+ 'Marked items │ ' .. info.n_marked,
2983
+ 'Current index │ ' .. info.relative_current_ind,
2984
+ }
2985
+ local hl_lines = { 1 }
2986
+
2987
+ local append_char_data = function(data, header)
2988
+ if #data == 0 then return end
2989
+ table.insert(lines, '')
2990
+ table.insert(lines, header)
2991
+ table.insert(hl_lines, #lines)
2992
+
2993
+ local width_max = 0
2994
+ for _, t in ipairs(data) do
2995
+ local desc = t.name:gsub('[%s%p]', ' ')
2996
+ t.desc = vim.fn.toupper(desc:sub(1, 1)) .. desc:sub(2)
2997
+ t.width = vim.fn.strchars(t.desc)
2998
+ width_max = math.max(width_max, t.width)
2999
+ end
3000
+ table.sort(data, function(a, b) return a.desc < b.desc or (a.desc == b.desc and a.char < b.char) end)
3001
+
3002
+ for _, t in ipairs(data) do
3003
+ table.insert(lines, string.format('%s%s │ %s', t.desc, string.rep(' ', width_max - t.width), t.char))
3004
+ end
3005
+ end
3006
+
3007
+ local action_keys = H.normalize_mappings(picker.opts.mappings)
3008
+ append_char_data(vim.tbl_filter(function(x) return x.is_custom end, action_keys), 'Mappings (custom)')
3009
+ append_char_data(vim.tbl_filter(function(x) return not x.is_custom end, action_keys), 'Mappings (built-in)')
3010
+
3011
+ -- Manage buffer/window/state
3012
+ local buf_id_info = picker.buffers.info
3013
+ if not H.is_valid_buf(buf_id_info) then buf_id_info = H.create_scratch_buf('info') end
3014
+ picker.buffers.info = buf_id_info
3015
+
3016
+ H.set_buflines(buf_id_info, lines)
3017
+ H.set_winbuf(picker.windows.main, buf_id_info)
3018
+ picker.view_state = 'info'
3019
+
3020
+ local ns_id = H.ns_id.headers
3021
+ H.clear_namespace(buf_id_info, ns_id)
3022
+ for _, lnum in ipairs(hl_lines) do
3023
+ H.set_extmark(buf_id_info, ns_id, lnum - 1, 0, { end_row = lnum, end_col = 0, hl_group = 'MiniPickHeader' })
3024
+ end
3025
+ end
3026
+
3027
+ H.picker_get_general_info = function(picker)
3028
+ local has_items = picker.items ~= nil
3029
+ return {
3030
+ source_name = picker.opts.source.name or '---',
3031
+ source_cwd = vim.fn.fnamemodify(picker.opts.source.cwd, ':~') or '---',
3032
+ n_total = has_items and #picker.items or '-',
3033
+ n_matched = has_items and #picker.match_inds or '-',
3034
+ n_marked = has_items and vim.tbl_count(picker.marked_inds_map) or '-',
3035
+ relative_current_ind = has_items and picker.current_ind or '-',
3036
+ }
3037
+ end
3038
+
3039
+ H.picker_show_preview = function(picker)
3040
+ local preview = picker.opts.source.preview
3041
+ local item = H.picker_get_current_item(picker)
3042
+ if item == nil then return end
3043
+
3044
+ local win_id, buf_id = picker.windows.main, H.create_scratch_buf('preview')
3045
+ vim.bo[buf_id].bufhidden = 'wipe'
3046
+ H.set_winbuf(win_id, buf_id)
3047
+ vim.api.nvim_set_option_value('list', vim.go.list, { scope = 'local', win = win_id })
3048
+ preview(buf_id, item)
3049
+ picker.buffers.preview = vim.api.nvim_win_get_buf(win_id)
3050
+ picker.view_state = 'preview'
3051
+ H.ensure_redraw(picker)
3052
+ end
3053
+
3054
+ -- Default match --------------------------------------------------------------
3055
+ H.match_filter = function(inds, stritems, query)
3056
+ -- 'abc' and '*abc' - fuzzy; "'abc" and 'a' - exact substring;
3057
+ -- 'ab c' - grouped fuzzy; '^abc' and 'abc$' - exact substring at start/end.
3058
+ local is_fuzzy_forced, is_exact_plain, is_exact_start, is_exact_end =
3059
+ query[1] == '*', query[1] == "'", query[1] == '^', query[#query] == '$'
3060
+ local is_grouped, grouped_parts = H.match_query_group(query)
3061
+
3062
+ if is_fuzzy_forced or is_exact_plain or is_exact_start or is_exact_end then
3063
+ local start_offset = (is_fuzzy_forced or is_exact_plain or is_exact_start) and 2 or 1
3064
+ local end_offset = #query - ((not is_fuzzy_forced and not is_exact_plain and is_exact_end) and 1 or 0)
3065
+ query = vim.list_slice(query, start_offset, end_offset)
3066
+ elseif is_grouped then
3067
+ query = grouped_parts
3068
+ end
3069
+
3070
+ if #query == 0 then return {}, 'useall', query end
3071
+
3072
+ local is_fuzzy_plain = not (is_exact_plain or is_exact_start or is_exact_end) and #query > 1
3073
+ if is_fuzzy_forced or is_fuzzy_plain then return H.match_filter_fuzzy(inds, stritems, query), 'fuzzy', query end
3074
+
3075
+ local prefix = is_exact_start and '^' or ''
3076
+ local suffix = is_exact_end and '$' or ''
3077
+ local pattern = prefix .. vim.pesc(table.concat(query)) .. suffix
3078
+
3079
+ return H.match_filter_exact(inds, stritems, query, pattern), 'exact', query
3080
+ end
3081
+
3082
+ H.match_filter_exact = function(inds, stritems, query, pattern)
3083
+ local match_single = H.match_filter_exact_single
3084
+ local poke_picker = H.poke_picker_throttle(H.querytick)
3085
+ local match_data = {}
3086
+ for _, ind in ipairs(inds) do
3087
+ if not poke_picker() then return nil end
3088
+ local data = match_single(stritems[ind], ind, pattern)
3089
+ if data ~= nil then table.insert(match_data, data) end
3090
+ end
3091
+
3092
+ return match_data
3093
+ end
3094
+
3095
+ H.match_filter_exact_single = function(candidate, index, pattern)
3096
+ local start = string.find(candidate, pattern)
3097
+ if start == nil then return nil end
3098
+
3099
+ return { 0, start, index }
3100
+ end
3101
+
3102
+ H.match_ranges_exact = function(match_data, query)
3103
+ -- All matches have same match ranges relative to match start
3104
+ local cur_start, rel_ranges = 0, {}
3105
+ for i = 1, #query do
3106
+ rel_ranges[i] = { cur_start, cur_start + query[i]:len() - 1 }
3107
+ cur_start = rel_ranges[i][2] + 1
3108
+ end
3109
+
3110
+ local res = {}
3111
+ for i = 1, #match_data do
3112
+ local start = match_data[i][2]
3113
+ res[i] = vim.tbl_map(function(x) return { start + x[1], start + x[2] } end, rel_ranges)
3114
+ end
3115
+
3116
+ return res
3117
+ end
3118
+
3119
+ H.match_filter_fuzzy = function(inds, stritems, query)
3120
+ local match_single, find_query = H.match_filter_fuzzy_single, H.match_find_query
3121
+ local poke_picker = H.poke_picker_throttle(H.querytick)
3122
+ local match_data = {}
3123
+ for _, ind in ipairs(inds) do
3124
+ if not poke_picker() then return nil end
3125
+ local data = match_single(stritems[ind], ind, query, find_query)
3126
+ if data ~= nil then table.insert(match_data, data) end
3127
+ end
3128
+ return match_data
3129
+ end
3130
+
3131
+ H.match_filter_fuzzy_single = function(candidate, index, query, find_query)
3132
+ -- Search for query chars match positions with the following properties:
3133
+ -- - All are present in `candidate` in the same order.
3134
+ -- - Has smallest width among all such match positions.
3135
+ -- - Among same width has smallest first match.
3136
+
3137
+ -- Search forward to find matching positions with left-most last char match
3138
+ local first, last = find_query(candidate, query, 1)
3139
+ if first == nil then return nil end
3140
+ if first == last then return { 0, first, index, { first } } end
3141
+
3142
+ -- NOTE: This approach doesn't iterate **all** query matches. It is fine for
3143
+ -- width optimization but maybe not for more (like contiguous groups number).
3144
+ -- Example: for query {'a', 'b', 'c'} candidate 'aaxbbbc' will be matched as
3145
+ -- having 3 groups (indexes 2, 4, 7) but correct one is 2 groups (2, 6, 7).
3146
+
3147
+ -- Iteratively try to find better matches by advancing last match
3148
+ local best_first, best_last, best_width = first, last, last - first
3149
+ while last do
3150
+ local width = last - first
3151
+ if width < best_width then
3152
+ best_first, best_last, best_width = first, last, width
3153
+ end
3154
+
3155
+ first, last = find_query(candidate, query, first + 1)
3156
+ end
3157
+
3158
+ -- NOTE: No field names is not clear code, but consistently better performant
3159
+ return { best_last - best_first, best_first, index }
3160
+ end
3161
+
3162
+ H.match_ranges_fuzzy = function(match_data, query, stritems)
3163
+ local res, n_query, query_lens = {}, #query, vim.tbl_map(string.len, query)
3164
+ for i_match, data in ipairs(match_data) do
3165
+ local s, from, to = stritems[data[3]], data[2], data[2] + query_lens[1] - 1
3166
+ local ranges = { { from, to } }
3167
+ for j_query = 2, n_query do
3168
+ from, to = string.find(s, query[j_query], to + 1, true)
3169
+ ranges[j_query] = { from, to }
3170
+ end
3171
+ res[i_match] = ranges
3172
+ end
3173
+ return res
3174
+ end
3175
+
3176
+ H.match_find_query = function(s, query, init)
3177
+ local first, to = string.find(s, query[1], init, true)
3178
+ if first == nil then return nil, nil end
3179
+
3180
+ -- Both `first` and `last` indicate the start byte of first and last match
3181
+ local last = first
3182
+ for i = 2, #query do
3183
+ last, to = string.find(s, query[i], to + 1, true)
3184
+ if not last then return nil, nil end
3185
+ end
3186
+ return first, last
3187
+ end
3188
+
3189
+ H.match_query_group = function(query)
3190
+ local parts = { {} }
3191
+ for _, x in ipairs(query) do
3192
+ local is_whitespace = x:find('^%s+$') ~= nil
3193
+ if is_whitespace then table.insert(parts, {}) end
3194
+ if not is_whitespace then table.insert(parts[#parts], x) end
3195
+ end
3196
+ return #parts > 1, vim.tbl_map(table.concat, parts)
3197
+ end
3198
+
3199
+ H.match_sort = function(match_data)
3200
+ -- Spread indexes in width-start buckets
3201
+ local buckets, max_width, width_max_start = {}, 0, {}
3202
+ for i = 1, #match_data do
3203
+ local data, width, start = match_data[i], match_data[i][1], match_data[i][2]
3204
+ local buck_width = buckets[width] or {}
3205
+ local buck_start = buck_width[start] or {}
3206
+ table.insert(buck_start, data[3])
3207
+ buck_width[start] = buck_start
3208
+ buckets[width] = buck_width
3209
+
3210
+ max_width = math.max(max_width, width)
3211
+ width_max_start[width] = math.max(width_max_start[width] or 0, start)
3212
+ end
3213
+
3214
+ -- Sort index in place (to make stable sort) within buckets
3215
+ local poke_picker = H.poke_picker_throttle(H.querytick)
3216
+ for _, buck_width in pairs(buckets) do
3217
+ for _, buck_start in pairs(buck_width) do
3218
+ if not poke_picker() then return nil end
3219
+ table.sort(buck_start)
3220
+ end
3221
+ end
3222
+
3223
+ -- Gather indexes back in order
3224
+ local res = {}
3225
+ for width = 0, max_width do
3226
+ local buck_width = buckets[width]
3227
+ for start = 1, (width_max_start[width] or 0) do
3228
+ local buck_start = buck_width[start] or {}
3229
+ for i = 1, #buck_start do
3230
+ table.insert(res, buck_start[i])
3231
+ end
3232
+ end
3233
+ end
3234
+
3235
+ return res
3236
+ end
3237
+
3238
+ H.match_no_sort = function(match_data)
3239
+ return vim.tbl_map(function(x) return x[3] end, match_data)
3240
+ end
3241
+
3242
+ -- Default show ---------------------------------------------------------------
3243
+ H.get_icon = function(x, icons)
3244
+ local item_data = H.parse_item(x)
3245
+ local path = item_data.path or item_data.text or ''
3246
+ local path_type = H.get_fs_type(path)
3247
+ if path_type == 'none' then return { text = icons.none, hl = 'MiniPickNormal' } end
3248
+
3249
+ -- Prefer 'mini.icons'
3250
+ if _G.MiniIcons ~= nil then
3251
+ local category = path_type == 'directory' and 'directory' or 'file'
3252
+ local icon, hl = _G.MiniIcons.get(category, path)
3253
+ return { text = icon .. ' ', hl = hl }
3254
+ end
3255
+
3256
+ -- Try falling back to 'nvim-web-devicons'
3257
+ if path_type == 'directory' then return { text = icons.directory, hl = 'MiniPickIconDirectory' } end
3258
+ local has_devicons, devicons = pcall(require, 'nvim-web-devicons')
3259
+ if not has_devicons then return { text = icons.file, hl = 'MiniPickIconFile' } end
3260
+
3261
+ local icon, hl = devicons.get_icon(vim.fn.fnamemodify(path, ':t'), nil, { default = false })
3262
+ icon = type(icon) == 'string' and (icon .. ' ') or icons.file
3263
+ return { text = icon, hl = hl or 'MiniPickIconFile' }
3264
+ end
3265
+
3266
+ H.show_with_icons = function(buf_id, items, query) MiniPick.default_show(buf_id, items, query, { show_icons = true }) end
3267
+
3268
+ -- Items helpers for default functions ----------------------------------------
3269
+ H.parse_item = function(item)
3270
+ -- Try parsing table item first
3271
+ if type(item) == 'table' then return H.parse_item_table(item) end
3272
+
3273
+ -- Parse item's string representation
3274
+ local stritem = H.item_to_string(item)
3275
+
3276
+ -- - Buffer
3277
+ local ok, numitem = pcall(tonumber, stritem)
3278
+ if ok and H.is_valid_buf(numitem) then return { type = 'buffer', buf_id = numitem } end
3279
+
3280
+ -- File or Directory
3281
+ local path_type, path, lnum, col, rest = H.parse_path(stritem)
3282
+ if path_type ~= 'none' then return { type = path_type, path = path, lnum = lnum, col = col, text = rest } end
3283
+
3284
+ return {}
3285
+ end
3286
+
3287
+ H.parse_item_table = function(item)
3288
+ -- Buffer
3289
+ local buf_id = item.bufnr or item.buf_id or item.buf
3290
+ if H.is_valid_buf(buf_id) then
3291
+ --stylua: ignore
3292
+ return {
3293
+ type = 'buffer', buf_id = buf_id, path = item.path or vim.api.nvim_buf_get_name(buf_id),
3294
+ lnum = item.lnum, end_lnum = item.end_lnum,
3295
+ col = item.col, end_col = item.end_col,
3296
+ text = item.text,
3297
+ }
3298
+ end
3299
+
3300
+ -- File or Directory
3301
+ if type(item.path) == 'string' then
3302
+ local path_type = H.get_fs_type(item.path)
3303
+ if path_type == 'file' or path_type == 'uri' then
3304
+ --stylua: ignore
3305
+ return {
3306
+ type = path_type, path = item.path,
3307
+ lnum = item.lnum, end_lnum = item.end_lnum,
3308
+ col = item.col, end_col = item.end_col,
3309
+ text = item.text,
3310
+ }
3311
+ end
3312
+
3313
+ if path_type == 'directory' then return { type = 'directory', path = item.path } end
3314
+ end
3315
+
3316
+ return {}
3317
+ end
3318
+
3319
+ H.parse_path = function(x)
3320
+ if type(x) ~= 'string' or x == '' then return nil end
3321
+ -- Allow inputs like 'aa/bb', 'aa-5'. Also allow inputs for line/position
3322
+ -- separated by null character:
3323
+ -- - 'aa/bb\00010' (line 10).
3324
+ -- - 'aa/bb\00010\0005' (line 10, col 5).
3325
+ -- - 'aa/bb\00010\0005\000xx' (line 10, col 5, with "xx" description).
3326
+ local location_pattern = '()%z(%d+)%z?(%d*)%z?(.*)$'
3327
+ local from, lnum, col, rest = x:match(location_pattern)
3328
+ local path = x:sub(1, (from or 0) - 1)
3329
+ path = path:sub(1, 1) == '~' and ((vim.loop.os_homedir() or '~') .. path:sub(2)) or path
3330
+
3331
+ -- Verify that path is real
3332
+ local path_type = H.get_fs_type(path)
3333
+ if path_type == 'none' and path ~= '' then
3334
+ local cwd = H.pickers.active == nil and vim.fn.getcwd() or H.pickers.active.opts.source.cwd
3335
+ path = string.format('%s/%s', cwd, path)
3336
+ path_type = H.get_fs_type(path)
3337
+ end
3338
+
3339
+ return path_type, path, tonumber(lnum), tonumber(col), rest or ''
3340
+ end
3341
+
3342
+ H.get_fs_type = function(path)
3343
+ if path == '' then return 'none' end
3344
+ if vim.fn.filereadable(path) == 1 then return 'file' end
3345
+ if vim.fn.isdirectory(path) == 1 then return 'directory' end
3346
+ if H.parse_uri(path) ~= nil then return 'uri' end
3347
+ return 'none'
3348
+ end
3349
+
3350
+ -- Default preview ------------------------------------------------------------
3351
+ H.preview_file = function(buf_id, item_data, opts)
3352
+ -- Fully preview only accessible text files
3353
+ local is_text = H.is_file_text(item_data.path)
3354
+ if not is_text then return H.set_buflines(buf_id, { is_text == nil and '-No-access-' or '-Non-text-file-' }) end
3355
+
3356
+ -- Compute lines. Limit number of read lines to work better on large files.
3357
+ local has_lines, lines = pcall(vim.fn.readfile, item_data.path, '', (item_data.lnum or 1) + opts.n_context_lines)
3358
+ if not has_lines then return end
3359
+
3360
+ item_data.line_position = opts.line_position
3361
+ H.preview_set_lines(buf_id, lines, item_data)
3362
+ end
3363
+
3364
+ H.preview_directory = function(buf_id, item_data)
3365
+ local path = item_data.path
3366
+ local format = function(x) return x .. (vim.fn.isdirectory(path .. '/' .. x) == 1 and '/' or '') end
3367
+ local lines = vim.tbl_map(format, vim.fn.readdir(path))
3368
+ H.set_buflines(buf_id, lines)
3369
+ end
3370
+
3371
+ H.preview_buffer = function(buf_id, item_data, opts)
3372
+ -- NOTE: ideally just setting target buffer to window would be enough, but it
3373
+ -- has side effects. See https://github.com/neovim/neovim/issues/24973 .
3374
+ -- Reading lines and applying custom styling is a passable alternative.
3375
+ local buf_id_source = item_data.buf_id
3376
+
3377
+ -- Get lines from buffer ensuring it is loaded without important consequences
3378
+ local cache_eventignore = vim.o.eventignore
3379
+ vim.o.eventignore = 'BufEnter'
3380
+ vim.fn.bufload(buf_id_source)
3381
+ vim.o.eventignore = cache_eventignore
3382
+ local lines = vim.api.nvim_buf_get_lines(buf_id_source, 0, (item_data.lnum or 1) + opts.n_context_lines, false)
3383
+
3384
+ item_data.filetype, item_data.line_position = vim.bo[buf_id_source].filetype, opts.line_position
3385
+ H.preview_set_lines(buf_id, lines, item_data)
3386
+ end
3387
+
3388
+ H.preview_uri = function(buf_id, item_data, opts)
3389
+ item_data.buf_id = vim.uri_to_bufnr(item_data.path)
3390
+ H.preview_buffer(buf_id, item_data, opts)
3391
+ end
3392
+
3393
+ H.preview_inspect = function(buf_id, obj) H.set_buflines(buf_id, vim.split(vim.inspect(obj), '\n')) end
3394
+
3395
+ H.preview_set_lines = function(buf_id, lines, extra)
3396
+ -- Lines
3397
+ H.set_buflines(buf_id, lines)
3398
+
3399
+ -- Highlighting
3400
+ H.preview_highlight_region(buf_id, extra.lnum, extra.col, extra.end_lnum, extra.end_col)
3401
+
3402
+ if H.preview_should_highlight(buf_id) then
3403
+ local ft = extra.filetype or vim.filetype.match({ buf = buf_id, filename = extra.path })
3404
+ local has_lang, lang = pcall(vim.treesitter.language.get_lang, ft)
3405
+ lang = has_lang and lang or ft
3406
+ -- TODO: Remove `opts.error` after compatibility with Neovim=0.11 is dropped
3407
+ local has_parser, parser = pcall(vim.treesitter.get_parser, buf_id, lang, { error = false })
3408
+ has_parser = has_parser and parser ~= nil
3409
+ if has_parser then has_parser = pcall(vim.treesitter.start, buf_id, lang) end
3410
+ if not has_parser then vim.bo[buf_id].syntax = ft end
3411
+ end
3412
+
3413
+ -- Cursor position and window view. Find window (and not use picker window)
3414
+ -- for "outside window preview" (preview and main are different) to work.
3415
+ local win_id = vim.fn.bufwinid(buf_id)
3416
+ if win_id == -1 then return end
3417
+ H.set_cursor(win_id, extra.lnum, extra.col)
3418
+ local pos_keys = ({ top = 'zt', center = 'zz', bottom = 'zb' })[extra.line_position] or 'zt'
3419
+ pcall(vim.api.nvim_win_call, win_id, function() vim.cmd('normal! ' .. pos_keys) end)
3420
+ end
3421
+
3422
+ H.preview_should_highlight = function(buf_id)
3423
+ -- Highlight if buffer size is not too big, both in total and per line
3424
+ local buf_size = vim.api.nvim_buf_call(buf_id, function() return vim.fn.line2byte(vim.fn.line('$') + 1) end)
3425
+ return buf_size <= 1000000 and buf_size <= 1000 * vim.api.nvim_buf_line_count(buf_id)
3426
+ end
3427
+
3428
+ H.preview_highlight_region = function(buf_id, lnum, col, end_lnum, end_col)
3429
+ -- Highlight line
3430
+ if lnum == nil then return end
3431
+ local hl_line_opts = { end_row = lnum, end_col = 0, hl_eol = true, hl_group = 'MiniPickPreviewLine', priority = 201 }
3432
+ H.set_extmark(buf_id, H.ns_id.preview, lnum - 1, 0, hl_line_opts)
3433
+
3434
+ -- Highlight position/region
3435
+ if col == nil then return end
3436
+
3437
+ local ext_end_row, ext_end_col = lnum - 1, col
3438
+ if end_lnum ~= nil and end_col ~= nil then
3439
+ ext_end_row, ext_end_col = end_lnum - 1, end_col - 1
3440
+ end
3441
+ ext_end_col = H.get_next_char_bytecol(vim.fn.getbufline(buf_id, ext_end_row + 1)[1], ext_end_col)
3442
+
3443
+ local hl_region_opts = { end_row = ext_end_row, end_col = ext_end_col, priority = 202 }
3444
+ hl_region_opts.hl_group = 'MiniPickPreviewRegion'
3445
+ H.set_extmark(buf_id, H.ns_id.preview, lnum - 1, col - 1, hl_region_opts)
3446
+ end
3447
+
3448
+ -- Default choose -------------------------------------------------------------
3449
+ H.choose_path = function(win_target, item_data)
3450
+ local path = H.parse_uri(item_data.path) or item_data.path
3451
+ if item_data.type == 'directory' then
3452
+ return vim.api.nvim_win_call(win_target, function() vim.cmd('edit ' .. vim.fn.fnameescape(path)) end)
3453
+ end
3454
+ pcall(vim.api.nvim_win_call, win_target, function() vim.cmd("normal! m'") end)
3455
+ H.edit(path, win_target)
3456
+ H.choose_set_cursor(win_target, item_data.lnum, item_data.col)
3457
+ end
3458
+
3459
+ H.choose_buffer = function(win_target, item_data)
3460
+ pcall(vim.api.nvim_win_call, win_target, function() vim.cmd("normal! m'") end)
3461
+ H.set_winbuf(win_target, item_data.buf_id)
3462
+ H.choose_set_cursor(win_target, item_data.lnum, item_data.col)
3463
+ end
3464
+
3465
+ H.choose_print = function(x) print(vim.inspect(x)) end
3466
+
3467
+ H.choose_set_cursor = function(win_id, lnum, col)
3468
+ if lnum == nil then return end
3469
+ H.set_cursor(win_id, lnum, col)
3470
+ pcall(vim.api.nvim_win_call, win_id, function() vim.cmd('normal! zvzz') end)
3471
+ end
3472
+
3473
+ -- Builtins -------------------------------------------------------------------
3474
+ -- Stdout feed is the final result split into unknown places. This function is
3475
+ -- a longer version of `vim.split(table.concat(feed, ''), '\r?\n')` but allows
3476
+ -- to interrupt computation and doesn't create big string for the whole feed.
3477
+ H.set_picker_items_from_feed = function(feed, pattern, opts)
3478
+ -- Check active picker as `poke_picker()` will always be `true` in this case
3479
+ if not MiniPick.is_picker_active() then return end
3480
+
3481
+ -- Reuse input querytick to preserve intended reaction to query change
3482
+ -- NOTE: For intended effect, relies on presence of the active coroutine
3483
+ local poke_picker = H.poke_picker_throttle(opts.set_items_opts.querytick)
3484
+
3485
+ -- Realign feed so that each one ends in a pattern
3486
+ for i = 2, #feed do
3487
+ if not poke_picker() then return end
3488
+ local _, to = feed[i]:find(pattern)
3489
+ -- Account for chunks with no matches. Possibly several in a row.
3490
+ to = to or feed[i]:len()
3491
+ local j = i - 1
3492
+ while j > 1 and feed[j] == '' do
3493
+ j = j - 1
3494
+ end
3495
+ feed[j], feed[i] = feed[j] .. feed[i]:sub(1, to), feed[i]:sub(to + 1)
3496
+ end
3497
+
3498
+ local res, insert = {}, table.insert
3499
+ for i = 1, #feed do
3500
+ -- Poke once per feed item and not match, because latter is too much and
3501
+ -- might be less performant as each poke computes time.
3502
+ if not poke_picker() then return end
3503
+ for s in vim.gsplit(feed[i], pattern, { trimempty = true }) do
3504
+ insert(res, s)
3505
+ end
3506
+ -- Cleanup
3507
+ feed[i] = nil
3508
+ end
3509
+
3510
+ vim.schedule(function() MiniPick.set_picker_items(opts.postprocess(res), opts.set_items_opts) end)
3511
+ end
3512
+
3513
+ H.cli_postprocess = function(items)
3514
+ while items[#items] == '' do
3515
+ items[#items] = nil
3516
+ end
3517
+ return items
3518
+ end
3519
+
3520
+ H.is_executable = function(tool)
3521
+ if tool == 'fallback' then return true end
3522
+ return vim.fn.executable(tool) == 1
3523
+ end
3524
+
3525
+ H.files_get_tool = function()
3526
+ if H.is_executable('rg') then return 'rg' end
3527
+ if H.is_executable('fd') then return 'fd' end
3528
+ if H.is_executable('git') then return 'git' end
3529
+ return 'fallback'
3530
+ end
3531
+
3532
+ H.files_get_command = function(tool)
3533
+ if tool == 'rg' then return { 'rg', '--files', '--color=never' } end
3534
+ if tool == 'fd' then return { 'fd', '--type=f', '--color=never' } end
3535
+ if tool == 'git' then
3536
+ return { 'git', '-c', 'core.quotepath=false', 'ls-files', '--cached', '--others', '--exclude-standard' }
3537
+ end
3538
+ H.error([[Wrong 'tool' for `files` builtin.]])
3539
+ end
3540
+
3541
+ H.files_fallback_items = function(cwd)
3542
+ local poke_picker = H.poke_picker_throttle()
3543
+ local f = function()
3544
+ local items = {}
3545
+ for path, path_type in vim.fs.dir(cwd, { depth = math.huge }) do
3546
+ if not poke_picker() then return end
3547
+ if path_type == 'file' and H.is_file_text(string.format('%s/%s', cwd, path)) then table.insert(items, path) end
3548
+ end
3549
+ MiniPick.set_picker_items(items)
3550
+ end
3551
+
3552
+ vim.schedule(coroutine.wrap(f))
3553
+ end
3554
+
3555
+ H.grep_get_tool = function()
3556
+ if H.is_executable('rg') then return 'rg' end
3557
+ if H.is_executable('git') then return 'git' end
3558
+ return 'fallback'
3559
+ end
3560
+
3561
+ --stylua: ignore
3562
+ H.grep_get_command = function(tool, pattern, globs, method)
3563
+ if tool == 'rg' then
3564
+ local res = { 'rg', '--column', '--line-number', '--no-heading', '--field-match-separator', '\\x00', '--color=never' }
3565
+ table.insert(res, method == 'regex' and '--no-fixed-strings' or '--fixed-strings')
3566
+ for _, g in ipairs(globs) do
3567
+ table.insert(res, '--glob')
3568
+ -- NOTE: no `*` as default is important to not "override" ignoring files
3569
+ table.insert(res, g)
3570
+ end
3571
+ local case = vim.o.ignorecase and (vim.o.smartcase and 'smart-case' or 'ignore-case') or 'case-sensitive'
3572
+ vim.list_extend(res, { '--' .. case, '--', pattern })
3573
+ return res
3574
+ end
3575
+ if tool == 'git' then
3576
+ local res = { 'git', 'grep', '--column', '--line-number', '--null', '--color=never', '-e', pattern, '--', unpack(globs) }
3577
+ if vim.o.ignorecase then table.insert(res, 6, '--ignore-case') end
3578
+ if method == 'plain' then table.insert(res, 7, '--fixed-strings') end
3579
+ return res
3580
+ end
3581
+ H.error([[Wrong 'tool' for `grep` builtin.]])
3582
+ end
3583
+
3584
+ H.grep_fallback_items = function(pattern, cwd)
3585
+ local poke_picker = H.poke_picker_throttle()
3586
+ local f = function()
3587
+ local files, files_full = {}, {}
3588
+ for path, path_type in vim.fs.dir(cwd, { depth = math.huge }) do
3589
+ if not poke_picker() then return end
3590
+ local path_full = string.format('%s/%s', cwd, path)
3591
+ if path_type == 'file' and H.is_file_text(path_full) then
3592
+ table.insert(files, path)
3593
+ table.insert(files_full, path_full)
3594
+ end
3595
+ end
3596
+
3597
+ local items = {}
3598
+ for i, path in ipairs(files_full) do
3599
+ local file = files[i]
3600
+ if not poke_picker() then return end
3601
+ for lnum, l in ipairs(vim.fn.readfile(path)) do
3602
+ local col = string.find(l, pattern)
3603
+ if col ~= nil then table.insert(items, string.format('%s\0%d\0%d\0%s', file, lnum, col, l)) end
3604
+ end
3605
+ end
3606
+
3607
+ MiniPick.set_picker_items(items)
3608
+ end
3609
+
3610
+ vim.schedule(coroutine.wrap(f))
3611
+ end
3612
+
3613
+ -- Async ----------------------------------------------------------------------
3614
+ H.schedule_resume_is_active = vim.schedule_wrap(function(co) coroutine.resume(co, MiniPick.is_picker_active()) end)
3615
+
3616
+ H.poke_picker_throttle = function(querytick_ref)
3617
+ -- Allow calling this even if no picker is active
3618
+ if not MiniPick.is_picker_active() then
3619
+ return function() return true end
3620
+ end
3621
+
3622
+ local latest_time, dont_check_querytick = vim.loop.hrtime(), querytick_ref == nil
3623
+ local threshold = 1000000 * H.pickers.active.opts.delay.async
3624
+ local hrtime = vim.loop.hrtime
3625
+ local poke_is_picker_active = MiniPick.poke_is_picker_active
3626
+ return function()
3627
+ local now = hrtime()
3628
+ if (now - latest_time) < threshold then return true end
3629
+ latest_time = now
3630
+ -- Return positive if picker is active and no query updates (if asked)
3631
+ return poke_is_picker_active() and (dont_check_querytick or querytick_ref == H.querytick)
3632
+ end
3633
+ end
3634
+
3635
+ -- Utilities ------------------------------------------------------------------
3636
+ H.error = function(msg) error('(mini.pick) ' .. msg, 0) end
3637
+
3638
+ H.check_type = function(name, val, ref, allow_nil)
3639
+ if type(val) == ref or (ref == 'callable' and vim.is_callable(val)) or (allow_nil and val == nil) then return end
3640
+ H.error(string.format('`%s` should be %s, not %s', name, ref, type(val)))
3641
+ end
3642
+
3643
+ H.set_buf_name = function(buf_id, name) vim.api.nvim_buf_set_name(buf_id, 'minipick://' .. buf_id .. '/' .. name) end
3644
+
3645
+ H.notify = function(msg, level_name) vim.notify('(mini.pick) ' .. msg, vim.log.levels[level_name]) end
3646
+
3647
+ H.edit = function(path, win_id)
3648
+ if type(path) ~= 'string' then return end
3649
+ local b = vim.api.nvim_win_get_buf(win_id or 0)
3650
+ local try_mimic_buf_reuse = (vim.fn.bufname(b) == '' and vim.bo[b].buftype ~= 'quickfix' and not vim.bo[b].modified)
3651
+ and (#vim.fn.win_findbuf(b) == 1 and vim.deep_equal(vim.fn.getbufline(b, 1, '$'), { '' }))
3652
+ local buf_id = vim.fn.bufadd(vim.fn.fnamemodify(path, ':.'))
3653
+ -- Showing in window also loads. Use `pcall` to not error with swap messages.
3654
+ pcall(vim.api.nvim_win_set_buf, win_id or 0, buf_id)
3655
+ vim.bo[buf_id].buflisted = true
3656
+ if try_mimic_buf_reuse then pcall(vim.api.nvim_buf_delete, b, { unload = false }) end
3657
+ return buf_id
3658
+ end
3659
+
3660
+ H.is_valid_buf = function(buf_id) return type(buf_id) == 'number' and vim.api.nvim_buf_is_valid(buf_id) end
3661
+
3662
+ H.is_valid_win = function(win_id) return type(win_id) == 'number' and vim.api.nvim_win_is_valid(win_id) end
3663
+
3664
+ H.is_array_of = function(x, ref_type)
3665
+ if not H.islist(x) then return false end
3666
+ for i = 1, #x do
3667
+ if type(x[i]) ~= ref_type then return false end
3668
+ end
3669
+ return true
3670
+ end
3671
+
3672
+ H.create_scratch_buf = function(name)
3673
+ local buf_id = vim.api.nvim_create_buf(false, true)
3674
+ H.set_buf_name(buf_id, name)
3675
+ vim.bo[buf_id].matchpairs = ''
3676
+ vim.b[buf_id].minicursorword_disable = true
3677
+ vim.b[buf_id].miniindentscope_disable = true
3678
+ return buf_id
3679
+ end
3680
+
3681
+ H.get_first_valid_normal_window = function()
3682
+ for _, win_id in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
3683
+ if vim.api.nvim_win_get_config(win_id).relative == '' then return win_id end
3684
+ end
3685
+ end
3686
+
3687
+ H.set_buflines = function(buf_id, lines) pcall(vim.api.nvim_buf_set_lines, buf_id, 0, -1, false, lines) end
3688
+
3689
+ H.set_winbuf = function(win_id, buf_id) vim.api.nvim_win_set_buf(win_id, buf_id) end
3690
+
3691
+ H.set_extmark = function(...) pcall(vim.api.nvim_buf_set_extmark, ...) end
3692
+
3693
+ H.set_cursor = function(win_id, lnum, col) pcall(vim.api.nvim_win_set_cursor, win_id, { lnum or 1, (col or 1) - 1 }) end
3694
+
3695
+ H.set_curwin = function(win_id)
3696
+ if not H.is_valid_win(win_id) then return end
3697
+ -- Explicitly preserve cursor to fix Neovim<0.10 after choosing position in
3698
+ -- already shown buffer
3699
+ local buf_id, cursor = vim.api.nvim_win_get_buf(win_id), vim.api.nvim_win_get_cursor(win_id)
3700
+ vim.api.nvim_set_current_win(win_id)
3701
+ if buf_id == vim.api.nvim_win_get_buf(win_id) then H.set_cursor(win_id, cursor[1], cursor[2] + 1) end
3702
+ end
3703
+
3704
+ H.clear_namespace = function(buf_id, ns_id) pcall(vim.api.nvim_buf_clear_namespace, buf_id, ns_id, 0, -1) end
3705
+
3706
+ H.replace_termcodes = function(x) return vim.api.nvim_replace_termcodes(x, true, true, true) end
3707
+
3708
+ H.expand_callable = function(x, ...)
3709
+ if vim.is_callable(x) then return x(...) end
3710
+ return x
3711
+ end
3712
+
3713
+ H.expandcmd = function(x)
3714
+ local ok, res = pcall(vim.fn.expandcmd, x)
3715
+ return ok and res or x
3716
+ end
3717
+
3718
+ H.getcharstr = function(lmap)
3719
+ H.cache.is_in_getcharstr = true
3720
+ local ok, char = pcall(vim.fn.getcharstr)
3721
+ H.cache.is_in_getcharstr = nil
3722
+
3723
+ -- Cache possible error if it doesn't come from pressing <C-c>
3724
+ local is_ctrl_c = (not ok and char == 'Keyboard interrupt') or (ok and char == '\3')
3725
+ if not ok and not is_ctrl_c then H.cache.getcharstr_errmsg = char end
3726
+
3727
+ -- Terminate if no input, on hard-coded <C-c>, or outside mouse click
3728
+ local main_win_id
3729
+ if H.pickers.active ~= nil then main_win_id = H.pickers.active.windows.main end
3730
+ local is_bad_mouse_click = vim.v.mouse_winid ~= 0 and vim.v.mouse_winid ~= main_win_id
3731
+ if not ok or char == '' or char == '\3' or is_bad_mouse_click then return end
3732
+ -- Respect language mappings only if needed
3733
+ return vim.o.iminsert == 0 and char or (lmap[char] or char)
3734
+ end
3735
+
3736
+ H.ensure_redraw = function(picker)
3737
+ -- Ensure only one sequence of scheduled redraws
3738
+ H.timers.redraw:stop()
3739
+ local n = 0
3740
+ local f = function()
3741
+ vim.cmd('redraw')
3742
+ -- Do several (but limited) `:redraw` for slow async changes
3743
+ n = n + 1
3744
+ if n >= 100 then H.timers.redraw:stop() end
3745
+ end
3746
+ H.timers.redraw:start(0, picker.opts.delay.async, vim.schedule_wrap(f))
3747
+ end
3748
+
3749
+ H.tolower = (function()
3750
+ -- Cache `tolower` for speed
3751
+ local tolower = vim.fn.tolower
3752
+ return function(x)
3753
+ -- `vim.fn.tolower` can throw errors on bad string (like with '\0')
3754
+ local ok, res = pcall(tolower, x)
3755
+ return ok and res or string.lower(x)
3756
+ end
3757
+ end)()
3758
+
3759
+ H.win_update_hl = function(win_id, new_from, new_to)
3760
+ if not H.is_valid_win(win_id) then return end
3761
+
3762
+ local new_entry = new_from .. ':' .. new_to
3763
+ local replace_pattern = string.format('(%s:[^,]*)', vim.pesc(new_from))
3764
+ local new_winhighlight, n_replace = vim.wo[win_id].winhighlight:gsub(replace_pattern, new_entry)
3765
+ if n_replace == 0 then new_winhighlight = new_winhighlight .. ',' .. new_entry end
3766
+
3767
+ vim.wo[win_id].winhighlight = new_winhighlight
3768
+ end
3769
+
3770
+ H.fit_to_width = function(text, width)
3771
+ local t_width = vim.fn.strchars(text)
3772
+ return t_width <= width and text or ('…' .. vim.fn.strcharpart(text, t_width - width + 1, width - 1))
3773
+ end
3774
+
3775
+ H.win_get_bottom_border = function(win_id, border_part_id)
3776
+ local border = vim.api.nvim_win_get_config(win_id).border or {}
3777
+ local res = border[border_part_id]
3778
+ return (type(res) == 'table' and res[1] or res) or ' '
3779
+ end
3780
+
3781
+ H.win_set_cwd = function(win_id, cwd)
3782
+ -- Avoid needlessly setting cwd as it has side effects (like for `:buffers`)
3783
+ if cwd == nil or vim.fn.getcwd(win_id or 0) == cwd then return end
3784
+ local f = function() vim.cmd('lcd ' .. vim.fn.fnameescape(cwd)) end
3785
+ if win_id == nil or win_id == vim.api.nvim_get_current_win() then return f() end
3786
+ vim.api.nvim_win_call(win_id, f)
3787
+ end
3788
+
3789
+ H.seq_along = function(arr)
3790
+ if arr == nil then return nil end
3791
+ local res = {}
3792
+ for i = 1, #arr do
3793
+ table.insert(res, i)
3794
+ end
3795
+ return res
3796
+ end
3797
+
3798
+ H.str_utfindex = function(s, i) return vim.str_utfindex(s, 'utf-32', i) end
3799
+ if vim.fn.has('nvim-0.11') == 0 then H.str_utfindex = function(s, i) return (vim.str_utfindex(s, i)) end end
3800
+
3801
+ H.str_byteindex = function(s, i) return vim.str_byteindex(s, 'utf-32', i) end
3802
+ if vim.fn.has('nvim-0.11') == 0 then H.str_byteindex = function(s, i) return vim.str_byteindex(s, i) end end
3803
+
3804
+ H.get_next_char_bytecol = function(line_str, col)
3805
+ if type(line_str) ~= 'string' then return col end
3806
+ local utf_index = H.str_utfindex(line_str, math.min(line_str:len(), col))
3807
+ return H.str_byteindex(line_str, utf_index)
3808
+ end
3809
+
3810
+ H.is_file_text = function(path)
3811
+ local fd = vim.loop.fs_open(path, 'r', 1)
3812
+ if fd == nil then return nil end
3813
+ local is_text = vim.loop.fs_read(fd, 1024):find('\0') == nil
3814
+ vim.loop.fs_close(fd)
3815
+ return is_text
3816
+ end
3817
+
3818
+ H.full_path = function(path) return (vim.fn.fnamemodify(path, ':p'):gsub('(.)/$', '%1')) end
3819
+ if H.is_windows then
3820
+ H.full_path = function(path) return (vim.fn.fnamemodify(path, ':p'):gsub('(.)[\\/]$', '%1')) end
3821
+ end
3822
+
3823
+ H.parse_uri = function(x)
3824
+ local ok, path = pcall(vim.uri_to_fname, x)
3825
+ if not ok then return nil end
3826
+ -- Don't accept Windows paths with volume letter as URI
3827
+ if H.is_windows and x:find('^%a:') ~= nil and path:find('^%a:') ~= nil then return nil end
3828
+ return path
3829
+ end
3830
+
3831
+ H.user_input = function(prompt, text, completion, scope)
3832
+ prompt = '(mini.pick) ' .. prompt
3833
+ if _G.MiniInput ~= nil then
3834
+ local opts = { prompt = prompt, scope = scope, init_keys = { text }, completion = completion }
3835
+ -- Show in UI line in active picker window as other styles are distracting
3836
+ if MiniPick.is_picker_active() then
3837
+ local from_bottom = MiniPick.get_picker_opts().options.content_from_bottom
3838
+ local style = (from_bottom and vim.fn.has('nvim-0.12') == 1) and 'statusline' or 'winbar'
3839
+ opts.handlers = { view = MiniInput.gen_view.uiline({ style = style }) }
3840
+ end
3841
+ return MiniInput.get(opts)
3842
+ end
3843
+
3844
+ -- Use `on_key` to distinguish cancel with `<Esc>` and immediate `<CR>`
3845
+ local was_cancelled = false
3846
+ vim.on_key(function(key) was_cancelled = was_cancelled or key == '\27' end, H.ns_id.input)
3847
+
3848
+ -- Ask for input. Use `pcall` to allow `<C-c>` to cancel user input
3849
+ vim.cmd('echohl Question')
3850
+ local ok, res = pcall(vim.fn.input, { prompt = prompt .. ': ', default = text or '', completion = completion })
3851
+ vim.cmd('echohl None | echo "" | redraw')
3852
+
3853
+ vim.on_key(nil, H.ns_id.input)
3854
+ return (ok and not was_cancelled) and res or nil
3855
+ end
3856
+
3857
+ -- TODO: Remove after compatibility with Neovim=0.9 is dropped
3858
+ H.islist = vim.fn.has('nvim-0.10') == 1 and vim.islist or vim.tbl_islist
3859
+
3860
+ H.get_lmap = function()
3861
+ local lmap = {}
3862
+ for _, map in ipairs(vim.fn.maplist()) do
3863
+ -- NOTE: Account only for characters that resolve to proper query character
3864
+ local is_query_lmap = map.mode == 'l' and H.is_query_char(map.rhs)
3865
+ if is_query_lmap then lmap[map.lhs] = map.rhs end
3866
+ end
3867
+ return lmap
3868
+ end
3869
+ if vim.fn.has('nvim-0.10') == 0 then H.get_lmap = function() return {} end end
3870
+
3871
+ -- A copy of `vim.deepcopy()` that doesn't error on userdata and threads
3872
+ H.copy_tables = function(x)
3873
+ return type(x) == 'table' and setmetatable(vim.tbl_map(H.copy_tables, x), getmetatable(x)) or x
3874
+ end
3875
+
3876
+ return MiniPick