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,1577 @@
1
+ --- *mini.visits* Track and reuse file system visits
2
+ ---
3
+ --- MIT License Copyright (c) 2023 Evgeni Chasnovski
4
+
5
+ --- Features:
6
+ ---
7
+ --- - Persistently track file system visits (both files and directories)
8
+ --- per project directory. Store visit index is human readable and editable.
9
+ ---
10
+ --- - Visit index is normalized on every write to contain relevant information.
11
+ --- Exact details can be customized. See |MiniVisits.normalize_index()|.
12
+ ---
13
+ --- - Built-in ability to persistently add labels to path for later use.
14
+ --- See |MiniVisits.add_label()| and |MiniVisits.remove_label()|.
15
+ ---
16
+ --- - Exported functions to reuse visit data:
17
+ --- - List visited paths/labels with custom filter and sort (uses "robust
18
+ --- frecency" by default). Can be used as source for pickers.
19
+ --- See |MiniVisits.list_paths()| and |MiniVisits.list_labels()|.
20
+ --- See |MiniVisits.gen_filter| and |MiniVisits.gen_sort|.
21
+ ---
22
+ --- - Select visited paths/labels using |vim.ui.select()|.
23
+ --- See |MiniVisits.select_path()| and |MiniVisits.select_label()|.
24
+ ---
25
+ --- - Iterate through visit paths in target direction ("forward", "backward",
26
+ --- "first", "last"). See |MiniVisits.iterate_paths()|.
27
+ ---
28
+ --- - Exported functions to manually update visit index allowing persistent
29
+ --- track of any user information. See `*_index()` functions.
30
+ ---
31
+ --- Notes:
32
+ --- - All data is stored _only_ in in-session Lua variable (for quick operation)
33
+ --- and at `config.store.path` on disk (for persistent usage).
34
+ --- - Most of functions affect an in-session data which gets written to disk only
35
+ --- before Neovim is closing or when users asks to.
36
+ --- - It doesn't account for paths being renamed or moved (because there is no
37
+ --- general way to detect that). Usually a manual intervention to the visit
38
+ --- index is required after the change but _before_ the next writing to disk
39
+ --- (usually before closing current session) because it will treat previous
40
+ --- path as deleted and remove it from index.
41
+ --- There is a |MiniVisits.rename_in_index()| helper for that.
42
+ --- If rename/move is done with |mini.files|, index is autoupdated.
43
+ ---
44
+ --- Sources with more details:
45
+ --- - |MiniVisits-overview|
46
+ --- - |MiniVisits-index-specification|
47
+ --- - |MiniVisits-examples|
48
+ ---
49
+ --- # Setup ~
50
+ ---
51
+ --- This module needs a setup with `require('mini.visits').setup({})` (replace
52
+ --- `{}` with your `config` table). It will create global Lua table `MiniVisits`
53
+ --- which you can use for scripting or manually (with `:lua MiniVisits.*`).
54
+ ---
55
+ --- See |MiniVisits.config| for `config` structure and default values.
56
+ ---
57
+ --- You can override runtime config settings locally to buffer inside
58
+ --- `vim.b.minivisits_config` which should have same structure as
59
+ --- `MiniVisits.config`. See |mini.nvim-buffer-local-config| for more details.
60
+ ---
61
+ --- # Comparisons ~
62
+ ---
63
+ --- - [nvim-telescope/telescope-frecency.nvim](https://github.com/nvim-telescope/telescope-frecency.nvim):
64
+ --- - It stores array of actual visit timestamps, while this module tracks
65
+ --- only total number and latest timestamp of visits. This is by design
66
+ --- as a different trade-off between how much data is being used/stored
67
+ --- and complexity of underlying "frecency" sorting.
68
+ --- - By default tracks a buffer only once per session, while this module
69
+ --- tracks on every meaningful buffer enter. This leads to a more relevant
70
+ --- in-session sorting.
71
+ --- - Implements an original frecency algorithm of Firefox's address bar,
72
+ --- while this module uses own "robust frecency" approach.
73
+ --- - Mostly designed to work with `nvim-telescope/telescope.nvim`, while
74
+ --- this module provides general function to list paths and select
75
+ --- with |vim.ui.select()|.
76
+ --- - Does not allow use of custom data (like labels), while this module does.
77
+ ---
78
+ --- - [ThePrimeagen/harpoon](https://github.com/ThePrimeagen/harpoon):
79
+ --- - Has slightly different concept than general labeling, which more
80
+ --- resembles adding paths to an ordered stack. This module implements
81
+ --- a more common labeling which does not imply order with ability to
82
+ --- make it automated depending on the task and/or preference.
83
+ --- - Implements marks as positions in a path, while this module labels paths.
84
+ --- - Writes data on disk after every meaning change, while this module is
85
+ --- more conservative and read only when Neovim closes or when asked to.
86
+ --- - Has support for labeling terminals, while this modules is oriented
87
+ --- only towards paths.
88
+ --- - Has dedicated UI to manage marks, while this module does not by design.
89
+ --- There are functions for adding and removing label from the path.
90
+ --- - Does not provide functionality to track and reuse any visited path,
91
+ --- while this module does.
92
+ ---
93
+ --- # Disabling ~
94
+ ---
95
+ --- To disable automated tracking, set `vim.g.minivisits_disable` (globally) or
96
+ --- `vim.b.minivisits_disable` (for a buffer) to `true`. Considering high
97
+ --- number of different scenarios and customization intentions, writing exact
98
+ --- rules for disabling module's functionality is left to user. See
99
+ --- |mini.nvim-disabling-recipes| for common recipes.
100
+ ---@tag MiniVisits
101
+
102
+ --- # Tracking visits ~
103
+ ---
104
+ --- File system visits (both directory and files) tracking is done in two steps:
105
+ --- - On every dedicated event (`config.track.event`, |BufEnter| by default) timer
106
+ --- is (re)started to actually register visit after certain amount of time
107
+ --- (`config.track.delay` milliseconds, 1000 by default). It is not registered
108
+ --- immediately to allow navigation to target buffer in several steps
109
+ --- (for example, with series of |:bnext| / |:bprevious|).
110
+ ---
111
+ --- - When delay time passes without any dedicated events being triggered
112
+ --- (meaning user is "settled" on certain buffer), |MiniVisits.register_visit()|
113
+ --- is called if all of the following conditions are met:
114
+ --- - Module is not disabled (see "Disabling" section in |mini.visits|).
115
+ --- - Buffer is normal with non-empty name (used as visit path).
116
+ --- - Visit path does not equal to the latest tracked one. This is to allow
117
+ --- temporary enter of non-normal buffers (like help, terminal, etc.)
118
+ --- without artificial increase of visit count.
119
+ ---
120
+ --- Visit is autoregistered for |current-directory| and leads to increase of count
121
+ --- and latest time of visit. See |MiniVisits-index-specification| for more details.
122
+ ---
123
+ --- Notes:
124
+ --- - All data is stored _only_ in in-session Lua variable (for quick operation)
125
+ --- and at `config.store.path` on disk (for persistent usage). It is automatically
126
+ --- written to disk before every Neovim exit (if `config.store.autowrite` is set).
127
+ ---
128
+ --- - Tracking can be disabled by supplying empty string as `track.event`.
129
+ --- Then it is up to the user to properly call |MiniVisits.register_visit()|.
130
+ ---
131
+ --- # Reusing visits ~
132
+ ---
133
+ --- Visit data can be reused in at least these ways:
134
+ ---
135
+ --- - Get a list of visited paths (see |MiniVisits.list_paths()|) and use it
136
+ --- to visualize/pick/navigate visit history.
137
+ ---
138
+ --- - Select one of the visited paths to open it (see |MiniVisits.select_path()|).
139
+ ---
140
+ --- - Move along visit history (see |MiniVisits.iterate_paths()|).
141
+ ---
142
+ --- - Utilize labels. Any visit can be added one or more labels (like "core",
143
+ --- "tmp", etc.). They are bound to the visit (path registered for certain
144
+ --- directory) and are stored persistently.
145
+ --- Labels can be used to manually create groups of files and/or directories
146
+ --- that have particular interest to the user.
147
+ --- There is no one right way to use them, though. See |MiniVisits-examples|
148
+ --- for some inspiration.
149
+ ---
150
+ --- - Utilizing custom data. Visit index can be manipulated manually using
151
+ --- `_index()` set of functions. All "storable" (i.e. not functions or
152
+ --- metatables) user data inside index is then stored on disk, so it can be
153
+ --- used to create any kind of workflow user wants.
154
+ ---
155
+ --- See |MiniVisits-examples| for some actual configuration and workflow examples.
156
+ ---@tag MiniVisits-overview
157
+
158
+ --- # Structure ~
159
+ ---
160
+ --- Visit index is a table containing actual data in two level deep nested tables.
161
+ ---
162
+ --- First level keys are paths of project directory (a.k.a "cwd") for which
163
+ --- visits are registered.
164
+ ---
165
+ --- Second level keys are actual visit paths. Their values are tables with visit
166
+ --- data which should follow these requirements:
167
+ --- - Field <count> should be present and be a number. It represents the number
168
+ --- of times this path was visited under particular cwd.
169
+ --- - Field <latest> should be present and be a number. It represents the time
170
+ --- of latest path visit under particular cwd.
171
+ --- By default computed with |os.time()| (up to a second).
172
+ --- - Field <labels> might not be present. If present, it should be a table
173
+ --- with string labels as keys and `true` as values. It represents labels of
174
+ --- the path under particular cwd.
175
+ ---
176
+ --- Notes:
177
+ --- - All paths are absolute.
178
+ --- - Visit path should not necessarily be a part of corresponding cwd.
179
+ --- - Both `count` and `latest` can be any number: whole, fractional, negative, etc.
180
+ ---
181
+ --- Example of an index data: >lua
182
+ ---
183
+ --- {
184
+ --- ['/home/user/project_1'] = {
185
+ --- ['home/user/project_1/file'] = { count = 3, latest = 1699796000 },
186
+ --- ['home/user/project_1/subdir'] = {
187
+ --- count = 10, latest = 1699797000, labels = { core = true },
188
+ --- },
189
+ --- },
190
+ --- ['/home/user/project_2'] = {
191
+ --- ['home/user/project_1/file'] = {
192
+ --- count = 0, latest = 0, labels = { other = true },
193
+ --- },
194
+ --- ['home/user/project_2/README'] = { count = 1, latest = 1699798000 },
195
+ --- },
196
+ --- }
197
+ --- <
198
+ --- # Storage ~
199
+ ---
200
+ --- When stored on disk, visit index is a file containing Lua code returning
201
+ --- visit index table. It can be edited by hand as long as it contains a valid
202
+ --- Lua code (to be executed with |dofile()|).
203
+ ---
204
+ --- Notes:
205
+ --- - Storage is implemented in such a way that it doesn't really support more
206
+ --- than one parallel Neovim processes. Meaning that if there are two or more
207
+ --- simultaneous Neovim processes with same visit index storage path, the last
208
+ --- one writing to it will preserve its visit history while others - won't.
209
+ ---
210
+ --- # Normalization ~
211
+ ---
212
+ --- To ensure that visit index contains mostly relevant data, it gets normalized:
213
+ --- automatically inside |MiniVisits.write_index()| or
214
+ --- via |MiniVisits.normalize_index()|.
215
+ ---
216
+ --- What normalization actually does can be configured in `config.store.normalize`.
217
+ ---
218
+ --- See |MiniVisits.gen_normalize.default()| for default normalization approach.
219
+ ---@tag MiniVisits-index-specification
220
+
221
+ --- This module provides a flexible framework for working with file system visits.
222
+ --- Exact choice of how to organize workflow is left to the user.
223
+ --- Here are some examples for inspiration which can be combined together.
224
+ ---
225
+ --- # Use different sorting ~
226
+ ---
227
+ --- Default sorting in |MiniVisits.gen_sort.default()| allows flexible adjustment
228
+ --- of which feature to prefer more: recency or frequency. Here is an example of
229
+ --- how to make set of keymaps for three types of sorting combined with two types
230
+ --- of scopes (all visits and only for current cwd): >lua
231
+ ---
232
+ --- local make_select_path = function(select_global, recency_weight)
233
+ --- local visits = require('mini.visits')
234
+ --- local sort = visits.gen_sort.default({ recency_weight = recency_weight })
235
+ --- local select_opts = { sort = sort }
236
+ --- return function()
237
+ --- local cwd = select_global and '' or vim.fn.getcwd()
238
+ --- visits.select_path(cwd, select_opts)
239
+ --- end
240
+ --- end
241
+ ---
242
+ --- local map = function(lhs, desc, ...)
243
+ --- vim.keymap.set('n', lhs, make_select_path(...), { desc = desc })
244
+ --- end
245
+ ---
246
+ --- -- Adjust LHS and description to your liking
247
+ --- map('<Leader>vr', 'Select recent (all)', true, 1)
248
+ --- map('<Leader>vR', 'Select recent (cwd)', false, 1)
249
+ --- map('<Leader>vy', 'Select frecent (all)', true, 0.5)
250
+ --- map('<Leader>vY', 'Select frecent (cwd)', false, 0.5)
251
+ --- map('<Leader>vf', 'Select frequent (all)', true, 0)
252
+ --- map('<Leader>vF', 'Select frequent (cwd)', false, 0)
253
+ --- <
254
+ --- Note: If using |mini.pick|, consider |MiniExtra.pickers.visit_paths()|.
255
+ ---
256
+ --- # Use manual labels ~
257
+ ---
258
+ --- Labels is a powerful tool to create groups of associated paths.
259
+ --- Usual workflow consists of:
260
+ --- - Add label with |MiniVisits.add_label()| (prompts for actual label).
261
+ --- - Remove label with |MiniVisits.remove_label()| (prompts for actual label).
262
+ --- - When need to use labeled groups, call |MiniVisits.select_label()| which
263
+ --- will then call |MiniVisits.select_path()| to select path among those
264
+ --- having selected label.
265
+ --- Note: If using |mini.pick|, consider |MiniExtra.pickers.visit_labels()|.
266
+ ---
267
+ --- To make this workflow smoother, here is an example of keymaps: >lua
268
+ ---
269
+ --- local map_vis = function(keys, call, desc)
270
+ --- local rhs = '<Cmd>lua MiniVisits.' .. call .. '<CR>'
271
+ --- vim.keymap.set('n', '<Leader>' .. keys, rhs, { desc = desc })
272
+ --- end
273
+ ---
274
+ --- map_vis('vv', 'add_label()', 'Add label')
275
+ --- map_vis('vV', 'remove_label()', 'Remove label')
276
+ --- map_vis('vl', 'select_label("", "")', 'Select label (all)')
277
+ --- map_vis('vL', 'select_label()', 'Select label (cwd)')
278
+ --- <
279
+ --- # Use fixed labels ~
280
+ ---
281
+ --- During work on every project there is usually a handful of files where core
282
+ --- activity is concentrated. This can be made easier by creating mappings
283
+ --- which add/remove special fixed label (for example, "core") and select paths
284
+ --- with that label for both all and current cwd. Example: >lua
285
+ ---
286
+ --- -- Create and select
287
+ --- local map_vis = function(keys, call, desc)
288
+ --- local rhs = '<Cmd>lua MiniVisits.' .. call .. '<CR>'
289
+ --- vim.keymap.set('n', '<Leader>' .. keys, rhs, { desc = desc })
290
+ --- end
291
+ ---
292
+ --- map_vis('vv', 'add_label("core")', 'Add to core')
293
+ --- map_vis('vV', 'remove_label("core")', 'Remove from core')
294
+ --- map_vis('vc', 'select_path("", { filter = "core" })', 'Select core (all)')
295
+ --- map_vis('vC', 'select_path(nil, { filter = "core" })', 'Select core (cwd)')
296
+ ---
297
+ --- -- Iterate based on recency
298
+ --- local sort_latest = MiniVisits.gen_sort.default({ recency_weight = 1 })
299
+ --- local map_iterate_core = function(lhs, direction, desc)
300
+ --- local opts = { filter = 'core', sort = sort_latest, wrap = true }
301
+ --- local rhs = function()
302
+ --- MiniVisits.iterate_paths(direction, vim.fn.getcwd(), opts)
303
+ --- end
304
+ --- vim.keymap.set('n', lhs, rhs, { desc = desc })
305
+ --- end
306
+ ---
307
+ --- map_iterate_core('[{', 'last', 'Core label (earliest)')
308
+ --- map_iterate_core('[[', 'forward', 'Core label (earlier)')
309
+ --- map_iterate_core(']]', 'backward', 'Core label (later)')
310
+ --- map_iterate_core(']}', 'first', 'Core label (latest)')
311
+ --- <
312
+ --- # Use automated labels ~
313
+ ---
314
+ --- When using version control system (such as Git), usually there is already
315
+ --- an identifier that groups files you are working with - branch name.
316
+ --- Here is an example of keymaps to add/remove label equal to branch name: >lua
317
+ ---
318
+ --- local map_branch = function(keys, action, desc)
319
+ --- local rhs = function()
320
+ --- local branch = vim.fn.system('git rev-parse --abbrev-ref HEAD')
321
+ --- if vim.v.shell_error ~= 0 then return nil end
322
+ --- branch = vim.trim(branch)
323
+ --- require('mini.visits')[action](branch)
324
+ --- end
325
+ --- vim.keymap.set('n', '<Leader>' .. keys, rhs, { desc = desc })
326
+ --- end
327
+ ---
328
+ --- map_branch('vb', 'add_label', 'Add branch label')
329
+ --- map_branch('vB', 'remove_label', 'Remove branch label')
330
+ --- <
331
+ ---@tag MiniVisits-examples
332
+
333
+ ---@alias __visits_path string|nil Visit path. Can be empty string to mean "all visited
334
+ --- paths for `cwd`". Default: path of current buffer if normal, error otherwise.
335
+ ---@alias __visits_cwd string|nil Visit cwd (project directory). Can be empty string to mean
336
+ --- "all visited cwd". Default: |current-directory|.
337
+ ---@alias __visits_filter - <filter> `(function)` - predicate to filter paths. For more information
338
+ --- about how it is used, see |MiniVisits.config.list|.
339
+ --- Default: value of `config.list.filter` with |MiniVisits.gen_filter.default()|
340
+ --- as its default.
341
+ ---@alias __visits_sort - <sort> `(function)` - path data sorter. For more information about how
342
+ --- it is used, see |MiniVisits.config.list|.
343
+ --- Default: value of `config.list.sort` or |MiniVisits.gen_sort.default()|
344
+ --- as its default.
345
+
346
+ ---@diagnostic disable:undefined-field
347
+ ---@diagnostic disable:discard-returns
348
+ ---@diagnostic disable:unused-local
349
+ ---@diagnostic disable:cast-local-type
350
+ ---@diagnostic disable:undefined-doc-name
351
+ ---@diagnostic disable:luadoc-miss-type-name
352
+
353
+ -- Module definition ==========================================================
354
+ local MiniVisits = {}
355
+ local H = {}
356
+
357
+ --- Module setup
358
+ ---
359
+ ---@param config table|nil Module config table. See |MiniVisits.config|.
360
+ ---
361
+ ---@usage >lua
362
+ --- require('mini.visits').setup() -- use default config
363
+ --- -- OR
364
+ --- require('mini.visits').setup({}) -- replace {} with your config table
365
+ --- <
366
+ MiniVisits.setup = function(config)
367
+ -- TODO: Remove after Neovim=0.9 support is dropped
368
+ if vim.fn.has('nvim-0.10') == 0 then
369
+ vim.notify(
370
+ '(mini.visits) Neovim<0.10 is soft deprecated (module works but is not supported).'
371
+ .. " It will be deprecated after the next 'mini.nvim' release (module might not work)."
372
+ .. ' Please update your Neovim version.'
373
+ )
374
+ end
375
+
376
+ -- Export module
377
+ _G.MiniVisits = MiniVisits
378
+
379
+ -- Setup config
380
+ config = H.setup_config(config)
381
+
382
+ -- Apply config
383
+ H.apply_config(config)
384
+
385
+ -- Define behavior
386
+ H.create_autocommands(config)
387
+ end
388
+
389
+ --- Defaults ~
390
+ ---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
391
+ ---@text # List ~
392
+ --- *MiniVisits.config.list*
393
+ ---
394
+ --- `config.list` defines how visit index is converted to a path list by default.
395
+ ---
396
+ --- `list.filter` is a callable which should take a path data and return `true` if
397
+ --- this path should be present in the list.
398
+ --- Default: output of |MiniVisits.gen_filter.default()|.
399
+ ---
400
+ --- Path data is a table with at least these fields:
401
+ --- - <path> `(string)` - absolute path of visit.
402
+ --- - <count> `(number)` - number of visits.
403
+ --- - <latest> `(number)` - timestamp of latest visit.
404
+ --- - <labels> `(table|nil)` - table of labels (has string keys with `true` values).
405
+ ---
406
+ --- Notes:
407
+ --- - Both `count` and `latest` (in theory) can be any number. But built-in tracking
408
+ --- results into positive integer `count` and `latest` coming from |os.time()|.
409
+ --- - There can be other entries if they are set by user as index entry.
410
+ ---
411
+ --- `list.sort` is a callable which should take an array of path data and return
412
+ --- a sorted array of path data (or at least tables each containing <path> field).
413
+ --- Default: output of |MiniVisits.gen_sort.default()|.
414
+ --- Single path data entry is a table with a same structure as for `list.filter`.
415
+ ---
416
+ --- Note, that `list.sort` can be used both to filter, sort, or even return paths
417
+ --- unrelated to the input.
418
+ ---
419
+ --- # Silent ~
420
+ ---
421
+ --- `config.silent` is a boolean controlling whether to show non-error feedback
422
+ --- (like adding/removing labels, etc.). Default: `false`.
423
+ ---
424
+ --- # Store ~
425
+ ---
426
+ --- `config.store` defines how visit index is stored on disk to enable persistent
427
+ --- data across several sessions.
428
+ ---
429
+ --- `store.autowrite` is a boolean controlling whether to write visit data to
430
+ --- disk on |VimLeavePre| event. Default: `true`.
431
+ ---
432
+ --- `store.normalize` is a callable which should take visit index
433
+ --- (see |MiniVisits-index-specification|) as input and return "normalized" visit
434
+ --- index as output. This is used to ensure that visit index is up to date and
435
+ --- contains only relevant data. For example, it controls how old and
436
+ --- irrelevant visits are "forgotten", and more.
437
+ --- Default: output of |MiniVisits.gen_normalize.default()|.
438
+ ---
439
+ --- `store.path` is a path to which visit index is written. See "Storage" section
440
+ --- of |MiniVisits-index-specification| for more details.
441
+ --- Note: set to empty string to disable any writing with not explicitly set
442
+ --- path (including the one on |VimLeavePre|).
443
+ --- Default: "mini-visits-index" file inside |$XDG_DATA_HOME|.
444
+ ---
445
+ --- # Track ~
446
+ ---
447
+ --- `config.track` defines how visits are tracked (index entry is autoupdated).
448
+ --- See "Tracking visits" section in |MiniVisits-overview| for more details.
449
+ ---
450
+ --- `track.event` is a proper Neovim |{event}| on which track get triggered.
451
+ --- Note: set to empty string to disable automated tracking.
452
+ --- Default: |BufEnter|.
453
+ ---
454
+ --- `track.delay` is a delay in milliseconds after event is triggered and visit
455
+ --- is autoregistered.
456
+ --- Default: 1000 (to allow navigation between buffers without tracking
457
+ --- intermediate ones).
458
+ MiniVisits.config = {
459
+ -- How visit index is converted to list of paths
460
+ list = {
461
+ -- Predicate for which paths to include (all by default)
462
+ filter = nil,
463
+
464
+ -- Sort paths based on the visit data (robust frecency by default)
465
+ sort = nil,
466
+ },
467
+
468
+ -- Whether to disable showing non-error feedback
469
+ silent = false,
470
+
471
+ -- How visit index is stored
472
+ store = {
473
+ -- Whether to write all visits before Neovim is closed
474
+ autowrite = true,
475
+
476
+ -- Function to ensure that written index is relevant
477
+ normalize = nil,
478
+
479
+ -- Path to store visit index
480
+ path = vim.fn.stdpath('data') .. '/mini-visits-index',
481
+ },
482
+
483
+ -- How visit tracking is done
484
+ track = {
485
+ -- Start visit register timer at this event
486
+ -- Supply empty string (`''`) to not do this automatically
487
+ event = 'BufEnter',
488
+
489
+ -- Debounce delay after event to register a visit
490
+ delay = 1000,
491
+ },
492
+ }
493
+ --minidoc_afterlines_end
494
+
495
+ --- Register visit
496
+ ---
497
+ --- Steps:
498
+ --- - Ensure that there is an entry for path-cwd pair.
499
+ --- - Add 1 to visit `count`.
500
+ --- - Set `latest` visit time to equal current time.
501
+ ---
502
+ ---@param path string|nil Visit path. Default: path of current buffer if normal,
503
+ --- error otherwise.
504
+ ---@param cwd string|nil Visit cwd (project directory). Default: |current-directory|.
505
+ MiniVisits.register_visit = function(path, cwd)
506
+ path = H.validate_path(path)
507
+ cwd = H.validate_cwd(cwd)
508
+ if path == '' or cwd == '' then H.error('Both `path` and `cwd` should not be empty.') end
509
+
510
+ H.ensure_index_entry(path, cwd)
511
+ local path_tbl = H.index[cwd][path]
512
+ path_tbl.count = path_tbl.count + 1
513
+ path_tbl.latest = os.time()
514
+ end
515
+
516
+ --- Add path to index
517
+ ---
518
+ --- Ensures that there is a (one or more) entry for path-cwd pair. If entry is
519
+ --- already present, does nothing. If not - creates it with both `count` and
520
+ --- `latest` set to 0.
521
+ ---
522
+ ---@param path __visits_path
523
+ ---@param cwd __visits_cwd
524
+ MiniVisits.add_path = function(path, cwd)
525
+ path = H.validate_path(path)
526
+ cwd = H.validate_cwd(cwd)
527
+
528
+ local path_cwd_pairs = H.resolve_path_cwd(path, cwd)
529
+ for _, pair in ipairs(path_cwd_pairs) do
530
+ H.ensure_index_entry(pair.path, pair.cwd)
531
+ end
532
+ end
533
+
534
+ --- Add label to path
535
+ ---
536
+ --- Steps:
537
+ --- - Ensure that there is an entry for path-cwd pair.
538
+ --- - Add label to the entry.
539
+ ---
540
+ ---@param label string|nil Label string. Default: `nil` to ask with |vim.ui.input()|.
541
+ ---@param path __visits_path
542
+ ---@param cwd __visits_cwd
543
+ MiniVisits.add_label = function(label, path, cwd)
544
+ path = H.validate_path(path)
545
+ cwd = H.validate_cwd(cwd)
546
+ if label ~= nil then H.validate_string(label, 'label') end
547
+
548
+ local on_label = function(l)
549
+ if l == nil then return end
550
+
551
+ -- Add label to all target path-cwd pairs
552
+ local path_cwd_pairs = H.resolve_path_cwd(path, cwd)
553
+ for _, pair in ipairs(path_cwd_pairs) do
554
+ H.ensure_index_entry(pair.path, pair.cwd)
555
+ local path_tbl = H.index[pair.cwd][pair.path]
556
+ local labels = path_tbl.labels or {}
557
+ labels[l] = true
558
+ path_tbl.labels = labels
559
+ end
560
+
561
+ H.echo(string.format('Added %s label.', vim.inspect(l)))
562
+ end
563
+
564
+ if label ~= nil then return on_label(label) end
565
+ -- Suggest all labels from cwd in completion
566
+ H.on_label_from_user('Enter label to add', on_label, MiniVisits.list_labels('', cwd))
567
+ end
568
+
569
+ --- Remove path
570
+ ---
571
+ --- Deletes a (one or more) entry for path-cwd pair from an index. If entry is
572
+ --- already absent, does nothing.
573
+ ---
574
+ --- Notes:
575
+ --- - Affects only in-session Lua variable. Call |MiniVisits.write_index()| to
576
+ --- make it persistent.
577
+ ---
578
+ ---@param path __visits_path
579
+ ---@param cwd __visits_cwd
580
+ MiniVisits.remove_path = function(path, cwd)
581
+ path = H.validate_path(path)
582
+ cwd = H.validate_cwd(cwd)
583
+
584
+ -- Remove all target visits
585
+ H.ensure_read_index()
586
+ local path_cwd_pairs = H.resolve_path_cwd(path, cwd)
587
+ for _, pair in ipairs(path_cwd_pairs) do
588
+ local cwd_tbl = H.index[pair.cwd]
589
+ if type(cwd_tbl) == 'table' then cwd_tbl[pair.path] = nil end
590
+ end
591
+
592
+ for dir, _ in pairs(H.index) do
593
+ if vim.tbl_count(H.index[dir]) == 0 then H.index[dir] = nil end
594
+ end
595
+ end
596
+
597
+ --- Remove label from path
598
+ ---
599
+ --- Steps:
600
+ --- - Remove label from (one or more) index entry.
601
+ --- - If it was last label in an entry, remove `labels` key.
602
+ ---
603
+ ---@param label string|nil Label string. Default: `nil` to ask with |vim.ui.input()|.
604
+ ---@param path __visits_path
605
+ ---@param cwd __visits_cwd
606
+ MiniVisits.remove_label = function(label, path, cwd)
607
+ path = H.validate_path(path)
608
+ cwd = H.validate_cwd(cwd)
609
+ if label ~= nil then H.validate_string(label, 'label') end
610
+
611
+ local on_label = function(l)
612
+ if l == nil then return end
613
+
614
+ -- Remove label from all target path-cwd pairs (ignoring not present ones and
615
+ -- collapsing `labels` if removed last label)
616
+ H.ensure_read_index()
617
+ local path_cwd_pairs = H.resolve_path_cwd(path, cwd)
618
+ for _, pair in ipairs(path_cwd_pairs) do
619
+ local path_tbl = (H.index[pair.cwd] or {})[pair.path]
620
+ if type(path_tbl) == 'table' and type(path_tbl.labels) == 'table' then
621
+ path_tbl.labels[l] = nil
622
+ if vim.tbl_count(path_tbl.labels) == 0 then path_tbl.labels = nil end
623
+ end
624
+ end
625
+
626
+ H.echo(string.format('Removed %s label.', vim.inspect(l)))
627
+ end
628
+
629
+ if label ~= nil then return on_label(label) end
630
+ -- Suggest only labels from target path-cwd pairs
631
+ H.on_label_from_user('Enter label to remove', on_label, MiniVisits.list_labels(path, cwd))
632
+ end
633
+
634
+ --- List visit paths
635
+ ---
636
+ --- Convert visit index for certain cwd into an ordered list of visited paths.
637
+ --- Supports custom filtering and sorting.
638
+ ---
639
+ --- Examples: >lua
640
+ ---
641
+ --- -- Get paths sorted from most to least recent
642
+ --- local sort_recent = MiniVisits.gen_sort.default({ recency_weight = 1 })
643
+ --- MiniVisits.list_paths(nil, { sort = sort_recent })
644
+ ---
645
+ --- -- Get paths from all cwd sorted from most to least frequent
646
+ --- local sort_frequent = MiniVisits.gen_sort.default({ recency_weight = 0 })
647
+ --- MiniVisits.list_paths('', { sort = sort_frequent })
648
+ ---
649
+ --- -- Get paths not including hidden
650
+ --- local is_not_hidden = function(path_data)
651
+ --- return not vim.startswith(vim.fn.fnamemodify(path_data.path, ':t'), '.')
652
+ --- end
653
+ --- MiniVisits.list_paths(nil, { filter = is_not_hidden })
654
+ --- <
655
+ ---@param cwd __visits_cwd
656
+ ---@param opts table|nil Options. Possible fields:
657
+ --- __visits_filter
658
+ --- __visits_sort
659
+ ---
660
+ ---@return table Array of visited paths.
661
+ MiniVisits.list_paths = function(cwd, opts)
662
+ cwd = H.validate_cwd(cwd)
663
+
664
+ opts = vim.tbl_deep_extend('force', H.get_config().list, opts or {})
665
+ local filter = H.validate_filter(opts.filter)
666
+ local sort = H.validate_sort(opts.sort)
667
+
668
+ local path_data_arr = H.make_path_array('', cwd)
669
+ local res_arr = sort(vim.tbl_filter(filter, path_data_arr))
670
+ return vim.tbl_map(function(x) return x.path end, res_arr)
671
+ end
672
+
673
+ --- List visit labels
674
+ ---
675
+ --- Convert visit index for certain path-cwd pair into an ordered list of labels.
676
+ --- Supports custom filtering for paths. Result is ordered from most to least
677
+ --- frequent label.
678
+ ---
679
+ --- Examples: >lua
680
+ ---
681
+ --- -- Get labels for current path-cwd pair
682
+ --- MiniVisits.list_labels()
683
+ ---
684
+ --- -- Get labels for current path across all cwd
685
+ --- MiniVisits.list_labels(nil, '')
686
+ ---
687
+ --- -- Get all available labels excluding ones from hidden files
688
+ --- local is_not_hidden = function(path_data)
689
+ --- return not vim.startswith(vim.fn.fnamemodify(path_data.path, ':t'), '.')
690
+ --- end
691
+ --- MiniVisits.list_labels('', '', { filter = is_not_hidden })
692
+ --- <
693
+ ---@param path __visits_path
694
+ ---@param cwd __visits_cwd
695
+ ---@param opts table|nil Options. Possible fields:
696
+ --- __visits_filter
697
+ --- __visits_sort
698
+ ---
699
+ ---@return table Array of available labels.
700
+ MiniVisits.list_labels = function(path, cwd, opts)
701
+ path = H.validate_path(path)
702
+ cwd = H.validate_cwd(cwd)
703
+
704
+ opts = vim.tbl_deep_extend('force', { filter = H.get_config().list.filter }, opts or {})
705
+ local filter = H.validate_filter(opts.filter)
706
+
707
+ local path_data_arr = H.make_path_array(path, cwd)
708
+ local res_arr = vim.tbl_filter(filter, path_data_arr)
709
+
710
+ -- Count labels
711
+ local label_counts = {}
712
+ for _, path_data in ipairs(res_arr) do
713
+ for label, _ in pairs(path_data.labels or {}) do
714
+ label_counts[label] = (label_counts[label] or 0) + 1
715
+ end
716
+ end
717
+
718
+ -- Sort from most to least common
719
+ local label_arr = {}
720
+ for label, count in pairs(label_counts) do
721
+ table.insert(label_arr, { count, label })
722
+ end
723
+ table.sort(label_arr, function(a, b) return a[1] > b[1] or (a[1] == b[1] and a[2] < b[2]) end)
724
+ return vim.tbl_map(function(x) return x[2] end, label_arr)
725
+ end
726
+
727
+ --- Select visit path
728
+ ---
729
+ --- Uses |vim.ui.select()| with an output of |MiniVisits.list_paths()| and
730
+ --- calls |:edit| on the chosen item.
731
+ ---
732
+ --- Note: if you have |mini.pick|, consider using |MiniExtra.pickers.visit_labels()|
733
+ --- and |MiniExtra.pickers.visit_paths()|.
734
+ ---
735
+ --- Examples:
736
+ ---
737
+ --- - Select from all visited paths: `MiniVisits.select_path('')`
738
+ ---
739
+ --- - Select from paths under current directory sorted from most to least recent: >lua
740
+ ---
741
+ --- local sort_recent = MiniVisits.gen_sort.default({ recency_weight = 1 })
742
+ --- MiniVisits.select_path(nil, { sort = sort_recent })
743
+ --- <
744
+ ---@param cwd string|nil Forwarded to |MiniVisits.list_paths()|.
745
+ ---@param opts table|nil Forwarded to |MiniVisits.list_paths()|.
746
+ MiniVisits.select_path = function(cwd, opts)
747
+ local paths = MiniVisits.list_paths(cwd, opts)
748
+ local cwd_to_short = cwd == '' and vim.fn.getcwd() or cwd
749
+ local items = vim.tbl_map(function(path) return { path = path, text = H.short_path(path, cwd_to_short) } end, paths)
750
+ local select_opts = { prompt = 'Visited paths', format_item = function(item) return item.text end }
751
+ local on_choice = function(item) H.edit((item or {}).path) end
752
+
753
+ vim.ui.select(items, select_opts, on_choice)
754
+ end
755
+
756
+ --- Select visit label
757
+ ---
758
+ --- Uses |vim.ui.select()| with an output of |MiniVisits.list_labels()| and
759
+ --- calls |MiniVisits.select_path()| to get target paths with selected label.
760
+ ---
761
+ --- Note: if you have |mini.pick|, consider using |MiniExtra.pickers.visit_labels()|.
762
+ ---
763
+ --- Examples:
764
+ ---
765
+ --- - Select from labels of current path: `MiniVisits.select_label()`
766
+ ---
767
+ --- - Select from all visited labels: `MiniVisits.select_label('', '')`
768
+ ---
769
+ --- - Select from current project labels and sort paths (after choosing) from most
770
+ --- to least recent: >lua
771
+ ---
772
+ --- local sort_recent = MiniVisits.gen_sort.default({ recency_weight = 1 })
773
+ --- MiniVisits.select_label('', nil, { sort = sort_recent })
774
+ --- <
775
+ ---@param path string|nil Forwarded to |MiniVisits.list_labels()|.
776
+ ---@param cwd string|nil Forwarded to |MiniVisits.list_labels()|.
777
+ ---@param opts table|nil Forwarded to both |MiniVisits.list_labels()|
778
+ --- and |MiniVisits.select_path()| (after choosing a label).
779
+ MiniVisits.select_label = function(path, cwd, opts)
780
+ local items = MiniVisits.list_labels(path, cwd, opts)
781
+ opts = opts or {}
782
+ local on_choice = function(label)
783
+ if label == nil then return end
784
+
785
+ -- Select among subset of paths with chosen label
786
+ local filter_cur = (opts or {}).filter or MiniVisits.gen_filter.default()
787
+ local new_opts = vim.deepcopy(opts)
788
+ new_opts.filter = function(path_data)
789
+ return filter_cur(path_data) and type(path_data.labels) == 'table' and path_data.labels[label]
790
+ end
791
+ MiniVisits.select_path(cwd, new_opts)
792
+ end
793
+
794
+ vim.ui.select(items, { prompt = 'Visited labels' }, on_choice)
795
+ end
796
+
797
+ --- Iterate visit paths
798
+ ---
799
+ --- Steps:
800
+ --- - Compute a sorted array of target paths using |MiniVisits.list_paths()|.
801
+ --- - Identify the current index inside the array based on path of current buffer.
802
+ --- - Iterate through the array certain amount of times in a dedicated direction:
803
+ --- - For "first" direction - forward starting from index 0 (so that single
804
+ --- first iteration leads to first path).
805
+ --- - For "backward" direction - backward starting from current index.
806
+ --- - For "forward" direction - forward starting from current index.
807
+ --- - For "last" direction - backward starting from index after the last one
808
+ --- (so that single first iteration leads to the last path).
809
+ ---
810
+ --- Notes:
811
+ --- - Mostly designed to be used as a mapping. See `MiniVisits-examples`.
812
+ --- - If path from current buffer is not in the output of `MiniVisits.list_paths()`,
813
+ --- starting index is inferred such that first iteration lands on first item
814
+ --- (if iterating forward) or last item (if iterating backward).
815
+ --- - Navigation with this function is not tracked (see |MiniVisits-overview|).
816
+ --- This is done to allow consecutive application without affecting
817
+ --- underlying list of paths.
818
+ ---
819
+ --- Examples assuming underlying array of files `{ "file1", "file2", "file3" }`:
820
+ ---
821
+ --- - `MiniVisits("first")` results into focusing on "file1".
822
+ --- - `MiniVisits("backward", { n_times = 2 })` from "file3" results into "file1".
823
+ --- - `MiniVisits("forward", { n_times = 10 })` from "file1" results into "file3".
824
+ --- - `MiniVisits("last", { n_times = 4, wrap = true })` results into "file3".
825
+ ---
826
+ ---@param direction string One of "first", "backward", "forward", "last".
827
+ ---@param cwd string|nil Forwarded to |MiniVisits.list_paths()|.
828
+ ---@param opts table|nil Options. Possible fields:
829
+ --- - <filter> `(function)` - forwarded to |MiniVisits.list_paths()|.
830
+ --- - <sort> `(function)` - forwarded to |MiniVisits.list_paths()|.
831
+ --- - <n_times> `(number)` - number of steps to go in certain direction.
832
+ --- Default: |v:count1|.
833
+ --- - <wrap> `(boolean)` - whether to wrap around list edges. Default: `false`.
834
+ MiniVisits.iterate_paths = function(direction, cwd, opts)
835
+ if not (direction == 'first' or direction == 'backward' or direction == 'forward' or direction == 'last') then
836
+ H.error('`direction` should be one of "first", "backward", "forward", "last".')
837
+ end
838
+ local is_move_forward = (direction == 'first' or direction == 'forward')
839
+
840
+ local default_opts = { filter = nil, sort = nil, n_times = vim.v.count1, wrap = false }
841
+ opts = vim.tbl_deep_extend('force', default_opts, opts or {})
842
+ local all_paths = MiniVisits.list_paths(cwd, { filter = opts.filter, sort = opts.sort })
843
+
844
+ local n_tot = #all_paths
845
+ if n_tot == 0 then return end
846
+
847
+ -- Compute current index
848
+ local cur_ind
849
+ if direction == 'first' then cur_ind = 0 end
850
+ if direction == 'last' then cur_ind = n_tot + 1 end
851
+ if direction == 'backward' or direction == 'forward' then
852
+ local cur_path = H.buf_get_path(vim.api.nvim_get_current_buf())
853
+ for i, path in ipairs(all_paths) do
854
+ if path == cur_path then
855
+ cur_ind = i
856
+ break
857
+ end
858
+ end
859
+ end
860
+
861
+ -- - If not on path from the list, make going forward start from the
862
+ -- beginning and backward - from end
863
+ if cur_ind == nil then cur_ind = is_move_forward and 0 or (n_tot + 1) end
864
+
865
+ -- Compute target index ensuring that it is inside `[1, #all_paths]`
866
+ local res_ind = cur_ind + opts.n_times * (is_move_forward and 1 or -1)
867
+ res_ind = opts.wrap and ((res_ind - 1) % n_tot + 1) or math.min(math.max(res_ind, 1), n_tot)
868
+
869
+ -- Open path with no visit track (for default `track.event`)
870
+ -- Use `vim.g` instead of `vim.b` to not register in **next** buffer
871
+ local cache_disabled = vim.g.minivisits_disable
872
+ vim.g.minivisits_disable = true
873
+ H.edit(all_paths[res_ind])
874
+ vim.g.minivisits_disable = cache_disabled
875
+ end
876
+
877
+ --- Get active visit index
878
+ ---
879
+ ---@return table Copy of currently active visit index table.
880
+ MiniVisits.get_index = function()
881
+ H.ensure_read_index()
882
+ return vim.deepcopy(H.index)
883
+ end
884
+
885
+ --- Set active visit index
886
+ ---
887
+ ---@param index table Visit index table.
888
+ MiniVisits.set_index = function(index)
889
+ H.validate_index(index, '`index`')
890
+ H.index = vim.deepcopy(index)
891
+ H.cache.needs_index_read = false
892
+ end
893
+
894
+ --- Reset active visit index
895
+ ---
896
+ --- Set currently active visit index to the output of |MiniVisits.read_index()|.
897
+ --- Does nothing if reading the index failed.
898
+ MiniVisits.reset_index = function()
899
+ local ok, stored_index = pcall(MiniVisits.read_index)
900
+ if not ok or stored_index == nil then return end
901
+ MiniVisits.set_index(stored_index)
902
+ end
903
+
904
+ --- Normalize visit index
905
+ ---
906
+ --- Applies `config.store.normalize` (|MiniVisits.gen_normalize.default()| by default)
907
+ --- to the input index object and returns the output (if it fits in the definition
908
+ --- of index object; see |MiniVisits-index-specification|).
909
+ ---
910
+ ---@param index table|nil Index object. Default: copy of the current index.
911
+ ---
912
+ ---@return table Normalized index object.
913
+ MiniVisits.normalize_index = function(index)
914
+ index = index or MiniVisits.get_index()
915
+ H.validate_index(index, '`index`')
916
+
917
+ local config = H.get_config()
918
+ local normalize = config.store.normalize
919
+ if not vim.is_callable(normalize) then normalize = MiniVisits.gen_normalize.default() end
920
+ local new_index = normalize(vim.deepcopy(index))
921
+ H.validate_index(new_index, '`index` after normalization')
922
+
923
+ return new_index
924
+ end
925
+
926
+ --- Read visit index from disk
927
+ ---
928
+ ---@param store_path string|nil Path on the disk containing visit index data.
929
+ --- Default: `config.store.path`.
930
+ --- Notes:
931
+ --- - Can return `nil` if path is empty string or file is not readable.
932
+ --- - File is sourced with |dofile()| as a regular Lua file.
933
+ ---
934
+ ---@return table|nil Output of the file source.
935
+ MiniVisits.read_index = function(store_path)
936
+ store_path = store_path or H.get_config().store.path
937
+ if store_path == '' then return nil end
938
+ H.validate_string(store_path, 'store_path')
939
+ if vim.fn.filereadable(store_path) == 0 then return nil end
940
+
941
+ return dofile(store_path)
942
+ end
943
+
944
+ --- Write visit index to disk
945
+ ---
946
+ --- Steps:
947
+ --- - Normalize index with |MiniVisits.normalize_index()|.
948
+ --- - Ensure path is valid (all parent directories are created, etc.).
949
+ --- - Write index object to the path so that it is readable
950
+ --- with |MiniVisits.read_index()|.
951
+ ---
952
+ ---@param store_path string|nil Path on the disk where to write visit index data.
953
+ --- Default: `config.store.path`. Note: if empty string, nothing is written.
954
+ ---@param index table|nil Index object to write to disk.
955
+ --- Default: current session index.
956
+ MiniVisits.write_index = function(store_path, index)
957
+ store_path = store_path or H.get_config().store.path
958
+ H.validate_string(store_path, 'store_path')
959
+ if store_path == '' then return end
960
+ index = index or MiniVisits.get_index()
961
+ H.validate_index(index, '`index`')
962
+
963
+ -- Normalize index
964
+ index = MiniVisits.normalize_index(index)
965
+
966
+ -- Ensure writable path
967
+ store_path = vim.fn.fnamemodify(store_path, ':p')
968
+ local path_dir = vim.fn.fnamemodify(store_path, ':h')
969
+ vim.fn.mkdir(path_dir, 'p')
970
+
971
+ -- Write
972
+ local lines = vim.split(vim.inspect(index), '\n')
973
+ lines[1] = 'return ' .. lines[1]
974
+ vim.fn.writefile(lines, store_path)
975
+ end
976
+
977
+ --- Rename path in index
978
+ ---
979
+ --- A helper to react for a path rename/move in order to preserve its visit data.
980
+ --- It works both for file and directory paths.
981
+ ---
982
+ --- Notes:
983
+ --- - It does not update current index, but returns a modified index object.
984
+ --- Use |MiniVisits.set_index()| to make it current.
985
+ --- - Use only full paths.
986
+ --- - Do not append `/` to directory paths. Use same format as for files.
987
+ ---
988
+ --- Assuming `path_from` and `path_to` are variables containing full paths
989
+ --- before and after rename/move, here is an example to update current index: >lua
990
+ ---
991
+ --- local new_index = MiniVisits.rename_in_index(path_from, path_to)
992
+ --- MiniVisits.set_index(new_index)
993
+ --- <
994
+ ---@param path_from string Full path to be renamed.
995
+ ---@param path_to string Full path to be replaced with.
996
+ ---@param index table|nil Index object inside which to perform renaming.
997
+ --- Default: current session index.
998
+ ---
999
+ ---@return table Index object with renamed path.
1000
+ MiniVisits.rename_in_index = function(path_from, path_to, index)
1001
+ path_from = H.validate_string(path_from, 'path_from')
1002
+ path_to = H.validate_string(path_to, 'path_to')
1003
+ index = index or MiniVisits.get_index()
1004
+ H.validate_index(index, '`index`')
1005
+
1006
+ local path_from_pattern = vim.pesc(path_from)
1007
+ local pattern_from_full = string.format('^%s$', path_from_pattern)
1008
+ local pattern_from_parent_dir = string.format('^(%s)/', path_from_pattern)
1009
+ local path_to_parent_dir = path_to .. '/'
1010
+
1011
+ local replace = function(x)
1012
+ if string.find(x, pattern_from_full) ~= nil then return path_to end
1013
+ return string.gsub(x, pattern_from_parent_dir, path_to_parent_dir)
1014
+ end
1015
+
1016
+ local res = {}
1017
+ for cwd, cwd_tbl in pairs(index) do
1018
+ local new_cwd_tbl = {}
1019
+ for path, path_tbl in pairs(cwd_tbl) do
1020
+ new_cwd_tbl[replace(path)] = vim.deepcopy(path_tbl)
1021
+ end
1022
+ res[replace(cwd)] = new_cwd_tbl
1023
+ end
1024
+
1025
+ return res
1026
+ end
1027
+
1028
+ --- Generate filter function
1029
+ ---
1030
+ --- This is a table with function elements. Call to actually get specification.
1031
+ MiniVisits.gen_filter = {}
1032
+
1033
+ --- Default filter
1034
+ ---
1035
+ --- Always returns `true` resulting in no actual filter.
1036
+ ---
1037
+ ---@return function Visit filter function. See |MiniVisits.config.list| for more details.
1038
+ MiniVisits.gen_filter.default = function()
1039
+ return function(path_data) return true end
1040
+ end
1041
+
1042
+ --- Filter visits from current session
1043
+ ---
1044
+ ---@return function Visit filter function. See |MiniVisits.config.list| for more details.
1045
+ MiniVisits.gen_filter.this_session = function()
1046
+ return function(path_data) return H.cache.session_start_time <= path_data.latest end
1047
+ end
1048
+
1049
+ --- Generate sort function
1050
+ ---
1051
+ --- This is a table with function elements. Call to actually get specification.
1052
+ MiniVisits.gen_sort = {}
1053
+
1054
+ --- Default sort
1055
+ ---
1056
+ --- Sort paths using "robust frecency" approach. It relies on the rank operation:
1057
+ --- based on certain reference number for every item, assign it a number
1058
+ --- between 1 (best) and number of items (worst). Ties are dealt with "average
1059
+ --- rank" approach: each element with a same reference number is assigned
1060
+ --- an average rank among such elements. This way total rank sum depends only
1061
+ --- on number of paths.
1062
+ ---
1063
+ --- Here is an algorithm outline:
1064
+ --- - Rank paths based on frequency (`count` value): from most to least frequent.
1065
+ --- - Rank paths based on recency (`latest` value): from most to least recent.
1066
+ --- - Combine ranks from previous steps with weights:
1067
+ --- `score = (1 - w) * rank_frequency + w * rank_recency`, where `w` is
1068
+ --- "recency weight". The smaller this weight the less recency affects outcome.
1069
+ ---
1070
+ --- Examples:
1071
+ --- - Default recency weight 0.5 results into "robust frecency" sorting: it
1072
+ --- combines both frequency and recency.
1073
+ --- This is called a "robust frecency" because actual values don't have direct
1074
+ --- effect on the outcome, only ordering matters. For example, if there is
1075
+ --- a very frequent file with `count = 100` while all others have `count = 5`,
1076
+ --- it will not massively dominate the outcome as long as it is not very recent.
1077
+ ---
1078
+ --- - Having recency weight 1 results into "from most to least recent" sorting.
1079
+ ---
1080
+ --- - Having recency weight 0 results into "from most to least frequent" sorting.
1081
+ ---
1082
+ ---@param opts table|nil Option. Possible fields:
1083
+ --- - <recency_weight> `(number)` - a number between 0 and 1 for recency weight.
1084
+ --- Default: 0.5.
1085
+ ---
1086
+ ---@return function Visit sort function. See |MiniVisits.config.list| for more details.
1087
+ MiniVisits.gen_sort.default = function(opts)
1088
+ opts = vim.tbl_deep_extend('force', { recency_weight = 0.5 }, opts or {})
1089
+ local recency_weight = opts.recency_weight
1090
+ local is_weight = type(recency_weight) == 'number' and 0 <= recency_weight and recency_weight <= 1
1091
+ if not is_weight then H.error('`opts.recency_weight` should be number between 0 and 1.') end
1092
+
1093
+ return function(path_data_arr)
1094
+ path_data_arr = vim.deepcopy(path_data_arr)
1095
+
1096
+ -- Add ranks for `count` and `latest`
1097
+ table.sort(path_data_arr, function(a, b) return a.count > b.count end)
1098
+ H.tbl_add_rank(path_data_arr, 'count')
1099
+ table.sort(path_data_arr, function(a, b) return a.latest > b.latest end)
1100
+ H.tbl_add_rank(path_data_arr, 'latest')
1101
+
1102
+ -- Compute final rank and sort by it
1103
+ for _, path_data in ipairs(path_data_arr) do
1104
+ path_data.rank = (1 - recency_weight) * path_data.count_rank + recency_weight * path_data.latest_rank
1105
+ end
1106
+ table.sort(path_data_arr, function(a, b) return a.rank < b.rank or (a.rank == b.rank and a.path < b.path) end)
1107
+ return path_data_arr
1108
+ end
1109
+ end
1110
+
1111
+ --- Z sort
1112
+ ---
1113
+ --- Sort as in https://github.com/rupa/z.
1114
+ ---
1115
+ ---@return function Visit sort function. See |MiniVisits.config.list| for more details.
1116
+ MiniVisits.gen_sort.z = function()
1117
+ return function(path_data_arr)
1118
+ path_data_arr = vim.deepcopy(path_data_arr)
1119
+ local now = os.time()
1120
+ for _, path_data in ipairs(path_data_arr) do
1121
+ -- Source: https://github.com/rupa/z/blob/master/z.sh#L151
1122
+ local dtime = math.max(now - path_data.latest, 0.0001)
1123
+ path_data.z = 10000 * path_data.count * (3.75 / ((0.0001 * dtime + 1) + 0.25))
1124
+ end
1125
+ table.sort(path_data_arr, function(a, b) return a.z > b.z or (a.z == b.z and a.path < b.path) end)
1126
+ return path_data_arr
1127
+ end
1128
+ end
1129
+
1130
+ --- Generate normalize function
1131
+ ---
1132
+ --- This is a table with function elements. Call to actually get specification.
1133
+ MiniVisits.gen_normalize = {}
1134
+
1135
+ --- Generate default normalize function
1136
+ ---
1137
+ --- Steps:
1138
+ --- - Prune visits, i.e. remove outdated visits:
1139
+ --- - If `count` number of visits is below prune threshold, remove that visit
1140
+ --- entry from particular cwd (it can still be present in others).
1141
+ --- - If either first (cwd) or second (path) level key doesn't represent an
1142
+ --- actual path on disk, remove the whole associated value.
1143
+ --- - NOTE: if visit has any label, it is not automatically pruned.
1144
+ ---
1145
+ --- - Decay visits, i.e. possibly make visits more outdated. This is an important
1146
+ --- part to the whole usability: together with pruning it results into automated
1147
+ --- removing of paths which were visited long ago and are not relevant.
1148
+ ---
1149
+ --- Decay is done per cwd if its total `count` values sum exceeds decay threshold.
1150
+ --- It is performed through multiplying each `count` by same coefficient so that
1151
+ --- the new total sum of `count` is equal to some smaller target value.
1152
+ --- Note: only two decimal places are preserved, so the sum might not be exact.
1153
+ ---
1154
+ --- - Prune once more to ensure that there are no outdated paths after decay.
1155
+ ---
1156
+ ---@param opts table|nil Options. Possible fields:
1157
+ --- - <decay_threshold> `(number)` - decay threshold. Default: 1000.
1158
+ --- - <decay_target> `(number)` - decay target. Default: 800.
1159
+ --- - <prune_threshold> `(number)` - prune threshold. Default: 0.5.
1160
+ --- - <prune_paths> `(boolean)` - whether to prune outdated paths. Default: `true`.
1161
+ ---
1162
+ ---@return function Visit index normalize function. See "Store" in |MiniVisits.config|.
1163
+ MiniVisits.gen_normalize.default = function(opts)
1164
+ local default_opts = { decay_threshold = 1000, decay_target = 800, prune_threshold = 0.5, prune_paths = true }
1165
+ opts = vim.tbl_deep_extend('force', default_opts, opts or {})
1166
+
1167
+ return function(index)
1168
+ H.validate_index(index)
1169
+ local res = vim.deepcopy(index)
1170
+ H.index_prune(res, opts.prune_paths, opts.prune_threshold)
1171
+ for cwd, cwd_tbl in pairs(res) do
1172
+ H.index_decay_cwd(cwd_tbl, opts.decay_threshold, opts.decay_target)
1173
+ end
1174
+ -- Ensure that no path has count smaller than threshold
1175
+ H.index_prune(res, false, opts.prune_threshold)
1176
+ return res
1177
+ end
1178
+ end
1179
+
1180
+ -- Helper data ================================================================
1181
+ -- Module default config
1182
+ H.default_config = MiniVisits.config
1183
+
1184
+ -- Various timers
1185
+ H.timers = {
1186
+ track = vim.loop.new_timer(),
1187
+ }
1188
+
1189
+ -- Current visit index
1190
+ H.index = {}
1191
+
1192
+ -- Various cache
1193
+ H.cache = {
1194
+ -- Latest tracked path used to not autoregister same path in a row
1195
+ latest_tracked_path = nil,
1196
+
1197
+ -- Whether index is yet to be read from the stored path, as it is not read
1198
+ -- right away delaying until it is absolutely necessary
1199
+ needs_index_read = true,
1200
+
1201
+ -- Start time of this session to be used in `gen_filter.this_session`
1202
+ session_start_time = os.time(),
1203
+ }
1204
+
1205
+ -- File system information
1206
+ H.is_windows = vim.loop.os_uname().sysname == 'Windows_NT'
1207
+
1208
+ -- Helper functionality =======================================================
1209
+ -- Settings -------------------------------------------------------------------
1210
+ H.setup_config = function(config)
1211
+ H.check_type('config', config, 'table', true)
1212
+ config = vim.tbl_deep_extend('force', vim.deepcopy(H.default_config), config or {})
1213
+
1214
+ H.check_type('list', config.list, 'table')
1215
+ H.check_type('list.filter', config.list.filter, 'function', true)
1216
+ H.check_type('list.sort', config.list.sort, 'function', true)
1217
+
1218
+ H.check_type('silent', config.silent, 'boolean')
1219
+
1220
+ H.check_type('store', config.store, 'table')
1221
+ H.check_type('store.autowrite', config.store.autowrite, 'boolean')
1222
+ H.check_type('store.normalize', config.store.normalize, 'function', true)
1223
+ H.check_type('store.path', config.store.path, 'string')
1224
+
1225
+ H.check_type('track', config.track, 'table')
1226
+ H.check_type('track.delay', config.track.delay, 'number')
1227
+ H.check_type('track.event', config.track.event, 'string')
1228
+
1229
+ return config
1230
+ end
1231
+
1232
+ H.apply_config = function(config) MiniVisits.config = config end
1233
+
1234
+ H.create_autocommands = function(config)
1235
+ local gr = vim.api.nvim_create_augroup('MiniVisits', {})
1236
+
1237
+ local au = function(event, pattern, callback, desc)
1238
+ vim.api.nvim_create_autocmd(event, { group = gr, pattern = pattern, callback = callback, desc = desc })
1239
+ end
1240
+
1241
+ if config.track.event ~= '' then au(config.track.event, '*', H.autoregister_visit, 'Auto register visit') end
1242
+ au('VimLeavePre', '*', function()
1243
+ if not H.get_config().store.autowrite then return end
1244
+ pcall(MiniVisits.write_index)
1245
+ end, 'Autowrite visit index')
1246
+
1247
+ -- React to file manipulation with 'mini.files'
1248
+ au('User', { 'MiniFilesActionRename', 'MiniFilesActionMove' }, function(args)
1249
+ local cur_index = MiniVisits.get_index()
1250
+ local ok, new_index = pcall(MiniVisits.rename_in_index, args.data.from, args.data.to, cur_index)
1251
+ if not ok then return end
1252
+ MiniVisits.set_index(new_index)
1253
+ end, 'Rename in index')
1254
+ end
1255
+
1256
+ H.is_disabled = function(buf_id)
1257
+ local buf_disable = H.get_buf_var(buf_id, 'minivisits_disable')
1258
+ return vim.g.minivisits_disable == true or buf_disable == true
1259
+ end
1260
+
1261
+ H.get_config = function(config, buf_id)
1262
+ local buf_config = H.get_buf_var(buf_id, 'minivisits_config') or {}
1263
+ return vim.tbl_deep_extend('force', MiniVisits.config, buf_config, config or {})
1264
+ end
1265
+
1266
+ H.get_buf_var = function(buf_id, name)
1267
+ if not H.is_valid_buf(buf_id) then return nil end
1268
+ return vim.b[buf_id or 0][name]
1269
+ end
1270
+
1271
+ -- Autocommands ---------------------------------------------------------------
1272
+ H.autoregister_visit = function(data)
1273
+ -- Recognize the register opportunity by stopping timer before check for
1274
+ -- disabling. This is important for `iterate_paths` functionality.
1275
+ H.timers.track:stop()
1276
+ local buf_id = data.buf
1277
+ if H.is_disabled(buf_id) then return end
1278
+
1279
+ local f = vim.schedule_wrap(function()
1280
+ if H.is_disabled(buf_id) then return end
1281
+
1282
+ -- Register only normal buffer if it is not the latest registered (avoids
1283
+ -- tracking visits from switching between normal and non-normal buffers)
1284
+ local path = H.buf_get_path(buf_id)
1285
+ if path == nil or path == H.cache.latest_tracked_path then return end
1286
+
1287
+ local ok = pcall(MiniVisits.register_visit, path, vim.fn.getcwd())
1288
+ if not ok then return end
1289
+
1290
+ H.cache.latest_tracked_path = path
1291
+ end)
1292
+
1293
+ H.timers.track:start(H.get_config(nil, buf_id).track.delay, 0, f)
1294
+ end
1295
+
1296
+ -- Visit index ----------------------------------------------------------------
1297
+ H.ensure_read_index = function()
1298
+ if not H.cache.needs_index_read then return end
1299
+
1300
+ -- Try reading previous index
1301
+ local ok, res_index = pcall(MiniVisits.read_index)
1302
+ if not ok then return end
1303
+ local is_index = pcall(H.validate_index, res_index)
1304
+ if not is_index then return end
1305
+
1306
+ -- Merge current index with stored
1307
+ for cwd, cwd_tbl in pairs(H.index) do
1308
+ local cwd_tbl_res = res_index[cwd] or {}
1309
+ for path, path_tbl_new in pairs(cwd_tbl) do
1310
+ local path_tbl_res = cwd_tbl_res[path] or { count = 0, latest = 0 }
1311
+ cwd_tbl_res[path] = H.merge_path_tbls(path_tbl_res, path_tbl_new)
1312
+ end
1313
+ res_index[cwd] = cwd_tbl_res
1314
+ end
1315
+
1316
+ H.index = res_index
1317
+ H.cache.needs_index_read = false
1318
+ end
1319
+
1320
+ H.ensure_index_entry = function(path, cwd)
1321
+ local cwd_tbl = H.index[cwd] or {}
1322
+ cwd_tbl[path] = cwd_tbl[path] or { count = 0, latest = 0 }
1323
+ H.index[cwd] = cwd_tbl
1324
+ end
1325
+
1326
+ H.resolve_path_cwd = function(path, cwd)
1327
+ H.ensure_read_index()
1328
+
1329
+ -- Empty cwd means all available cwds
1330
+ local cwd_arr = cwd == '' and vim.tbl_keys(H.index) or { cwd }
1331
+
1332
+ -- Empty path means all available paths in all target cwds
1333
+ if path ~= '' then
1334
+ return vim.tbl_map(function(x) return { path = path, cwd = x } end, cwd_arr)
1335
+ end
1336
+
1337
+ local res = {}
1338
+ for _, d in ipairs(cwd_arr) do
1339
+ local cwd_tbl = H.index[d] or {}
1340
+ for p, _ in pairs(cwd_tbl) do
1341
+ table.insert(res, { path = p, cwd = d })
1342
+ end
1343
+ end
1344
+ return res
1345
+ end
1346
+
1347
+ H.make_path_array = function(path, cwd)
1348
+ local index = MiniVisits.get_index()
1349
+ local path_tbl = {}
1350
+ for _, pair in ipairs(H.resolve_path_cwd(path, cwd)) do
1351
+ local path_tbl_to_merge = (index[pair.cwd] or {})[pair.path]
1352
+ if type(path_tbl_to_merge) == 'table' then
1353
+ local p = pair.path
1354
+ path_tbl[p] = path_tbl[p] or { path = p, count = 0, latest = 0 }
1355
+ path_tbl[p] = H.merge_path_tbls(path_tbl[p], path_tbl_to_merge)
1356
+ end
1357
+ end
1358
+
1359
+ return vim.tbl_values(path_tbl)
1360
+ end
1361
+
1362
+ H.merge_path_tbls = function(path_tbl_ref, path_tbl_new)
1363
+ local path_tbl = vim.tbl_deep_extend('force', path_tbl_ref, path_tbl_new)
1364
+
1365
+ -- Add all counts together
1366
+ path_tbl.count = path_tbl_ref.count + path_tbl_new.count
1367
+
1368
+ -- Compute the latest visit
1369
+ path_tbl.latest = math.max(path_tbl_ref.latest, path_tbl_new.latest)
1370
+
1371
+ -- Labels should be already a proper union of both labels
1372
+
1373
+ return path_tbl
1374
+ end
1375
+
1376
+ H.index_prune = function(index, prune_paths, threshold)
1377
+ if type(threshold) ~= 'number' then H.error('Prune threshold should be number.') end
1378
+
1379
+ -- Possibly prune non-path cwds
1380
+ for cwd, cwd_tbl in pairs(index) do
1381
+ if prune_paths and vim.fn.isdirectory(cwd) == 0 then index[cwd] = nil end
1382
+ end
1383
+
1384
+ -- Prune on path level
1385
+ for cwd, cwd_tbl in pairs(index) do
1386
+ for path, path_tbl in pairs(cwd_tbl) do
1387
+ local should_prune_path = prune_paths and not (vim.fn.filereadable(path) == 1 or vim.fn.isdirectory(path) == 1)
1388
+ local should_prune = should_prune_path or path_tbl.count < threshold
1389
+ -- Don't prune if visit has labels (can happen if label was added
1390
+ -- manually without actual visit, thus `count = 0`)
1391
+ if path_tbl.labels == nil and should_prune then cwd_tbl[path] = nil end
1392
+ end
1393
+ end
1394
+
1395
+ -- Remove possible cwd tables which were only with non-paths entries
1396
+ for cwd, cwd_tbl in pairs(index) do
1397
+ if vim.tbl_count(cwd_tbl) == 0 then index[cwd] = nil end
1398
+ end
1399
+ end
1400
+
1401
+ H.index_decay_cwd = function(cwd_tbl, threshold, target)
1402
+ if type(threshold) ~= 'number' then H.error('Decay threshold should be number.') end
1403
+ if type(target) ~= 'number' then H.error('Decay target should be number.') end
1404
+
1405
+ -- Decide whether to decay (if total count exceeds threshold)
1406
+ local total_count = 0
1407
+ for _, path_tbl in pairs(cwd_tbl) do
1408
+ total_count = total_count + path_tbl.count
1409
+ end
1410
+ if total_count == 0 or total_count <= threshold then return end
1411
+
1412
+ -- Decay (multiply counts by coefficient to have total count equal target)
1413
+ local coef = target / total_count
1414
+ for _, path_tbl in pairs(cwd_tbl) do
1415
+ -- Round to preserve only two decimal places
1416
+ path_tbl.count = math.floor(100 * coef * path_tbl.count + 0.5) / 100
1417
+ end
1418
+ end
1419
+
1420
+ H.on_label_from_user = function(prompt, on_label, labels_complete)
1421
+ MiniVisits._complete = function(arg_lead)
1422
+ return vim.tbl_filter(function(x) return x:find(arg_lead, 1, true) ~= nil end, labels_complete)
1423
+ end
1424
+ local completion = 'customlist,v:lua.MiniVisits._complete'
1425
+ vim.ui.input({ prompt = '(mini.visits) ' .. prompt .. ' ', completion = completion }, function(x)
1426
+ MiniVisits._complete = nil
1427
+ on_label(x)
1428
+ end)
1429
+ end
1430
+
1431
+ -- Validators -----------------------------------------------------------------
1432
+ H.validate_path = function(x)
1433
+ x = x or H.buf_get_path(vim.api.nvim_get_current_buf())
1434
+ if x == nil then H.error('Current buffer is not for a regular file') end
1435
+ H.validate_string(x, 'path')
1436
+ return x == '' and '' or H.full_path(x)
1437
+ end
1438
+
1439
+ H.validate_cwd = function(x)
1440
+ x = x or vim.fn.getcwd()
1441
+ H.validate_string(x, 'cwd')
1442
+ return x == '' and '' or H.full_path(x)
1443
+ end
1444
+
1445
+ H.validate_filter = function(x)
1446
+ x = x or MiniVisits.gen_filter.default()
1447
+ if type(x) == 'string' then
1448
+ local label = x
1449
+ x = function(path_data) return (path_data.labels or {})[label] end
1450
+ end
1451
+ if not vim.is_callable(x) then H.error('`filter` should be callable or string label name.') end
1452
+ return x
1453
+ end
1454
+
1455
+ H.validate_sort = function(x)
1456
+ x = x or MiniVisits.gen_sort.default()
1457
+ if not vim.is_callable(x) then H.error('`sort` should be callable.') end
1458
+ return x
1459
+ end
1460
+
1461
+ H.validate_index = function(x, name)
1462
+ name = name or '`index`'
1463
+ if type(x) ~= 'table' then H.error(name .. ' should be a table.') end
1464
+ for cwd, cwd_tbl in pairs(x) do
1465
+ if type(cwd) ~= 'string' then H.error('First level keys in ' .. name .. ' should be strings.') end
1466
+ if type(cwd_tbl) ~= 'table' then H.error('First level values in ' .. name .. ' should be tables.') end
1467
+
1468
+ for path, path_tbl in pairs(cwd_tbl) do
1469
+ if type(path) ~= 'string' then H.error('Second level keys in ' .. name .. ' should be strings.') end
1470
+ if type(path_tbl) ~= 'table' then H.error('Second level values in ' .. name .. ' should be tables.') end
1471
+
1472
+ if type(path_tbl.count) ~= 'number' then H.error('`count` entries in ' .. name .. ' should be numbers.') end
1473
+ if type(path_tbl.latest) ~= 'number' then H.error('`latest` entries in ' .. name .. ' should be numbers.') end
1474
+
1475
+ H.validate_labels_field(path_tbl.labels)
1476
+ end
1477
+ end
1478
+ end
1479
+
1480
+ H.validate_labels_field = function(x)
1481
+ if x == nil then return end
1482
+ if type(x) ~= 'table' then H.error('`labels` should be a table.') end
1483
+
1484
+ for key, value in pairs(x) do
1485
+ if type(key) ~= 'string' then H.error('Keys in `labels` table should be strings.') end
1486
+ if value ~= true then H.error('Values in `labels` table should only be `true`.') end
1487
+ end
1488
+ end
1489
+
1490
+ H.validate_string = function(x, name)
1491
+ if type(x) == 'string' then return x end
1492
+ H.error(string.format('`%s` should be string.', name))
1493
+ end
1494
+
1495
+ -- Utilities ------------------------------------------------------------------
1496
+ H.error = function(msg) error('(mini.visits) ' .. msg, 0) end
1497
+
1498
+ H.check_type = function(name, val, ref, allow_nil)
1499
+ if type(val) == ref or (ref == 'callable' and vim.is_callable(val)) or (allow_nil and val == nil) then return end
1500
+ H.error(string.format('`%s` should be %s, not %s', name, ref, type(val)))
1501
+ end
1502
+
1503
+ H.echo = function(msg)
1504
+ if H.get_config().silent then return end
1505
+
1506
+ -- Construct message chunks
1507
+ msg = type(msg) == 'string' and { { msg } } or msg
1508
+ table.insert(msg, 1, { '(mini.visits) ', 'WarningMsg' })
1509
+
1510
+ -- Echo. Force redraw to ensure that it is effective (`:h echo-redraw`)
1511
+ vim.cmd([[echo '' | redraw]])
1512
+ vim.api.nvim_echo(msg, false, {})
1513
+ end
1514
+
1515
+ H.is_valid_buf = function(buf_id) return type(buf_id) == 'number' and vim.api.nvim_buf_is_valid(buf_id) end
1516
+
1517
+ H.buf_get_path = function(buf_id)
1518
+ -- Get path only for valid normal buffers
1519
+ if not (H.is_valid_buf(buf_id) and vim.bo[buf_id].buftype == '') then return nil end
1520
+ local res = H.full_path(vim.api.nvim_buf_get_name(buf_id))
1521
+ if res == '' then return end
1522
+ return res
1523
+ end
1524
+
1525
+ H.tbl_add_rank = function(arr, key)
1526
+ local rank_key, ties = key .. '_rank', {}
1527
+ for i, tbl in ipairs(arr) do
1528
+ -- Assumes `arr` is an array of tables sorted from best to worst
1529
+ tbl[rank_key] = i
1530
+
1531
+ -- Track ties
1532
+ if i > 1 and tbl[key] == arr[i - 1][key] then
1533
+ local val = tbl[key]
1534
+ local data = ties[val] or { n = 1, sum = i - 1 }
1535
+ data.n, data.sum = data.n + 1, data.sum + i
1536
+ ties[val] = data
1537
+ end
1538
+ end
1539
+
1540
+ -- Correct for ties using mid-rank
1541
+ for i, tbl in ipairs(arr) do
1542
+ local tie_data = ties[tbl[key]]
1543
+ if tie_data ~= nil then tbl[rank_key] = tie_data.sum / tie_data.n end
1544
+ end
1545
+ end
1546
+
1547
+ H.edit = function(path, win_id)
1548
+ if type(path) ~= 'string' then return end
1549
+ local b = vim.api.nvim_win_get_buf(win_id or 0)
1550
+ local try_mimic_buf_reuse = (vim.fn.bufname(b) == '' and vim.bo[b].buftype ~= 'quickfix' and not vim.bo[b].modified)
1551
+ and (#vim.fn.win_findbuf(b) == 1 and vim.deep_equal(vim.fn.getbufline(b, 1, '$'), { '' }))
1552
+ local buf_id = vim.fn.bufadd(vim.fn.fnamemodify(path, ':.'))
1553
+ -- Showing in window also loads. Use `pcall` to not error with swap messages.
1554
+ pcall(vim.api.nvim_win_set_buf, win_id or 0, buf_id)
1555
+ vim.bo[buf_id].buflisted = true
1556
+ if try_mimic_buf_reuse then pcall(vim.api.nvim_buf_delete, b, { unload = false }) end
1557
+ return buf_id
1558
+ end
1559
+
1560
+ H.full_path = function(path) return (vim.fn.fnamemodify(path, ':p'):gsub('/+', '/'):gsub('(.)/$', '%1')) end
1561
+ if H.is_windows then
1562
+ H.full_path = function(path)
1563
+ return (vim.fn.fnamemodify(path, ':p'):gsub('\\', '/'):gsub('/+', '/'):gsub('(.)/$', '%1'))
1564
+ end
1565
+ end
1566
+
1567
+ H.short_path = function(path, cwd)
1568
+ cwd = cwd or vim.fn.getcwd()
1569
+ -- Ensure `cwd` is treated as directory path (to not match similar prefix)
1570
+ cwd = cwd:sub(-1) == '/' and cwd or (cwd .. '/')
1571
+ if vim.startswith(path, cwd) then return path:sub(cwd:len() + 1) end
1572
+ local res = vim.fn.fnamemodify(path, ':~')
1573
+ if H.is_windows then res = res:gsub('\\', '/') end
1574
+ return res
1575
+ end
1576
+
1577
+ return MiniVisits