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,3145 @@
1
+ --- *mini.files* Navigate and manipulate file system
2
+ ---
3
+ --- MIT License Copyright (c) 2023 Evgeni Chasnovski
4
+
5
+ --- Features:
6
+ --- - Navigate file system using column view (Miller columns) to display nested
7
+ --- directories. See |MiniFiles-navigation| for overview.
8
+ ---
9
+ --- - Opt-in preview of file or directory under cursor.
10
+ ---
11
+ --- - Manipulate files and directories by editing text buffers: create, delete,
12
+ --- rename (all three are LSP aware), copy, move.
13
+ --- See |MiniFiles-manipulation| for an overview.
14
+ ---
15
+ --- - Use as default file explorer instead of `netrw`.
16
+ ---
17
+ --- - Configurable:
18
+ --- - Filter/prefix/sort of file system entries.
19
+ --- - Mappings used for common explorer actions.
20
+ --- - UI options: whether to show preview of file/directory under cursor, etc.
21
+ --- - Bookmarks for quicker navigation.
22
+ ---
23
+ --- What it doesn't do:
24
+ --- - Try to be replacement of system file explorer. It is mostly designed to
25
+ --- be used within Neovim to quickly explore file system structure, open
26
+ --- files, and perform some quick file system edits.
27
+ ---
28
+ --- - Work on remote locations. Only local file system is supported.
29
+ ---
30
+ --- - Provide built-in interactive toggle of content `filter` and `sort`.
31
+ --- See |MiniFiles-examples| for some common examples.
32
+ ---
33
+ --- - Provide out of the box extra information like git or diagnostic status.
34
+ --- This can be achieved by setting |extmarks| on appropriate event(s)
35
+ --- (see |MiniFiles-events|)
36
+ ---
37
+ --- Notes:
38
+ --- - This module is written and thoroughly tested on Linux. Support for other
39
+ --- platform/OS (like Windows or MacOS) is a goal, but there is no guarantee.
40
+ ---
41
+ --- - This module silently reacts to not enough permissions:
42
+ --- - In case of missing file, check its or its parent read permissions.
43
+ --- - In case of no manipulation result, check write permissions.
44
+ ---
45
+ --- # Dependencies ~
46
+ ---
47
+ --- Suggested dependencies (provide extra functionality, will work without them):
48
+ ---
49
+ --- - Enabled |mini.icons| module to show icons near file/directory names.
50
+ --- Falls back to [nvim-tree/nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons)
51
+ --- plugin or uses default icons.
52
+ ---
53
+ --- # Setup ~
54
+ ---
55
+ --- This module needs a setup with `require('mini.files').setup({})` (replace
56
+ --- `{}` with your `config` table). It will create global Lua table `MiniFiles`
57
+ --- which you can use for scripting or manually (with `:lua MiniFiles.*`).
58
+ ---
59
+ --- See |MiniFiles.config| for available config settings.
60
+ ---
61
+ --- You can override runtime config settings (like mappings or window options)
62
+ --- locally to buffer inside `vim.b.minifiles_config` which should have same
63
+ --- structure as `MiniFiles.config`. See |mini.nvim-buffer-local-config| for
64
+ --- more details.
65
+ ---
66
+ --- # Comparisons ~
67
+ ---
68
+ --- - [nvim-tree/nvim-tree.lua](https://github.com/nvim-tree/nvim-tree.lua):
69
+ --- - Provides tree view of file system, while this module uses column view.
70
+ --- - File system manipulation is done with custom set of mappings for each
71
+ --- action, while this module is designed to do that by editing text.
72
+ --- - Has more out of the box functionality with extra configuration, while
73
+ --- this module has not (by design).
74
+ ---
75
+ --- - [stevearc/oil.nvim](https://github.com/stevearc/oil.nvim):
76
+ --- - Uses single window to show information only about currently explored
77
+ --- directory, while this module uses column view to show whole currently
78
+ --- explored branch.
79
+ --- - Also uses text editing to manipulate file system entries.
80
+ --- - Can work for remote file systems, while this module can not (by design).
81
+ --- - Both provide LSP integration.
82
+ ---
83
+ --- - [nvim-neo-tree/neo-tree.nvim](https://github.com/nvim-neo-tree/neo-tree.nvim):
84
+ --- - Compares to this module mostly the same as `nvim-tree/nvim-tree.lua`.
85
+ ---
86
+ --- # Highlight groups ~
87
+ --- *MiniFiles-hl-groups*
88
+ ---
89
+ --- - `MiniFilesBorder` - border of regular windows.
90
+ --- - `MiniFilesBorderModified` - border of windows showing modified buffer.
91
+ --- - `MiniFilesCursorLine` - cursor line in explorer windows.
92
+ --- - `MiniFilesDirectory` - text and icon representing directory.
93
+ --- - `MiniFilesFile` - text representing file.
94
+ --- - `MiniFilesNormal` - basic foreground/background highlighting.
95
+ --- - `MiniFilesTitle` - title of regular windows.
96
+ --- - `MiniFilesTitleFocused` - title of focused window.
97
+ ---
98
+ --- To change any highlight group, set it directly with |nvim_set_hl()|.
99
+ ---
100
+ --- # Disabling ~
101
+ ---
102
+ --- This plugin provides only manually started functionality, so no disabling
103
+ --- is available.
104
+ ---@tag MiniFiles
105
+
106
+ --- Every navigation starts by calling |MiniFiles.open()|, either directly or via
107
+ --- mapping (see its help for examples of some common scenarios). It will show
108
+ --- an explorer consisting of side-by-side floating windows with the following
109
+ --- principles:
110
+ ---
111
+ --- - Explorer shows one branch of nested directories at a time.
112
+ ---
113
+ --- - Explorer consists from several windows:
114
+ ---
115
+ --- - Each window displays entries of a single directory in a modifiable
116
+ --- scratch buffer.
117
+ ---
118
+ --- - Windows are organized left to right: for any particular window the left
119
+ --- neighbor is its parent directory and right neighbor - its child.
120
+ ---
121
+ --- - Explorer windows are the viewport to some part of current branch, meaning
122
+ --- that their opening/closing does not affect the branch. This matters, for
123
+ --- example, if there are more elements in the branch than can be shown windows.
124
+ ---
125
+ --- - Every buffer line represents separate file system entry following certain
126
+ --- format (not visible for users by default; set |'conceallevel'| to 0 to see it)
127
+ ---
128
+ --- - Once directory is shown, its buffer is not updated automatically following
129
+ --- external file system changes. Manually use |MiniFiles.synchronize()| for that.
130
+ ---
131
+ --- After opening explorer, in-buffer navigation is done the same way as any
132
+ --- regular buffer, except without some keys reserved for built-in actions.
133
+ ---
134
+ --- Most common ways to navigate are:
135
+ ---
136
+ --- - Press `j` to move cursor onto next (lower) entry in current directory.
137
+ --- - Press `k` to move cursor onto previous (higher) entry in current directory.
138
+ --- - Press `l` to expand entry under cursor (see "Go in" action).
139
+ --- - Press `h` to focus on parent directory (see "Go out" action).
140
+ ---
141
+ --- Cursor positions in each directory buffer are tracked and saved during
142
+ --- navigation. This allows for more convenient repeated navigation to some
143
+ --- previously visited branch.
144
+ ---
145
+ --- Available built-in actions (see "Details" for more information): >
146
+ ---
147
+ --- | Action | Keys | Description |
148
+ --- |-------------|------|------------------------------------------------|
149
+ --- | Close | q | Close explorer |
150
+ --- |-------------|------|------------------------------------------------|
151
+ --- | Go in | l | Expand entry (show directory or open file) |
152
+ --- |-------------|------|------------------------------------------------|
153
+ --- | Go in plus | L | Expand entry plus extra action |
154
+ --- |-------------|------|------------------------------------------------|
155
+ --- | Go out | h | Focus on parent directory |
156
+ --- |-------------|------|------------------------------------------------|
157
+ --- | Go out plus | H | Focus on parent directory plus extra action |
158
+ --- |-------------|------|------------------------------------------------|
159
+ --- | Go to mark | ' | Jump to bookmark (waits for single key id) |
160
+ --- |-------------|------|------------------------------------------------|
161
+ --- | Set mark | m | Set bookmark (waits for single key id) |
162
+ --- |-------------|------|------------------------------------------------|
163
+ --- | Reset | <BS> | Reset current explorer |
164
+ --- |-------------|------|------------------------------------------------|
165
+ --- | Reveal cwd | @ | Reset current current working directory |
166
+ --- |-------------|------|------------------------------------------------|
167
+ --- | Show help | g? | Show help window |
168
+ --- |-------------|------|------------------------------------------------|
169
+ --- | Synchronize | = | Synchronize user edits and/or external changes |
170
+ --- |-------------|------|------------------------------------------------|
171
+ --- | Trim left | < | Trim left part of branch |
172
+ --- |-------------|------|------------------------------------------------|
173
+ --- | Trim right | > | Trim right part of branch |
174
+ --- |-------------|------|------------------------------------------------|
175
+ --- <
176
+ --- Details:
177
+ ---
178
+ --- - "Go in":
179
+ --- - Always opens file in the latest window before `MiniFiles.open()` call.
180
+ --- - Never closes explorer.
181
+ --- - Works in linewise Visual mode to expand multiple entries.
182
+ ---
183
+ --- - "Go in plus" is regular "Go in" but closes explorer after opening a file.
184
+ ---
185
+ --- - "Go out plus" is regular "Go out" but trims right part of branch.
186
+ ---
187
+ --- - "Set mark" and "Go to mark" both wait for user to press a single character
188
+ --- of a bookmark id. Example: `ma` sets directory path of focused window as
189
+ --- bookmark "a"; `'a` jumps (sets as whole branch) to bookmark "a".
190
+ --- Special bookmark `'` always points to path before the latest bookmark jump.
191
+ ---
192
+ --- - "Reset" focuses only on "anchor" directory (the one used to open current
193
+ --- explorer) and resets all stored directory cursor positions.
194
+ ---
195
+ --- - "Reveal cwd" extends branch to include |current-directory|.
196
+ --- If it is not an ancestor of the current branch, nothing is done.
197
+ ---
198
+ --- - "Show help" results into new window with helpful information about current
199
+ --- explorer (like buffer mappings and bookmarks). Press `q` to close it.
200
+ ---
201
+ --- - "Synchronize" parses user edits in directory buffers, applies them (after
202
+ --- confirmation), and updates all directory buffers with the most relevant
203
+ --- file system information. Can also be used without user edits to show up
204
+ --- to date file system entries.
205
+ --- See |MiniFiles-manipulation| for more info about file system manipulation.
206
+ ---
207
+ --- - "Trim left" and "Trim right" trim parts of the whole branch, not only its
208
+ --- currently visible parts.
209
+ ---
210
+ --- Notes:
211
+ ---
212
+ --- - Each action has exported function with more details about it.
213
+ ---
214
+ --- - Keys can be configured with `mappings` table of |MiniFiles.config|.
215
+ ---@tag MiniFiles-navigation
216
+
217
+ --- File system manipulation is done by editing text inside directory buffers,
218
+ --- which are shown inside dedicated window(s). See |MiniFiles-navigation| for
219
+ --- more information about navigating to a particular directory.
220
+ ---
221
+ --- General workflow:
222
+ ---
223
+ --- - Navigate to the directory in which manipulation should be done.
224
+ ---
225
+ --- - Edit buffer in the way representing file system action.
226
+ ---
227
+ --- - Repeat previous steps until all necessary file system actions are recorded.
228
+ --- Note: even if directory buffer is hidden, its modifications are preserved,
229
+ --- so you can navigate in and out of directory with modified buffer.
230
+ ---
231
+ --- - Execute |MiniFiles.synchronize()| (default key is `=`). This will prompt
232
+ --- confirmation dialog listing all file system actions (per directory) it is
233
+ --- about to perform. READ IT CAREFULLY.
234
+ ---
235
+ --- - Confirm by pressing `y` / `<CR>` (apply edits and update buffers) or
236
+ --- don't confirm by pressing `n` / `<Esc>` (update buffers without applying edits).
237
+ ---
238
+ --- Note: prefer small and not related steps with more frequent synchronization
239
+ --- over single complex manipulation. There are (known) cases which won't work.
240
+ ---
241
+ --- # How does it work ~
242
+ ---
243
+ --- All manipulation functionality is powered by creating and keeping track of
244
+ --- path indexes: text of the form `/xxx` (`xxx` is the number path index) placed
245
+ --- at the start of every line representing file system entry.
246
+ ---
247
+ --- By default they are hidden as concealed text (along with prefix separators)
248
+ --- for more convenience but you can see them by setting |'conceallevel'| to 0.
249
+ --- DO NOT modify text to the left of entry name.
250
+ ---
251
+ --- During synchronization, actual text for entry name is compared to path index
252
+ --- at that line (if present) to deduce which file system action to perform.
253
+ --- Note that order of text manipulation steps does not affect performed actions.
254
+ ---
255
+ --- # Supported file system actions ~
256
+ ---
257
+ --- ## Create ~
258
+ ---
259
+ --- - Create file by creating new line with file name (including extension).
260
+ ---
261
+ --- - Create directory by creating new line with directory name followed by `/`.
262
+ ---
263
+ --- - Create file or directory inside nested directories by creating new line
264
+ --- with text like `dir/nested-dir/` or `dir/nested-dir/file`.
265
+ --- Always use `/` on any OS.
266
+ ---
267
+ --- ## Delete ~
268
+ ---
269
+ --- - Delete file or directory by deleting WHOLE LINE describing it.
270
+ ---
271
+ --- - If `options.permanent_delete` is `true`, delete is permanent. Otherwise
272
+ --- file system entry is moved to a module-specific trash directory
273
+ --- (see |MiniFiles.config| for more details).
274
+ ---
275
+ --- ## Rename ~
276
+ ---
277
+ --- - Rename file or directory by editing its name (not icon or path index to
278
+ --- the left of it).
279
+ ---
280
+ --- - With default mappings for `h` / `l` it might be not convenient to rename
281
+ --- only part of an entry. You can adopt any of the following approaches:
282
+ --- - Use different motions, like |$|, |e|, |f|, etc.
283
+ --- - Go into Insert mode and navigate inside it.
284
+ --- - Change mappings to be more suited for manipulation and not navigation.
285
+ --- See "Mappings" section in |MiniFiles.config|.
286
+ ---
287
+ --- - It is not needed to end directory name with `/`.
288
+ ---
289
+ --- - Appending `/` to a file name will delete it and create empty directory
290
+ --- with the same name.
291
+ ---
292
+ --- - Cyclic renames ("a" to "b" and "b" to "a") are not supported.
293
+ ---
294
+ --- ## Copy ~
295
+ ---
296
+ --- - Copy file or directory by copying WHOLE LINE describing it and pasting
297
+ --- it inside buffer of target directory.
298
+ ---
299
+ --- - Change of target path is allowed. Edit only entry name in target location
300
+ --- (not icon or path index to the left of it).
301
+ ---
302
+ --- - Copying inside same parent directory is supported only if target path has
303
+ --- different name.
304
+ ---
305
+ --- - Copying inside child directory is supported.
306
+ ---
307
+ --- ## Move ~
308
+ ---
309
+ --- - Move file or directory by cutting WHOLE LINE describing it and then
310
+ --- pasting it inside target directory.
311
+ ---
312
+ --- - Change of target path is allowed. Edit only entry name in target location
313
+ --- (not icon or path index to the left of it).
314
+ ---
315
+ --- - Moving directory inside itself is not supported.
316
+ ---
317
+ --- # LSP integration ~
318
+ ---
319
+ --- Create, delete, and rename are LSP aware (on Neovim>=0.11): the information
320
+ --- is forwarded to all active LSP servers for them to perform additional actions.
321
+ --- This means that LSP servers can, for example, update imports after renaming
322
+ --- a file or populate a file with a boilerplate code after creation.
323
+ ---
324
+ --- The actual changes depend entirely on the LSP server and whether it supports
325
+ --- relevant methods:
326
+ --- - `workspace/will{Create,Delete,Rename}Files` before a file system action.
327
+ --- - `workspace/did{Create,Delete,Rename}Files` after a file system action.
328
+ ---
329
+ --- It can be disabled by setting `options.lsp_timeout = 0` in |MiniFiles.config|.
330
+ ---@tag MiniFiles-manipulation
331
+
332
+ --- To allow user customization and integration of external tools, certain |User|
333
+ --- autocommand events are triggered under common circumstances.
334
+ ---
335
+ --- # UI events ~
336
+ ---
337
+ --- - `MiniFilesExplorerOpen` - just after explorer finishes opening.
338
+ ---
339
+ --- - `MiniFilesExplorerClose` - just before explorer starts closing.
340
+ ---
341
+ --- - `MiniFilesBufferCreate` - when buffer is created to show a particular
342
+ --- directory/file. Triggered once per path during explorer session.
343
+ --- Can be used to create buffer-local mappings.
344
+ ---
345
+ --- - `MiniFilesBufferUpdate` - when path buffer is updated with new content.
346
+ --- Can be used for integrations to set useful |extmarks|.
347
+ ---
348
+ --- - `MiniFilesWindowOpen` - when new window is opened. Can be used to set
349
+ --- window-local settings (like border, |'winblend'|, etc.)
350
+ ---
351
+ --- - `MiniFilesWindowUpdate` - when a window is updated. Triggers VERY frequently.
352
+ --- At least after every cursor movement and "go in" / "go out" action.
353
+ ---
354
+ --- Callback for each buffer/window UI event will receive <data> field
355
+ --- (see |nvim_create_autocmd()|) with the following information:
356
+ ---
357
+ --- - <buf_id> - index of target buffer.
358
+ --- - <win_id> - index of target window. Can be `nil`, like in
359
+ --- `MiniFilesBufferCreate` and buffer's first `MiniFilesBufferUpdate` as
360
+ --- they are triggered before window is created.
361
+ ---
362
+ --- # File action events ~
363
+ ---
364
+ --- - `MiniFilesActionCreate` - after entry is successfully created.
365
+ ---
366
+ --- - `MiniFilesActionDelete` - after entry is successfully deleted.
367
+ ---
368
+ --- - `MiniFilesActionRename` - after entry is successfully renamed.
369
+ ---
370
+ --- - `MiniFilesActionCopy` - after entry is successfully copied.
371
+ ---
372
+ --- - `MiniFilesActionMove` - after entry is successfully moved.
373
+ ---
374
+ --- Callback for each file action event will receive `data` field
375
+ --- (see |nvim_create_autocmd()|) with the following information:
376
+ ---
377
+ --- - <action> - string with action name.
378
+ --- - <from> - full path of entry before action (`nil` for "create" action).
379
+ --- - <to> - full path of entry after action (`nil` for permanent "delete" action).
380
+ ---@tag MiniFiles-events
381
+
382
+ --- # Toggle explorer ~
383
+ ---
384
+ --- Use a combination of |MiniFiles.open()| and |MiniFiles.close()|: >lua
385
+ ---
386
+ --- local minifiles_toggle = function(...)
387
+ --- if not MiniFiles.close() then MiniFiles.open(...) end
388
+ --- end
389
+ --- <
390
+ --- # Customize windows ~
391
+ ---
392
+ --- For most of the common customizations using `MiniFilesWindowOpen` event
393
+ --- autocommand is the suggested approach: >lua
394
+ ---
395
+ --- vim.api.nvim_create_autocmd('User', {
396
+ --- pattern = 'MiniFilesWindowOpen',
397
+ --- callback = function(args)
398
+ --- local win_id = args.data.win_id
399
+ ---
400
+ --- -- Customize window-local settings
401
+ --- vim.wo[win_id].winblend = 50
402
+ --- local config = vim.api.nvim_win_get_config(win_id)
403
+ --- config.border, config.title_pos = 'double', 'right'
404
+ --- vim.api.nvim_win_set_config(win_id, config)
405
+ --- end,
406
+ --- })
407
+ --- <
408
+ --- However, some parts (like window title and height) of window config are later
409
+ --- updated internally. Use `MiniFilesWindowUpdate` event for them: >lua
410
+ ---
411
+ --- vim.api.nvim_create_autocmd('User', {
412
+ --- pattern = 'MiniFilesWindowUpdate',
413
+ --- callback = function(args)
414
+ --- local config = vim.api.nvim_win_get_config(args.data.win_id)
415
+ ---
416
+ --- -- Ensure fixed height
417
+ --- config.height = 10
418
+ ---
419
+ --- -- Ensure no title padding
420
+ --- local n = #config.title
421
+ --- config.title[1][1] = config.title[1][1]:gsub('^ ', '')
422
+ --- config.title[n][1] = config.title[n][1]:gsub(' $', '')
423
+ ---
424
+ --- vim.api.nvim_win_set_config(args.data.win_id, config)
425
+ --- end,
426
+ --- })
427
+ --- <
428
+ --- # Customize icons ~
429
+ ---
430
+ --- Use different directory icon (if you don't use |mini.icons|): >lua
431
+ ---
432
+ --- local my_prefix = function(fs_entry)
433
+ --- if fs_entry.fs_type == 'directory' then
434
+ --- -- NOTE: it is usually a good idea to use icon followed by space
435
+ --- return ' ', 'MiniFilesDirectory'
436
+ --- end
437
+ --- return MiniFiles.default_prefix(fs_entry)
438
+ --- end
439
+ ---
440
+ --- require('mini.files').setup({ content = { prefix = my_prefix } })
441
+ --- <
442
+ --- Show no icons: >lua
443
+ ---
444
+ --- require('mini.files').setup({ content = { prefix = function() end } })
445
+ --- <
446
+ --- # Create mapping to show/hide dot-files ~
447
+ ---
448
+ --- Create an autocommand for `MiniFilesBufferCreate` event which calls
449
+ --- |MiniFiles.refresh()| with explicit `content.filter` functions: >lua
450
+ ---
451
+ --- local show_dotfiles = true
452
+ ---
453
+ --- local filter_show = function(fs_entry) return true end
454
+ ---
455
+ --- local filter_hide = function(fs_entry)
456
+ --- return not vim.startswith(fs_entry.name, '.')
457
+ --- end
458
+ ---
459
+ --- local toggle_dotfiles = function()
460
+ --- show_dotfiles = not show_dotfiles
461
+ --- local new_filter = show_dotfiles and filter_show or filter_hide
462
+ --- MiniFiles.refresh({ content = { filter = new_filter } })
463
+ --- end
464
+ ---
465
+ --- vim.api.nvim_create_autocmd('User', {
466
+ --- pattern = 'MiniFilesBufferCreate',
467
+ --- callback = function(args)
468
+ --- local buf_id = args.data.buf_id
469
+ --- -- Tweak left-hand side of mapping to your liking
470
+ --- vim.keymap.set('n', 'g.', toggle_dotfiles, { buffer = buf_id })
471
+ --- end,
472
+ --- })
473
+ --- <
474
+ --- # Create mappings to modify target window via split ~
475
+ ---
476
+ --- Combine |MiniFiles.get_explorer_state()| and |MiniFiles.set_target_window()|: >lua
477
+ ---
478
+ --- local map_split = function(buf_id, lhs, direction)
479
+ --- local rhs = function()
480
+ --- -- Make new window and set it as target
481
+ --- local cur_target = MiniFiles.get_explorer_state().target_window
482
+ --- local new_target = vim.api.nvim_win_call(cur_target, function()
483
+ --- vim.cmd(direction .. ' split')
484
+ --- return vim.api.nvim_get_current_win()
485
+ --- end)
486
+ ---
487
+ --- MiniFiles.set_target_window(new_target)
488
+ ---
489
+ --- -- This intentionally doesn't act on file under cursor in favor of
490
+ --- -- explicit "go in" action (`l` / `L`). To immediately open file,
491
+ --- -- add appropriate `MiniFiles.go_in()` call instead of this comment.
492
+ --- end
493
+ ---
494
+ --- -- Adding `desc` will result into `show_help` entries
495
+ --- local desc = 'Split ' .. direction
496
+ --- vim.keymap.set('n', lhs, rhs, { buffer = buf_id, desc = desc })
497
+ --- end
498
+ ---
499
+ --- vim.api.nvim_create_autocmd('User', {
500
+ --- pattern = 'MiniFilesBufferCreate',
501
+ --- callback = function(args)
502
+ --- local buf_id = args.data.buf_id
503
+ --- -- Tweak keys to your liking
504
+ --- map_split(buf_id, '<C-s>', 'belowright horizontal')
505
+ --- map_split(buf_id, '<C-v>', 'belowright vertical')
506
+ --- map_split(buf_id, '<C-t>', 'tab')
507
+ --- end,
508
+ --- })
509
+ --- <
510
+ --- # Create mappings which use data from entry under cursor ~
511
+ ---
512
+ --- Use |MiniFiles.get_fs_entry()|: >lua
513
+ ---
514
+ --- -- Set focused directory as current working directory
515
+ --- local set_cwd = function()
516
+ --- local path = (MiniFiles.get_fs_entry() or {}).path
517
+ --- if path == nil then return vim.notify('Cursor is not on valid entry') end
518
+ --- vim.fn.chdir(vim.fs.dirname(path))
519
+ --- end
520
+ ---
521
+ --- -- Yank in register full path of entry under cursor
522
+ --- local yank_path = function()
523
+ --- local path = (MiniFiles.get_fs_entry() or {}).path
524
+ --- if path == nil then return vim.notify('Cursor is not on valid entry') end
525
+ --- vim.fn.setreg(vim.v.register, path)
526
+ --- end
527
+ ---
528
+ --- -- Open path with system default handler (useful for non-text files)
529
+ --- local ui_open = function() vim.ui.open(MiniFiles.get_fs_entry().path) end
530
+ ---
531
+ --- vim.api.nvim_create_autocmd('User', {
532
+ --- pattern = 'MiniFilesBufferCreate',
533
+ --- callback = function(args)
534
+ --- local b = args.data.buf_id
535
+ --- vim.keymap.set('n', 'g~', set_cwd, { buffer = b, desc = 'Set cwd' })
536
+ --- vim.keymap.set('n', 'gX', ui_open, { buffer = b, desc = 'OS open' })
537
+ --- vim.keymap.set('n', 'gy', yank_path, { buffer = b, desc = 'Yank path' })
538
+ --- end,
539
+ --- })
540
+ --- <
541
+ --- # Set custom bookmarks ~
542
+ ---
543
+ --- Use |MiniFiles.set_bookmark()| inside `MiniFilesExplorerOpen` event: >lua
544
+ ---
545
+ --- local set_mark = function(id, path, desc)
546
+ --- MiniFiles.set_bookmark(id, path, { desc = desc })
547
+ --- end
548
+ --- vim.api.nvim_create_autocmd('User', {
549
+ --- pattern = 'MiniFilesExplorerOpen',
550
+ --- callback = function()
551
+ --- set_mark('c', vim.fn.stdpath('config'), 'Config') -- path
552
+ --- set_mark('w', vim.fn.getcwd, 'Working directory') -- callable
553
+ --- set_mark('~', '~', 'Home directory')
554
+ --- end,
555
+ --- })
556
+ --- <
557
+ ---@tag MiniFiles-examples
558
+
559
+ ---@diagnostic disable:luadoc-miss-type-name
560
+ ---@alias __minifiles_fs_entry_data_fields - <fs_type> `(string)` - one of "file" or "directory".
561
+ --- - <name> `(string)` - basename of an entry (including extension).
562
+ --- - <path> `(string)` - full path of an entry.
563
+
564
+ ---@diagnostic disable:undefined-field
565
+ ---@diagnostic disable:discard-returns
566
+ ---@diagnostic disable:unused-local
567
+ ---@diagnostic disable:cast-local-type
568
+
569
+ -- Module definition ==========================================================
570
+ local MiniFiles = {}
571
+ local H = {}
572
+
573
+ --- Module setup
574
+ ---
575
+ ---@param config table|nil Module config table. See |MiniFiles.config|.
576
+ ---
577
+ ---@usage >lua
578
+ --- require('mini.files').setup() -- use default config
579
+ --- -- OR
580
+ --- require('mini.files').setup({}) -- replace {} with your config table
581
+ --- <
582
+ MiniFiles.setup = function(config)
583
+ -- TODO: Remove after Neovim=0.9 support is dropped
584
+ if vim.fn.has('nvim-0.10') == 0 then
585
+ vim.notify(
586
+ '(mini.files) Neovim<0.10 is soft deprecated (module works but is not supported).'
587
+ .. " It will be deprecated after the next 'mini.nvim' release (module might not work)."
588
+ .. ' Please update your Neovim version.'
589
+ )
590
+ end
591
+
592
+ -- Export module
593
+ _G.MiniFiles = MiniFiles
594
+
595
+ -- Setup config
596
+ config = H.setup_config(config)
597
+
598
+ -- Apply config
599
+ H.apply_config(config)
600
+
601
+ -- Define behavior
602
+ H.create_autocommands(config)
603
+
604
+ -- Create default highlighting
605
+ H.create_default_hl()
606
+ end
607
+
608
+ --stylua: ignore
609
+ --- Defaults ~
610
+ ---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
611
+ ---@text # Content ~
612
+ ---
613
+ --- `content.filter` is a predicate which takes file system entry data as input
614
+ --- and returns `true`-ish value if it should be shown.
615
+ --- Uses |MiniFiles.default_filter()| by default.
616
+ ---
617
+ --- A file system entry data is a table with the following fields:
618
+ --- __minifiles_fs_entry_data_fields
619
+ ---
620
+ --- `content.highlight` describes how file system entry name should be highlighted.
621
+ --- Takes file system entry data as input and returns a highlight group name.
622
+ --- Uses |MiniFiles.default_highlight()| by default.
623
+ ---
624
+ --- `content.prefix` describes what text (prefix) to show to the left of file
625
+ --- system entry name (if any) and how to highlight it. It also takes file
626
+ --- system entry data as input and returns tuple of text and highlight group
627
+ --- name to be used to highlight prefix. See |MiniFiles-examples| for common
628
+ --- examples of how to use it.
629
+ --- Note: due to how lines are parsed to detect user edits for file system
630
+ --- manipulation, output of `content.prefix` should not contain `/` character.
631
+ --- Uses |MiniFiles.default_prefix()| by default.
632
+ ---
633
+ --- `content.sort` describes in which order directory entries should be shown
634
+ --- in directory buffer. Takes as input and returns as output an array of file
635
+ --- system entry data. Note: technically, it can be used to filter and modify
636
+ --- its elements as well.
637
+ --- Uses |MiniFiles.default_sort()| by default.
638
+ ---
639
+ --- # Mappings ~
640
+ ---
641
+ --- `mappings` table can be used to customize buffer-local mappings created in each
642
+ --- directory buffer for built-in actions. Entry name corresponds to the function
643
+ --- name of the action, value - right hand side of the mapping. Supply empty
644
+ --- string to not create a particular mapping.
645
+ ---
646
+ --- Default mappings are mostly designed for consistent navigation experience.
647
+ --- Here are some alternatives: >lua
648
+ ---
649
+ --- -- Close explorer after opening file with `l`
650
+ --- mappings = {
651
+ --- go_in = 'L',
652
+ --- go_in_plus = 'l',
653
+ --- }
654
+ ---
655
+ --- -- Don't use `h`/`l` for easier cursor navigation during text edit
656
+ --- mappings = {
657
+ --- go_in = 'L',
658
+ --- go_in_plus = '',
659
+ --- go_out = 'H',
660
+ --- go_out_plus = '',
661
+ --- }
662
+ --- <
663
+ --- # Options ~
664
+ ---
665
+ --- `options.use_as_default_explorer` is a boolean indicating whether this module
666
+ --- will be used as a default file explorer to edit directory (instead of `netrw`).
667
+ --- Note: to work with directory in |arglist|, do not lazy load this module.
668
+ ---
669
+ --- `options.permanent_delete` is a boolean indicating whether to perform
670
+ --- permanent delete or move into special trash directory.
671
+ --- This is a module-specific variant of "remove to trash".
672
+ --- Target directory is `mini.files/trash` inside standard path of Neovim data
673
+ --- directory (execute `:echo stdpath('data')` to see its path in your case).
674
+ ---
675
+ --- `options.lsp_timeout` is a number that defines a timeout for synchronous
676
+ --- LSP integration requests (see |MiniFiles-manipulation|).
677
+ --- Set to 0 to disable LSP integration.
678
+ ---
679
+ --- # Windows ~
680
+ ---
681
+ --- `windows.max_number` is a maximum number of windows allowed to be open
682
+ --- simultaneously. For example, use value 1 to always show single window.
683
+ --- There is no constraint by default.
684
+ ---
685
+ --- `windows.preview` is a boolean indicating whether to show preview of
686
+ --- file/directory under cursor. Notes:
687
+ --- - It is always shown, even if current line is for not yet existing path.
688
+ --- - File preview is highlighted if its size is small enough (less than 1K
689
+ --- bytes per line or 1M bytes in total).
690
+ ---
691
+ --- `windows.width_focus`, `windows.width_nofocus` and `windows.width_preview`
692
+ --- are number of columns used as `width` for the corresponding window type.
693
+ MiniFiles.config = {
694
+ -- Customization of shown content
695
+ content = {
696
+ -- Predicate for which file system entries to show
697
+ filter = nil,
698
+ -- Highlight group to use for a file system entry
699
+ highlight = nil,
700
+ -- Prefix text and highlight to show to the left of file system entry
701
+ prefix = nil,
702
+ -- Order in which to show file system entries
703
+ sort = nil,
704
+ },
705
+
706
+ -- Module mappings created only inside explorer.
707
+ -- Use `''` (empty string) to not create one.
708
+ mappings = {
709
+ close = 'q',
710
+ go_in = 'l',
711
+ go_in_plus = 'L',
712
+ go_out = 'h',
713
+ go_out_plus = 'H',
714
+ mark_goto = "'",
715
+ mark_set = 'm',
716
+ reset = '<BS>',
717
+ reveal_cwd = '@',
718
+ show_help = 'g?',
719
+ synchronize = '=',
720
+ trim_left = '<',
721
+ trim_right = '>',
722
+ },
723
+
724
+ -- General options
725
+ options = {
726
+ -- Whether to delete permanently or move into module-specific trash
727
+ permanent_delete = true,
728
+ -- Whether to use for editing directories
729
+ use_as_default_explorer = true,
730
+ -- Timeout for synchronous LSP integration requests
731
+ lsp_timeout = 1000,
732
+ },
733
+
734
+ -- Customization of explorer windows
735
+ windows = {
736
+ -- Maximum number of windows to show side by side
737
+ max_number = math.huge,
738
+ -- Whether to show preview of file/directory under cursor
739
+ preview = false,
740
+ -- Width of focused window
741
+ width_focus = 50,
742
+ -- Width of non-focused window
743
+ width_nofocus = 15,
744
+ -- Width of preview window
745
+ width_preview = 25,
746
+ },
747
+ }
748
+ --minidoc_afterlines_end
749
+
750
+ --- Open file explorer
751
+ ---
752
+ --- Common ways to use this function: >lua
753
+ ---
754
+ --- -- Open current working directory in a last used state
755
+ --- MiniFiles.open()
756
+ ---
757
+ --- -- Fresh explorer in current working directory
758
+ --- MiniFiles.open(nil, false)
759
+ ---
760
+ --- -- Open directory of current file (in last used state) focused on the file
761
+ --- MiniFiles.open(vim.api.nvim_buf_get_name(0))
762
+ ---
763
+ --- -- Fresh explorer in directory of current file
764
+ --- MiniFiles.open(vim.api.nvim_buf_get_name(0), false)
765
+ ---
766
+ --- -- Open last used `path` (per tabpage)
767
+ --- -- Current working directory for the first time
768
+ --- MiniFiles.open(MiniFiles.get_latest_path())
769
+ --- <
770
+ ---@param path string|nil A valid file system path used as anchor.
771
+ --- If it is a path to directory, used directly.
772
+ --- If it is a path to file, its parent directory is used as anchor while
773
+ --- explorer will focus on the supplied file.
774
+ --- Default: path of |current-directory|.
775
+ ---@param use_latest boolean|nil Whether to load explorer state from history
776
+ --- (based on the supplied anchor path). Default: `true`.
777
+ ---@param opts table|nil Table of options overriding |MiniFiles.config| and
778
+ --- `vim.b.minifiles_config` for this particular explorer session.
779
+ MiniFiles.open = function(path, use_latest, opts)
780
+ -- Validate path: allow only valid file system path
781
+ path = H.fs_full_path(path or vim.fn.getcwd())
782
+
783
+ local fs_type = H.fs_get_type(path)
784
+ if fs_type == nil then H.error('`path` is not a valid path ("' .. path .. '")') end
785
+
786
+ -- - Allow file path to use its parent while focusing on file
787
+ local entry_name
788
+ if fs_type == 'file' then
789
+ path, entry_name = H.fs_get_parent(path), H.fs_get_basename(path)
790
+ end
791
+
792
+ -- Validate rest of the arguments
793
+ if use_latest == nil then use_latest = true end
794
+
795
+ -- Properly close possibly opened in the tabpage explorer
796
+ local did_close = MiniFiles.close()
797
+ if did_close == false then return end
798
+
799
+ -- Get explorer to open
800
+ local explorer
801
+ if use_latest then explorer = H.explorer_path_history[path] end
802
+ explorer = explorer or H.explorer_new(path)
803
+
804
+ -- Update explorer data. Don't use current explorer's data to allow more
805
+ -- interactive config change by modifying global/local configs.
806
+ explorer.opts = H.normalize_opts(nil, opts)
807
+ explorer.target_window = vim.api.nvim_get_current_win()
808
+
809
+ -- Possibly focus on file entry
810
+ explorer = H.explorer_focus_on_entry(explorer, path, entry_name)
811
+
812
+ -- Refresh and register as opened
813
+ H.explorer_refresh(explorer)
814
+
815
+ -- Register latest used path
816
+ H.latest_paths[vim.api.nvim_get_current_tabpage()] = path
817
+
818
+ -- Track lost focus
819
+ H.explorer_track_lost_focus()
820
+
821
+ -- Adjust some `vim.ui` methods to not track lost focus when they are active,
822
+ -- since most of their implementations use separate floating windows
823
+ H.explorer_adjust_vim_ui()
824
+
825
+ -- Trigger appropriate event
826
+ H.trigger_event('MiniFilesExplorerOpen')
827
+ end
828
+
829
+ --- Refresh explorer
830
+ ---
831
+ --- Notes:
832
+ --- - If in `opts` at least one of `content` entry is not `nil`, all directory
833
+ --- buffers are forced to update.
834
+ ---
835
+ ---@param opts table|nil Table of options overriding local options of active
836
+ --- explorer session.
837
+ MiniFiles.refresh = function(opts)
838
+ local explorer = H.explorer_get()
839
+ if explorer == nil then return end
840
+
841
+ -- Decide whether buffers should be forcefully updated
842
+ local content_opts = (opts or {}).content or {}
843
+ local force_update = #vim.tbl_keys(content_opts) > 0
844
+
845
+ -- Confirm refresh if there is modified buffer
846
+ if force_update then force_update = H.explorer_ignore_pending_fs_actions(explorer, 'Update buffers') end
847
+
848
+ -- Respect explorer local options supplied inside its `open()` call but give
849
+ -- current `opts` higher precedence
850
+ explorer.opts = H.normalize_opts(explorer.opts, opts)
851
+
852
+ H.explorer_refresh(explorer, { force_update = force_update })
853
+ end
854
+
855
+ --- Synchronize explorer
856
+ ---
857
+ --- - Parse user edits in directory buffers.
858
+ --- - Convert edits to file system actions and apply them after confirmation.
859
+ --- Choosing "No" skips application while "Cancel" stops synchronization.
860
+ --- - Update all directory buffers with the most relevant file system information.
861
+ --- Can be used without user edits to account for external file system changes.
862
+ ---
863
+ ---@return boolean Whether synchronization was done.
864
+ MiniFiles.synchronize = function()
865
+ local explorer = H.explorer_get()
866
+ if explorer == nil then return end
867
+
868
+ -- Parse and apply file system operations
869
+ local fs_actions = H.explorer_compute_fs_actions(explorer)
870
+ if fs_actions ~= nil then
871
+ local msg = table.concat(H.fs_actions_to_lines(fs_actions), '\n')
872
+ local confirm_res = vim.fn.confirm(msg, '&Yes\n&No\n&Cancel', 1, 'Question')
873
+ if confirm_res == 3 then return false end
874
+ if confirm_res == 1 then H.fs_actions_apply(fs_actions, explorer.opts.options.lsp_timeout) end
875
+ end
876
+
877
+ H.explorer_refresh(explorer, { force_update = true })
878
+ return true
879
+ end
880
+
881
+ --- Reset explorer
882
+ ---
883
+ --- - Show single window focused on anchor directory (which was used as first
884
+ --- argument for |MiniFiles.open()|).
885
+ --- - Reset all tracked directory cursors to point at first entry.
886
+ MiniFiles.reset = function()
887
+ local explorer = H.explorer_get()
888
+ if explorer == nil then return end
889
+
890
+ -- Reset branch
891
+ explorer.branch = { explorer.anchor }
892
+ explorer.depth_focus = 1
893
+
894
+ -- Reset views
895
+ for _, view in pairs(explorer.views) do
896
+ view.cursor = { 1, 0 }
897
+ end
898
+
899
+ -- Skip update cursors, as they are already set
900
+ H.explorer_refresh(explorer, { skip_update_cursor = true })
901
+ end
902
+
903
+ --- Close explorer
904
+ ---
905
+ ---@return boolean|nil Whether closing was done or `nil` if there was nothing to close.
906
+ MiniFiles.close = function()
907
+ -- Stop possible tracking lost focus
908
+ pcall(vim.loop.timer_stop, H.timers.focus)
909
+
910
+ -- Act if there is explorer to close (even invisible after improper quit)
911
+ local explorer = H.explorer_get(nil, true)
912
+ if explorer == nil then return nil end
913
+
914
+ -- Confirm close if there is modified buffer
915
+ if not H.explorer_ignore_pending_fs_actions(explorer, 'Close') then return false end
916
+
917
+ -- Unadjust `vim.ui` methods
918
+ vim.ui.select = H.vim_ui.select or vim.ui.select
919
+ vim.ui.input = H.vim_ui.input or vim.ui.input
920
+ H.vim_ui = { select_active = false, input_active = false }
921
+
922
+ -- Trigger appropriate event
923
+ H.trigger_event('MiniFilesExplorerClose')
924
+
925
+ -- Focus on target window
926
+ explorer = H.explorer_ensure_target_window(explorer)
927
+ -- - Use `pcall()` because window might still be invalid
928
+ pcall(vim.api.nvim_set_current_win, explorer.target_window)
929
+
930
+ -- Update currently shown cursors
931
+ explorer = H.explorer_update_cursors(explorer)
932
+
933
+ -- Close shown explorer windows
934
+ for i, win_id in pairs(explorer.windows) do
935
+ H.window_close(win_id)
936
+ explorer.windows[i] = nil
937
+ end
938
+
939
+ -- Close possibly visible help window
940
+ for _, win_id in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
941
+ local buf_id = vim.api.nvim_win_get_buf(win_id)
942
+ if vim.bo[buf_id].filetype == 'minifiles-help' then vim.api.nvim_win_close(win_id, true) end
943
+ end
944
+
945
+ -- Invalidate views
946
+ for path, view in pairs(explorer.views) do
947
+ explorer.views[path] = H.view_invalidate_buffer(H.view_encode_cursor(view))
948
+ end
949
+
950
+ -- Update histories and unmark as opened
951
+ H.explorer_path_history[explorer.anchor] = explorer
952
+ H.opened_explorers[explorer.tabpage_id] = nil
953
+
954
+ -- Return `true` indicating success in closing
955
+ return true
956
+ end
957
+
958
+ --- Go in entry under cursor
959
+ ---
960
+ --- Depends on entry under cursor:
961
+ --- - If directory, focus on it in the window to the right.
962
+ --- - If file, open it in the window which was current during |MiniFiles.open()|.
963
+ --- Explorer is not closed after that.
964
+ ---
965
+ ---@param opts table|nil Options. Possible fields:
966
+ --- - <close_on_file> `(boolean)` - whether to close explorer after going
967
+ --- inside a file. Powers the `go_in_plus` mapping.
968
+ --- Default: `false`.
969
+ MiniFiles.go_in = function(opts)
970
+ local explorer = H.explorer_get()
971
+ if explorer == nil then return end
972
+
973
+ opts = vim.tbl_deep_extend('force', { close_on_file = false }, opts or {})
974
+
975
+ local should_close = opts.close_on_file
976
+ if should_close then
977
+ local fs_entry = MiniFiles.get_fs_entry()
978
+ should_close = fs_entry ~= nil and fs_entry.fs_type == 'file'
979
+ end
980
+
981
+ local cur_line = vim.fn.line('.')
982
+ explorer = H.explorer_go_in_range(explorer, vim.api.nvim_get_current_buf(), cur_line, cur_line)
983
+
984
+ H.explorer_refresh(explorer)
985
+
986
+ if should_close then MiniFiles.close() end
987
+ end
988
+
989
+ --- Go out to parent directory
990
+ ---
991
+ --- - Focus on window to the left showing parent of current directory.
992
+ MiniFiles.go_out = function()
993
+ local explorer = H.explorer_get()
994
+ if explorer == nil then return end
995
+
996
+ if explorer.depth_focus == 1 then
997
+ explorer = H.explorer_open_root_parent(explorer)
998
+ else
999
+ explorer.depth_focus = explorer.depth_focus - 1
1000
+ end
1001
+
1002
+ H.explorer_refresh(explorer)
1003
+ end
1004
+
1005
+ --- Trim left part of branch
1006
+ ---
1007
+ --- - Remove all branch paths to the left of currently focused one. This also
1008
+ --- results into current window becoming the most left one.
1009
+ MiniFiles.trim_left = function()
1010
+ local explorer = H.explorer_get()
1011
+ if explorer == nil then return end
1012
+
1013
+ explorer = H.explorer_trim_branch_left(explorer)
1014
+ H.explorer_refresh(explorer)
1015
+ end
1016
+
1017
+ --- Trim right part of branch
1018
+ ---
1019
+ --- - Remove all branch paths to the right of currently focused one. This also
1020
+ --- results into current window becoming the most right one.
1021
+ MiniFiles.trim_right = function()
1022
+ local explorer = H.explorer_get()
1023
+ if explorer == nil then return end
1024
+
1025
+ explorer = H.explorer_trim_branch_right(explorer)
1026
+ H.explorer_refresh(explorer)
1027
+ end
1028
+
1029
+ --- Reveal current working directory
1030
+ ---
1031
+ --- - Prepend branch with parent paths until current working directory is reached.
1032
+ --- Do nothing if not inside it.
1033
+ MiniFiles.reveal_cwd = function()
1034
+ local state = MiniFiles.get_explorer_state()
1035
+ if state == nil then return end
1036
+ local branch, depth_focus = state.branch, state.depth_focus
1037
+
1038
+ local cwd = H.fs_full_path(vim.fn.getcwd())
1039
+ local cwd_ancestor_pattern = string.format('^%s%s.', vim.pesc(cwd), cwd == '/' and '' or '/')
1040
+ while branch[1]:find(cwd_ancestor_pattern) ~= nil do
1041
+ table.insert(branch, 1, H.fs_get_parent(branch[1]))
1042
+ depth_focus = depth_focus + 1
1043
+ end
1044
+
1045
+ MiniFiles.set_branch(branch, { depth_focus = depth_focus })
1046
+ end
1047
+
1048
+ --- Show help window
1049
+ ---
1050
+ --- - Open window with helpful information about currently shown explorer and
1051
+ --- focus on it. To close it, press `q`.
1052
+ MiniFiles.show_help = function()
1053
+ local explorer = H.explorer_get()
1054
+ if explorer == nil then return end
1055
+
1056
+ local buf_id = vim.api.nvim_get_current_buf()
1057
+ if not H.is_opened_buffer(buf_id) then return end
1058
+
1059
+ H.explorer_show_help(explorer, buf_id, vim.api.nvim_get_current_win())
1060
+ end
1061
+
1062
+ --- Get file system entry data
1063
+ ---
1064
+ ---@param buf_id number|nil Buffer identifier of valid directory buffer.
1065
+ --- Default: current buffer.
1066
+ ---@param line number|nil Line number of entry for which to return information.
1067
+ --- Default: cursor line.
1068
+ ---
1069
+ ---@return table|nil Table of file system entry data with the following fields:
1070
+ --- __minifiles_fs_entry_data_fields
1071
+ ---
1072
+ --- Returns `nil` if there is no proper file system entry path at the line.
1073
+ MiniFiles.get_fs_entry = function(buf_id, line)
1074
+ buf_id = H.validate_opened_buffer(buf_id)
1075
+ line = H.validate_line(buf_id, line)
1076
+
1077
+ local path_id = H.match_line_path_id(H.get_bufline(buf_id, line))
1078
+ return H.get_fs_entry_from_path_index(path_id)
1079
+ end
1080
+
1081
+ --- Get state of active explorer
1082
+ ---
1083
+ ---@return table|nil Table with explorer state data or `nil` if no active explorer.
1084
+ --- State data is a table with the following fields:
1085
+ --- - <anchor> `(string)` - anchor directory path (see |MiniFiles.open()|).
1086
+ --- - <bookmarks> `(table)` - map from bookmark id (single character) to its data:
1087
+ --- table with <path> and <desc> fields (see |MiniFiles.set_bookmark()|).
1088
+ --- - <branch> `(table)` - array of nested paths for currently opened branch.
1089
+ --- - <depth_focus> `(number)` - an index in <branch> for currently focused path.
1090
+ --- - <target_window> `(number)` - identifier of target window.
1091
+ --- - <windows> `(table)` - array with data about currently opened windows.
1092
+ --- Each element is a table with <win_id> (window identifier) and <path> (path
1093
+ --- shown in the window) fields.
1094
+ ---
1095
+ ---@seealso - |MiniFiles.set_bookmark()|
1096
+ --- - |MiniFiles.set_branch()|
1097
+ --- - |MiniFiles.set_target_window()|
1098
+ MiniFiles.get_explorer_state = function()
1099
+ local explorer = H.explorer_get()
1100
+ if explorer == nil then return end
1101
+
1102
+ H.explorer_ensure_target_window(explorer)
1103
+ local windows = {}
1104
+ for _, win_id in ipairs(explorer.windows) do
1105
+ local buf_id = vim.api.nvim_win_get_buf(win_id)
1106
+ local path = (H.opened_buffers[buf_id] or {}).path
1107
+ table.insert(windows, { win_id = win_id, path = path })
1108
+ end
1109
+
1110
+ return {
1111
+ anchor = explorer.anchor,
1112
+ bookmarks = vim.deepcopy(explorer.bookmarks),
1113
+ branch = vim.deepcopy(explorer.branch),
1114
+ depth_focus = explorer.depth_focus,
1115
+ target_window = explorer.target_window,
1116
+ windows = windows,
1117
+ }
1118
+ end
1119
+
1120
+ --- Set target window
1121
+ ---
1122
+ ---@param win_id number Window identifier inside which file will be opened.
1123
+ MiniFiles.set_target_window = function(win_id)
1124
+ if not H.is_valid_win(win_id) then H.error('`win_id` should be valid window identifier.') end
1125
+
1126
+ local explorer = H.explorer_get()
1127
+ if explorer == nil then return end
1128
+
1129
+ explorer.target_window = win_id
1130
+ end
1131
+
1132
+ --- Set branch
1133
+ ---
1134
+ --- Set which paths to display. Preview (if enabled) is applied afterwards.
1135
+ ---
1136
+ ---@param branch table Array of strings representing actually present on disk paths.
1137
+ --- Each consecutive pair should represent direct parent-child paths.
1138
+ --- Should contain at least one directory path.
1139
+ --- May end with file path (will be previewed).
1140
+ --- Relative paths are resolved using |current-directory|.
1141
+ ---@param opts table|nil Options. Possible fields:
1142
+ --- - <depth_focus> `(number)` - an index in `branch` for path to focus. Will
1143
+ --- be normalized to fit inside `branch`. Default: index of deepest directory.
1144
+ ---
1145
+ ---@seealso |MiniFiles.get_explorer_state()|
1146
+ MiniFiles.set_branch = function(branch, opts)
1147
+ local explorer = H.explorer_get()
1148
+ if explorer == nil then return end
1149
+
1150
+ -- Validate and normalize input
1151
+ branch = H.validate_branch(branch)
1152
+ opts = opts or {}
1153
+ local depth_focus = opts.depth_focus or math.huge
1154
+ if type(depth_focus) ~= 'number' then H.error('`depth_focus` should be a number') end
1155
+ local max_depth = #branch - (H.fs_get_type(branch[#branch]) == 'file' and 1 or 0)
1156
+ depth_focus = math.min(math.max(math.floor(depth_focus), 1), max_depth)
1157
+
1158
+ -- Set data and ensure cursors are on child entries
1159
+ explorer.branch, explorer.depth_focus = branch, depth_focus
1160
+ for i = 1, #branch - 1 do
1161
+ local parent, child = branch[i], H.fs_get_basename(branch[i + 1])
1162
+ local parent_view = explorer.views[parent] or {}
1163
+ parent_view.cursor = child
1164
+ explorer.views[parent] = parent_view
1165
+ end
1166
+
1167
+ -- Skip update cursors, as they are already set
1168
+ H.explorer_refresh(explorer, { skip_update_cursor = true })
1169
+ -- Refresh second time to ensure that preview is shown. Doing that in other
1170
+ -- way is not really feasible, as it requires knowing cursor at deepest path,
1171
+ -- which might not yet be set before first refresh.
1172
+ H.explorer_refresh(explorer)
1173
+ end
1174
+
1175
+ --- Set bookmark
1176
+ ---
1177
+ ---@param id string Single character bookmark id.
1178
+ ---@param path string|function Path of a present on disk directory to set as
1179
+ --- a bookmark's path. If callable, should return such path.
1180
+ ---@param opts table|nil Options. Possible fields:
1181
+ --- - <desc> `(string)` - bookmark description (used in help window).
1182
+ MiniFiles.set_bookmark = function(id, path, opts)
1183
+ local explorer = H.explorer_get()
1184
+ if explorer == nil then return end
1185
+
1186
+ if not (type(id) == 'string' and id:len() == 1) then H.error('Bookmark id should be single character') end
1187
+ local is_valid_path = vim.is_callable(path)
1188
+ or (type(path) == 'string' and H.fs_get_type(vim.fn.expand(path)) == 'directory')
1189
+ if not is_valid_path then H.error('Bookmark path should be a valid path to directory or a callable.') end
1190
+ opts = opts or {}
1191
+ if not (opts.desc == nil or type(opts.desc) == 'string') then H.error('Bookmark description should be string') end
1192
+
1193
+ explorer.bookmarks[id] = { path = path, desc = opts.desc }
1194
+ end
1195
+
1196
+ --- Get latest used anchor path
1197
+ ---
1198
+ --- Note: if latest used `path` argument for |MiniFiles.open()| was for file,
1199
+ --- this will return its parent (as it was used as anchor path).
1200
+ MiniFiles.get_latest_path = function() return H.latest_paths[vim.api.nvim_get_current_tabpage()] end
1201
+
1202
+ --- Default filter of file system entries
1203
+ ---
1204
+ --- Currently does not filter anything out.
1205
+ ---
1206
+ ---@param fs_entry table Table with the following fields:
1207
+ --- __minifiles_fs_entry_data_fields
1208
+ ---
1209
+ ---@return boolean Always `true`.
1210
+ MiniFiles.default_filter = function(fs_entry) return true end
1211
+
1212
+ --- Default prefix of file system entries
1213
+ ---
1214
+ --- - If set up |mini.icons|, use |MiniIcons.get()| for "directory"/"file" category.
1215
+ --- - Otherwise:
1216
+ --- - For directory return fixed icon and "MiniFilesDirectory" group name.
1217
+ --- - For file try to use `get_icon()` from `nvim-tree/nvim-web-devicons`.
1218
+ --- If missing, return fixed icon and `MiniFilesFile` group name.
1219
+ ---
1220
+ ---@param fs_entry table Table with the following fields:
1221
+ --- __minifiles_fs_entry_data_fields
1222
+ ---
1223
+ ---@return ... Icon and highlight group name. For more details, see |MiniFiles.config|
1224
+ --- and |MiniFiles-examples|.
1225
+ MiniFiles.default_prefix = function(fs_entry)
1226
+ -- Prefer 'mini.icons'
1227
+ if _G.MiniIcons ~= nil then
1228
+ local category = fs_entry.fs_type == 'directory' and 'directory' or 'file'
1229
+ local icon, hl = _G.MiniIcons.get(category, fs_entry.path)
1230
+ return icon .. ' ', hl
1231
+ end
1232
+
1233
+ -- Try falling back to 'nvim-web-devicons'
1234
+ if fs_entry.fs_type == 'directory' then return ' ', 'MiniFilesDirectory' end
1235
+ local has_devicons, devicons = pcall(require, 'nvim-web-devicons')
1236
+ if not has_devicons then return ' ', 'MiniFilesFile' end
1237
+
1238
+ local icon, hl = devicons.get_icon(fs_entry.name, nil, { default = false })
1239
+ return (icon or '') .. ' ', hl or 'MiniFilesFile'
1240
+ end
1241
+
1242
+ --- Default sort of file system entries
1243
+ ---
1244
+ --- Sort directories and files separately (alphabetically ignoring case) and
1245
+ --- put directories first.
1246
+ ---
1247
+ ---@param fs_entries table Array of file system entry data.
1248
+ --- Each one is a table with the following fields:
1249
+ --- __minifiles_fs_entry_data_fields
1250
+ ---
1251
+ ---@return table Sorted array of file system entries.
1252
+ MiniFiles.default_sort = function(fs_entries)
1253
+ -- Sort ignoring case
1254
+ local res = vim.tbl_map(
1255
+ function(x)
1256
+ return {
1257
+ fs_type = x.fs_type,
1258
+ name = x.name,
1259
+ path = x.path,
1260
+ lower_name = x.name:lower(),
1261
+ is_dir = x.fs_type == 'directory',
1262
+ }
1263
+ end,
1264
+ fs_entries
1265
+ )
1266
+
1267
+ -- Sort based on default order
1268
+ table.sort(res, H.compare_fs_entries)
1269
+
1270
+ return vim.tbl_map(function(x) return { name = x.name, fs_type = x.fs_type, path = x.path } end, res)
1271
+ end
1272
+
1273
+ --- Default file system entry highlight
1274
+ ---
1275
+ --- Returns `'MiniFilesDirectory'` for directory and `'MiniFilesFile'` otherwise.
1276
+ ---
1277
+ ---@param fs_entry table Table with the following fields:
1278
+ --- __minifiles_fs_entry_data_fields
1279
+ ---
1280
+ ---@return string Highlight group name.
1281
+ MiniFiles.default_highlight = function(fs_entry)
1282
+ return fs_entry.fs_type == 'directory' and 'MiniFilesDirectory' or 'MiniFilesFile'
1283
+ end
1284
+
1285
+ -- Helper data ================================================================
1286
+ -- Module default config
1287
+ H.default_config = vim.deepcopy(MiniFiles.config)
1288
+
1289
+ -- Namespaces
1290
+ H.ns_id = {
1291
+ highlight = vim.api.nvim_create_namespace('MiniFilesHighlight'),
1292
+ }
1293
+
1294
+ -- Timers
1295
+ H.timers = {
1296
+ focus = vim.loop.new_timer(),
1297
+ }
1298
+
1299
+ -- Index of all visited files
1300
+ H.path_index = {}
1301
+
1302
+ -- History of explorers per root directory
1303
+ H.explorer_path_history = {}
1304
+
1305
+ -- Register of opened explorers per tabpage
1306
+ H.opened_explorers = {}
1307
+
1308
+ -- Register of latest used paths per tabpage
1309
+ H.latest_paths = {}
1310
+
1311
+ -- Register of opened buffer data for quick access. Tables per buffer id:
1312
+ -- - <path> - path which contents this buffer displays.
1313
+ -- - <children_path_ids> - array of shown children path ids.
1314
+ -- - <win_id> - id of window this buffer is shown. Can be `nil`.
1315
+ -- - <n_modified> - number of modifications since last update from this module.
1316
+ -- Values bigger than 0 can be treated as if buffer was modified by user.
1317
+ -- It uses number instead of boolean to overcome `TextChanged` event on
1318
+ -- initial `buf_set_lines` (`noautocmd` doesn't quick work for this event).
1319
+ H.opened_buffers = {}
1320
+
1321
+ -- Cache for `vim.ui` methods that are adjusted while explorer is open
1322
+ H.vim_ui = { select = nil, select_active = false, input = nil, input_active = false }
1323
+
1324
+ -- File system information
1325
+ H.is_windows = vim.loop.os_uname().sysname == 'Windows_NT'
1326
+
1327
+ -- Helper functionality =======================================================
1328
+ -- Settings -------------------------------------------------------------------
1329
+ H.setup_config = function(config)
1330
+ H.check_type('config', config, 'table', true)
1331
+ config = vim.tbl_deep_extend('force', vim.deepcopy(H.default_config), config or {})
1332
+
1333
+ H.check_type('content', config.content, 'table')
1334
+ H.check_type('content.filter', config.content.filter, 'function', true)
1335
+ H.check_type('content.highlight', config.content.highlight, 'function', true)
1336
+ H.check_type('content.prefix', config.content.prefix, 'function', true)
1337
+ H.check_type('content.sort', config.content.sort, 'function', true)
1338
+
1339
+ H.check_type('mappings', config.mappings, 'table')
1340
+ H.check_type('mappings.close', config.mappings.close, 'string')
1341
+ H.check_type('mappings.go_in', config.mappings.go_in, 'string')
1342
+ H.check_type('mappings.go_in_plus', config.mappings.go_in_plus, 'string')
1343
+ H.check_type('mappings.go_out', config.mappings.go_out, 'string')
1344
+ H.check_type('mappings.go_out_plus', config.mappings.go_out_plus, 'string')
1345
+ H.check_type('mappings.mark_goto', config.mappings.mark_goto, 'string')
1346
+ H.check_type('mappings.mark_set', config.mappings.mark_set, 'string')
1347
+ H.check_type('mappings.reset', config.mappings.reset, 'string')
1348
+ H.check_type('mappings.reveal_cwd', config.mappings.reveal_cwd, 'string')
1349
+ H.check_type('mappings.show_help', config.mappings.show_help, 'string')
1350
+ H.check_type('mappings.synchronize', config.mappings.synchronize, 'string')
1351
+ H.check_type('mappings.trim_left', config.mappings.trim_left, 'string')
1352
+ H.check_type('mappings.trim_right', config.mappings.trim_right, 'string')
1353
+
1354
+ H.check_type('options', config.options, 'table')
1355
+ H.check_type('options.use_as_default_explorer', config.options.use_as_default_explorer, 'boolean')
1356
+ H.check_type('options.permanent_delete', config.options.permanent_delete, 'boolean')
1357
+ H.check_type('options.lsp_timeout', config.options.lsp_timeout, 'number')
1358
+
1359
+ H.check_type('windows', config.windows, 'table')
1360
+ H.check_type('windows.max_number', config.windows.max_number, 'number')
1361
+ H.check_type('windows.preview', config.windows.preview, 'boolean')
1362
+ H.check_type('windows.width_focus', config.windows.width_focus, 'number')
1363
+ H.check_type('windows.width_nofocus', config.windows.width_nofocus, 'number')
1364
+ H.check_type('windows.width_preview', config.windows.width_preview, 'number')
1365
+
1366
+ return config
1367
+ end
1368
+
1369
+ H.apply_config = function(config) MiniFiles.config = config end
1370
+
1371
+ H.create_autocommands = function(config)
1372
+ local gr = vim.api.nvim_create_augroup('MiniFiles', {})
1373
+
1374
+ local au = function(event, pattern, callback, desc)
1375
+ vim.api.nvim_create_autocmd(event, { group = gr, pattern = pattern, callback = callback, desc = desc })
1376
+ end
1377
+
1378
+ if config.options.use_as_default_explorer then
1379
+ -- Stop 'netrw' from showing. Needs `VimEnter` event autocommand if
1380
+ -- this is called prior 'netrw' is set up
1381
+ vim.cmd('silent! autocmd! FileExplorer *')
1382
+ vim.cmd('autocmd VimEnter * ++once silent! autocmd! FileExplorer *')
1383
+
1384
+ au('BufEnter', '*', H.track_dir_edit, 'Track directory edit')
1385
+ end
1386
+
1387
+ au('VimResized', '*', MiniFiles.refresh, 'Refresh on resize')
1388
+ au('ColorScheme', '*', H.create_default_hl, 'Ensure colors')
1389
+ end
1390
+
1391
+ --stylua: ignore
1392
+ H.create_default_hl = function()
1393
+ local hi = function(name, opts)
1394
+ opts.default = true
1395
+ vim.api.nvim_set_hl(0, name, opts)
1396
+ end
1397
+
1398
+ hi('MiniFilesBorder', { link = 'FloatBorder' })
1399
+ hi('MiniFilesBorderModified', { link = 'DiagnosticFloatingWarn' })
1400
+ hi('MiniFilesCursorLine', { link = 'CursorLine' })
1401
+ hi('MiniFilesDirectory', { link = 'Directory' })
1402
+ hi('MiniFilesFile', {})
1403
+ hi('MiniFilesNormal', { link = 'NormalFloat' })
1404
+ hi('MiniFilesTitle', { link = 'FloatTitle' })
1405
+ hi('MiniFilesTitleFocused', { link = 'FloatTitle' })
1406
+ end
1407
+
1408
+ H.get_config = function(config)
1409
+ return vim.tbl_deep_extend('force', MiniFiles.config, vim.b.minifiles_config or {}, config or {})
1410
+ end
1411
+
1412
+ H.normalize_opts = function(explorer_opts, opts)
1413
+ opts = vim.tbl_deep_extend('force', H.get_config(), explorer_opts or {}, opts or {})
1414
+ opts.content.filter = opts.content.filter or MiniFiles.default_filter
1415
+ opts.content.highlight = opts.content.highlight or MiniFiles.default_highlight
1416
+ opts.content.prefix = opts.content.prefix or MiniFiles.default_prefix
1417
+ opts.content.sort = opts.content.sort or MiniFiles.default_sort
1418
+
1419
+ return opts
1420
+ end
1421
+
1422
+ -- Autocommands ---------------------------------------------------------------
1423
+ H.track_dir_edit = function(data)
1424
+ -- Make early returns
1425
+ if vim.api.nvim_get_current_buf() ~= data.buf then return end
1426
+
1427
+ if vim.b.minifiles_processed_dir then
1428
+ -- Smartly delete directory buffer if already visited
1429
+ local alt_buf = vim.fn.bufnr('#')
1430
+ -- - Setting alternative buffer is enough for the "directory buffer" to be
1431
+ -- wiped out, as it has `bufhidden=wipe`. Forcing delete after showing alt
1432
+ -- buffer might result in hard-to-track errors (like when opening directory
1433
+ -- in 'mini.pick' when there is an altbuf for the buffer in target window).
1434
+ if alt_buf ~= data.buf and vim.fn.buflisted(alt_buf) == 1 then return vim.api.nvim_win_set_buf(0, alt_buf) end
1435
+ return vim.api.nvim_buf_delete(data.buf, { force = true })
1436
+ end
1437
+
1438
+ local path = vim.api.nvim_buf_get_name(0)
1439
+ if vim.fn.isdirectory(path) ~= 1 then return end
1440
+
1441
+ -- Make directory buffer disappear when it is not needed
1442
+ vim.bo.bufhidden = 'wipe'
1443
+ vim.b.minifiles_processed_dir = true
1444
+
1445
+ -- Open directory without history
1446
+ vim.schedule(function() MiniFiles.open(path, false) end)
1447
+ end
1448
+
1449
+ -- Explorers ------------------------------------------------------------------
1450
+ ---@class Explorer
1451
+ ---
1452
+ ---@field bookmarks table Map from single characters to bookmark data: table
1453
+ --- with <path> and <desc> fields.
1454
+ ---@field branch table Array of absolute directory paths from parent to child.
1455
+ --- Its ids are called depth.
1456
+ ---@field depth_focus number Depth to focus.
1457
+ ---@field views table Views for paths. Each view is a table with:
1458
+ --- - <buf_id> where to show directory content.
1459
+ --- - <was_focused> - whether buffer was focused during current session.
1460
+ --- - <cursor> to position cursor; can be:
1461
+ --- - `{ line, col }` table to set cursor when buffer changes window.
1462
+ --- - `entry_name` string entry name to find inside directory buffer.
1463
+ ---@field windows table Array of currently opened window ids (left to right).
1464
+ ---@field anchor string Anchor directory of the explorer. Used as index in
1465
+ --- history and for `reset()` operation.
1466
+ ---@field tabpage_id number Id of current tabpage
1467
+ ---@field target_window number Id of window in which files will be opened.
1468
+ ---@field opts table Options used for this particular explorer.
1469
+ ---@field is_corrupted boolean Whether this particular explorer can not be
1470
+ --- normalized and should be closed.
1471
+ ---@private
1472
+ H.explorer_new = function(path)
1473
+ return {
1474
+ branch = { path },
1475
+ depth_focus = 1,
1476
+ views = {},
1477
+ windows = {},
1478
+ anchor = path,
1479
+ tabpage_id = vim.api.nvim_get_current_tabpage(),
1480
+ target_window = vim.api.nvim_get_current_win(),
1481
+ bookmarks = {},
1482
+ opts = {},
1483
+ }
1484
+ end
1485
+
1486
+ H.explorer_get = function(tabpage_id, ignore_visibility)
1487
+ tabpage_id = tabpage_id or vim.api.nvim_get_current_tabpage()
1488
+ local res = H.opened_explorers[tabpage_id]
1489
+
1490
+ if ignore_visibility or H.explorer_is_visible(res) then return res end
1491
+
1492
+ H.opened_explorers[tabpage_id] = nil
1493
+ return nil
1494
+ end
1495
+
1496
+ H.explorer_is_visible = function(explorer)
1497
+ if explorer == nil then return nil end
1498
+ for _, win_id in ipairs(explorer.windows) do
1499
+ if H.is_valid_win(win_id) then return true end
1500
+ end
1501
+ return false
1502
+ end
1503
+
1504
+ H.explorer_refresh = function(explorer, opts)
1505
+ explorer = H.explorer_normalize(explorer)
1506
+ if explorer.is_corrupted then
1507
+ -- Make sure that same explorer can be opened later from history
1508
+ explorer.is_corrupted = false
1509
+ MiniFiles.close()
1510
+ return
1511
+ end
1512
+ if #explorer.branch == 0 then return end
1513
+ opts = opts or {}
1514
+
1515
+ -- Update cursor data in shown views. Do this prior to buffer updates for
1516
+ -- cursors to "stick" to current items.
1517
+ if not opts.skip_update_cursor then explorer = H.explorer_update_cursors(explorer) end
1518
+
1519
+ -- Ensure no outdated views
1520
+ for path, view in pairs(explorer.views) do
1521
+ if not H.fs_is_present_path(path) then
1522
+ H.buffer_delete(view.buf_id)
1523
+ explorer.views[path] = nil
1524
+ end
1525
+ end
1526
+
1527
+ -- Possibly force content updates on all explorer buffers. Doing it for *all*
1528
+ -- of them and not only on modified ones to allow sync changes from outside.
1529
+ if opts.force_update then
1530
+ for path, view in pairs(explorer.views) do
1531
+ -- Encode cursors to allow them to "stick" to current entry
1532
+ view = H.view_encode_cursor(view)
1533
+ -- Force update of shown path ids
1534
+ if H.opened_buffers[view.buf_id] then H.opened_buffers[view.buf_id].children_path_ids = nil end
1535
+ H.buffer_update(view.buf_id, path, explorer.opts, not view.was_focused)
1536
+ explorer.views[path] = view
1537
+ end
1538
+ end
1539
+
1540
+ -- Make sure that cursors point at paths to their right.
1541
+ -- NOTE: Doing this here and not relying on `CursorMoved` autocommand ensures
1542
+ -- that no more windows are opened than necessary (reduces flickering).
1543
+ for depth = 1, #explorer.branch do
1544
+ explorer = H.explorer_sync_cursor_and_branch(explorer, depth)
1545
+ end
1546
+
1547
+ -- Unregister windows from showed buffers, as they might get outdated
1548
+ for _, win_id in ipairs(explorer.windows) do
1549
+ -- NOTE: window can be invalid if it was showing buffer that was deleted
1550
+ if H.is_valid_win(win_id) then
1551
+ local buf_id = vim.api.nvim_win_get_buf(win_id)
1552
+ H.opened_buffers[buf_id].win_id = nil
1553
+ end
1554
+ end
1555
+
1556
+ -- Compute depth range which is possible to show in current window
1557
+ local depth_range = H.compute_visible_depth_range(explorer, explorer.opts)
1558
+
1559
+ -- Refresh window for every target depth keeping track of position column
1560
+ local cur_win_col, cur_win_count, opened_windows = 0, 0, {}
1561
+ for depth = depth_range.from, depth_range.to do
1562
+ cur_win_count = cur_win_count + 1
1563
+ local cur_width, was_opened = H.explorer_refresh_depth_window(explorer, depth, cur_win_count, cur_win_col)
1564
+ opened_windows[explorer.windows[cur_win_count]] = was_opened
1565
+
1566
+ -- Add 2 to account for left and right borders
1567
+ cur_win_col = cur_win_col + cur_width + 2
1568
+ end
1569
+
1570
+ -- Close possibly opened window that don't fit (like after `VimResized`)
1571
+ for depth = cur_win_count + 1, #explorer.windows do
1572
+ H.window_close(explorer.windows[depth])
1573
+ explorer.windows[depth] = nil
1574
+ end
1575
+
1576
+ -- Focus on proper window
1577
+ local win_focus_count = explorer.depth_focus - depth_range.from + 1
1578
+ local win_id_focused = explorer.windows[win_focus_count]
1579
+ H.window_focus(win_id_focused)
1580
+
1581
+ -- Register as currently opened
1582
+ explorer.tabpage_id = vim.api.nvim_win_get_tabpage(win_id_focused)
1583
+ H.opened_explorers[explorer.tabpage_id] = explorer
1584
+
1585
+ -- Trigger window related events after all windows are fully refreshed
1586
+ for _, win_id in ipairs(explorer.windows) do
1587
+ local data = { buf_id = vim.api.nvim_win_get_buf(win_id), win_id = win_id }
1588
+ if opened_windows[win_id] then H.trigger_event('MiniFilesWindowOpen', data) end
1589
+ H.trigger_event('MiniFilesWindowUpdate', data)
1590
+ end
1591
+
1592
+ return explorer
1593
+ end
1594
+
1595
+ H.explorer_track_lost_focus = function()
1596
+ local track = vim.schedule_wrap(function()
1597
+ if H.vim_ui.select_active or H.vim_ui.input_active then return end
1598
+ local ft = vim.bo.filetype
1599
+ if ft == 'minifiles' or ft == 'minifiles-help' then return end
1600
+ local cur_win_id = vim.api.nvim_get_current_win()
1601
+ MiniFiles.close()
1602
+ pcall(vim.api.nvim_set_current_win, cur_win_id)
1603
+ end)
1604
+ H.timers.focus:start(1000, 1000, track)
1605
+ end
1606
+
1607
+ H.explorer_adjust_vim_ui = function()
1608
+ H.vim_ui.select = vim.ui.select
1609
+ vim.ui.select = function(items, opts, on_choice)
1610
+ H.vim_ui.select_active = true
1611
+ H.vim_ui.select(items, opts, function(...)
1612
+ H.vim_ui.select_active = false
1613
+ on_choice(...)
1614
+ end)
1615
+ end
1616
+
1617
+ H.vim_ui.input = vim.ui.input
1618
+ vim.ui.input = function(opts, on_confirm)
1619
+ H.vim_ui.input_active = true
1620
+ H.vim_ui.input(opts, function(...)
1621
+ H.vim_ui.input_active = false
1622
+ on_confirm(...)
1623
+ end)
1624
+ end
1625
+ end
1626
+
1627
+ H.explorer_normalize = function(explorer)
1628
+ -- Ensure that all paths from branch are valid present paths
1629
+ local norm_branch = {}
1630
+ for _, path in ipairs(explorer.branch) do
1631
+ if not H.fs_is_present_path(path) then break end
1632
+ table.insert(norm_branch, path)
1633
+ end
1634
+
1635
+ local cur_max_depth = #norm_branch
1636
+
1637
+ explorer.branch = norm_branch
1638
+ explorer.depth_focus = math.min(math.max(explorer.depth_focus, 1), cur_max_depth)
1639
+
1640
+ -- Close all guaranteed to be unnecessary windows. NOTE: some windows might
1641
+ -- still get outdated later if branch is too deep to fit into Neovim's width.
1642
+ for i = cur_max_depth + 1, #explorer.windows do
1643
+ H.window_close(explorer.windows[i])
1644
+ explorer.windows[i] = nil
1645
+ end
1646
+
1647
+ -- Compute if explorer is corrupted and should not operate further
1648
+ for _, win_id in pairs(explorer.windows) do
1649
+ if not H.is_valid_win(win_id) then explorer.is_corrupted = true end
1650
+ end
1651
+
1652
+ return explorer
1653
+ end
1654
+
1655
+ H.explorer_sync_cursor_and_branch = function(explorer, depth)
1656
+ -- Compute helper data while making early returns
1657
+ if #explorer.branch < depth then return explorer end
1658
+
1659
+ local path, path_to_right = explorer.branch[depth], explorer.branch[depth + 1]
1660
+ local view = explorer.views[path]
1661
+ if view == nil then return explorer end
1662
+
1663
+ local buf_id, cursor = view.buf_id, view.cursor
1664
+ if cursor == nil then return explorer end
1665
+
1666
+ -- Compute if path at cursor and path to the right are equal (in sync)
1667
+ local cursor_path
1668
+ if type(cursor) == 'table' and H.is_valid_buf(buf_id) then
1669
+ local l = H.get_bufline(buf_id, cursor[1])
1670
+ -- Fall back to treating current line as full basename, but for not yet
1671
+ -- existing (a.k.a. "imaginary") path keep showing preview.
1672
+ cursor_path = H.path_index[H.match_line_path_id(l)] or H.fs_child_path(path, l .. '\000')
1673
+ elseif type(cursor) == 'string' then
1674
+ cursor_path = H.fs_child_path(path, cursor)
1675
+ else
1676
+ return explorer
1677
+ end
1678
+
1679
+ if cursor_path == path_to_right then return explorer end
1680
+
1681
+ -- Trim branch if cursor path is not in sync with path to the right
1682
+ for i = depth + 1, #explorer.branch do
1683
+ explorer.branch[i] = nil
1684
+ end
1685
+ explorer.depth_focus = math.min(explorer.depth_focus, #explorer.branch)
1686
+
1687
+ -- Show preview to the right of current buffer if needed
1688
+ local show_preview = explorer.opts.windows.preview
1689
+ local is_cur_buf = explorer.depth_focus == depth
1690
+ if show_preview and is_cur_buf then table.insert(explorer.branch, cursor_path) end
1691
+
1692
+ return explorer
1693
+ end
1694
+
1695
+ H.explorer_go_in_range = function(explorer, buf_id, from_line, to_line)
1696
+ -- Compute which entries to go in: all files and only last directory
1697
+ local files, path, line = {}, nil, nil
1698
+ for i = from_line, to_line do
1699
+ local fs_entry = MiniFiles.get_fs_entry(buf_id, i) or {}
1700
+ if fs_entry.fs_type == 'file' then table.insert(files, fs_entry.path) end
1701
+ if fs_entry.fs_type == 'directory' then
1702
+ path, line = fs_entry.path, i
1703
+ end
1704
+ if fs_entry.fs_type == nil and fs_entry.path == nil then
1705
+ local entry = vim.inspect(H.get_bufline(buf_id, i))
1706
+ H.notify('Line ' .. entry .. ' does not have proper format. Did you modify without synchronization?', 'WARN')
1707
+ end
1708
+ if fs_entry.fs_type == nil and fs_entry.path ~= nil then
1709
+ local path_resolved = vim.fn.resolve(fs_entry.path)
1710
+ local symlink_info = path_resolved == fs_entry.path and ''
1711
+ or (' Looks like miscreated symlink (resolved to ' .. path_resolved .. ').')
1712
+ H.notify('Path ' .. fs_entry.path .. ' is not present on disk.' .. symlink_info, 'WARN')
1713
+ end
1714
+ end
1715
+
1716
+ for _, file_path in ipairs(files) do
1717
+ explorer = H.explorer_open_file(explorer, file_path)
1718
+ end
1719
+
1720
+ if path ~= nil then
1721
+ explorer = H.explorer_open_directory(explorer, path, explorer.depth_focus + 1)
1722
+
1723
+ -- Ensure that cursor points to the directory in current window (can be not
1724
+ -- the case if cursor is not on the actually opened directory)
1725
+ local win_id = H.opened_buffers[buf_id].win_id
1726
+ if H.is_valid_win(win_id) then vim.api.nvim_win_set_cursor(win_id, { line, 0 }) end
1727
+ end
1728
+
1729
+ return explorer
1730
+ end
1731
+
1732
+ H.explorer_focus_on_entry = function(explorer, path, entry_name)
1733
+ if entry_name == nil then return explorer end
1734
+
1735
+ -- Set focus on directory. Reset if it is not in current branch.
1736
+ explorer.depth_focus = H.explorer_get_path_depth(explorer, path)
1737
+ if explorer.depth_focus == nil then
1738
+ explorer.branch, explorer.depth_focus = { path }, 1
1739
+ end
1740
+
1741
+ -- Set cursor on entry
1742
+ local path_view = explorer.views[path] or {}
1743
+ path_view.cursor = entry_name
1744
+ explorer.views[path] = path_view
1745
+
1746
+ return explorer
1747
+ end
1748
+
1749
+ H.explorer_compute_fs_actions = function(explorer)
1750
+ -- Compute differences
1751
+ local fs_diffs = {}
1752
+ for _, view in pairs(explorer.views) do
1753
+ local dir_fs_diff = H.buffer_compute_fs_diff(view.buf_id)
1754
+ if #dir_fs_diff > 0 then vim.list_extend(fs_diffs, dir_fs_diff) end
1755
+ end
1756
+ if #fs_diffs == 0 then return nil end
1757
+
1758
+ -- Convert differences into actions
1759
+ local create, delete_map, raw_copy = {}, {}, {}
1760
+
1761
+ -- - Differentiate between create, delete, and copy
1762
+ for _, diff in ipairs(fs_diffs) do
1763
+ if diff.from == nil then
1764
+ table.insert(create, { action = 'create', dir = diff.dir, to = diff.to })
1765
+ elseif diff.to == nil then
1766
+ delete_map[diff.from] = true
1767
+ else
1768
+ table.insert(raw_copy, diff)
1769
+ end
1770
+ end
1771
+
1772
+ -- - Narrow down copy action into rename or move: `delete + copy` is `rename`
1773
+ -- if in same directory and `move` otherwise
1774
+ local rename, move, copy = {}, {}, {}
1775
+ for _, diff in pairs(raw_copy) do
1776
+ local action, target = 'copy', copy
1777
+ if delete_map[diff.from] then
1778
+ -- Treat appending `/` to file name as file -> directory conversion
1779
+ -- (i.e. delete file + create directory)
1780
+ if diff.to == (diff.from .. '/') and H.fs_get_type(diff.from) == 'file' then
1781
+ target, action, diff.from = create, 'create', nil
1782
+ else
1783
+ action = H.fs_get_parent(diff.from) == H.fs_get_parent(diff.to) and 'rename' or 'move'
1784
+ target = action == 'rename' and rename or move
1785
+ -- NOTE: Use map instead of array to ensure single move/rename per path
1786
+ delete_map[diff.from] = nil
1787
+ end
1788
+ end
1789
+ table.insert(target, { action = action, dir = diff.dir, from = diff.from, to = diff.to })
1790
+ end
1791
+
1792
+ -- Compute delete actions accounting for (non) permanent delete
1793
+ local delete, is_trash = {}, not explorer.opts.options.permanent_delete
1794
+ local trash_dir = H.fs_child_path(vim.fn.stdpath('data'), 'mini.files/trash')
1795
+ for p, _ in pairs(delete_map) do
1796
+ local to = is_trash and H.fs_child_path(trash_dir, H.fs_get_basename(p)) or nil
1797
+ table.insert(delete, { action = 'delete', from = p, to = to })
1798
+ end
1799
+
1800
+ -- Construct final array with proper order of actions:
1801
+ -- - If action depends on the path which will be deleted, perform it first.
1802
+ -- - "Delete"/"move"/"rename" before "copy"/"create" to free space for them.
1803
+ -- - Move/rename (if successful) will later adjust next steps at execution.
1804
+ local before_delete, after_delete = {}, {}
1805
+ for _, arr in ipairs({ move, rename, copy, create }) do
1806
+ for _, diff in ipairs(arr) do
1807
+ local will_be_deleted = false
1808
+ for _, del in ipairs(delete) do
1809
+ local del_from_dir = del.from .. '/'
1810
+ local from_is_affected = del.from == diff.from or vim.startswith(diff.from or '', del_from_dir)
1811
+ -- Don't directly account for deleted path to allow "act on freed path"
1812
+ -- But make "append `/` to file" work as "delete file" + "create dir"
1813
+ local to_is_affected = vim.startswith(diff.to, del_from_dir) and diff.to ~= del_from_dir
1814
+ will_be_deleted = will_be_deleted or from_is_affected or to_is_affected
1815
+ end
1816
+ table.insert(will_be_deleted and before_delete or after_delete, diff)
1817
+ end
1818
+ end
1819
+
1820
+ local res = {}
1821
+ vim.list_extend(res, before_delete)
1822
+ vim.list_extend(res, delete)
1823
+ vim.list_extend(res, after_delete)
1824
+ return res
1825
+ end
1826
+
1827
+ H.explorer_update_cursors = function(explorer)
1828
+ for _, win_id in ipairs(explorer.windows) do
1829
+ if H.is_valid_win(win_id) then
1830
+ local buf_id = vim.api.nvim_win_get_buf(win_id)
1831
+ local path = H.opened_buffers[buf_id].path
1832
+ explorer.views[path].cursor = vim.api.nvim_win_get_cursor(win_id)
1833
+ end
1834
+ end
1835
+
1836
+ return explorer
1837
+ end
1838
+
1839
+ H.explorer_refresh_depth_window = function(explorer, depth, win_count, win_col)
1840
+ local path = explorer.branch[depth]
1841
+ local views, windows, opts = explorer.views, explorer.windows, explorer.opts
1842
+
1843
+ -- Compute width based on window role
1844
+ local win_is_focused = depth == explorer.depth_focus
1845
+ local win_is_preview = opts.windows.preview and (depth == (explorer.depth_focus + 1))
1846
+ local cur_width = win_is_focused and opts.windows.width_focus
1847
+ or (win_is_preview and opts.windows.width_preview or opts.windows.width_nofocus)
1848
+
1849
+ -- Prepare target view
1850
+ local view = views[path] or {}
1851
+ view = H.view_ensure_proper(view, path, opts, win_is_focused, win_is_preview)
1852
+ views[path] = view
1853
+
1854
+ -- Create relevant window config
1855
+ local config = {
1856
+ col = win_col,
1857
+ height = vim.api.nvim_buf_line_count(view.buf_id),
1858
+ width = cur_width,
1859
+ -- Use shortened full path in left most window
1860
+ title = win_count == 1 and H.fs_shorten_path(H.fs_full_path(path)) or H.fs_get_basename(path),
1861
+ }
1862
+ config.title = ' ' .. H.sanitize_string(config.title) .. ' '
1863
+
1864
+ -- Prepare and register window
1865
+ local win_id, was_opened = windows[win_count], false
1866
+ if not H.is_valid_win(win_id) then
1867
+ H.window_close(win_id)
1868
+ win_id = H.window_open(view.buf_id, config)
1869
+ windows[win_count] = win_id
1870
+ was_opened = true
1871
+ end
1872
+
1873
+ H.window_update(win_id, config)
1874
+
1875
+ -- Show view in window
1876
+ H.window_set_view(win_id, view)
1877
+
1878
+ -- Update explorer data
1879
+ explorer.views = views
1880
+ explorer.windows = windows
1881
+
1882
+ -- Return width of current window to keep track of window column
1883
+ return cur_width, was_opened
1884
+ end
1885
+
1886
+ H.explorer_get_path_depth = function(explorer, path)
1887
+ for depth, depth_path in pairs(explorer.branch) do
1888
+ if path == depth_path then return depth end
1889
+ end
1890
+ end
1891
+
1892
+ H.explorer_ignore_pending_fs_actions = function(explorer, action_name)
1893
+ -- Exit if nothing to ignore
1894
+ if H.explorer_compute_fs_actions(explorer) == nil then return true end
1895
+
1896
+ local msg = string.format('There are pending file system actions\n\n%s without synchronization?', action_name)
1897
+ local confirm_res = vim.fn.confirm(msg, '&Yes\n&No', 1, 'Question')
1898
+ return confirm_res == 1
1899
+ end
1900
+
1901
+ H.explorer_open_file = function(explorer, path)
1902
+ explorer = H.explorer_ensure_target_window(explorer)
1903
+ H.edit(path, explorer.target_window)
1904
+ return explorer
1905
+ end
1906
+
1907
+ H.explorer_ensure_target_window = function(explorer)
1908
+ if not H.is_valid_win(explorer.target_window) then explorer.target_window = H.get_first_valid_normal_window() end
1909
+ return explorer
1910
+ end
1911
+
1912
+ H.explorer_open_directory = function(explorer, path, target_depth)
1913
+ -- Update focused depth
1914
+ explorer.depth_focus = target_depth
1915
+
1916
+ -- Truncate rest of the branch if opening another path at target depth
1917
+ local show_new_path_at_depth = path ~= explorer.branch[target_depth]
1918
+ if show_new_path_at_depth then
1919
+ explorer.branch[target_depth] = path
1920
+ explorer = H.explorer_trim_branch_right(explorer)
1921
+ end
1922
+
1923
+ return explorer
1924
+ end
1925
+
1926
+ H.explorer_open_root_parent = function(explorer)
1927
+ local root = explorer.branch[1]
1928
+ local root_parent = H.fs_get_parent(root)
1929
+ if root_parent == nil then return explorer end
1930
+
1931
+ -- Update branch data
1932
+ table.insert(explorer.branch, 1, root_parent)
1933
+
1934
+ -- Focus on previous root entry in its parent
1935
+ return H.explorer_focus_on_entry(explorer, root_parent, H.fs_get_basename(root))
1936
+ end
1937
+
1938
+ H.explorer_trim_branch_right = function(explorer)
1939
+ for i = explorer.depth_focus + 1, #explorer.branch do
1940
+ explorer.branch[i] = nil
1941
+ end
1942
+ return explorer
1943
+ end
1944
+
1945
+ H.explorer_trim_branch_left = function(explorer)
1946
+ local new_branch = {}
1947
+ for i = explorer.depth_focus, #explorer.branch do
1948
+ table.insert(new_branch, explorer.branch[i])
1949
+ end
1950
+ explorer.branch = new_branch
1951
+ explorer.depth_focus = 1
1952
+ return explorer
1953
+ end
1954
+
1955
+ H.explorer_show_help = function(explorer, explorer_buf_id, explorer_win_id)
1956
+ -- Compute lines
1957
+ local buf_mappings = vim.api.nvim_buf_get_keymap(explorer_buf_id, 'n')
1958
+ local map_data, desc_width = {}, 0
1959
+ for _, data in ipairs(buf_mappings) do
1960
+ if data.desc ~= nil then
1961
+ map_data[data.desc] = data.lhs:lower() == '<lt>' and '<' or data.lhs
1962
+ desc_width = math.max(desc_width, data.desc:len())
1963
+ end
1964
+ end
1965
+
1966
+ local desc_arr = vim.tbl_keys(map_data)
1967
+ table.sort(desc_arr)
1968
+ local map_format = string.format('%%-%ds │ %%s', desc_width)
1969
+
1970
+ local lines = { 'Buffer mappings:', '' }
1971
+ for _, desc in ipairs(desc_arr) do
1972
+ table.insert(lines, string.format(map_format, desc, map_data[desc]))
1973
+ end
1974
+ table.insert(lines, '')
1975
+
1976
+ local bookmark_ids = vim.tbl_keys(explorer.bookmarks)
1977
+ if #bookmark_ids > 0 then
1978
+ table.insert(lines, 'Bookmarks:')
1979
+ table.insert(lines, '')
1980
+ table.sort(bookmark_ids)
1981
+ for _, id in ipairs(bookmark_ids) do
1982
+ local data = explorer.bookmarks[id]
1983
+ local desc = data.desc or (vim.is_callable(data.path) and data.path() or data.path)
1984
+ table.insert(lines, id .. ' │ ' .. desc)
1985
+ end
1986
+ table.insert(lines, '')
1987
+ end
1988
+
1989
+ table.insert(lines, '(Press `q` to close)')
1990
+
1991
+ -- Create buffer
1992
+ local buf_id = vim.api.nvim_create_buf(false, true)
1993
+ H.set_buflines(buf_id, lines)
1994
+ H.set_buf_name(buf_id, 'help')
1995
+
1996
+ vim.keymap.set('n', 'q', '<Cmd>close<CR>', { buffer = buf_id, desc = 'Close this window' })
1997
+
1998
+ vim.b[buf_id].minicursorword_disable = true
1999
+ vim.b[buf_id].miniindentscope_disable = true
2000
+
2001
+ vim.bo[buf_id].filetype = 'minifiles-help'
2002
+ vim.bo[buf_id].bufhidden = 'wipe'
2003
+
2004
+ -- Compute window data
2005
+ local line_widths = vim.tbl_map(vim.fn.strdisplaywidth, lines)
2006
+ local max_line_width = math.max(unpack(line_widths))
2007
+
2008
+ local config = vim.api.nvim_win_get_config(explorer_win_id)
2009
+ config.relative = 'win'
2010
+ config.row = 0
2011
+ config.col = 0
2012
+ config.width = max_line_width
2013
+ config.height = #lines
2014
+ config.title = " 'mini.files' help "
2015
+ config.zindex = config.zindex + 1
2016
+ local default_border = (vim.fn.exists('+winborder') == 0 or vim.o.winborder == '') and 'single' or nil
2017
+ config.border = config.border or default_border
2018
+ config.style = 'minimal'
2019
+
2020
+ -- Open window
2021
+ local win_id = vim.api.nvim_open_win(buf_id, false, config)
2022
+ H.window_update_highlight(win_id, 'NormalFloat', 'MiniFilesNormal')
2023
+ H.window_update_highlight(win_id, 'FloatTitle', 'MiniFilesTitle')
2024
+ H.window_update_highlight(win_id, 'CursorLine', 'MiniFilesCursorLine')
2025
+ vim.wo[win_id].cursorline = true
2026
+ local culopt = vim.wo[win_id].cursorlineopt
2027
+ if culopt:find('line') == nil then vim.wo[win_id].cursorlineopt = culopt .. ',line' end
2028
+
2029
+ vim.api.nvim_set_current_win(win_id)
2030
+ return win_id
2031
+ end
2032
+
2033
+ H.compute_visible_depth_range = function(explorer, opts)
2034
+ -- Compute maximum number of windows possible to fit in current Neovim width
2035
+ -- Add 2 to widths to take into account width of left and right borders
2036
+ local width_focus, width_nofocus = opts.windows.width_focus + 2, opts.windows.width_nofocus + 2
2037
+
2038
+ local has_preview = explorer.opts.windows.preview and explorer.depth_focus < #explorer.branch
2039
+ local width_preview = has_preview and (opts.windows.width_preview + 2) or width_nofocus
2040
+
2041
+ local max_number = 1
2042
+ if (width_focus + width_preview) <= vim.o.columns then max_number = max_number + 1 end
2043
+ if (width_focus + width_preview + width_nofocus) <= vim.o.columns then
2044
+ max_number = max_number + math.floor((vim.o.columns - width_focus - width_preview) / width_nofocus)
2045
+ end
2046
+
2047
+ -- - Account for dedicated option
2048
+ max_number = math.min(math.max(max_number, 1), opts.windows.max_number)
2049
+
2050
+ -- Compute which branch entries to show with the following idea:
2051
+ -- - Always show focused depth as centered as possible.
2052
+ -- - Show as much as possible.
2053
+ -- Logic is similar to how text for 'mini.tabline' is computed.
2054
+ local branch_depth, depth_focus = #explorer.branch, explorer.depth_focus
2055
+ local n_panes = math.min(branch_depth, max_number)
2056
+
2057
+ local to = math.min(branch_depth, math.floor(depth_focus + 0.5 * n_panes))
2058
+ local from = math.max(1, to - n_panes + 1)
2059
+ to = from + math.min(n_panes, branch_depth) - 1
2060
+
2061
+ return { from = from, to = to }
2062
+ end
2063
+
2064
+ -- Views ----------------------------------------------------------------------
2065
+ H.view_ensure_proper = function(view, path, opts, is_focused, is_preview)
2066
+ -- Ensure proper buffer
2067
+ local needs_recreate, needs_reprocess = not H.is_valid_buf(view.buf_id), not view.was_focused and is_focused
2068
+ if needs_recreate then
2069
+ H.buffer_delete(view.buf_id)
2070
+ view.buf_id = H.buffer_create(path, opts.mappings)
2071
+ end
2072
+ if needs_recreate or needs_reprocess then
2073
+ -- Make sure that pressing `u` in new buffer does nothing
2074
+ local cache_undolevels = vim.bo[view.buf_id].undolevels
2075
+ vim.bo[view.buf_id].undolevels = -1
2076
+ H.buffer_update(view.buf_id, path, opts, is_preview)
2077
+ vim.bo[view.buf_id].undolevels = cache_undolevels
2078
+ end
2079
+ view.was_focused = view.was_focused or is_focused
2080
+
2081
+ -- Ensure proper cursor. If string, find it as line in current buffer.
2082
+ view.cursor = view.cursor or { 1, 0 }
2083
+ if type(view.cursor) == 'string' then view = H.view_decode_cursor(view) end
2084
+
2085
+ return view
2086
+ end
2087
+
2088
+ H.view_encode_cursor = function(view)
2089
+ local buf_id, cursor = view.buf_id, view.cursor
2090
+ if not H.is_valid_buf(buf_id) or type(cursor) ~= 'table' then return view end
2091
+
2092
+ -- Replace exact cursor coordinates with entry name to try and find later.
2093
+ -- This allows more robust opening explorer from history (as directory
2094
+ -- content may have changed and exact cursor position would be not valid).
2095
+ local l = H.get_bufline(buf_id, cursor[1])
2096
+ view.cursor = H.match_line_entry_name(l)
2097
+ return view
2098
+ end
2099
+
2100
+ H.view_decode_cursor = function(view)
2101
+ local buf_id, cursor = view.buf_id, view.cursor
2102
+ if not H.is_valid_buf(buf_id) or type(cursor) ~= 'string' then return view end
2103
+
2104
+ -- Find entry name named as stored in `cursor`. If not - use {1, 0}.
2105
+ local lines = vim.api.nvim_buf_get_lines(buf_id, 0, -1, false)
2106
+ for i, l in ipairs(lines) do
2107
+ if cursor == H.match_line_entry_name(l) then view.cursor = { i, 0 } end
2108
+ end
2109
+
2110
+ if type(view.cursor) ~= 'table' then view.cursor = { 1, 0 } end
2111
+
2112
+ return view
2113
+ end
2114
+
2115
+ H.view_invalidate_buffer = function(view)
2116
+ H.buffer_delete(view.buf_id)
2117
+ view.buf_id = nil
2118
+ return view
2119
+ end
2120
+
2121
+ H.view_track_cursor = vim.schedule_wrap(function(data)
2122
+ -- Schedule this in order to react *after* all pending changes are applied
2123
+ local buf_id = data.buf
2124
+ local buf_data = H.opened_buffers[buf_id]
2125
+ if buf_data == nil then return end
2126
+
2127
+ local win_id = buf_data.win_id
2128
+ if not H.is_valid_win(win_id) then return end
2129
+
2130
+ -- Ensure cursor doesn't go over path id and icon
2131
+ local cur_cursor = H.window_tweak_cursor(win_id, buf_id)
2132
+
2133
+ -- Ensure cursor line doesn't contradict window on the right
2134
+ local tabpage_id = vim.api.nvim_win_get_tabpage(win_id)
2135
+ local explorer = H.explorer_get(tabpage_id)
2136
+ if explorer == nil then return end
2137
+
2138
+ local buf_depth = H.explorer_get_path_depth(explorer, buf_data.path)
2139
+ if buf_depth == nil then return end
2140
+
2141
+ -- Update cursor in view and sync it with branch
2142
+ local view = explorer.views[buf_data.path]
2143
+ if view ~= nil then
2144
+ view.cursor = cur_cursor
2145
+ explorer.views[buf_data.path] = view
2146
+ end
2147
+
2148
+ explorer = H.explorer_sync_cursor_and_branch(explorer, buf_depth)
2149
+
2150
+ H.explorer_refresh(explorer)
2151
+ end)
2152
+
2153
+ H.view_track_text_change = function(data)
2154
+ -- Track 'modified'
2155
+ local buf_id = data.buf
2156
+ local new_n_modified = H.opened_buffers[buf_id].n_modified + 1
2157
+ H.opened_buffers[buf_id].n_modified = new_n_modified
2158
+ local win_id = H.opened_buffers[buf_id].win_id
2159
+ if new_n_modified > 0 and H.is_valid_win(win_id) then H.window_update_border_hl(win_id) end
2160
+
2161
+ -- Track window height
2162
+ if not H.is_valid_win(win_id) then return end
2163
+
2164
+ local cur_height = vim.api.nvim_win_get_height(win_id)
2165
+ local n_lines = vim.api.nvim_buf_line_count(buf_id)
2166
+ local new_height = math.min(n_lines, H.window_get_max_height())
2167
+ vim.api.nvim_win_set_height(win_id, new_height)
2168
+
2169
+ -- Trigger appropriate event if window height has changed
2170
+ if cur_height ~= new_height then
2171
+ H.trigger_event('MiniFilesWindowUpdate', { buf_id = buf_id, win_id = win_id })
2172
+ new_height = vim.api.nvim_win_get_height(win_id)
2173
+ end
2174
+
2175
+ -- Ensure that only buffer lines are shown. This can be not the case if after
2176
+ -- text edit cursor moved past previous last line.
2177
+ local last_visible_line = vim.fn.line('w0', win_id) + new_height - 1
2178
+ local out_of_buf_lines = last_visible_line - n_lines
2179
+ -- - Possibly scroll window upward (`\25` is an escaped `<C-y>`)
2180
+ if out_of_buf_lines > 0 then
2181
+ -- Preserve cursor as scrolling might affect it (like in Insert mode)
2182
+ local cursor = vim.api.nvim_win_get_cursor(win_id)
2183
+ vim.cmd('normal! ' .. out_of_buf_lines .. '\25')
2184
+ vim.api.nvim_win_set_cursor(win_id, cursor)
2185
+ end
2186
+ end
2187
+
2188
+ -- Buffers --------------------------------------------------------------------
2189
+ H.buffer_create = function(path, mappings)
2190
+ -- Create buffer
2191
+ local buf_id = vim.api.nvim_create_buf(false, true)
2192
+ H.set_buf_name(buf_id, path)
2193
+
2194
+ -- Register buffer
2195
+ H.opened_buffers[buf_id] = { path = path }
2196
+
2197
+ -- Set buffer options
2198
+ vim.bo[buf_id].filetype = 'minifiles'
2199
+
2200
+ -- Do not set up tracking behavior for imaginary paths
2201
+ if H.fs_is_imaginary_path(path) then return buf_id end
2202
+
2203
+ -- Make buffer mappings
2204
+ H.buffer_make_mappings(buf_id, mappings)
2205
+
2206
+ -- Make buffer autocommands
2207
+ local augroup = vim.api.nvim_create_augroup('MiniFiles', { clear = false })
2208
+ local au = function(events, desc, callback)
2209
+ vim.api.nvim_create_autocmd(events, { group = augroup, buffer = buf_id, desc = desc, callback = callback })
2210
+ end
2211
+
2212
+ au({ 'CursorMoved', 'CursorMovedI', 'TextChangedP' }, 'Tweak cursor position', H.view_track_cursor)
2213
+ au({ 'TextChanged', 'TextChangedI', 'TextChangedP' }, 'Track buffer modification', H.view_track_text_change)
2214
+
2215
+ -- Tweak buffer to be used nicely with other 'mini.nvim' modules
2216
+ vim.b[buf_id].minicursorword_disable = true
2217
+
2218
+ -- Trigger dedicated event
2219
+ H.trigger_event('MiniFilesBufferCreate', { buf_id = buf_id })
2220
+
2221
+ return buf_id
2222
+ end
2223
+
2224
+ H.buffer_make_mappings = function(buf_id, mappings)
2225
+ local go_in_with_count = function()
2226
+ for _ = 1, vim.v.count1 do
2227
+ MiniFiles.go_in()
2228
+ end
2229
+ end
2230
+
2231
+ local go_in_plus = function()
2232
+ for _ = 1, vim.v.count1 do
2233
+ MiniFiles.go_in({ close_on_file = true })
2234
+ end
2235
+ end
2236
+
2237
+ local go_out_with_count = function()
2238
+ for _ = 1, vim.v.count1 do
2239
+ MiniFiles.go_out()
2240
+ end
2241
+ end
2242
+
2243
+ local go_out_plus = function()
2244
+ go_out_with_count()
2245
+ MiniFiles.trim_right()
2246
+ end
2247
+
2248
+ local go_in_visual = function()
2249
+ -- React only on linewise mode, as others can be used for editing
2250
+ if vim.fn.mode() ~= 'V' then return mappings.go_in end
2251
+
2252
+ -- Schedule actions because they are not allowed inside expression mapping
2253
+ local line_1, line_2 = vim.fn.line('v'), vim.fn.line('.')
2254
+ local from_line, to_line = math.min(line_1, line_2), math.max(line_1, line_2)
2255
+ vim.schedule(function()
2256
+ local explorer = H.explorer_get()
2257
+ explorer = H.explorer_go_in_range(explorer, buf_id, from_line, to_line)
2258
+ H.explorer_refresh(explorer)
2259
+ end)
2260
+
2261
+ -- Go to Normal mode. '\28\14' is an escaped version of `<C-\><C-n>`.
2262
+ return [[<C-\><C-n>]]
2263
+ end
2264
+
2265
+ local mark_goto = function()
2266
+ local id = H.getcharstr()
2267
+ if id == nil then return end
2268
+ local data = MiniFiles.get_explorer_state().bookmarks[id]
2269
+ if data == nil then return H.notify('No bookmark with id ' .. vim.inspect(id), 'WARN') end
2270
+
2271
+ local path = data.path
2272
+ if vim.is_callable(path) then path = path() end
2273
+ local is_valid_path = type(path) == 'string' and H.fs_get_type(vim.fn.expand(path)) == 'directory'
2274
+ if not is_valid_path then return H.notify('Bookmark path should be a valid path to directory', 'WARN') end
2275
+
2276
+ local state = MiniFiles.get_explorer_state()
2277
+ MiniFiles.set_bookmark("'", state.branch[state.depth_focus], { desc = 'Before latest jump' })
2278
+ MiniFiles.set_branch({ path })
2279
+ end
2280
+
2281
+ local mark_set = function()
2282
+ local id = H.getcharstr()
2283
+ if id == nil then return end
2284
+ local state = MiniFiles.get_explorer_state()
2285
+ MiniFiles.set_bookmark(id, state.branch[state.depth_focus])
2286
+ H.notify('Bookmark ' .. vim.inspect(id) .. ' is set', 'INFO')
2287
+ end
2288
+
2289
+ local buf_map = function(mode, lhs, rhs, desc)
2290
+ -- Use `nowait` to account for non-buffer mappings starting with `lhs`
2291
+ H.map(mode, lhs, rhs, { buffer = buf_id, desc = desc, nowait = true })
2292
+ end
2293
+
2294
+ --stylua: ignore start
2295
+ buf_map('n', mappings.close, MiniFiles.close, 'Close')
2296
+ buf_map('n', mappings.go_in, go_in_with_count, 'Go in entry')
2297
+ buf_map('n', mappings.go_in_plus, go_in_plus, 'Go in entry plus')
2298
+ buf_map('n', mappings.go_out, go_out_with_count, 'Go out of directory')
2299
+ buf_map('n', mappings.go_out_plus, go_out_plus, 'Go out of directory plus')
2300
+ buf_map('n', mappings.mark_goto, mark_goto, 'Go to bookmark')
2301
+ buf_map('n', mappings.mark_set, mark_set, 'Set bookmark')
2302
+ buf_map('n', mappings.reset, MiniFiles.reset, 'Reset')
2303
+ buf_map('n', mappings.reveal_cwd, MiniFiles.reveal_cwd, 'Reveal cwd')
2304
+ buf_map('n', mappings.show_help, MiniFiles.show_help, 'Show Help')
2305
+ buf_map('n', mappings.synchronize, MiniFiles.synchronize, 'Synchronize')
2306
+ buf_map('n', mappings.trim_left, MiniFiles.trim_left, 'Trim branch left')
2307
+ buf_map('n', mappings.trim_right, MiniFiles.trim_right, 'Trim branch right')
2308
+
2309
+ H.map('x', mappings.go_in, go_in_visual, { buffer = buf_id, desc = 'Go in selected entries', expr = true })
2310
+ --stylua: ignore end
2311
+ end
2312
+
2313
+ H.buffer_update = function(buf_id, path, opts, is_preview)
2314
+ if not H.is_valid_buf(buf_id) then return end
2315
+
2316
+ -- Perform entry type specific updates
2317
+ local fs_type = H.fs_get_type(path)
2318
+ if fs_type == 'directory' then H.buffer_update_directory(buf_id, path, opts, is_preview) end
2319
+ if fs_type == 'file' then H.buffer_update_file(buf_id, path, opts, is_preview) end
2320
+ if fs_type == nil then H.set_buflines(buf_id, { '-No-fs-entry-' .. string.rep('-', opts.windows.width_preview) }) end
2321
+
2322
+ -- Trigger dedicated event
2323
+ local data = { buf_id = buf_id, win_id = H.opened_buffers[buf_id].win_id }
2324
+ if fs_type ~= nil then H.trigger_event('MiniFilesBufferUpdate', data) end
2325
+
2326
+ -- Reset buffer as not modified
2327
+ H.opened_buffers[buf_id].n_modified = -1
2328
+ end
2329
+
2330
+ H.buffer_update_directory = function(buf_id, path, opts, is_preview)
2331
+ -- Compute and cache (to use during sync) shown file system entries
2332
+ local children_path_ids = H.opened_buffers[buf_id].children_path_ids
2333
+ local fs_entries = children_path_ids == nil and H.fs_read_dir(path, opts.content)
2334
+ or vim.tbl_map(H.get_fs_entry_from_path_index, children_path_ids)
2335
+ H.opened_buffers[buf_id].children_path_ids = children_path_ids
2336
+ or vim.tbl_map(function(x) return x.path_id end, fs_entries)
2337
+
2338
+ -- Compute format expression resulting into same width path ids
2339
+ local path_width = math.floor(math.log10(#H.path_index)) + 1
2340
+ local line_format = '/%0' .. path_width .. 'd/%s/%s'
2341
+
2342
+ -- Compute lines
2343
+ local lines, icon_hl, name_hl = {}, {}, {}
2344
+ local prefix_fun, hl_fun = opts.content.prefix, opts.content.highlight
2345
+ local n_computed_prefixes = is_preview and vim.o.lines or math.huge
2346
+ for i, entry in ipairs(fs_entries) do
2347
+ local prefix, hl, name
2348
+ -- Compute prefix only in visible preview (for performance).
2349
+ -- NOTE: limiting entries in `fs_read_dir()` is not possible because all
2350
+ -- entries are needed for a proper filter and sort.
2351
+ if i <= n_computed_prefixes then
2352
+ prefix, hl = prefix_fun(entry)
2353
+ end
2354
+ prefix, hl, name = prefix or '', hl or '', H.sanitize_string(entry.name)
2355
+ table.insert(lines, string.format(line_format, H.path_index[entry.path], prefix, name))
2356
+ table.insert(icon_hl, hl)
2357
+ table.insert(name_hl, hl_fun(entry) or 'MiniFilesNormal')
2358
+ end
2359
+
2360
+ -- Set lines
2361
+ H.set_buflines(buf_id, lines)
2362
+
2363
+ -- Add highlighting
2364
+ local ns_id = H.ns_id.highlight
2365
+ vim.api.nvim_buf_clear_namespace(buf_id, ns_id, 0, -1)
2366
+
2367
+ local set_hl = function(line, col, hl_opts) H.set_extmark(buf_id, ns_id, line, col, hl_opts) end
2368
+
2369
+ for i, l in ipairs(lines) do
2370
+ local icon_start, name_start = l:match('^/%d+/().-()/')
2371
+
2372
+ -- NOTE: Use `right_gravity = false` for persistent highlights during edit
2373
+ local icon_opts = { hl_group = icon_hl[i], end_col = name_start - 1, right_gravity = false }
2374
+ set_hl(i - 1, icon_start - 1, icon_opts)
2375
+
2376
+ local name_opts = { hl_group = name_hl[i], end_row = i, end_col = 0, right_gravity = false }
2377
+ set_hl(i - 1, name_start - 1, name_opts)
2378
+ end
2379
+ end
2380
+
2381
+ H.buffer_update_file = function(buf_id, path, opts, _)
2382
+ -- Work only with readable text file. This is not 100% proof, but good enough.
2383
+ -- Source: https://github.com/sharkdp/content_inspector
2384
+ local fd, width_preview = vim.loop.fs_open(path, 'r', 1), opts.windows.width_preview
2385
+ if fd == nil then return H.set_buflines(buf_id, { '-No-access' .. string.rep('-', width_preview) }) end
2386
+ local is_text = vim.loop.fs_read(fd, 1024):find('\0') == nil
2387
+ vim.loop.fs_close(fd)
2388
+ if not is_text then return H.set_buflines(buf_id, { '-Non-text-file' .. string.rep('-', width_preview) }) end
2389
+
2390
+ -- Compute lines. Limit number of read lines to work better on large files.
2391
+ local has_lines, read_res = pcall(vim.fn.readfile, path, '', vim.o.lines)
2392
+ -- - Make sure that lines don't contain '\n' (might happen in binary files).
2393
+ local lines = has_lines and vim.split(table.concat(read_res, '\n'), '\n') or {}
2394
+
2395
+ -- Set lines
2396
+ H.set_buflines(buf_id, lines)
2397
+
2398
+ -- Add highlighting if reasonable (for performance or functionality reasons)
2399
+ if H.buffer_should_highlight(buf_id) then
2400
+ local ft = vim.filetype.match({ buf = buf_id, filename = path })
2401
+ local has_lang, lang = pcall(vim.treesitter.language.get_lang, ft)
2402
+ lang = has_lang and lang or ft
2403
+ -- TODO: Remove `opts.error` after compatibility with Neovim=0.11 is dropped
2404
+ local has_parser, parser = pcall(vim.treesitter.get_parser, buf_id, lang, { error = false })
2405
+ has_parser = has_parser and parser ~= nil
2406
+ if has_parser then has_parser = pcall(vim.treesitter.start, buf_id, lang) end
2407
+ if not has_parser then vim.bo[buf_id].syntax = ft end
2408
+ end
2409
+ end
2410
+
2411
+ H.buffer_delete = function(buf_id)
2412
+ if buf_id == nil then return end
2413
+ pcall(vim.api.nvim_buf_delete, buf_id, { force = true })
2414
+ H.opened_buffers[buf_id] = nil
2415
+ end
2416
+
2417
+ H.buffer_compute_fs_diff = function(buf_id)
2418
+ if not H.is_modified_buffer(buf_id) then return {} end
2419
+
2420
+ local path = H.opened_buffers[buf_id].path
2421
+ local lines = vim.api.nvim_buf_get_lines(buf_id, 0, -1, false)
2422
+ local res, present_path_ids = {}, {}
2423
+
2424
+ -- Process present file system entries
2425
+ for _, l in ipairs(lines) do
2426
+ local path_id = H.match_line_path_id(l)
2427
+ local path_from = H.path_index[path_id]
2428
+
2429
+ -- Use whole line as name if no path id is detected
2430
+ local name_to = path_id ~= nil and l:sub(H.match_line_offset(l)) or l
2431
+
2432
+ -- Preserve trailing '/' to distinguish between creating file or directory
2433
+ local path_to = H.fs_child_path(path, name_to) .. (vim.endswith(name_to, '/') and '/' or '')
2434
+
2435
+ -- Ignore blank lines and already synced entries (even several user-copied)
2436
+ if l:find('^%s*$') == nil and H.sanitize_string(path_from) ~= H.sanitize_string(path_to) then
2437
+ table.insert(res, { from = path_from, to = path_to, dir = path })
2438
+ elseif path_id ~= nil then
2439
+ present_path_ids[path_id] = true
2440
+ end
2441
+ end
2442
+
2443
+ -- Detect missing file system entries
2444
+ local ref_path_ids = H.opened_buffers[buf_id].children_path_ids
2445
+ for _, ref_id in ipairs(ref_path_ids) do
2446
+ if not present_path_ids[ref_id] then table.insert(res, { from = H.path_index[ref_id], to = nil, dir = path }) end
2447
+ end
2448
+
2449
+ return res
2450
+ end
2451
+
2452
+ H.buffer_should_highlight = function(buf_id)
2453
+ -- Highlight if buffer size is not too big, both in total and per line
2454
+ local buf_size = vim.api.nvim_buf_call(buf_id, function() return vim.fn.line2byte(vim.fn.line('$') + 1) end)
2455
+ return buf_size <= 1000000 and buf_size <= 1000 * vim.api.nvim_buf_line_count(buf_id)
2456
+ end
2457
+
2458
+ H.is_opened_buffer = function(buf_id) return H.opened_buffers[buf_id] ~= nil end
2459
+
2460
+ H.is_modified_buffer = function(buf_id)
2461
+ local data = H.opened_buffers[buf_id]
2462
+ return data ~= nil and data.n_modified > 0
2463
+ end
2464
+
2465
+ H.match_line_entry_name = function(l)
2466
+ if l == nil then return nil end
2467
+ local offset = H.match_line_offset(l)
2468
+ -- Go up until first occurrence of path separator allowing to track entries
2469
+ -- like `a/b.lua` when creating nested structure
2470
+ local res = l:sub(offset):gsub('/.*$', '')
2471
+ return res
2472
+ end
2473
+
2474
+ H.match_line_offset = function(l)
2475
+ if l == nil then return nil end
2476
+ return l:match('^/.-/.-/()') or 1
2477
+ end
2478
+
2479
+ H.match_line_path_id = function(l)
2480
+ if l == nil then return nil end
2481
+
2482
+ local id_str = l:match('^/(%d+)')
2483
+ local ok, res = pcall(tonumber, id_str)
2484
+ if not ok then return nil end
2485
+ return res
2486
+ end
2487
+
2488
+ -- Windows --------------------------------------------------------------------
2489
+ H.window_open = function(buf_id, config)
2490
+ -- Add always the same extra data
2491
+ config.anchor = 'NW'
2492
+ config.border = (vim.fn.exists('+winborder') == 0 or vim.o.winborder == '') and 'single' or nil
2493
+ config.focusable = true
2494
+ config.relative = 'editor'
2495
+ config.style = 'minimal'
2496
+ -- - Use 99 to allow built-in completion to be on top
2497
+ config.zindex = 99
2498
+
2499
+ -- Add temporary data which will be updated later
2500
+ config.row = 1
2501
+
2502
+ -- Open without entering
2503
+ local win_id = vim.api.nvim_open_win(buf_id, false, config)
2504
+
2505
+ -- Set permanent window options
2506
+ vim.wo[win_id].concealcursor = 'nvic'
2507
+ vim.wo[win_id].foldenable = false
2508
+ vim.wo[win_id].foldmethod = 'manual'
2509
+ vim.wo[win_id].wrap = false
2510
+
2511
+ -- Conceal path id and prefix separators
2512
+ vim.api.nvim_win_call(win_id, function()
2513
+ vim.fn.matchadd('Conceal', [[^/\d\+/]])
2514
+ vim.fn.matchadd('Conceal', [[^/\d\+/[^/]*\zs/\ze]])
2515
+ end)
2516
+
2517
+ -- Set permanent window highlights
2518
+ H.window_update_highlight(win_id, 'NormalFloat', 'MiniFilesNormal')
2519
+ H.window_update_highlight(win_id, 'FloatTitle', 'MiniFilesTitle')
2520
+ H.window_update_highlight(win_id, 'CursorLine', 'MiniFilesCursorLine')
2521
+
2522
+ return win_id
2523
+ end
2524
+
2525
+ H.window_update = function(win_id, config)
2526
+ -- Preserve some config values
2527
+ local win_config = vim.api.nvim_win_get_config(win_id)
2528
+ config.border, config.title_pos = win_config.border, win_config.title_pos
2529
+
2530
+ -- Compute helper data
2531
+ local has_tabline = vim.o.showtabline == 2 or (vim.o.showtabline == 1 and #vim.api.nvim_list_tabpages() > 1)
2532
+ local max_height = H.window_get_max_height()
2533
+ local max_width = vim.o.columns - (config.border == 'none' and 0 or 2)
2534
+
2535
+ -- Ensure proper fit
2536
+ config.row = has_tabline and 1 or 0
2537
+ config.height = config.height ~= nil and math.min(config.height, max_height) or nil
2538
+ config.width = config.width ~= nil and math.min(config.width, max_width) or nil
2539
+
2540
+ -- Ensure proper title
2541
+ if type(config.title) == 'string' then config.title = H.fit_to_width(config.title, config.width) end
2542
+
2543
+ -- Update config
2544
+ config.relative = 'editor'
2545
+ vim.api.nvim_win_set_config(win_id, config)
2546
+
2547
+ -- Reset basic highlighting (removes possible "focused" highlight group)
2548
+ H.window_update_highlight(win_id, 'FloatTitle', 'MiniFilesTitle')
2549
+
2550
+ -- Make sure proper `conceallevel` (can be not the case with 'noice.nvim')
2551
+ vim.wo[win_id].conceallevel = 3
2552
+ end
2553
+
2554
+ H.window_update_highlight = function(win_id, new_from, new_to)
2555
+ local new_entry = new_from .. ':' .. new_to
2556
+ local replace_pattern = string.format('(%s:[^,]*)', vim.pesc(new_from))
2557
+ local new_winhighlight, n_replace = vim.wo[win_id].winhighlight:gsub(replace_pattern, new_entry)
2558
+ if n_replace == 0 then new_winhighlight = new_winhighlight .. ',' .. new_entry end
2559
+
2560
+ vim.wo[win_id].winhighlight = new_winhighlight
2561
+ end
2562
+
2563
+ H.window_focus = function(win_id)
2564
+ vim.api.nvim_set_current_win(win_id)
2565
+ H.window_update_highlight(win_id, 'FloatTitle', 'MiniFilesTitleFocused')
2566
+ end
2567
+
2568
+ H.window_close = function(win_id)
2569
+ if win_id == nil then return end
2570
+ local has_buffer, buf_id = pcall(vim.api.nvim_win_get_buf, win_id)
2571
+ if has_buffer then H.opened_buffers[buf_id].win_id = nil end
2572
+ pcall(vim.api.nvim_win_close, win_id, true)
2573
+ end
2574
+
2575
+ H.window_set_view = function(win_id, view)
2576
+ -- Set buffer
2577
+ local buf_id, buf_data = view.buf_id, H.opened_buffers[view.buf_id]
2578
+ H.win_set_buf(win_id, buf_id)
2579
+ -- - Update buffer register. No need to update previous buffer data, as it
2580
+ -- should already be invalidated.
2581
+ buf_data.win_id = win_id
2582
+
2583
+ -- Set cursor (if defined), visible only in directories
2584
+ pcall(H.window_set_cursor, win_id, view.cursor)
2585
+ -- NOTE: set 'cursorline[opt]' here because changing buffer might remove it
2586
+ vim.wo[win_id].cursorline = H.fs_get_type(buf_data.path) == 'directory'
2587
+ local culopt = vim.wo[win_id].cursorlineopt
2588
+ if culopt:find('line') == nil then vim.wo[win_id].cursorlineopt = culopt .. ',line' end
2589
+
2590
+ -- Respect global 'list' option, as it is disabled in floating windows
2591
+ -- TODO: Use vim.wo[win_id][0] after compatibility with Neovim=0.9 is dropped
2592
+ local opts_scope = { scope = 'local', win = win_id }
2593
+ vim.api.nvim_set_option_value('list', vim.go.list, opts_scope)
2594
+ vim.api.nvim_set_option_value('listchars', vim.go.listchars, opts_scope)
2595
+
2596
+ -- Update border highlight based on buffer status
2597
+ H.window_update_border_hl(win_id)
2598
+ end
2599
+
2600
+ H.window_set_cursor = function(win_id, cursor)
2601
+ if type(cursor) ~= 'table' then return end
2602
+
2603
+ vim.api.nvim_win_set_cursor(win_id, cursor)
2604
+
2605
+ -- Tweak cursor here and don't rely on `CursorMoved` event to reduce flicker
2606
+ H.window_tweak_cursor(win_id, vim.api.nvim_win_get_buf(win_id))
2607
+ end
2608
+
2609
+ H.window_tweak_cursor = function(win_id, buf_id)
2610
+ local cursor = vim.api.nvim_win_get_cursor(win_id)
2611
+ local l = H.get_bufline(buf_id, cursor[1])
2612
+
2613
+ local cur_offset = H.match_line_offset(l)
2614
+ if cursor[2] < (cur_offset - 1) then
2615
+ cursor[2] = cur_offset - 1
2616
+ vim.api.nvim_win_set_cursor(win_id, cursor)
2617
+ -- Ensure icons are shown (may be not the case after horizontal scroll)
2618
+ vim.cmd('normal! 1000zh')
2619
+ end
2620
+
2621
+ return cursor
2622
+ end
2623
+
2624
+ H.window_update_border_hl = function(win_id)
2625
+ if not H.is_valid_win(win_id) then return end
2626
+ local buf_id = vim.api.nvim_win_get_buf(win_id)
2627
+
2628
+ local border_hl = H.is_modified_buffer(buf_id) and 'MiniFilesBorderModified' or 'MiniFilesBorder'
2629
+ H.window_update_highlight(win_id, 'FloatBorder', border_hl)
2630
+ end
2631
+
2632
+ H.window_get_max_height = function()
2633
+ local has_tabline = vim.o.showtabline == 2 or (vim.o.showtabline == 1 and #vim.api.nvim_list_tabpages() > 1)
2634
+ local has_statusline = vim.o.laststatus > 0
2635
+ -- Remove 2 from maximum height to account for top and bottom borders
2636
+ return vim.o.lines - vim.o.cmdheight - (has_tabline and 1 or 0) - (has_statusline and 1 or 0) - 2
2637
+ end
2638
+
2639
+ -- File system ----------------------------------------------------------------
2640
+ ---@class fs_entry
2641
+ ---@field name string Base name.
2642
+ ---@field fs_type string One of "directory" or "file".
2643
+ ---@field path string Full path.
2644
+ ---@field path_id number Id of full path.
2645
+ ---@private
2646
+ H.fs_read_dir = function(path, content_opts)
2647
+ local fs = vim.loop.fs_scandir(path)
2648
+ local res = {}
2649
+ if not fs then return res end
2650
+
2651
+ -- Read all entries
2652
+ local name, fs_type = vim.loop.fs_scandir_next(fs)
2653
+ while name do
2654
+ if not (fs_type == 'file' or fs_type == 'directory') then fs_type = H.fs_get_type(H.fs_child_path(path, name)) end
2655
+ table.insert(res, { fs_type = fs_type, name = name, path = H.fs_child_path(path, name) })
2656
+ name, fs_type = vim.loop.fs_scandir_next(fs)
2657
+ end
2658
+
2659
+ -- Filter and sort entries
2660
+ res = content_opts.sort(vim.tbl_filter(content_opts.filter, res))
2661
+
2662
+ -- Add new data: absolute file path and its index
2663
+ for _, entry in ipairs(res) do
2664
+ entry.path_id = H.add_path_to_index(entry.path)
2665
+ end
2666
+
2667
+ return res
2668
+ end
2669
+
2670
+ H.add_path_to_index = function(path)
2671
+ local cur_id = H.path_index[path]
2672
+ if cur_id ~= nil then return cur_id end
2673
+
2674
+ local new_id = #H.path_index + 1
2675
+ H.path_index[new_id] = path
2676
+ H.path_index[path] = new_id
2677
+
2678
+ return new_id
2679
+ end
2680
+
2681
+ H.get_fs_entry_from_path_index = function(path_id)
2682
+ local path = H.path_index[path_id]
2683
+ if path == nil then return nil end
2684
+ return { fs_type = H.fs_get_type(path), name = H.fs_get_basename(path), path = path }
2685
+ end
2686
+
2687
+ H.replace_path_in_index = function(from, to)
2688
+ local from_id, to_id = H.path_index[from], H.path_index[to]
2689
+ H.path_index[from_id], H.path_index[to] = to, from_id
2690
+ if to_id then H.path_index[to_id] = nil end
2691
+ -- Remove `from` from index assuming it doesn't exist anymore (no duplicates)
2692
+ H.path_index[from] = nil
2693
+ end
2694
+
2695
+ H.compare_fs_entries = function(a, b)
2696
+ -- Put directory first
2697
+ if a.is_dir and not b.is_dir then return true end
2698
+ if not a.is_dir and b.is_dir then return false end
2699
+
2700
+ -- Otherwise order alphabetically ignoring case
2701
+ return a.lower_name < b.lower_name
2702
+ end
2703
+
2704
+ H.fs_normalize_path = function(path) return (path:gsub('/+', '/'):gsub('(.)/$', '%1')) end
2705
+ if H.is_windows then
2706
+ H.fs_normalize_path = function(path)
2707
+ return (path:gsub('\\', '/'):gsub('([^/:])/+', '%1/'):gsub('([^:])/+$', '%1'):gsub('^(%a):/+([^/])', '%1://%2'))
2708
+ end
2709
+ end
2710
+
2711
+ H.fs_is_imaginary_path = function(path) return path:sub(-1) == '\000' end
2712
+
2713
+ H.fs_is_present_path = function(path) return vim.loop.fs_stat(path) ~= nil and not H.fs_is_imaginary_path(path) end
2714
+
2715
+ H.fs_child_path = function(dir, name) return H.fs_normalize_path(string.format('%s/%s', dir, name)) end
2716
+
2717
+ H.fs_full_path = function(path) return H.fs_normalize_path(vim.fn.fnamemodify(path, ':p')) end
2718
+
2719
+ H.fs_shorten_path = function(path)
2720
+ -- Replace home directory with '~'
2721
+ path = H.fs_normalize_path(path)
2722
+ local home_dir = H.fs_normalize_path(vim.loop.os_homedir() or '~')
2723
+ return (path:gsub('^' .. vim.pesc(home_dir), '~'))
2724
+ end
2725
+
2726
+ H.fs_relcwd_path = function(path) return vim.fn.fnamemodify(path, ':.') end
2727
+ if H.is_windows then
2728
+ -- Make manually because `C://dir/file` paths conflict with `C:/dir` cwd path
2729
+ H.fs_relcwd_path = function(path)
2730
+ local cwd = H.fs_normalize_path(vim.fn.getcwd())
2731
+ return (H.fs_normalize_path(path):gsub('^' .. vim.pesc(cwd) .. '/', ''))
2732
+ end
2733
+ end
2734
+
2735
+ H.fs_get_basename = function(path) return H.fs_normalize_path(path):match('[^/]+$') end
2736
+
2737
+ H.fs_get_parent = function(path)
2738
+ path = H.fs_full_path(path)
2739
+
2740
+ -- Deal with top root paths
2741
+ local is_top = H.fs_is_windows_top(path) or path == '/'
2742
+ if is_top then return nil end
2743
+
2744
+ -- Compute parent
2745
+ local res = H.fs_normalize_path(path:match('^.*/'))
2746
+ -- - Deal with Windows top directory separately
2747
+ local suffix = H.fs_is_windows_top(res) and '/' or ''
2748
+ return res .. suffix
2749
+ end
2750
+
2751
+ H.fs_is_windows_top = function(path) return H.is_windows and path:find('^%w:[\\/]?$') ~= nil end
2752
+
2753
+ H.fs_get_type = function(path)
2754
+ if not (not H.fs_is_imaginary_path(path) and H.fs_is_present_path(path)) then return nil end
2755
+ return vim.fn.isdirectory(path) == 1 and 'directory' or 'file'
2756
+ end
2757
+
2758
+ -- File system actions --------------------------------------------------------
2759
+ H.fs_actions_to_lines = function(fs_actions)
2760
+ -- Gather actions per source directory
2761
+ local short = H.fs_shorten_path
2762
+ local dir
2763
+ local rel = function(p) return vim.startswith(p, dir .. '/') and p:sub(#dir + 2):gsub('/$', '') or short(p) end
2764
+
2765
+ local actions_per_dir = {}
2766
+ --stylua: ignore
2767
+ for _, diff in ipairs(fs_actions) do
2768
+ -- Set grouping directory to also be used to compute relative paths
2769
+ dir = diff.action == 'create' and diff.dir or H.fs_get_parent(diff.from)
2770
+
2771
+ -- Compute line depending on action
2772
+ local action, l = diff.action, nil
2773
+ local to_type = (diff.to or ''):sub(-1) == '/' and 'directory' or 'file'
2774
+ local del_type = diff.to == nil and 'permanently' or 'to trash'
2775
+ if action == 'create' then l = string.format("CREATE │ %s (%s)", rel(diff.to), to_type) end
2776
+ if action == 'delete' then l = string.format("DELETE │ %s (%s)", rel(diff.from), del_type) end
2777
+ if action == 'copy' then l = string.format("COPY │ %s => %s", rel(diff.from), rel(diff.to)) end
2778
+ if action == 'move' then l = string.format("MOVE │ %s => %s", rel(diff.from), rel(diff.to)) end
2779
+ if action == 'rename' then l = string.format("RENAME │ %s => %s", rel(diff.from), rel(diff.to)) end
2780
+
2781
+ -- Add to per directory lines
2782
+ local dir_actions = actions_per_dir[dir] or {}
2783
+ table.insert(dir_actions, ' ' .. H.sanitize_string(l))
2784
+ actions_per_dir[dir] = dir_actions
2785
+ end
2786
+
2787
+ -- Convert to final lines
2788
+ local res = { 'CONFIRM FILE SYSTEM ACTIONS', '' }
2789
+ for path, dir_actions in pairs(actions_per_dir) do
2790
+ table.insert(res, short(path))
2791
+ vim.list_extend(res, dir_actions)
2792
+ table.insert(res, '')
2793
+ end
2794
+
2795
+ return res
2796
+ end
2797
+
2798
+ H.fs_actions_apply = function(fs_actions, lsp_timeout)
2799
+ H.lsp_fs_hook('willCreate', fs_actions, lsp_timeout)
2800
+ H.lsp_fs_hook('willDelete', fs_actions, lsp_timeout)
2801
+ H.lsp_fs_hook('willRename', fs_actions, lsp_timeout)
2802
+
2803
+ local ok_actions = {}
2804
+ for i = 1, #fs_actions do
2805
+ local diff, action = fs_actions[i], fs_actions[i].action
2806
+ local ok, success = pcall(H.fs_do[action], diff.from, diff.to)
2807
+ if ok and success then
2808
+ table.insert(ok_actions, diff)
2809
+
2810
+ -- Trigger event
2811
+ local to = action == 'create' and diff.to:gsub('/$', '') or diff.to
2812
+ local data = { action = action, from = diff.from, to = to }
2813
+ local action_titlecase = action:sub(1, 1):upper() .. action:sub(2)
2814
+ H.trigger_event('MiniFilesAction' .. action_titlecase, data)
2815
+
2816
+ -- Modify later actions to account for file movement
2817
+ local has_moved = to ~= nil and not (action == 'copy' or action == 'create')
2818
+ if has_moved then H.adjust_after_move(diff.from, to, fs_actions, i + 1) end
2819
+ end
2820
+ end
2821
+
2822
+ H.lsp_fs_hook('didCreate', ok_actions, lsp_timeout)
2823
+ H.lsp_fs_hook('didDelete', ok_actions, lsp_timeout)
2824
+ H.lsp_fs_hook('didRename', ok_actions, lsp_timeout)
2825
+ end
2826
+
2827
+ H.lsp_fs_hook = function(method, diffs, lsp_timeout)
2828
+ if lsp_timeout == 0 then return end
2829
+
2830
+ local full_method = 'workspace/' .. method .. 'Files'
2831
+ local clients = vim.lsp.get_clients({ method = full_method })
2832
+ if #clients == 0 then return end
2833
+
2834
+ -- Transform 'mini.files' diffs into LSP file actions for the input method
2835
+ -- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#createFilesParams
2836
+ -- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#deleteFilesParams
2837
+ -- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#renameFilesParams
2838
+ local files, to_uri = {}, vim.uri_from_fname
2839
+ local needs_check = method == 'willCreate' or method == 'willRename'
2840
+ local is_create, is_delete, is_rename =
2841
+ vim.endswith(method, 'Create'), vim.endswith(method, 'Delete'), vim.endswith(method, 'Rename')
2842
+ for _, d in ipairs(diffs) do
2843
+ local file = {}
2844
+ if is_create and d.action == 'create' then file = { uri = to_uri(d.to) } end
2845
+ if is_delete and d.action == 'delete' then file = { uri = to_uri(d.from) } end
2846
+ if is_rename and d.action == 'rename' then file = { oldUri = to_uri(d.from), newUri = to_uri(d.to) } end
2847
+
2848
+ -- Precompute LSP file type for filters (path can be not yet on disk)
2849
+ file.fs_type = (d.from or d.to):find('/$') ~= nil and 'folder' or 'file'
2850
+
2851
+ -- Some actions might not succeed, so make best effort check before that
2852
+ local pass_check = not needs_check or (needs_check and d.to ~= nil and not H.fs_is_present_path(d.to))
2853
+ if (file.uri or file.oldUri) ~= nil and pass_check then table.insert(files, file) end
2854
+ end
2855
+
2856
+ -- Execute LSP action for every currently existing client
2857
+ if #files == 0 then return end
2858
+ for _, client in ipairs(clients) do
2859
+ H.lsp_fs_hook_client(client, full_method, files, lsp_timeout)
2860
+ end
2861
+ end
2862
+ -- TODO: Remove after compatibility with Neovim=0.10 is dropped
2863
+ if vim.fn.has('nvim-0.11') == 0 then H.lsp_fs_hook = function() end end
2864
+
2865
+ H.lsp_fs_hook_client = function(client, full_method, lsp_files, timeout)
2866
+ -- Compute parameters of the LSP action by filtering all input file actions
2867
+ -- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#fileOperationFilter
2868
+ local is_scheme = function(uri, scheme) return scheme == nil or vim.startswith(uri, scheme .. ':') end
2869
+ local is_fs_type = function(lsp_file, ref_fs_type) return ref_fs_type == nil or ref_fs_type == lsp_file.fs_type end
2870
+ local make_filter = function(scheme, ref_fs_type, glob, ignore_case)
2871
+ local adjust_case = ignore_case and vim.fn.tolower or function(x) return x end
2872
+ -- On Windows `uri_to_fname` forces `\`, but forcing / seems more robust
2873
+ local to_fname = H.is_windows and function(x) return (vim.uri_to_fname(x):gsub('\\', '/')) end or vim.uri_to_fname
2874
+ local glob_lpeg = vim.glob.to_lpeg(adjust_case(glob) or '**')
2875
+ return function(lsp_file)
2876
+ local uri = lsp_file.uri or lsp_file.oldUri
2877
+ local fname = adjust_case(to_fname(uri))
2878
+ return is_scheme(uri, scheme) and is_fs_type(lsp_file, ref_fs_type) and glob_lpeg:match(fname) ~= nil
2879
+ end
2880
+ end
2881
+
2882
+ local method = full_method:match('^workspace/(.+)Files$')
2883
+ local filter_configs = client.server_capabilities.workspace.fileOperations[method].filters
2884
+ local filters = {}
2885
+ for _, fc in ipairs(filter_configs) do
2886
+ local glob, matches, scheme = fc.pattern.glob, fc.pattern.matches, fc.scheme
2887
+ local ignore_case = type(fc.pattern.options) == 'table' and fc.pattern.options.ignoreCase
2888
+
2889
+ table.insert(filters, make_filter(scheme, matches, glob, ignore_case))
2890
+ end
2891
+
2892
+ local params = { files = {} }
2893
+ for _, f in ipairs(lsp_files) do
2894
+ -- https://github.com/microsoft/language-server-protocol/issues/2203
2895
+ -- Empty filters should match nothing, but this a useful default for misbehaving servers
2896
+ local ok = #filters == 0
2897
+ for _, filt in ipairs(filters) do
2898
+ -- It is not clear from LSP spec if it is `and` or `or`, but it needs to
2899
+ -- handle filters matching both `file` and `folder`. So `or`.
2900
+ ok = ok or filt(f)
2901
+ end
2902
+ -- Remove manually added field to comply with LSP spec
2903
+ f.fs_type = nil
2904
+ if ok then table.insert(params.files, f) end
2905
+ end
2906
+
2907
+ -- Perform an action
2908
+ if vim.startswith(method, 'did') then return client:notify(full_method, params) end
2909
+ -- - Use sync to comply with LSP spec (apply edit before file operations)
2910
+ local response, err = client:request_sync(full_method, params, timeout)
2911
+ if (response or {}).result ~= nil then vim.lsp.util.apply_workspace_edit(response.result, client.offset_encoding) end
2912
+ end
2913
+
2914
+ H.fs_do = {}
2915
+
2916
+ H.fs_do.create = function(_, path)
2917
+ -- Don't override existing path
2918
+ if H.fs_is_present_path(path) then return H.warn_existing_path(path, 'create') end
2919
+
2920
+ -- Create parent directory allowing nested names
2921
+ vim.fn.mkdir(H.fs_get_parent(path), 'p')
2922
+
2923
+ -- Create
2924
+ local fs_type = path:sub(-1) == '/' and 'directory' or 'file'
2925
+ if fs_type == 'directory' then return vim.fn.mkdir(path) == 1 end
2926
+ return vim.fn.writefile({}, path) == 0
2927
+ end
2928
+
2929
+ H.fs_do.copy = function(from, to)
2930
+ -- Don't override existing path
2931
+ if H.fs_is_present_path(to) then return H.warn_existing_path(from, 'copy') end
2932
+
2933
+ local from_type = H.fs_get_type(from)
2934
+ if from_type == nil then return false end
2935
+
2936
+ -- Allow copying inside non-existing directory
2937
+ vim.fn.mkdir(H.fs_get_parent(to), 'p')
2938
+
2939
+ -- Copy file directly
2940
+ if from_type == 'file' then return vim.loop.fs_copyfile(from, to) end
2941
+
2942
+ -- Recursively copy a directory
2943
+ local fs_entries = H.fs_read_dir(from, { filter = function() return true end, sort = function(x) return x end })
2944
+ -- NOTE: Create directory *after* reading entries to allow copy inside itself
2945
+ vim.fn.mkdir(to)
2946
+
2947
+ local success = true
2948
+ for _, entry in ipairs(fs_entries) do
2949
+ success = success and H.fs_do.copy(entry.path, H.fs_child_path(to, entry.name))
2950
+ end
2951
+
2952
+ return success
2953
+ end
2954
+
2955
+ H.fs_do.delete = function(from, to)
2956
+ -- Act based on whether delete is permanent or not
2957
+ if to == nil then return vim.fn.delete(from, 'rf') == 0 end
2958
+ pcall(vim.fn.delete, to, 'rf')
2959
+ -- Move to trash but skip renaming loaded buffer (same as with permanent
2960
+ -- delete). This also skips triggering extra autocommands that can have
2961
+ -- unwanted side effects (like loading LSP in the new "trash" project).
2962
+ return H.fs_do.move(from, to, true)
2963
+ end
2964
+
2965
+ H.fs_do.move = function(from, to, skip_buf_rename)
2966
+ -- Don't override existing path
2967
+ if H.fs_is_present_path(to) then return H.warn_existing_path(from, 'move or rename') end
2968
+
2969
+ -- Move while allowing to create directory
2970
+ vim.fn.mkdir(H.fs_get_parent(to), 'p')
2971
+ local success, _, err_code = vim.loop.fs_rename(from, to)
2972
+
2973
+ if err_code == 'EXDEV' then
2974
+ -- Handle cross-device move separately as `loop.fs_rename` does not work
2975
+ success = H.fs_do.copy(from, to)
2976
+ if success then success = pcall(vim.fn.delete, from, 'rf') end
2977
+ if not success then pcall(vim.fn.delete, to, 'rf') end
2978
+ end
2979
+
2980
+ if not success then return success end
2981
+
2982
+ -- Update path index to allow consecutive moves after undo (which also
2983
+ -- restores previous concealed path index)
2984
+ H.replace_path_in_index(from, to)
2985
+
2986
+ -- Rename in loaded buffers
2987
+ if not skip_buf_rename then
2988
+ for _, buf_id in ipairs(vim.api.nvim_list_bufs()) do
2989
+ H.rename_loaded_buffer(buf_id, from, to)
2990
+ end
2991
+ end
2992
+
2993
+ return success
2994
+ end
2995
+
2996
+ H.fs_do.rename = H.fs_do.move
2997
+
2998
+ H.rename_loaded_buffer = function(buf_id, from, to)
2999
+ if not (vim.api.nvim_buf_is_loaded(buf_id) and vim.bo[buf_id].buftype == '') then return end
3000
+ -- Make sure buffer name is normalized (same as `from` and `to`)
3001
+ local cur_name = H.fs_normalize_path(vim.api.nvim_buf_get_name(buf_id))
3002
+
3003
+ -- Use `gsub('^' ...)` to also take into account directory renames
3004
+ local new_name = cur_name:gsub('^' .. vim.pesc(from), to)
3005
+ if cur_name == new_name then return end
3006
+
3007
+ -- Rename buffer using relative form (for nicer `:buffers` output)
3008
+ vim.api.nvim_buf_set_name(buf_id, H.fs_relcwd_path(new_name))
3009
+
3010
+ -- Force write to avoid the 'overwrite existing file' error message on write
3011
+ -- for normal files
3012
+ vim.api.nvim_buf_call(buf_id, function() vim.cmd('silent! write! | edit') end)
3013
+ end
3014
+
3015
+ H.warn_existing_path = function(path, action)
3016
+ H.notify(string.format('Can not %s %s. Target path already exists.', action, path), 'WARN')
3017
+ return false
3018
+ end
3019
+
3020
+ H.adjust_after_move = function(from, to, fs_actions, start_ind)
3021
+ local from_dir_pattern, to_dir = '^' .. vim.pesc(from .. '/'), to .. '/'
3022
+ for i = start_ind, #fs_actions do
3023
+ local diff = fs_actions[i]
3024
+ -- Adjust completely to use entry at new location
3025
+ if diff.from ~= nil then diff.from = diff.from == from and to or diff.from:gsub(from_dir_pattern, to_dir) end
3026
+ -- Adjust only parent directory to correctly compute target
3027
+ if diff.to ~= nil then diff.to = diff.to:gsub(from_dir_pattern, to_dir) end
3028
+ end
3029
+ end
3030
+
3031
+ -- Validators -----------------------------------------------------------------
3032
+ H.validate_opened_buffer = function(x)
3033
+ if x == nil or x == 0 then x = vim.api.nvim_get_current_buf() end
3034
+ if not H.is_opened_buffer(x) then H.error('`buf_id` should be an identifier of an opened directory buffer.') end
3035
+ return x
3036
+ end
3037
+
3038
+ H.validate_line = function(buf_id, x)
3039
+ x = x or vim.fn.line('.')
3040
+ if not (type(x) == 'number' and 1 <= x and x <= vim.api.nvim_buf_line_count(buf_id)) then
3041
+ H.error('`line` should be a valid line number in buffer ' .. buf_id .. '.')
3042
+ end
3043
+ return x
3044
+ end
3045
+
3046
+ H.validate_branch = function(x)
3047
+ if not (H.islist(x) and x[1] ~= nil) then H.error('`branch` should be array with at least one element') end
3048
+ local res = {}
3049
+ for i, p in ipairs(x) do
3050
+ if type(p) ~= 'string' then H.error('`branch` contains not string: ' .. vim.inspect(p)) end
3051
+ p = H.fs_full_path(p)
3052
+ if not H.fs_is_present_path(p) then H.error('`branch` contains not present path: ' .. vim.inspect(p)) end
3053
+ res[i] = p
3054
+ end
3055
+ for i = 2, #res do
3056
+ local parent, child = res[i - 1], res[i]
3057
+ if H.fs_child_path(parent, child:match('[^/]+$')) ~= child then
3058
+ H.error('`branch` contains not a parent-child pair: ' .. vim.inspect(parent) .. ' and ' .. vim.inspect(child))
3059
+ end
3060
+ end
3061
+ if #res == 1 and H.fs_get_type(res[1]) == 'file' then H.error('`branch` should contain at least one directory') end
3062
+ return res
3063
+ end
3064
+
3065
+ -- Utilities ------------------------------------------------------------------
3066
+ H.error = function(msg) error('(mini.files) ' .. msg, 0) end
3067
+
3068
+ H.check_type = function(name, val, ref, allow_nil)
3069
+ if type(val) == ref or (ref == 'callable' and vim.is_callable(val)) or (allow_nil and val == nil) then return end
3070
+ H.error(string.format('`%s` should be %s, not %s', name, ref, type(val)))
3071
+ end
3072
+
3073
+ H.set_buf_name = function(buf_id, name) vim.api.nvim_buf_set_name(buf_id, 'minifiles://' .. buf_id .. '/' .. name) end
3074
+
3075
+ H.notify = function(msg, level_name) vim.notify('(mini.files) ' .. msg, vim.log.levels[level_name]) end
3076
+
3077
+ H.map = function(mode, lhs, rhs, opts)
3078
+ if lhs == '' then return end
3079
+ opts = vim.tbl_deep_extend('force', { silent = true }, opts or {})
3080
+ vim.keymap.set(mode, lhs, rhs, opts)
3081
+ end
3082
+
3083
+ H.edit = function(path, win_id)
3084
+ if type(path) ~= 'string' then return end
3085
+ local b = vim.api.nvim_win_get_buf(win_id or 0)
3086
+ local try_mimic_buf_reuse = (vim.fn.bufname(b) == '' and vim.bo[b].buftype ~= 'quickfix' and not vim.bo[b].modified)
3087
+ and (#vim.fn.win_findbuf(b) == 1 and vim.deep_equal(vim.fn.getbufline(b, 1, '$'), { '' }))
3088
+ local buf_id = vim.fn.bufadd(H.fs_relcwd_path(path))
3089
+ -- Showing in window also loads. Use `pcall` to not error with swap messages.
3090
+ pcall(vim.api.nvim_win_set_buf, win_id or 0, buf_id)
3091
+ vim.bo[buf_id].buflisted = true
3092
+ if try_mimic_buf_reuse then pcall(vim.api.nvim_buf_delete, b, { unload = false }) end
3093
+ return buf_id
3094
+ end
3095
+
3096
+ H.trigger_event = function(event_name, data) vim.api.nvim_exec_autocmds('User', { pattern = event_name, data = data }) end
3097
+
3098
+ H.is_valid_buf = function(buf_id) return type(buf_id) == 'number' and vim.api.nvim_buf_is_valid(buf_id) end
3099
+
3100
+ H.is_valid_win = function(win_id) return type(win_id) == 'number' and vim.api.nvim_win_is_valid(win_id) end
3101
+
3102
+ H.fit_to_width = function(text, width)
3103
+ local t_width = vim.fn.strchars(text)
3104
+ return t_width <= width and text or ('…' .. vim.fn.strcharpart(text, t_width - width + 1, width - 1))
3105
+ end
3106
+
3107
+ H.get_bufline = function(buf_id, line) return vim.api.nvim_buf_get_lines(buf_id, line - 1, line, false)[1] end
3108
+
3109
+ H.set_buflines = function(buf_id, lines)
3110
+ local cmd =
3111
+ string.format('lockmarks lua vim.api.nvim_buf_set_lines(%d, 0, -1, false, %s)', buf_id, vim.inspect(lines))
3112
+ vim.cmd(cmd)
3113
+ end
3114
+
3115
+ H.set_extmark = function(...) pcall(vim.api.nvim_buf_set_extmark, ...) end
3116
+
3117
+ H.win_set_buf = function(win_id, buf_id)
3118
+ vim.wo[win_id].winfixbuf = false
3119
+ -- Prevent `BufEnter,BufLeave` that come from `nvim_win_set_buf` and conflict
3120
+ -- with other modules (like 'mini.jump'). Use 'mini.files' events if needed.
3121
+ local cmd = string.format('noautocmd call nvim_win_set_buf(%d, %d)', win_id, buf_id)
3122
+ vim.cmd(cmd)
3123
+ vim.wo[win_id].winfixbuf = true
3124
+ end
3125
+ if vim.fn.has('nvim-0.10') == 0 then H.win_set_buf = vim.api.nvim_win_set_buf end
3126
+
3127
+ H.get_first_valid_normal_window = function()
3128
+ for _, win_id in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
3129
+ if vim.api.nvim_win_get_config(win_id).relative == '' then return win_id end
3130
+ end
3131
+ end
3132
+
3133
+ H.getcharstr = function()
3134
+ local ok, char = pcall(vim.fn.getcharstr)
3135
+ -- Terminate if couldn't get input (like with <C-c>) or on `<Esc>`
3136
+ if not ok or char == '' or char == '\3' or char == '\27' then return nil end
3137
+ return char
3138
+ end
3139
+
3140
+ H.sanitize_string = function(x) return ((x or ''):gsub('\n', '<NL>'):gsub('%z', '')) end
3141
+
3142
+ -- TODO: Remove after compatibility with Neovim=0.9 is dropped
3143
+ H.islist = vim.fn.has('nvim-0.10') == 1 and vim.islist or vim.tbl_islist
3144
+
3145
+ return MiniFiles