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,1938 @@
1
+ --- *mini.diff* Work with diff hunks
2
+ ---
3
+ --- MIT License Copyright (c) 2024 Evgeni Chasnovski
4
+
5
+ --- Features:
6
+ ---
7
+ --- - Visualize difference between buffer text and its configurable reference
8
+ --- interactively (updates as you type). This is done per line showing whether
9
+ --- it is inside added, changed, or deleted part of difference (called hunk).
10
+ --- Visualization can be with customizable colored signs or line numbers.
11
+ ---
12
+ --- - Special toggleable overlay view with more hunk details inside text area.
13
+ --- See |MiniDiff.toggle_overlay()|.
14
+ ---
15
+ --- - Completely configurable per buffer source(s) of reference text used to keep
16
+ --- it up to date and define interactions with it. Can be array of sources which
17
+ --- are attempted to attach in order. See |MiniDiff-source-specification|.
18
+ --- By default uses Git source. See |MiniDiff.gen_source.git()|.
19
+ ---
20
+ --- - Configurable mappings to manage diff hunks:
21
+ --- - Apply and reset hunks inside region (selected visually or with
22
+ --- a dot-repeatable operator).
23
+ --- - "Hunk range under cursor" textobject to be used as operator target.
24
+ --- - Navigate to first/previous/next/last hunk. See |MiniDiff.goto_hunk()|.
25
+ ---
26
+ --- What it doesn't do:
27
+ ---
28
+ --- - Provide functionality to work directly with Git outside of visualizing
29
+ --- and staging (applying) hunks with (default) Git source. In particular,
30
+ --- unstaging hunks is not supported. See |MiniDiff.gen_source.git()|.
31
+ ---
32
+ --- Sources with more details:
33
+ --- - |MiniDiff-overview|
34
+ --- - |MiniDiff-source-specification|
35
+ --- - |MiniDiff-hunk-specification|
36
+ --- - |MiniDiff-diff-summary|
37
+ ---
38
+ --- # Setup ~
39
+ ---
40
+ --- This module needs a setup with `require('mini.diff').setup({})` (replace
41
+ --- `{}` with your `config` table). It will create global Lua table `MiniDiff`
42
+ --- which you can use for scripting or manually (with `:lua MiniDiff.*`).
43
+ ---
44
+ --- See |MiniDiff.config| for `config` structure and default values.
45
+ ---
46
+ --- You can override runtime config settings locally to buffer inside
47
+ --- `vim.b.minidiff_config` which should have same structure as
48
+ --- `MiniDiff.config`. See |mini.nvim-buffer-local-config| for more details.
49
+ ---
50
+ --- # Comparisons ~
51
+ ---
52
+ --- - [lewis6991/gitsigns.nvim](https://github.com/lewis6991/gitsigns.nvim):
53
+ --- - Main inspiration for this module, so there are many similarities.
54
+ --- - Can display only Git hunks, while this module has extensible design.
55
+ --- - Provides more functionality to work with Git outside of hunks.
56
+ --- This module does not (by design).
57
+ ---
58
+ --- # Highlight groups ~
59
+ --- *MiniDiff-hl-groups*
60
+ ---
61
+ --- - `MiniDiffSignAdd` - "add" hunk lines visualization.
62
+ --- - `MiniDiffSignChange` - "change" hunk lines visualization.
63
+ --- - `MiniDiffSignDelete` - "delete" hunk lines visualization.
64
+ --- - `MiniDiffOverAdd` - added buffer text shown in overlay.
65
+ --- - `MiniDiffOverChange` - changed reference text shown in overlay.
66
+ --- - `MiniDiffOverChangeBuf` - changed buffer text shown in overlay.
67
+ --- - `MiniDiffOverContext` - context of a change shown in reference overlay.
68
+ --- - `MiniDiffOverContextBuf` - context of a change shown in buffer overlay.
69
+ --- - `MiniDiffOverDelete` - deleted reference text shown in overlay.
70
+ ---
71
+ --- To change any highlight group, set it directly with |nvim_set_hl()|.
72
+ ---
73
+ --- # Disabling ~
74
+ ---
75
+ --- To temporarily disable features without relying on |MiniDiff.disable()|,
76
+ --- set `vim.g.minidiff_disable` (globally) or `vim.b.minidiff_disable` (for
77
+ --- a buffer) to `true`. Considering high number of different scenarios and
78
+ --- customization intentions, writing exact rules for disabling module's
79
+ --- functionality is left to user.
80
+ --- See |mini.nvim-disabling-recipes| for common recipes.
81
+ ---@tag MiniDiff
82
+
83
+ --- # Diffs and hunks ~
84
+ ---
85
+ --- The "diff" (short for "difference") is a result of computing how two text
86
+ --- strings differ from one another. This is done on per line basis, i.e. the
87
+ --- goal is to compute sequences of lines common to both files, interspersed
88
+ --- with groups of differing lines (called "hunks").
89
+ ---
90
+ --- Although computing diff is a general concept (used on its own, in Git, etc.),
91
+ --- this module computes difference between current text in a buffer and some
92
+ --- reference text which is kept up to date specifically for that buffer.
93
+ --- For example, default reference text is computed as file content in Git index.
94
+ --- This can be customized in `config.source` (see |MiniDiff-source-specification|).
95
+ ---
96
+ --- # Hunk specification ~
97
+ --- *MiniDiff-hunk-specification*
98
+ ---
99
+ --- Hunk describes two sets (one from buffer text, one - from reference) of
100
+ --- consecutive lines which are different. In this module hunk is stored as
101
+ --- a table with the following fields:
102
+ ---
103
+ --- - <buf_start> `(number)` - start of hunk buffer lines. First line is 1.
104
+ --- Can be 0 if first reference lines are deleted.
105
+ ---
106
+ --- - <buf_count> `(number)` - number of consecutive buffer lines. Can be 0 in
107
+ --- case reference lines are deleted.
108
+ ---
109
+ --- - <ref_start> `(number)` - start of hunk reference lines. First line is 1.
110
+ --- Can be 0 if lines are added before first reference line.
111
+ ---
112
+ --- - <ref_count> `(number)` - number of consecutive reference lines. Can be 0 in
113
+ --- case buffer lines are added.
114
+ ---
115
+ --- - <type> `(string)` - hunk type. Can be one of:
116
+ --- - "add" - lines are present in buffer but absent in reference.
117
+ --- - "change" - lines are present in both buffer and reference.
118
+ --- - "delete" - lines are absent in buffer but present in reference.
119
+ ---
120
+ --- # Life cycle ~
121
+ ---
122
+ --- - When entering proper (not already enabled, valid, showing text) buffer,
123
+ --- it is attempted to be enabled for diff processing.
124
+ --- - During enabling, attempt attaching the source. This should set up how
125
+ --- reference text is kept up to date.
126
+ --- - On every text change, diff computation is scheduled in debounced fashion
127
+ --- after customizable delay (200 ms by default).
128
+ --- - After the diff is computed, do the following:
129
+ --- - Update visualization based on configurable style: either by placing
130
+ --- colored text in sign column or coloring line numbers. Colors for both
131
+ --- styles are defined per hunk type in corresponding `MiniDiffSign*`
132
+ --- highlight group (see |mini.diff|) and sign text for "sign" style can
133
+ --- be configured in `view.signs` of |MiniDiff.config|.
134
+ --- - Update overlay view (if it is enabled).
135
+ --- - Update `vim.b.minidiff_summary` and `vim.b.minidiff_summary_string`
136
+ --- buffer-local variables. These can be used, for example, in statusline.
137
+ --- - *MiniDiff-update-event* Trigger `MiniDiffUpdated` `User` event.
138
+ --- See |MiniDiff-diff-summary| for example of how to use it.
139
+ ---
140
+ --- Notes:
141
+ --- - Use |:edit| to reset (disable and re-enable) current buffer.
142
+ --- - To work with BOM bytes, set |'bomb'| and have `ucs-bom` in |'fileencodings'|.
143
+ ---
144
+ --- # Overlay ~
145
+ ---
146
+ --- Along with basic visualization, there is a special view called "overlay".
147
+ --- Although it is meant for temporary overview of diff details and can be
148
+ --- manually toggled via |MiniDiff.toggle_overlay()|, text can be changed with
149
+ --- overlay reacting accordingly.
150
+ ---
151
+ --- It shows more diff details inside text area:
152
+ ---
153
+ --- - Added buffer lines are highlighted with `MiniDiffOverAdd` highlight group.
154
+ ---
155
+ --- - Deleted reference lines are shown as virtual lines and highlighted with
156
+ --- `MiniDiffOverDelete` highlight group.
157
+ ---
158
+ --- - "Change" hunks with equal number of buffer/reference lines show "word diff".
159
+ --- This is usually the case when `options.linematch` is enabled (as by default).
160
+ --- Reference line is shown next to its buffer counterpart. Changed parts are
161
+ --- highlighted with `MiniDiffOverChange` and `MiniDiffOverChangeBuf` in reference
162
+ --- and buffer lines. The rest of lines have `MiniDiffOverContext`
163
+ --- and `MiniDiffOverContextBuf` highlighting.
164
+ ---
165
+ --- Change with unequal number of buffer/reference lines is shown with reference
166
+ --- part as virtual lines highlighted with `MiniDiffOverChange` group.
167
+ --- Corresponding buffer lines are treated as context for the change and are
168
+ --- highlighted with `MiniDiffOverContextBuf` group.
169
+ ---
170
+ --- Notes:
171
+ --- - Word diff has non-zero context width. This means if changed characters
172
+ --- are close enough, whole range between them is also colored. This usually
173
+ --- reduces visual noise.
174
+ --- - Virtual lines above line 1 (like deleted or changed lines) need manual
175
+ --- scroll to become visible (with |CTRL-Y|).
176
+ ---
177
+ --- # Mappings ~
178
+ ---
179
+ --- This module provides mappings for common actions with diffs, like:
180
+ --- - Apply and reset hunks.
181
+ --- - "Hunk range under cursor" textobject.
182
+ --- - Go to first/previous/next/last hunk range.
183
+ ---
184
+ --- Examples:
185
+ --- - `vip` followed by `gh` / `gH` applies/resets hunks inside current paragraph.
186
+ --- Same can be achieved in operator form `ghip` / `gHip`, which has the
187
+ --- advantage of being dot-repeatable (see |single-repeat|).
188
+ --- - `gh_` / `gH_` applies/resets current line (even if it is not a full hunk).
189
+ --- - `ghgh` / `gHgh` applies/resets hunk range under cursor.
190
+ --- - `dgh` deletes hunk range under cursor.
191
+ --- - `[H` / `[h` / `]h` / `]H` navigate cursor to the first / previous / next / last
192
+ --- hunk range of the current buffer.
193
+ ---
194
+ --- Mappings for some functionality are assumed to be done manually.
195
+ --- See |MiniDiff.operator()|.
196
+ ---
197
+ --- # Buffer-local variables ~
198
+ --- *MiniDiff-diff-summary*
199
+ ---
200
+ --- Each enabled buffer has the following buffer-local variables which can be
201
+ --- used in custom statusline to show an overview of hunks in current buffer:
202
+ ---
203
+ --- - `vim.b.minidiff_summary` is a table with the following fields:
204
+ --- - `source_name` - name of the active source. This is the only present field
205
+ --- if buffer's reference text is not (yet) set.
206
+ --- - `n_ranges` - number of hunk ranges (sequences of contiguous hunks).
207
+ --- - `add` - number of added lines.
208
+ --- - `change` - number of changed lines.
209
+ --- - `delete` - number of deleted lines.
210
+ ---
211
+ --- - `vim.b.minidiff_summary_string` is a string representation of summary
212
+ --- with a fixed format. Empty string if there is no reference text (yet).
213
+ --- It is expected to be used as is. To achieve different formatting, use
214
+ --- `vim.b.minidiff_summary` to construct one. The best way to do this is by
215
+ --- overriding `vim.b.minidiff_summary_string` inside |MiniDiff-update-event|: >lua
216
+ ---
217
+ --- local format_summary = function(data)
218
+ --- local summary = vim.b[data.buf].minidiff_summary
219
+ --- local t = {}
220
+ --- if summary.add > 0 then table.insert(t, '+' .. summary.add) end
221
+ --- if summary.change > 0 then table.insert(t, '~' .. summary.change) end
222
+ --- if summary.delete > 0 then table.insert(t, '-' .. summary.delete) end
223
+ --- vim.b[data.buf].minidiff_summary_string = table.concat(t, ' ')
224
+ --- end
225
+ --- local au_opts = { pattern = 'MiniDiffUpdated', callback = format_summary }
226
+ --- vim.api.nvim_create_autocmd('User', au_opts)
227
+ --- <
228
+ ---@tag MiniDiff-overview
229
+
230
+ ---@alias __diff_buf_id number Target buffer identifier. Default: 0 for current buffer.
231
+
232
+ ---@diagnostic disable:undefined-field
233
+ ---@diagnostic disable:discard-returns
234
+ ---@diagnostic disable:unused-local
235
+ ---@diagnostic disable:cast-local-type
236
+ ---@diagnostic disable:undefined-doc-name
237
+ ---@diagnostic disable:luadoc-miss-type-name
238
+
239
+ -- Module definition ==========================================================
240
+ local MiniDiff = {}
241
+ local H = {}
242
+
243
+ --- Module setup
244
+ ---
245
+ ---@param config table|nil Module config table. See |MiniDiff.config|.
246
+ ---
247
+ ---@usage >lua
248
+ --- require('mini.diff').setup() -- use default config
249
+ --- -- OR
250
+ --- require('mini.diff').setup({}) -- replace {} with your config table
251
+ --- <
252
+ MiniDiff.setup = function(config)
253
+ -- TODO: Remove after Neovim=0.9 support is dropped
254
+ if vim.fn.has('nvim-0.10') == 0 then
255
+ vim.notify(
256
+ '(mini.diff) Neovim<0.10 is soft deprecated (module works but is not supported).'
257
+ .. " It will be deprecated after the next 'mini.nvim' release (module might not work)."
258
+ .. ' Please update your Neovim version.'
259
+ )
260
+ end
261
+
262
+ -- Export module
263
+ _G.MiniDiff = MiniDiff
264
+
265
+ -- Setup config
266
+ config = H.setup_config(config)
267
+
268
+ -- Apply config
269
+ H.apply_config(config)
270
+
271
+ -- Define behavior
272
+ H.create_autocommands()
273
+ for _, buf_id in ipairs(vim.api.nvim_list_bufs()) do
274
+ H.auto_enable({ buf = buf_id })
275
+ end
276
+
277
+ -- Create default highlighting
278
+ H.create_default_hl()
279
+ end
280
+
281
+ --stylua: ignore
282
+ --- Defaults ~
283
+ ---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
284
+ ---@text # View ~
285
+ ---
286
+ --- `config.view` contains settings for how diff hunks are visualized.
287
+ --- Example of using custom signs: >lua
288
+ ---
289
+ --- require('mini.diff').setup({
290
+ --- view = {
291
+ --- style = 'sign',
292
+ --- signs = { add = '+', change = '~', delete = '-' },
293
+ --- },
294
+ --- })
295
+ --- <
296
+ --- `view.style` is a string defining visualization style. Can be one of "sign"
297
+ --- (as a colored sign in a |sign-column|) or "number" (colored line number).
298
+ --- Default: "number" if |'number'| option is enabled, "sign" otherwise.
299
+ --- Note: with "sign" style it is better to have |'signcolumn'| always shown.
300
+ ---
301
+ --- `view.signs` is a table with one or two character strings used as signs for
302
+ --- corresponding ("add", "change", "delete") hunks.
303
+ --- Default: all hunks use "▒" character resulting in a contiguous colored lines.
304
+ ---
305
+ --- `view.priority` is a number with priority used for visualization and
306
+ --- overlay |extmarks|.
307
+ --- Default: 199 which is one less than `user` in |vim.hl.priorities| (on Neovim<0.11
308
+ --- see |vim.hl.priorities|) to have higher priority than automated
309
+ --- extmarks but not as in user enabled ones.
310
+ ---
311
+ --- # Source ~
312
+ --- *MiniDiff-source-specification*
313
+ ---
314
+ --- `config.source` is a table with single source or array of them. Single source
315
+ --- defines how reference text is managed in a particular buffer. Sources in array
316
+ --- are attempted to attach in order; call |MiniDiff.disable()| if none attaches.
317
+ ---
318
+ --- A single source table can have the following fields:
319
+ ---
320
+ --- - <attach> `(function)` - callable which defines how and when reference text
321
+ --- is updated inside a particular buffer. It is used inside |MiniDiff.enable()|
322
+ --- with a buffer identifier as a single argument.
323
+ ---
324
+ --- Should execute logic which results into calling |MiniDiff.set_ref_text()|
325
+ --- when reference text for buffer needs to be updated. Like inside callback
326
+ --- for an |autocommand| or file watcher (see |watch-file|).
327
+ ---
328
+ --- For example, default Git source watches when ".git/index" file is changed
329
+ --- and computes reference text as the one from Git index for current file.
330
+ ---
331
+ --- Can return `false` to indicate that attach has failed. If attach fail can
332
+ --- not be inferred immediately (for example, due to asynchronous execution),
333
+ --- should explicitly call |MiniDiff.fail_attach()| with appropriate arguments.
334
+ --- This is important to properly process array of sources.
335
+ ---
336
+ --- No default value, should be always supplied.
337
+ ---
338
+ --- - <name> `(string|nil)` - source name. String `"unknown"` is used if not supplied.
339
+ ---
340
+ --- - <detach> `(function|nil)` - callable with cleanup action to be done when
341
+ --- buffer is disabled. It is called inside |MiniDiff.disable()| with a buffer
342
+ --- identifier as a single argument.
343
+ ---
344
+ --- If not supplied, nothing is done during detaching.
345
+ ---
346
+ --- - <apply_hunks> `(function|nil)` - callable which defines how hunks are applied.
347
+ --- It is called with buffer identifier as first argument and array of hunks
348
+ --- (see |MiniDiff-hunk-specification|) as second. It should eventually update
349
+ --- reference text: either by explicitly calling |MiniDiff.set_ref_text()| or
350
+ --- performing action triggering its call.
351
+ ---
352
+ --- For example, default Git source computes patch based on the hunks and
353
+ --- applies it inside file's git repo.
354
+ ---
355
+ --- If not supplied, applying hunks throws an error.
356
+ ---
357
+ --- Default: a single |MiniDiff.gen_source.git()|.
358
+ ---
359
+ --- # Delay ~
360
+ ---
361
+ --- `config.delay` contains settings for delays in asynchronous processes.
362
+ ---
363
+ --- `delay.text_change` is a number (in ms) defining how long to wait after latest
364
+ --- text change (in debounced fashion) before updating diff and visualization.
365
+ --- Default: 200.
366
+ ---
367
+ --- # Mappings ~
368
+ ---
369
+ --- `config.mappings` contains keys which are mapped during |MiniDiff.setup()|.
370
+ ---
371
+ --- `mappings.apply` keys can be used to apply hunks inside visual/operator region.
372
+ --- What exactly "apply hunks" means depends on the source and its `apply_hunks()`.
373
+ --- For example, in default Git source it means stage hunks.
374
+ ---
375
+ --- `mappings.reset` keys can be used to reset hunks inside visual/operator region.
376
+ --- Reset means replacing buffer text in region with corresponding reference text.
377
+ ---
378
+ --- `mappings.textobject` keys define "hunk range under cursor" textobject
379
+ --- which can be used in Operator-pending mode as target for operator (like
380
+ --- |d|, |y|, apply/reset hunks, etc.). It is also set up in Visual mode if
381
+ --- keys do not conflict with `mappings.apply` and `mappings.reset`.
382
+ --- "Hunk range" is used in a sense that contiguous (back-to-back) hunks are
383
+ --- considered as parts of a same hunk range.
384
+ ---
385
+ --- `mappings.goto_first` / `mappings.goto_prev` / `mappings.goto_next` /
386
+ --- `mappings.goto_last` keys can be used to navigate to first / previous / next /
387
+ --- last hunk range in the current buffer.
388
+ ---
389
+ --- # Options ~
390
+ ---
391
+ --- `config.options` contains various customization options.
392
+ ---
393
+ --- `options.algorithm` is a string defining which diff algorithm to use.
394
+ --- Default: "histogram". See |vim.text.diff()| (|vim.diff()| on Neovim<=0.11)
395
+ --- for possible values.
396
+ ---
397
+ --- `options.indent_heuristic` is a boolean defining whether to use indent
398
+ --- heuristic for a (possibly) more naturally aligned hunks.
399
+ --- Default: `true`.
400
+ ---
401
+ --- `options.linematch` is a number defining hunk size for which a second
402
+ --- stage diff is executed for a better aligned and more granular hunks.
403
+ --- Default: 60. See |vim.text.diff()| (|vim.diff()| on Neovim<=0.11) and |'diffopt'|
404
+ --- for more details.
405
+ ---
406
+ --- `options.wrap_goto` is a boolean indicating whether to wrap around edges during
407
+ --- hunk navigation (with |MiniDiff.goto_hunk()| or `goto_*` mappings). Like if
408
+ --- cursor is after the last hunk, going "next" will put cursor on the first hunk.
409
+ --- Default: `false`.
410
+ MiniDiff.config = {
411
+ -- Options for how hunks are visualized
412
+ view = {
413
+ -- Visualization style. Possible values are 'sign' and 'number'.
414
+ -- Default: 'number' if line numbers are enabled, 'sign' otherwise.
415
+ style = vim.go.number and 'number' or 'sign',
416
+
417
+ -- Signs used for hunks with 'sign' view
418
+ signs = { add = '▒', change = '▒', delete = '▒' },
419
+
420
+ -- Priority of used visualization extmarks
421
+ priority = 199,
422
+ },
423
+
424
+ -- Source(s) for how reference text is computed/updated/etc
425
+ -- Uses content from Git index by default
426
+ source = nil,
427
+
428
+ -- Delays (in ms) defining asynchronous processes
429
+ delay = {
430
+ -- How much to wait before update following every text change
431
+ text_change = 200,
432
+ },
433
+
434
+ -- Module mappings. Use `''` (empty string) to disable one.
435
+ mappings = {
436
+ -- Apply hunks inside a visual/operator region
437
+ apply = 'gh',
438
+
439
+ -- Reset hunks inside a visual/operator region
440
+ reset = 'gH',
441
+
442
+ -- Hunk range textobject to be used inside operator
443
+ -- Works also in Visual mode if mapping differs from apply and reset
444
+ textobject = 'gh',
445
+
446
+ -- Go to hunk range in corresponding direction
447
+ goto_first = '[H',
448
+ goto_prev = '[h',
449
+ goto_next = ']h',
450
+ goto_last = ']H',
451
+ },
452
+
453
+ -- Various options
454
+ options = {
455
+ -- Diff algorithm (see `:h vim.text.diff()`)
456
+ algorithm = 'histogram',
457
+
458
+ -- Whether to use "indent heuristic" (see `:h vim.text.diff()`)
459
+ indent_heuristic = true,
460
+
461
+ -- The amount of second-stage diff to align lines
462
+ linematch = 60,
463
+
464
+ -- Whether to wrap around edges during hunk navigation
465
+ wrap_goto = false,
466
+ },
467
+ }
468
+ --minidoc_afterlines_end
469
+
470
+ --- Enable diff processing in buffer
471
+ ---
472
+ ---@param buf_id __diff_buf_id
473
+ MiniDiff.enable = function(buf_id)
474
+ buf_id = H.validate_buf_id(buf_id)
475
+
476
+ -- Don't enable more than once
477
+ if H.is_buf_enabled(buf_id) or H.is_disabled(buf_id) then return end
478
+
479
+ -- Ensure buffer is loaded (to have up to date lines returned)
480
+ H.buf_ensure_loaded(buf_id)
481
+
482
+ -- Register enabled buffer with cached data for performance
483
+ H.update_buf_cache(buf_id)
484
+
485
+ -- Add buffer watchers
486
+ vim.api.nvim_buf_attach(buf_id, false, {
487
+ -- Called on every text change (`:h nvim_buf_lines_event`)
488
+ on_lines = function(_, _, _, from_line, _, to_line)
489
+ local buf_cache = H.cache[buf_id]
490
+ -- Properly detach if diffing is disabled
491
+ if buf_cache == nil then return true end
492
+ H.schedule_diff_update(buf_id, buf_cache.config.delay.text_change)
493
+ end,
494
+
495
+ -- Called when buffer content is changed outside of current session
496
+ on_reload = function() H.schedule_diff_update(buf_id, 0) end,
497
+
498
+ -- Called when buffer is unloaded from memory (`:h nvim_buf_detach_event`),
499
+ -- **including** `:edit` command
500
+ on_detach = function() MiniDiff.disable(buf_id) end,
501
+ })
502
+
503
+ -- Add buffer autocommands
504
+ H.setup_buf_autocommands(buf_id)
505
+
506
+ -- Try attaching source after all necessary watchers are set up. It is needed
507
+ -- to still have them set up if first source of many returned `false`.
508
+ local attach_output = H.get_active_source(H.cache[buf_id]).attach(buf_id)
509
+ if attach_output == false then MiniDiff.fail_attach(buf_id) end
510
+ end
511
+
512
+ --- Disable diff processing in buffer
513
+ ---
514
+ ---@param buf_id __diff_buf_id
515
+ MiniDiff.disable = function(buf_id)
516
+ buf_id = H.validate_buf_id(buf_id)
517
+
518
+ local buf_cache = H.cache[buf_id]
519
+ if buf_cache == nil then return end
520
+ H.cache[buf_id] = nil
521
+
522
+ pcall(vim.api.nvim_del_augroup_by_id, buf_cache.augroup)
523
+ vim.b[buf_id].minidiff_summary, vim.b[buf_id].minidiff_summary_string = nil, nil
524
+ H.clear_all_diff(buf_id)
525
+ pcall(H.get_active_source(buf_cache).detach, buf_id)
526
+ end
527
+
528
+ --- Toggle diff processing in buffer
529
+ ---
530
+ --- Enable if disabled, disable if enabled.
531
+ ---
532
+ ---@param buf_id __diff_buf_id
533
+ MiniDiff.toggle = function(buf_id)
534
+ buf_id = H.validate_buf_id(buf_id)
535
+ if H.is_buf_enabled(buf_id) then return MiniDiff.disable(buf_id) end
536
+ return MiniDiff.enable(buf_id)
537
+ end
538
+
539
+ --- Toggle overlay view in buffer
540
+ ---
541
+ ---@param buf_id __diff_buf_id
542
+ MiniDiff.toggle_overlay = function(buf_id)
543
+ buf_id = H.validate_buf_id(buf_id)
544
+ local buf_cache = H.cache[buf_id]
545
+ if buf_cache == nil then H.error(string.format('Buffer %d is not enabled.', buf_id)) end
546
+
547
+ buf_cache.overlay = not buf_cache.overlay
548
+ H.clear_all_diff(buf_id)
549
+ H.schedule_diff_update(buf_id, 0)
550
+ end
551
+
552
+ --- Export hunks
553
+ ---
554
+ --- Get and convert hunks from current/all buffers. Example of using it: >lua
555
+ ---
556
+ --- -- Set quickfix list from all available hunks
557
+ --- vim.fn.setqflist(MiniDiff.export('qf'))
558
+ --- <
559
+ ---@param format string Output format. Currently only `'qf'` value is supported.
560
+ ---@param opts table|nil Options. Possible fields:
561
+ --- - <scope> `(string)` - scope defining from which buffers to use hunks.
562
+ --- One of "all" (all enabled buffers) or "current".
563
+ ---
564
+ ---@return table Result of export. Depends on the `format`:
565
+ --- - If "qf", an array compatible with |setqflist()| and |setloclist()|.
566
+ MiniDiff.export = function(format, opts)
567
+ opts = vim.tbl_deep_extend('force', { scope = 'all' }, opts or {})
568
+ if format == 'qf' then return H.export_qf(opts) end
569
+ H.error('`format` should be one of "qf".')
570
+ end
571
+
572
+ --- Get buffer data
573
+ ---
574
+ ---@param buf_id __diff_buf_id
575
+ ---
576
+ ---@return table|nil Table with buffer diff data or `nil` if buffer is not enabled.
577
+ --- Table has the following fields:
578
+ --- - <config> `(table)` - config used for this particular buffer.
579
+ --- - <hunks> `(table)` - array of hunks. See |MiniDiff-hunk-specification|.
580
+ --- - <overlay> `(boolean)` - whether an overlay view is shown.
581
+ --- - <ref_text> `(string|nil)` - current value of reference text. Lines are
582
+ --- separated with newline character (`'\n'`). Can be `nil` indicating that
583
+ --- reference text was not yet set (for example, if source did not yet react).
584
+ --- - <summary> `(table)` - overall diff summary. See |MiniDiff-diff-summary|.
585
+ MiniDiff.get_buf_data = function(buf_id)
586
+ buf_id = H.validate_buf_id(buf_id)
587
+ local buf_cache = H.cache[buf_id]
588
+ if buf_cache == nil then return nil end
589
+ return vim.deepcopy({
590
+ config = buf_cache.config,
591
+ hunks = buf_cache.hunks,
592
+ overlay = buf_cache.overlay,
593
+ ref_text = buf_cache.ref_text,
594
+ summary = buf_cache.summary,
595
+ })
596
+ end
597
+
598
+ --- Set reference text for the buffer
599
+ ---
600
+ --- Note: this will call |MiniDiff.enable()| for target buffer if it is not
601
+ --- already enabled.
602
+ ---
603
+ ---@param buf_id __diff_buf_id
604
+ ---@param text string|table New reference text. Either a string with `\n` used to
605
+ --- separate lines or array of lines. Use empty table to unset current
606
+ --- reference text (results into no hunks shown). Default: `{}`.
607
+ --- Note: newline character is appended at the end (if it is not there already)
608
+ --- for better diffs.
609
+ MiniDiff.set_ref_text = function(buf_id, text)
610
+ buf_id = H.validate_buf_id(buf_id)
611
+ if not (type(text) == 'table' or type(text) == 'string') then H.error('`text` should be either string or array.') end
612
+ if type(text) == 'table' then text = #text > 0 and table.concat(text, '\n') or nil end
613
+
614
+ -- Enable if not already enabled
615
+ if not H.is_buf_enabled(buf_id) then MiniDiff.enable(buf_id) end
616
+ if not H.is_buf_enabled(buf_id) then H.error('Can not set reference text for not enabled buffer.') end
617
+
618
+ -- Appending '\n' makes more intuitive diffs at end-of-file
619
+ if text ~= nil and string.sub(text, -1) ~= '\n' then text = text .. '\n' end
620
+ if text == nil then
621
+ H.clear_all_diff(buf_id)
622
+ vim.cmd('redraw')
623
+ end
624
+
625
+ -- Immediately update diff
626
+ H.cache[buf_id].ref_text = text
627
+ H.schedule_diff_update(buf_id, 0)
628
+ end
629
+
630
+ --- Generate builtin sources
631
+ ---
632
+ --- This is a table with function elements. Call to actually get source.
633
+ --- Examples: >lua
634
+ ---
635
+ --- local diff = require('mini.diff')
636
+ ---
637
+ --- -- Single `save` source
638
+ --- diff.setup({ source = diff.gen_source.save() })
639
+ ---
640
+ --- -- Multiple sources (attempted to attach in order)
641
+ --- diff.setup({ source = { diff.gen_source.git(), diff.gen_source.save() } })
642
+ --- <
643
+ MiniDiff.gen_source = {}
644
+
645
+ --- Git source
646
+ ---
647
+ --- Default source. Uses file text from Git index as reference. This results in:
648
+ --- - "Add" hunks represent text present in current buffer, but not in index.
649
+ --- - "Change" hunks represent modified text already present in index.
650
+ --- - "Delete" hunks represent text deleted from index.
651
+ ---
652
+ --- Applying hunks means staging, a.k.a adding to index.
653
+ --- Notes:
654
+ --- - Requires Git version at least 2.38.0.
655
+ --- - There is no capability for unstaging hunks. Use full Git client for that.
656
+ ---
657
+ ---@return table Source. See |MiniDiff-source-specification|.
658
+ MiniDiff.gen_source.git = function()
659
+ local attach = function(buf_id)
660
+ -- Try attaching to a buffer only once
661
+ if H.git_cache[buf_id] ~= nil then return false end
662
+
663
+ -- Possibly resolve symlinks to get data from the original repo
664
+ local path = H.get_buf_realpath(buf_id)
665
+ if path == '' then return false end
666
+
667
+ -- Claim Git cache to not try to attach to the non-Git path several times.
668
+ -- NOTE: Do this after `path==''` check since the same buffer can be reused
669
+ -- for opening actual file (`nvim` -> `:edit file`).
670
+ H.git_cache[buf_id] = {}
671
+ H.git_start_watching_index(buf_id, path)
672
+ end
673
+
674
+ local detach = function(buf_id)
675
+ local cache = H.git_cache[buf_id]
676
+ H.git_cache[buf_id] = nil
677
+ H.git_invalidate_cache(cache)
678
+ end
679
+
680
+ local apply_hunks = function(buf_id, hunks)
681
+ local path_data = H.git_get_path_data(H.get_buf_realpath(buf_id))
682
+ if path_data == nil or path_data.rel_path == nil then return end
683
+ local patch = H.git_format_patch(buf_id, hunks, path_data)
684
+ H.git_apply_patch(path_data, patch)
685
+ end
686
+
687
+ -- Ensure to clean source cache for `:edit` to force reattach, since regular
688
+ -- `disable()` from `on_detach` is not enough as there can be no source
689
+ -- attached (like for files not in Git repo).
690
+ local augroup = vim.api.nvim_create_augroup('MiniDiffSourceGit', { clear = true })
691
+ local clear_buf_cache = function(ev) H.git_cache[ev.buf] = nil end
692
+ local au_opts = { group = augroup, callback = clear_buf_cache, desc = 'Clear Git cache' }
693
+ vim.api.nvim_create_autocmd({ 'BufUnload' }, au_opts)
694
+
695
+ return { name = 'git', attach = attach, detach = detach, apply_hunks = apply_hunks }
696
+ end
697
+
698
+ --- "Do nothing" source
699
+ ---
700
+ --- Allows buffers to be enabled while not setting any reference text.
701
+ --- Use this if the goal is to rely on manual |MiniDiff.set_ref_text()| calls.
702
+ ---
703
+ ---@return table Source. See |MiniDiff-source-specification|.
704
+ MiniDiff.gen_source.none = function()
705
+ return { name = 'none', attach = function() end }
706
+ end
707
+
708
+ --- Latest save source
709
+ ---
710
+ --- Uses text at latest save as the reference. This results into diff showing
711
+ --- difference after the latest save.
712
+ ---
713
+ ---@return table Source. See |MiniDiff-source-specification|.
714
+ MiniDiff.gen_source.save = function()
715
+ local augroups = {}
716
+ local attach = function(buf_id)
717
+ local augroup = vim.api.nvim_create_augroup('MiniDiffSourceSaveBuffer' .. buf_id, { clear = true })
718
+ augroups[buf_id] = augroup
719
+
720
+ local set_ref = function()
721
+ if vim.bo[buf_id].modified then return end
722
+ MiniDiff.set_ref_text(buf_id, H.get_buftext(buf_id))
723
+ end
724
+
725
+ -- Autocommand are more efficient than file watcher as it doesn't read disk
726
+ local au_opts = { group = augroup, buffer = buf_id, callback = set_ref, desc = 'Set reference text after save' }
727
+ vim.api.nvim_create_autocmd({ 'BufWritePost', 'FileChangedShellPost' }, au_opts)
728
+ set_ref()
729
+ end
730
+
731
+ local detach = function(buf_id) pcall(vim.api.nvim_del_augroup_by_id, augroups[buf_id]) end
732
+
733
+ return { name = 'save', attach = attach, detach = detach }
734
+ end
735
+
736
+ --- Perform action on hunks in region
737
+ ---
738
+ --- Compute hunks inside a target region (even for hunks only partially inside it)
739
+ --- and perform apply/reset/yank operation on them.
740
+ ---
741
+ --- The "yank" action yanks all reference lines of target hunks into
742
+ --- a specified register (should be one of |registers|).
743
+ ---
744
+ --- Notes:
745
+ --- - Whether hunk is inside a region is computed based on position of its
746
+ --- buffer lines.
747
+ --- - If "change" or "delete" is only partially inside a target region, all
748
+ --- reference lines are used in computed "intersection" hunk.
749
+ ---
750
+ --- Used directly in `config.mappings.apply` and `config.mappings.reset`.
751
+ --- Usually there is no need to use this function manually.
752
+ --- See |MiniDiff.operator()| for how to set up a mapping for "yank".
753
+ ---
754
+ ---@param buf_id __diff_buf_id
755
+ ---@param action string One of "apply", "reset", "yank".
756
+ ---@param opts table|nil Options. Possible fields:
757
+ --- - <line_start> `(number)` - start line of the region. Default: 1.
758
+ --- - <line_end> `(number)` - start line of the region. Default: last buffer line.
759
+ --- - <register> `(string)` - register to yank reference lines into.
760
+ --- Default: |v:register|.
761
+ MiniDiff.do_hunks = function(buf_id, action, opts)
762
+ buf_id = H.validate_buf_id(buf_id)
763
+ local buf_cache = H.cache[buf_id]
764
+ if buf_cache == nil then H.error(string.format('Buffer %d is not enabled.', buf_id)) end
765
+ if type(buf_cache.ref_text) ~= 'string' then H.error(string.format('Buffer %d has no reference text.', buf_id)) end
766
+
767
+ if not (action == 'apply' or action == 'reset' or action == 'yank') then
768
+ H.error('`action` should be one of "apply", "reset", "yank".')
769
+ end
770
+
771
+ local default_opts = { line_start = 1, line_end = vim.api.nvim_buf_line_count(buf_id), register = vim.v.register }
772
+ opts = vim.tbl_deep_extend('force', default_opts, opts or {})
773
+ local line_start, line_end = H.validate_target_lines(buf_id, opts.line_start, opts.line_end)
774
+ if type(opts.register) ~= 'string' then H.error('`opts.register` should be string.') end
775
+
776
+ local hunks = H.get_hunks_in_range(buf_cache.hunks, line_start, line_end)
777
+ if #hunks == 0 then return H.notify('No hunks to ' .. action, 'INFO') end
778
+ if action == 'apply' then H.get_active_source(buf_cache).apply_hunks(buf_id, hunks) end
779
+ if action == 'reset' then H.reset_hunks(buf_id, hunks) end
780
+ if action == 'yank' then H.yank_hunks_ref(buf_cache.ref_text, hunks, opts.register) end
781
+ end
782
+
783
+ --- Go to hunk range in current buffer
784
+ ---
785
+ ---@param direction string One of "first", "prev", "next", "last".
786
+ ---@param opts table|nil Options. A table with fields:
787
+ --- - <n_times> `(number)` - Number of times to advance. Default: |v:count1|.
788
+ --- - <line_start> `(number)` - Line number to start from for directions
789
+ --- "prev" and "next". Default: cursor line.
790
+ --- - <wrap> `(boolean)` - Whether to wrap around edges.
791
+ --- Default: `options.wrap` value of the config.
792
+ MiniDiff.goto_hunk = function(direction, opts)
793
+ local buf_id = vim.api.nvim_get_current_buf()
794
+ local buf_cache = H.cache[buf_id]
795
+ if buf_cache == nil then H.error(string.format('Buffer %d is not enabled.', buf_id)) end
796
+
797
+ if not vim.tbl_contains({ 'first', 'prev', 'next', 'last' }, direction) then
798
+ H.error('`direction` should be one of "first", "prev", "next", "last".')
799
+ end
800
+
801
+ local default_wrap = buf_cache.config.options.wrap_goto
802
+ local default_opts = { n_times = vim.v.count1, line_start = vim.fn.line('.'), wrap = default_wrap }
803
+ opts = vim.tbl_deep_extend('force', default_opts, opts or {})
804
+ if not (type(opts.n_times) == 'number' and opts.n_times >= 1) then
805
+ H.error('`opts.n_times` should be positive number.')
806
+ end
807
+ if type(opts.line_start) ~= 'number' then H.error('`opts.line_start` should be number.') end
808
+ if type(opts.wrap) ~= 'boolean' then H.error('`opts.wrap` should be boolean.') end
809
+
810
+ -- Prepare ranges to iterate.
811
+ local ranges = H.get_contiguous_hunk_ranges(buf_cache.hunks)
812
+ if #ranges == 0 then return H.notify('No hunks to go to', 'INFO') end
813
+
814
+ -- Iterate
815
+ local res_ind, did_wrap = H.iterate_hunk_ranges(ranges, direction, opts)
816
+ if res_ind == nil then return H.notify('No hunk ranges in direction ' .. vim.inspect(direction), 'INFO') end
817
+ local res_line = ranges[res_ind].from
818
+ if did_wrap then H.notify('Wrapped around edge in direction ' .. vim.inspect(direction), 'INFO') end
819
+
820
+ -- Add to jumplist
821
+ vim.cmd([[normal! m']])
822
+
823
+ -- Jump
824
+ local _, col = vim.fn.getline(res_line):find('^%s*')
825
+ vim.api.nvim_win_set_cursor(0, { res_line, col })
826
+
827
+ -- Open just enough folds
828
+ vim.cmd('normal! zv')
829
+ end
830
+
831
+ --- Perform action over region
832
+ ---
833
+ --- Perform action over region defined by marks. Used in mappings.
834
+ ---
835
+ --- Example of a mapping to yank reference lines of hunk range under cursor
836
+ --- (assuming default `config.mappings.textobject`): >lua
837
+ ---
838
+ --- local rhs = function() return MiniDiff.operator('yank') .. 'gh' end
839
+ --- vim.keymap.set('n', 'ghy', rhs, { expr = true, remap = true })
840
+ --- <
841
+ ---@param mode string One of "apply", "reset", "yank", or the ones used in |g@|.
842
+ MiniDiff.operator = function(mode)
843
+ local buf_id = vim.api.nvim_get_current_buf()
844
+ if H.is_disabled(buf_id) then return '' end
845
+
846
+ if mode == 'apply' or mode == 'reset' or mode == 'yank' then
847
+ H.operator_cache = { action = mode, win_view = vim.fn.winsaveview(), register = vim.v.register }
848
+ vim.o.operatorfunc = 'v:lua.MiniDiff.operator'
849
+ return 'g@'
850
+ end
851
+ local cache = H.operator_cache
852
+
853
+ -- NOTE: Using `[` / `]` marks also works in Visual mode as because it is
854
+ -- executed as part of `g@`, which treats visual selection as a result of
855
+ -- Operator-pending mode mechanics (for which visual selection is allowed to
856
+ -- define motion/textobject). The downside is that it sets 'operatorfunc',
857
+ -- but the upside is that it is "dot-repeatable" (for relative selection).
858
+ local opts = { line_start = vim.fn.line("'["), line_end = vim.fn.line("']"), register = cache.register }
859
+ if opts.line_end < opts.line_start then return H.notify('Not a proper textobject', 'INFO') end
860
+ MiniDiff.do_hunks(buf_id, cache.action, opts)
861
+
862
+ -- Restore window view for "apply" (as buffer text should not have changed)
863
+ if cache.action == 'apply' and cache.win_view ~= nil then
864
+ vim.fn.winrestview(cache.win_view)
865
+ -- NOTE: Restore only once because during dot-repeat it is not up to date
866
+ cache.win_view = nil
867
+ end
868
+ return ''
869
+ end
870
+
871
+ --- Select hunk range textobject
872
+ ---
873
+ --- Selects all contiguous lines adjacent to cursor line which are in any (not
874
+ --- necessarily same) hunk (if cursor line itself is in hunk).
875
+ --- Used in default mappings.
876
+ MiniDiff.textobject = function()
877
+ local buf_id = vim.api.nvim_get_current_buf()
878
+ local buf_cache = H.cache[buf_id]
879
+ if buf_cache == nil or H.is_disabled(buf_id) then H.error('Current buffer is not enabled.') end
880
+
881
+ -- Get hunk range under cursor
882
+ local cur_line = vim.fn.line('.')
883
+ local regions, cur_region = H.get_contiguous_hunk_ranges(buf_cache.hunks), nil
884
+ for _, r in ipairs(regions) do
885
+ if r.from <= cur_line and cur_line <= r.to then cur_region = r end
886
+ end
887
+ if cur_region == nil then return H.notify('No hunk range under cursor', 'INFO') end
888
+
889
+ -- Select target region
890
+ local is_visual = vim.tbl_contains({ 'v', 'V', '\22' }, vim.fn.mode())
891
+ if is_visual then vim.cmd('normal! \27') end
892
+ vim.cmd(string.format('normal! %dGV%dG', cur_region.from, cur_region.to))
893
+ end
894
+
895
+ --- Indicate source attach fail
896
+ ---
897
+ --- Try to attach next source; if there is none - call |MiniDiff.disable()|.
898
+ ---
899
+ ---@param buf_id integer Buffer identifier for which attach has failed.
900
+ MiniDiff.fail_attach = function(buf_id)
901
+ buf_id = H.validate_buf_id(buf_id)
902
+
903
+ -- Do nothing if there was no attempt to enable
904
+ local buf_cache = H.cache[buf_id]
905
+ if buf_cache == nil then return end
906
+
907
+ -- If no next source, disable buffer without calling any of `detach`
908
+ if buf_cache.source_id >= #buf_cache.source then
909
+ H.cache[buf_id].source_id = math.huge
910
+ return MiniDiff.disable(buf_id)
911
+ end
912
+
913
+ -- Try attaching next source
914
+ buf_cache.source_id = buf_cache.source_id + 1
915
+ local attach_output = H.get_active_source(H.cache[buf_id]).attach(buf_id)
916
+ if attach_output == false then MiniDiff.fail_attach(buf_id) end
917
+ end
918
+
919
+ -- Helper data ================================================================
920
+ -- Module default config
921
+ H.default_config = MiniDiff.config
922
+
923
+ H.default_source = { MiniDiff.gen_source.git() }
924
+
925
+ -- Timers
926
+ H.timer_diff_update = vim.loop.new_timer()
927
+
928
+ -- Namespaces per highlighter name
929
+ H.ns_id = {
930
+ viz = vim.api.nvim_create_namespace('MiniDiffViz'),
931
+ overlay = vim.api.nvim_create_namespace('MiniDiffOverlay'),
932
+ }
933
+
934
+ -- Cache of buffers waiting for debounced diff update
935
+ H.bufs_to_update = {}
936
+
937
+ -- Cache per enabled buffer
938
+ H.cache = {}
939
+
940
+ -- Cache per buffer for attached `git` source
941
+ H.git_cache = {}
942
+
943
+ -- Cache for operator
944
+ H.operator_cache = {}
945
+
946
+ -- Common extmark data for supported styles
947
+ --stylua: ignore
948
+ H.style_extmark_data = {
949
+ sign = { hl_group_prefix = 'MiniDiffSign', field = 'sign_hl_group' },
950
+ number = { hl_group_prefix = 'MiniDiffSign', field = 'number_hl_group' },
951
+ }
952
+
953
+ -- Suffix for overlay virtual lines to be highlighted as full line
954
+ H.overlay_suffix = string.rep(' ', vim.o.columns)
955
+
956
+ -- Flag for whether to invalidate extmarks
957
+ H.extmark_invalidate = vim.fn.has('nvim-0.10') == 1 and true or nil
958
+
959
+ -- Flag for whether to handle virtual lines overflow
960
+ H.extmark_virt_lines_overflow = vim.fn.has('nvim-0.11') == 1 and 'scroll' or nil
961
+
962
+ -- Permanent `vim.diff()` options
963
+ H.vimdiff_opts = { result_type = 'indices', ctxlen = 0, interhunkctxlen = 0 }
964
+
965
+ -- Options for `vim.diff()` during word diff. Use `interhunkctxlen = 4` to
966
+ -- reduce noisiness (chosen as slightly less than average English word length)
967
+ --stylua: ignore
968
+ H.worddiff_opts = {
969
+ algorithm = 'minimal', result_type = 'indices',
970
+ ctxlen = 0, interhunkctxlen = 4,
971
+ indent_heuristic = false, linematch = 0
972
+ }
973
+
974
+ -- BOM bytes prepended to buffer text if 'bomb' is enabled. See `:h bom-bytes`.
975
+ --stylua: ignore
976
+ H.bom_bytes = {
977
+ ['utf-8'] = string.char(0xef, 0xbb, 0xbf),
978
+ ['utf-16be'] = string.char(0xfe, 0xff),
979
+ ['utf-16'] = string.char(0xfe, 0xff),
980
+ ['utf-16le'] = string.char(0xff, 0xfe),
981
+ -- In 'fileencoding', 'utf-32' is transformed into 'ucs-4'
982
+ ['utf-32be'] = string.char(0x00, 0x00, 0xfe, 0xff),
983
+ ['ucs-4be'] = string.char(0x00, 0x00, 0xfe, 0xff),
984
+ ['utf-32'] = string.char(0x00, 0x00, 0xfe, 0xff),
985
+ ['ucs-4'] = string.char(0x00, 0x00, 0xfe, 0xff),
986
+ ['utf-32le'] = string.char(0xff, 0xfe, 0x00, 0x00),
987
+ ['ucs-4le'] = string.char(0xff, 0xfe, 0x00, 0x00),
988
+ }
989
+
990
+ -- Helper functionality =======================================================
991
+ -- Settings -------------------------------------------------------------------
992
+ H.setup_config = function(config)
993
+ H.check_type('config', config, 'table', true)
994
+ config = vim.tbl_deep_extend('force', vim.deepcopy(H.default_config), config or {})
995
+
996
+ H.check_type('view', config.view, 'table')
997
+ H.check_type('view.style', config.view.style, 'string')
998
+ H.check_type('view.signs', config.view.signs, 'table')
999
+ H.check_type('view.signs.add', config.view.signs.add, 'string')
1000
+ H.check_type('view.signs.change', config.view.signs.change, 'string')
1001
+ H.check_type('view.signs.delete', config.view.signs.delete, 'string')
1002
+ H.check_type('view.priority', config.view.priority, 'number')
1003
+
1004
+ H.check_type('source', config.source, 'table', true)
1005
+
1006
+ H.check_type('delay', config.delay, 'table')
1007
+ H.check_type('delay.text_change', config.delay.text_change, 'number')
1008
+
1009
+ H.check_type('mappings', config.mappings, 'table')
1010
+ H.check_type('mappings.apply', config.mappings.apply, 'string')
1011
+ H.check_type('mappings.reset', config.mappings.reset, 'string')
1012
+ H.check_type('mappings.textobject', config.mappings.textobject, 'string')
1013
+ H.check_type('mappings.goto_first', config.mappings.goto_first, 'string')
1014
+ H.check_type('mappings.goto_prev', config.mappings.goto_prev, 'string')
1015
+ H.check_type('mappings.goto_next', config.mappings.goto_next, 'string')
1016
+ H.check_type('mappings.goto_last', config.mappings.goto_last, 'string')
1017
+
1018
+ H.check_type('options', config.options, 'table')
1019
+ H.check_type('options.algorithm', config.options.algorithm, 'string')
1020
+ H.check_type('options.indent_heuristic', config.options.indent_heuristic, 'boolean')
1021
+ H.check_type('options.linematch', config.options.linematch, 'number')
1022
+ H.check_type('options.wrap_goto', config.options.wrap_goto, 'boolean')
1023
+
1024
+ return config
1025
+ end
1026
+
1027
+ H.apply_config = function(config)
1028
+ MiniDiff.config = config
1029
+
1030
+ -- Make mappings
1031
+ local mappings = config.mappings
1032
+
1033
+ local rhs_apply = function() return MiniDiff.operator('apply') end
1034
+ H.map({ 'n', 'x' }, mappings.apply, rhs_apply, { expr = true, desc = 'Apply hunks' })
1035
+ local rhs_reset = function() return MiniDiff.operator('reset') end
1036
+ H.map({ 'n', 'x' }, mappings.reset, rhs_reset, { expr = true, desc = 'Reset hunks' })
1037
+
1038
+ local is_tobj_conflict = mappings.textobject == mappings.apply or mappings.textobject == mappings.reset
1039
+ local modes = is_tobj_conflict and { 'o' } or { 'x', 'o' }
1040
+ H.map(modes, mappings.textobject, '<Cmd>lua MiniDiff.textobject()<CR>', { desc = 'Hunk range textobject' })
1041
+
1042
+ --stylua: ignore start
1043
+ H.map({ 'n', 'x' }, mappings.goto_first, "<Cmd>lua MiniDiff.goto_hunk('first')<CR>", { desc = 'First hunk' })
1044
+ H.map('o', mappings.goto_first, "V<Cmd>lua MiniDiff.goto_hunk('first')<CR>", { desc = 'First hunk' })
1045
+ H.map({ 'n', 'x' }, mappings.goto_prev, "<Cmd>lua MiniDiff.goto_hunk('prev')<CR>", { desc = 'Previous hunk' })
1046
+ H.map('o', mappings.goto_prev, "V<Cmd>lua MiniDiff.goto_hunk('prev')<CR>", { desc = 'Previous hunk' })
1047
+ H.map({ 'n', 'x' }, mappings.goto_next, "<Cmd>lua MiniDiff.goto_hunk('next')<CR>", { desc = 'Next hunk' })
1048
+ H.map('o', mappings.goto_next, "V<Cmd>lua MiniDiff.goto_hunk('next')<CR>", { desc = 'Next hunk' })
1049
+ H.map({ 'n', 'x' }, mappings.goto_last, "<Cmd>lua MiniDiff.goto_hunk('last')<CR>", { desc = 'Last hunk' })
1050
+ H.map('o', mappings.goto_last, "V<Cmd>lua MiniDiff.goto_hunk('last')<CR>", { desc = 'Last hunk' })
1051
+ --stylua: ignore end
1052
+
1053
+ -- Register decoration provider which actually makes visualization
1054
+ local ns_id_viz, ns_id_overlay = H.ns_id.viz, H.ns_id.overlay
1055
+ H.set_decoration_provider(ns_id_viz, ns_id_overlay)
1056
+ end
1057
+
1058
+ H.create_autocommands = function()
1059
+ local gr = vim.api.nvim_create_augroup('MiniDiff', {})
1060
+
1061
+ local au = function(event, pattern, callback, desc)
1062
+ vim.api.nvim_create_autocmd(event, { group = gr, pattern = pattern, callback = callback, desc = desc })
1063
+ end
1064
+
1065
+ -- NOTE: Try auto enabling buffer on every `BufEnter` to not have `:edit`
1066
+ -- disabling buffer, as it calls `on_detach()` from buffer watcher
1067
+ au('BufEnter', '*', H.auto_enable, 'Enable diff')
1068
+ au('VimResized', '*', H.on_resize, 'Track Neovim resizing')
1069
+ au('ColorScheme', '*', H.create_default_hl, 'Ensure colors')
1070
+ end
1071
+
1072
+ --stylua: ignore
1073
+ H.create_default_hl = function()
1074
+ local hi = function(name, opts)
1075
+ opts.default = true
1076
+ vim.api.nvim_set_hl(0, name, opts)
1077
+ end
1078
+
1079
+ local has_core_diff_hl = vim.fn.has('nvim-0.10') == 1
1080
+ hi('MiniDiffSignAdd', { link = has_core_diff_hl and 'Added' or 'diffAdded' })
1081
+ hi('MiniDiffSignChange', { link = has_core_diff_hl and 'Changed' or 'diffChanged' })
1082
+ hi('MiniDiffSignDelete', { link = has_core_diff_hl and 'Removed' or 'diffRemoved' })
1083
+ hi('MiniDiffOverAdd', { link = 'DiffAdd' })
1084
+ hi('MiniDiffOverChange', { link = 'DiffText' })
1085
+ hi('MiniDiffOverChangeBuf', { link = 'MiniDiffOverChange'})
1086
+ hi('MiniDiffOverContext', { link = 'DiffChange' })
1087
+ hi('MiniDiffOverContextBuf', {})
1088
+ hi('MiniDiffOverDelete', { link = 'DiffDelete' })
1089
+ end
1090
+
1091
+ H.is_disabled = function(buf_id)
1092
+ local buf_disable = H.get_buf_var(buf_id, 'minidiff_disable')
1093
+ return vim.g.minidiff_disable == true or buf_disable == true
1094
+ end
1095
+
1096
+ H.get_config = function(config, buf_id)
1097
+ local buf_config = H.get_buf_var(buf_id, 'minidiff_config') or {}
1098
+ return vim.tbl_deep_extend('force', MiniDiff.config, buf_config, config or {})
1099
+ end
1100
+
1101
+ H.get_buf_var = function(buf_id, name)
1102
+ if not vim.api.nvim_buf_is_valid(buf_id) then return nil end
1103
+ return vim.b[buf_id or 0][name]
1104
+ end
1105
+
1106
+ -- Autocommands ---------------------------------------------------------------
1107
+ H.auto_enable = vim.schedule_wrap(function(data)
1108
+ if H.is_buf_enabled(data.buf) or H.is_disabled(data.buf) then return end
1109
+ local buf = data.buf
1110
+ if not (vim.api.nvim_buf_is_loaded(buf) and vim.bo[buf].buftype == '' and vim.bo[buf].buflisted) then return end
1111
+ if not H.is_buf_text(buf) then return end
1112
+ MiniDiff.enable(buf)
1113
+ end)
1114
+
1115
+ H.on_resize = function()
1116
+ H.overlay_suffix = string.rep(' ', vim.o.columns)
1117
+ for buf_id, _ in pairs(H.cache) do
1118
+ if vim.api.nvim_buf_is_valid(buf_id) then
1119
+ H.clear_all_diff(buf_id)
1120
+ H.schedule_diff_update(buf_id, 0)
1121
+ end
1122
+ end
1123
+ end
1124
+
1125
+ -- Validators -----------------------------------------------------------------
1126
+ H.validate_buf_id = function(x)
1127
+ if x == nil or x == 0 then return vim.api.nvim_get_current_buf() end
1128
+ if not (type(x) == 'number' and vim.api.nvim_buf_is_valid(x)) then
1129
+ H.error('`buf_id` should be `nil` or valid buffer id.')
1130
+ end
1131
+ return x
1132
+ end
1133
+
1134
+ H.validate_target_lines = function(buf_id, line_start, line_end)
1135
+ local n_lines = vim.api.nvim_buf_line_count(buf_id)
1136
+
1137
+ if type(line_start) ~= 'number' then H.error('`line_start` should be number.') end
1138
+ if type(line_end) ~= 'number' then H.error('`line_end` should be number.') end
1139
+
1140
+ -- Allow negative lines to count from last line
1141
+ line_start = line_start < 0 and (n_lines + line_start + 1) or line_start
1142
+ line_end = line_end < 0 and (n_lines + line_end + 1) or line_end
1143
+
1144
+ -- Clamp to fit the allowed range
1145
+ line_start = math.min(math.max(line_start, 1), n_lines)
1146
+ line_end = math.min(math.max(line_end, 1), n_lines)
1147
+ if not (line_start <= line_end) then H.error('`line_start` should be less than or equal to `line_end`.') end
1148
+
1149
+ return line_start, line_end
1150
+ end
1151
+
1152
+ H.validate_callable = function(x, name)
1153
+ if vim.is_callable(x) then return x end
1154
+ H.error('`' .. name .. '` should be callable.')
1155
+ end
1156
+
1157
+ -- Enabling -------------------------------------------------------------------
1158
+ H.is_buf_enabled = function(buf_id) return H.cache[buf_id] ~= nil end
1159
+
1160
+ H.update_buf_cache = function(buf_id)
1161
+ local new_cache = H.cache[buf_id] or {}
1162
+
1163
+ local buf_config = H.get_config({}, buf_id)
1164
+ new_cache.config = buf_config
1165
+ new_cache.extmark_opts = H.convert_view_to_extmark_opts(buf_config.view)
1166
+ new_cache.source = H.normalize_source(buf_config.source or H.default_source)
1167
+ new_cache.source_id = new_cache.source_id or 1
1168
+
1169
+ new_cache.hunks = new_cache.hunks or {}
1170
+ new_cache.summary = new_cache.summary or {}
1171
+ new_cache.viz_lines = new_cache.viz_lines or {}
1172
+
1173
+ new_cache.overlay = false
1174
+ new_cache.overlay_lines = new_cache.overlay_lines or {}
1175
+
1176
+ H.cache[buf_id] = new_cache
1177
+ end
1178
+
1179
+ H.setup_buf_autocommands = function(buf_id)
1180
+ local augroup = vim.api.nvim_create_augroup('MiniDiffBuffer' .. buf_id, { clear = true })
1181
+ H.cache[buf_id].augroup = augroup
1182
+
1183
+ local buf_update = vim.schedule_wrap(function() H.update_buf_cache(buf_id) end)
1184
+ local bufwinenter_opts = { group = augroup, buffer = buf_id, callback = buf_update, desc = 'Update buffer cache' }
1185
+ vim.api.nvim_create_autocmd('BufWinEnter', bufwinenter_opts)
1186
+
1187
+ local reset_if_enabled = vim.schedule_wrap(function(data)
1188
+ if not H.is_buf_enabled(data.buf) then return end
1189
+ MiniDiff.disable(data.buf)
1190
+ MiniDiff.enable(data.buf)
1191
+ end)
1192
+ local bufrename_opts = { group = augroup, buffer = buf_id, callback = reset_if_enabled, desc = 'Reset on rename' }
1193
+ -- NOTE: `BufFilePost` does not look like a proper event, but it (yet) works
1194
+ vim.api.nvim_create_autocmd('BufFilePost', bufrename_opts)
1195
+
1196
+ local buf_disable = function() MiniDiff.disable(buf_id) end
1197
+ local bufdelete_opts = { group = augroup, buffer = buf_id, callback = buf_disable, desc = 'Disable on delete' }
1198
+ vim.api.nvim_create_autocmd('BufDelete', bufdelete_opts)
1199
+ end
1200
+
1201
+ H.normalize_source = function(source)
1202
+ -- Normalize to an array of sources
1203
+ if type(source) ~= 'table' then H.error('`source` should be table.') end
1204
+ if source[1] == nil then source = { source } end
1205
+
1206
+ local res = {}
1207
+ for i, s in ipairs(source) do
1208
+ local cur_s = { attach = s.attach }
1209
+ cur_s.name = s.name or 'unknown'
1210
+ cur_s.detach = s.detach or function(_) end
1211
+ cur_s.apply_hunks = s.apply_hunks or function(_) H.error('Current source does not support applying hunks.') end
1212
+
1213
+ if type(cur_s.name) ~= 'string' then H.error('`source.name` should be string.') end
1214
+ H.validate_callable(cur_s.attach, 'source.attach')
1215
+ H.validate_callable(cur_s.detach, 'source.detach')
1216
+ H.validate_callable(cur_s.apply_hunks, 'source.apply_hunks')
1217
+
1218
+ res[i] = cur_s
1219
+ end
1220
+
1221
+ return res
1222
+ end
1223
+
1224
+ H.get_active_source = function(buf_cache) return buf_cache.source[buf_cache.source_id] or {} end
1225
+
1226
+ H.convert_view_to_extmark_opts = function(view)
1227
+ local extmark_data = H.style_extmark_data[view.style]
1228
+ if extmark_data == nil then H.error('Style ' .. vim.inspect(view.style) .. ' is not supported.') end
1229
+
1230
+ local signs = view.style == 'sign' and view.signs or {}
1231
+ local field, hl_group_prefix = extmark_data.field, extmark_data.hl_group_prefix
1232
+ --stylua: ignore
1233
+ return {
1234
+ add = { [field] = hl_group_prefix .. 'Add', sign_text = signs.add, priority = view.priority, invalidate = H.extmark_invalidate },
1235
+ change = { [field] = hl_group_prefix .. 'Change', sign_text = signs.change, priority = view.priority, invalidate = H.extmark_invalidate },
1236
+ delete = { [field] = hl_group_prefix .. 'Delete', sign_text = signs.delete, priority = view.priority, invalidate = H.extmark_invalidate },
1237
+ }
1238
+ end
1239
+
1240
+ -- Processing -----------------------------------------------------------------
1241
+ H.set_decoration_provider = function(ns_id_viz, ns_id_overlay)
1242
+ local on_win = function(_, _, buf_id, top, bottom)
1243
+ local buf_cache = H.cache[buf_id]
1244
+ if buf_cache == nil then return false end
1245
+
1246
+ local viz_lines, overlay_lines = buf_cache.viz_lines, buf_cache.overlay_lines
1247
+ if buf_cache.needs_clear then
1248
+ H.clear_all_diff(buf_id)
1249
+ buf_cache.needs_clear, buf_cache.dummy_extmark = false, nil
1250
+ -- Ensure that sign column is visible even if hunks are outside of window
1251
+ -- view (matters with `signcolumn=auto`)
1252
+ if buf_cache.config.view.style == 'sign' and not vim.tbl_isempty(viz_lines) then
1253
+ local dummy_opts = { sign_text = ' ', priority = 0, right_gravity = false }
1254
+ dummy_opts.sign_hl_group, dummy_opts.cursorline_hl_group = 'SignColumn', 'CursorLineSign'
1255
+ buf_cache.dummy_extmark = vim.api.nvim_buf_set_extmark(buf_id, ns_id_viz, 0, 0, dummy_opts)
1256
+ end
1257
+ end
1258
+
1259
+ local has_viz_extmarks = false
1260
+ for i = top + 1, bottom + 1 do
1261
+ if viz_lines[i] ~= nil then
1262
+ H.set_extmark(buf_id, ns_id_viz, i - 1, 0, viz_lines[i])
1263
+ viz_lines[i] = nil
1264
+ has_viz_extmarks = true
1265
+ end
1266
+ if overlay_lines[i] ~= nil then
1267
+ -- Allow several overlays at one line (like for "delete" and "change")
1268
+ for j = 1, #overlay_lines[i] do
1269
+ H.draw_overlay_line(buf_id, ns_id_overlay, i - 1, overlay_lines[i][j])
1270
+ end
1271
+ overlay_lines[i] = nil
1272
+ end
1273
+ end
1274
+
1275
+ -- Make sure to clear dummy extmark when it is not needed (otherwise it
1276
+ -- affects signcolumn for cases like `yes:2` and `auto:2`)
1277
+ if buf_cache.dummy_extmark ~= nil and has_viz_extmarks then
1278
+ vim.api.nvim_buf_del_extmark(buf_id, ns_id_viz, buf_cache.dummy_extmark)
1279
+ buf_cache.dummy_extmark = nil
1280
+ end
1281
+ end
1282
+ vim.api.nvim_set_decoration_provider(ns_id_viz, { on_win = on_win })
1283
+ end
1284
+
1285
+ H.schedule_diff_update = vim.schedule_wrap(function(buf_id, delay_ms)
1286
+ H.bufs_to_update[buf_id] = true
1287
+ H.timer_diff_update:stop()
1288
+ H.timer_diff_update:start(delay_ms, 0, H.process_scheduled_buffers)
1289
+ end)
1290
+
1291
+ H.process_scheduled_buffers = vim.schedule_wrap(function()
1292
+ for buf_id, _ in pairs(H.bufs_to_update) do
1293
+ H.update_buf_diff(buf_id)
1294
+ end
1295
+ H.bufs_to_update = {}
1296
+ end)
1297
+
1298
+ H.update_buf_diff = vim.schedule_wrap(function(buf_id)
1299
+ -- Make early returns
1300
+ local buf_cache = H.cache[buf_id]
1301
+ if buf_cache == nil then return end
1302
+ if not vim.api.nvim_buf_is_valid(buf_id) then
1303
+ H.cache[buf_id] = nil
1304
+ return
1305
+ end
1306
+ if type(buf_cache.ref_text) ~= 'string' or H.is_disabled(buf_id) then
1307
+ local summary = { source_name = H.get_active_source(buf_cache).name }
1308
+ buf_cache.hunks, buf_cache.viz_lines, buf_cache.overlay_lines, buf_cache.summary = {}, {}, {}, summary
1309
+ vim.b[buf_id].minidiff_summary, vim.b[buf_id].minidiff_summary_string = summary, ''
1310
+ return
1311
+ end
1312
+
1313
+ -- Compute diff
1314
+ local options = buf_cache.config.options
1315
+ H.vimdiff_opts.algorithm = options.algorithm
1316
+ H.vimdiff_opts.indent_heuristic = options.indent_heuristic
1317
+ H.vimdiff_opts.linematch = options.linematch
1318
+
1319
+ local buf_text, buf_lines = H.get_buftext(buf_id)
1320
+ local diff = vim.diff(buf_cache.ref_text, buf_text, H.vimdiff_opts)
1321
+
1322
+ -- Recompute hunks with summary and draw information
1323
+ H.update_hunk_data(diff, buf_cache, buf_lines)
1324
+
1325
+ -- Set buffer-local variables with summary for easier external usage
1326
+ local summary = buf_cache.summary
1327
+ vim.b[buf_id].minidiff_summary = summary
1328
+
1329
+ local summary_string = {}
1330
+ if summary.n_ranges > 0 then table.insert(summary_string, '#' .. summary.n_ranges) end
1331
+ if summary.add > 0 then table.insert(summary_string, '+' .. summary.add) end
1332
+ if summary.change > 0 then table.insert(summary_string, '~' .. summary.change) end
1333
+ if summary.delete > 0 then table.insert(summary_string, '-' .. summary.delete) end
1334
+ vim.b[buf_id].minidiff_summary_string = table.concat(summary_string, ' ')
1335
+
1336
+ -- Request highlighting clear to be done in decoration provider
1337
+ buf_cache.needs_clear = true
1338
+
1339
+ -- Trigger event for users to possibly hook into. Ensure target buffer is
1340
+ -- current (for proper `buf` in event data)
1341
+ vim.api.nvim_buf_call(buf_id, function() vim.api.nvim_exec_autocmds('User', { pattern = 'MiniDiffUpdated' }) end)
1342
+
1343
+ -- Force redraw. NOTE: Using 'redraw' not always works (`<Cmd>update<CR>`
1344
+ -- from keymap with "save" source will not redraw) while 'redraw!' flickers.
1345
+ H.redraw_buffer(buf_id)
1346
+ end)
1347
+
1348
+ H.update_hunk_data = function(diff, buf_cache, buf_lines)
1349
+ local do_overlay = buf_cache.overlay
1350
+ local ref_lines = do_overlay and vim.split(buf_cache.ref_text, '\n') or nil
1351
+
1352
+ local extmark_opts, priority = buf_cache.extmark_opts, buf_cache.config.view.priority
1353
+ local hunks, viz_lines, overlay_lines = {}, {}, {}
1354
+ local n_add, n_change, n_delete = 0, 0, 0
1355
+ local n_ranges, last_range_to = 0, -math.huge
1356
+ for i, d in ipairs(diff) do
1357
+ -- Hunk
1358
+ local n_ref, n_buf = d[2], d[4]
1359
+ local hunk_type = n_ref == 0 and 'add' or (n_buf == 0 and 'delete' or 'change')
1360
+ local hunk = { type = hunk_type, ref_start = d[1], ref_count = n_ref, buf_start = d[3], buf_count = n_buf }
1361
+ hunks[i] = hunk
1362
+
1363
+ -- Hunk summary
1364
+ local hunk_n_change = math.min(n_ref, n_buf)
1365
+ n_add = n_add + n_buf - hunk_n_change
1366
+ n_change = n_change + hunk_n_change
1367
+ n_delete = n_delete + n_ref - hunk_n_change
1368
+
1369
+ -- Number of contiguous ranges.
1370
+ -- NOTE: this relies on `vim.diff()` output being sorted by `buf_start`.
1371
+ local range_from = math.max(d[3], 1)
1372
+ local range_to = range_from + math.max(n_buf, 1) - 1
1373
+ n_ranges = n_ranges + ((range_from <= last_range_to + 1) and 0 or 1)
1374
+ last_range_to = math.max(last_range_to, range_to)
1375
+
1376
+ -- Register lines for draw. At least one line should visualize hunk.
1377
+ local viz_ext_opts = extmark_opts[hunk_type]
1378
+ for l_num = range_from, range_to do
1379
+ -- Prefer showing "change" hunk over other types
1380
+ if viz_lines[l_num] == nil or hunk_type == 'change' then viz_lines[l_num] = viz_ext_opts end
1381
+ end
1382
+
1383
+ if do_overlay then
1384
+ if hunk_type == 'add' then H.append_overlay_add(overlay_lines, hunk, priority) end
1385
+ if hunk_type == 'change' then H.append_overlay_change(overlay_lines, hunk, ref_lines, buf_lines, priority) end
1386
+ if hunk_type == 'delete' then H.append_overlay_delete(overlay_lines, hunk, ref_lines, priority) end
1387
+ end
1388
+ end
1389
+
1390
+ buf_cache.hunks, buf_cache.viz_lines, buf_cache.overlay_lines = hunks, viz_lines, overlay_lines
1391
+ buf_cache.summary = { add = n_add, change = n_change, delete = n_delete, n_ranges = n_ranges }
1392
+ buf_cache.summary.source_name = H.get_active_source(buf_cache).name
1393
+ end
1394
+
1395
+ H.clear_all_diff = function(buf_id)
1396
+ H.clear_namespace(buf_id, H.ns_id.viz, 0, -1)
1397
+ H.clear_namespace(buf_id, H.ns_id.overlay, 0, -1)
1398
+ end
1399
+
1400
+ -- Overlay --------------------------------------------------------------------
1401
+ H.append_overlay = function(overlay_lines, l_num, data)
1402
+ local t = overlay_lines[l_num] or {}
1403
+ table.insert(t, data)
1404
+ overlay_lines[l_num] = t
1405
+ end
1406
+
1407
+ H.append_overlay_add = function(overlay_lines, hunk, priority)
1408
+ local data = { type = 'add', to = hunk.buf_start + hunk.buf_count - 1, priority = priority }
1409
+ H.append_overlay(overlay_lines, hunk.buf_start, data)
1410
+ end
1411
+
1412
+ H.append_overlay_change = function(overlay_lines, hunk, ref_lines, buf_lines, priority)
1413
+ -- For one-to-one change, show lines separately with word diff highlighted
1414
+ -- This is usually the case when `linematch` is on
1415
+ if hunk.buf_count == hunk.ref_count then
1416
+ for i = 0, hunk.ref_count - 1 do
1417
+ local ref_n, buf_n = hunk.ref_start + i, hunk.buf_start + i
1418
+ -- Defer actually computing word diff until in decoration provider as it
1419
+ -- will compute only for displayed lines
1420
+ local data =
1421
+ { type = 'change_worddiff', ref_line = ref_lines[ref_n], buf_line = buf_lines[buf_n], priority = priority }
1422
+ H.append_overlay(overlay_lines, buf_n, data)
1423
+ end
1424
+ return
1425
+ end
1426
+
1427
+ -- If not one-to-one change, show reference lines above first real one
1428
+ local changed_lines = {}
1429
+ for i = hunk.ref_start, hunk.ref_start + hunk.ref_count - 1 do
1430
+ local l = { { ref_lines[i] .. H.overlay_suffix, 'MiniDiffOverChange' } }
1431
+ table.insert(changed_lines, l)
1432
+ end
1433
+ local to = hunk.buf_start + hunk.buf_count - 1
1434
+ local data = { type = 'change', to = to, lines = changed_lines, show_above = true, priority = priority }
1435
+ H.append_overlay(overlay_lines, hunk.buf_start, data)
1436
+ end
1437
+
1438
+ H.append_overlay_delete = function(overlay_lines, hunk, ref_lines, priority)
1439
+ local deleted_lines = {}
1440
+ for i = hunk.ref_start, hunk.ref_start + hunk.ref_count - 1 do
1441
+ table.insert(deleted_lines, { { ref_lines[i], 'MiniDiffOverDelete' }, { H.overlay_suffix, 'MiniDiffOverDelete' } })
1442
+ end
1443
+ local l_num, show_above = math.max(hunk.buf_start, 1), hunk.buf_start == 0
1444
+ local data = { type = 'delete', lines = deleted_lines, show_above = show_above, priority = priority }
1445
+ H.append_overlay(overlay_lines, l_num, data)
1446
+ end
1447
+
1448
+ H.draw_overlay_line = function(buf_id, ns_id, row, data)
1449
+ -- "Change worddif" hunk: compute word diff and show it above and over text
1450
+ if data.type == 'change_worddiff' then return H.draw_overlay_line_worddiff(buf_id, ns_id, row, data) end
1451
+
1452
+ local opts = { priority = data.priority }
1453
+
1454
+ -- "Add"/"Change" hunks highlight whole lines in affected buffer range
1455
+ if data.type ~= 'delete' then
1456
+ opts.end_row, opts.end_col, opts.hl_eol = data.to, 0, true
1457
+ opts.hl_group = data.type == 'add' and 'MiniDiffOverAdd' or 'MiniDiffOverContextBuf'
1458
+ end
1459
+
1460
+ -- "Change"/"Delete" hunks show affected reference range as virtual lines
1461
+ opts.virt_lines, opts.virt_lines_above, opts.virt_lines_overflow =
1462
+ data.lines, data.show_above, H.extmark_virt_lines_overflow
1463
+ H.set_extmark(buf_id, ns_id, row, 0, opts)
1464
+ end
1465
+
1466
+ H.draw_overlay_line_worddiff = function(buf_id, ns_id, row, data)
1467
+ local ref_line, buf_line, priority = data.ref_line, data.buf_line, data.priority
1468
+ local ref_parts, buf_parts = H.compute_worddiff_changed_parts(ref_line, buf_line)
1469
+
1470
+ -- Show changes in reference as two-colored virtual line above
1471
+ local virt_line, index = {}, 1
1472
+ for i = 1, #ref_parts do
1473
+ local part = ref_parts[i]
1474
+ if index < part[1] then table.insert(virt_line, { ref_line:sub(index, part[1] - 1), 'MiniDiffOverContext' }) end
1475
+ table.insert(virt_line, { ref_line:sub(part[1], part[2]), 'MiniDiffOverChange' })
1476
+ index = part[2] + 1
1477
+ end
1478
+ if index <= ref_line:len() then table.insert(virt_line, { ref_line:sub(index), 'MiniDiffOverContext' }) end
1479
+ table.insert(virt_line, { H.overlay_suffix, 'MiniDiffOverContext' })
1480
+
1481
+ --stylua: ignore
1482
+ local ref_opts = {
1483
+ virt_lines = { virt_line }, virt_lines_above = true, virt_lines_overflow = H.extmark_virt_lines_overflow,
1484
+ priority = priority,
1485
+ }
1486
+ H.set_extmark(buf_id, ns_id, row, 0, ref_opts)
1487
+
1488
+ -- Show changes in buffer line as one whole-line highlighting with separate
1489
+ -- highlighting for changed regions on top (as priority of context is lower)
1490
+ local off = vim.bo[buf_id].bomb and (H.bom_bytes[vim.bo[buf_id].fileencoding] or ''):len() or 0
1491
+ for i = 1, #buf_parts do
1492
+ local part = buf_parts[i]
1493
+ local buf_opts = { end_row = row, end_col = part[2] - off, hl_group = 'MiniDiffOverChangeBuf', priority = priority }
1494
+ H.set_extmark(buf_id, ns_id, row, part[1] - 1 - off, buf_opts)
1495
+ end
1496
+ local context_opts =
1497
+ { end_row = row + 1, end_col = 0, hl_group = 'MiniDiffOverContextBuf', hl_eol = true, priority = priority - 1 }
1498
+ H.set_extmark(buf_id, ns_id, row, 0, context_opts)
1499
+ end
1500
+
1501
+ H.compute_worddiff_changed_parts = function(ref_line, buf_line)
1502
+ local ref_sliced, ref_byte_starts, ref_byte_ends = H.slice_line(ref_line)
1503
+ local buf_sliced, buf_byte_starts, buf_byte_ends = H.slice_line(buf_line)
1504
+ local diff = vim.diff(ref_sliced, buf_sliced, H.worddiff_opts)
1505
+ local ref_ranges, buf_ranges = {}, {}
1506
+ for i = 1, #diff do
1507
+ local d = diff[i]
1508
+ if d[2] > 0 then table.insert(ref_ranges, { ref_byte_starts[d[1]], ref_byte_ends[d[1] + d[2] - 1] }) end
1509
+ if d[4] > 0 then table.insert(buf_ranges, { buf_byte_starts[d[3]], buf_byte_ends[d[3] + d[4] - 1] }) end
1510
+ end
1511
+
1512
+ return ref_ranges, buf_ranges
1513
+ end
1514
+
1515
+ H.str_utfindex = function(s, i) return vim.str_utfindex(s, 'utf-32', i) end
1516
+ if vim.fn.has('nvim-0.11') == 0 then H.str_utfindex = function(s, i) return (vim.str_utfindex(s, i)) end end
1517
+
1518
+ H.slice_line = function(line)
1519
+ -- Intertwine every proper character with '\n'
1520
+ local line_len = line:len()
1521
+ local sliced, starts, ends
1522
+ -- Make short route for a very common case of no multibyte characters
1523
+ if H.str_utfindex(line) == line_len then
1524
+ sliced, starts, ends = line:gsub('(.)', '%1\n'), {}, {}
1525
+ for i = 1, string.len(line) do
1526
+ starts[i], ends[i] = i, i
1527
+ end
1528
+ else
1529
+ sliced, starts, ends = {}, vim.str_utf_pos(line), {}
1530
+ for i = 1, #starts - 1 do
1531
+ table.insert(sliced, line:sub(starts[i], starts[i + 1] - 1))
1532
+ table.insert(ends, starts[i + 1] - 1)
1533
+ end
1534
+ table.insert(sliced, line:sub(starts[#starts], line_len))
1535
+ table.insert(ends, line_len)
1536
+ sliced = table.concat(sliced, '\n') .. '\n'
1537
+ end
1538
+
1539
+ return sliced, starts, ends
1540
+ end
1541
+
1542
+ -- Hunks ----------------------------------------------------------------------
1543
+ H.get_hunk_buf_range = function(hunk)
1544
+ -- "Change" and "Add" hunks have the range `[from, from + buf_count - 1]`
1545
+ if hunk.buf_count > 0 then return hunk.buf_start, hunk.buf_start + hunk.buf_count - 1 end
1546
+ -- "Delete" hunks have `buf_count = 0` yet its range is `[from, from]`
1547
+ -- `buf_start` can be 0 for 'delete' hunk, yet range should be real lines
1548
+ local from = math.max(hunk.buf_start, 1)
1549
+ return from, from
1550
+ end
1551
+
1552
+ H.get_hunks_in_range = function(hunks, from, to)
1553
+ local res = {}
1554
+ for _, h in ipairs(hunks) do
1555
+ local h_from, h_to = H.get_hunk_buf_range(h)
1556
+
1557
+ local left, right = math.max(from, h_from), math.min(to, h_to)
1558
+ if left <= right then
1559
+ -- If any `cur` hunk part is selected, its `ref` part is used fully
1560
+ local new_h = { ref_start = h.ref_start, ref_count = h.ref_count }
1561
+ new_h.type = h.ref_count == 0 and 'add' or (h.buf_count == 0 and 'delete' or 'change')
1562
+
1563
+ -- It should be possible to work with only hunk part inside target range
1564
+ -- Also Treat "delete" hunks differently as they represent range differently
1565
+ -- and can have `buf_start=0`
1566
+ new_h.buf_start = new_h.type == 'delete' and h.buf_start or left
1567
+ new_h.buf_count = new_h.type == 'delete' and 0 or (right - left + 1)
1568
+
1569
+ table.insert(res, new_h)
1570
+ end
1571
+ end
1572
+
1573
+ table.sort(res, H.hunk_order)
1574
+ return res
1575
+ end
1576
+
1577
+ H.reset_hunks = function(buf_id, hunks)
1578
+ local ref_lines = vim.split(H.cache[buf_id].ref_text, '\n')
1579
+ local offset = 0
1580
+ for _, h in ipairs(hunks) do
1581
+ -- Replace current hunk lines with corresponding reference
1582
+ local new_lines = vim.list_slice(ref_lines, h.ref_start, h.ref_start + h.ref_count - 1)
1583
+
1584
+ -- Compute buffer offset from parts: result of previous replaces, "delete"
1585
+ -- hunk offset which starts below the `buf_start` line, zero-indexing.
1586
+ local buf_offset = offset + (h.buf_count == 0 and 1 or 0) - 1
1587
+ local from, to = h.buf_start + buf_offset, h.buf_start + h.buf_count + buf_offset
1588
+ vim.api.nvim_buf_set_lines(buf_id, from, to, false, new_lines)
1589
+
1590
+ -- Keep track of current hunk lines shift as a result of previous replaces
1591
+ offset = offset + (h.ref_count - h.buf_count)
1592
+ end
1593
+ end
1594
+
1595
+ H.yank_hunks_ref = function(ref_text, hunks, register)
1596
+ -- Collect reference lines
1597
+ local ref_lines, out_lines = vim.split(ref_text, '\n'), {}
1598
+ for _, h in ipairs(hunks) do
1599
+ for i = h.ref_start, h.ref_start + h.ref_count - 1 do
1600
+ out_lines[i] = ref_lines[i]
1601
+ end
1602
+ end
1603
+
1604
+ -- Construct reference lines in order
1605
+ local hunk_ref_lines = {}
1606
+ for i = 1, #ref_lines do
1607
+ table.insert(hunk_ref_lines, out_lines[i])
1608
+ end
1609
+
1610
+ -- Put lines into target register
1611
+ vim.fn.setreg(register, hunk_ref_lines, 'l')
1612
+ end
1613
+
1614
+ H.get_contiguous_hunk_ranges = function(hunks)
1615
+ if #hunks == 0 then return {} end
1616
+ hunks = vim.deepcopy(hunks)
1617
+ table.sort(hunks, H.hunk_order)
1618
+
1619
+ local h1_from, h1_to = H.get_hunk_buf_range(hunks[1])
1620
+ local res = { { from = h1_from, to = h1_to } }
1621
+ for i = 2, #hunks do
1622
+ local h, cur_region = hunks[i], res[#res]
1623
+ local h_from, h_to = H.get_hunk_buf_range(h)
1624
+ if h_from <= cur_region.to + 1 then
1625
+ cur_region.to = math.max(cur_region.to, h_to)
1626
+ else
1627
+ table.insert(res, { from = h_from, to = h_to })
1628
+ end
1629
+ end
1630
+ return res
1631
+ end
1632
+
1633
+ H.iterate_hunk_ranges = function(ranges, direction, opts)
1634
+ local n = #ranges
1635
+
1636
+ -- Compute initial index
1637
+ local init_ind
1638
+ if direction == 'first' then init_ind = 0 end
1639
+ if direction == 'prev' then init_ind = H.get_range_id_prev(ranges, opts.line_start) end
1640
+ if direction == 'next' then init_ind = H.get_range_id_next(ranges, opts.line_start) end
1641
+ if direction == 'last' then init_ind = n + 1 end
1642
+
1643
+ local is_on_edge = (direction == 'prev' and init_ind == 1) or (direction == 'next' and init_ind == n)
1644
+ if not opts.wrap and is_on_edge then return nil end
1645
+
1646
+ -- Compute destination index
1647
+ local is_move_forward = direction == 'first' or direction == 'next'
1648
+ local res_ind = init_ind + opts.n_times * (is_move_forward and 1 or -1)
1649
+ local did_wrap = opts.wrap and (res_ind < 1 or n < res_ind)
1650
+ res_ind = opts.wrap and ((res_ind - 1) % n + 1) or math.min(math.max(res_ind, 1), n)
1651
+
1652
+ return res_ind, did_wrap
1653
+ end
1654
+
1655
+ H.get_range_id_next = function(ranges, line_start)
1656
+ for i = #ranges, 1, -1 do
1657
+ if ranges[i].from <= line_start then return i end
1658
+ end
1659
+ return 0
1660
+ end
1661
+
1662
+ H.get_range_id_prev = function(ranges, line_start)
1663
+ for i = 1, #ranges do
1664
+ if line_start <= ranges[i].to then return i end
1665
+ end
1666
+ return #ranges + 1
1667
+ end
1668
+
1669
+ H.hunk_order = function(a, b)
1670
+ -- Ensure buffer order and that "change" hunks are listed earlier "delete"
1671
+ -- ones from the same line (important for `reset_hunks()`)
1672
+ return a.buf_start < b.buf_start or (a.buf_start == b.buf_start and a.type == 'change')
1673
+ end
1674
+
1675
+ -- Export ---------------------------------------------------------------------
1676
+ H.export_qf = function(opts)
1677
+ local buffers = opts.scope == 'current' and { vim.api.nvim_get_current_buf() } or vim.tbl_keys(H.cache)
1678
+ buffers = vim.tbl_filter(vim.api.nvim_buf_is_valid, buffers)
1679
+ table.sort(buffers)
1680
+
1681
+ local type_text = { add = 'Add', change = 'Change', delete = 'Delete' }
1682
+
1683
+ local res = {}
1684
+ for _, buf_id in ipairs(buffers) do
1685
+ local filename = vim.api.nvim_buf_get_name(buf_id)
1686
+ local buf_lines = vim.api.nvim_buf_get_lines(buf_id, 0, -1, false)
1687
+ for _, h in ipairs(H.cache[buf_id].hunks) do
1688
+ local text = type_text[h.type]
1689
+ local entry = { bufnr = buf_id, filename = filename, type = text:sub(1, 1), text = text }
1690
+ entry.lnum, entry.end_lnum = H.get_hunk_buf_range(h)
1691
+ -- Make 'add' and 'change' hunks represent actual buffer regions
1692
+ entry.col, entry.end_col = 1, h.type == 'delete' and 1 or buf_lines[entry.end_lnum]:len() + 1
1693
+ table.insert(res, entry)
1694
+ end
1695
+ end
1696
+ return res
1697
+ end
1698
+
1699
+ -- Git ------------------------------------------------------------------------
1700
+ H.git_start_watching_index = function(buf_id, path)
1701
+ -- NOTE: Watching single 'index' file is not enough as staging by Git is done
1702
+ -- via "create fresh 'index.lock' file, apply modifications, change file name
1703
+ -- to 'index'". Hence watch the whole '.git' (first level) and react only if
1704
+ -- change was in 'index' file.
1705
+ local stdout = vim.loop.new_pipe()
1706
+ local args = { 'rev-parse', '--path-format=absolute', '--git-dir' }
1707
+ local spawn_opts = { args = args, cwd = vim.fn.fnamemodify(path, ':h'), stdio = { nil, stdout, nil } }
1708
+
1709
+ -- If path is not in Git, disable buffer but make sure that it will not try
1710
+ -- to re-attach until buffer is properly disabled
1711
+ local on_not_in_git = vim.schedule_wrap(function()
1712
+ if not vim.api.nvim_buf_is_valid(buf_id) then
1713
+ H.cache[buf_id] = nil
1714
+ return
1715
+ end
1716
+ MiniDiff.fail_attach(buf_id)
1717
+ end)
1718
+
1719
+ local process, stdout_feed = nil, {}
1720
+ local on_exit = function(exit_code)
1721
+ process:close()
1722
+
1723
+ -- Watch index only if there was no error retrieving path to it
1724
+ if exit_code ~= 0 or stdout_feed[1] == nil then return on_not_in_git() end
1725
+
1726
+ -- Set up index watching
1727
+ local git_dir_path = table.concat(stdout_feed, ''):gsub('\n+$', '')
1728
+ H.git_setup_index_watch(buf_id, git_dir_path)
1729
+
1730
+ -- Set reference text immediately
1731
+ H.git_set_ref_text(buf_id)
1732
+ end
1733
+
1734
+ process = vim.loop.spawn('git', spawn_opts, on_exit)
1735
+ H.git_read_stream(stdout, stdout_feed)
1736
+ end
1737
+
1738
+ H.git_setup_index_watch = function(buf_id, git_dir_path)
1739
+ local buf_fs_event, timer = vim.loop.new_fs_event(), vim.loop.new_timer()
1740
+ local buf_git_set_ref_text = function() H.git_set_ref_text(buf_id) end
1741
+
1742
+ local watch_index = function(_, filename, _)
1743
+ if filename ~= 'index' then return end
1744
+ -- Debounce to not overload during incremental staging (like in script)
1745
+ timer:stop()
1746
+ timer:start(50, 0, buf_git_set_ref_text)
1747
+ end
1748
+ buf_fs_event:start(git_dir_path, { recursive = false }, watch_index)
1749
+
1750
+ H.git_invalidate_cache(H.git_cache[buf_id])
1751
+ H.git_cache[buf_id] = { fs_event = buf_fs_event, timer = timer }
1752
+ end
1753
+
1754
+ H.git_set_ref_text = vim.schedule_wrap(function(buf_id)
1755
+ if not vim.api.nvim_buf_is_valid(buf_id) then return end
1756
+ local buf_set_ref_text = vim.schedule_wrap(function(text) pcall(MiniDiff.set_ref_text, buf_id, text) end)
1757
+
1758
+ -- NOTE: Do not cache buffer's name to react to its possible rename
1759
+ local path = H.get_buf_realpath(buf_id)
1760
+ if path == '' then return buf_set_ref_text({}) end
1761
+ local cwd, basename = vim.fn.fnamemodify(path, ':h'), vim.fn.fnamemodify(path, ':t')
1762
+
1763
+ -- Set
1764
+ local stdout = vim.loop.new_pipe()
1765
+ local spawn_opts = { args = { 'show', ':0:./' .. basename }, cwd = cwd, stdio = { nil, stdout, nil } }
1766
+
1767
+ local process, stdout_feed = nil, {}
1768
+ local on_exit = function(exit_code)
1769
+ process:close()
1770
+
1771
+ -- Unset reference text in case of any error. This results into not showing
1772
+ -- hunks at all. Possible reasons to do so:
1773
+ -- - 'Not in index' files (new, ignored, etc.).
1774
+ -- - 'Neither in index nor on disk' files (after checking out commit which
1775
+ -- does not yet have file created).
1776
+ -- - 'Relative can not be used outside working tree' (when opening file
1777
+ -- inside '.git' directory).
1778
+ if exit_code ~= 0 or stdout_feed[1] == nil then return buf_set_ref_text({}) end
1779
+
1780
+ -- Set reference text accounting for possible 'crlf' end of line in index
1781
+ local text = table.concat(stdout_feed, ''):gsub('\r\n', '\n')
1782
+ buf_set_ref_text(text)
1783
+ end
1784
+
1785
+ process = vim.loop.spawn('git', spawn_opts, on_exit)
1786
+ H.git_read_stream(stdout, stdout_feed)
1787
+ end)
1788
+
1789
+ H.git_get_path_data = function(path)
1790
+ -- Get path data needed for proper patch header
1791
+ local cwd, basename = vim.fn.fnamemodify(path, ':h'), vim.fn.fnamemodify(path, ':t')
1792
+ local stdout = vim.loop.new_pipe()
1793
+ local args = { 'ls-files', '-z', '--full-name', '--format=%(objectmode) %(eolinfo:index) %(path)', '--', basename }
1794
+ local spawn_opts = { args = args, cwd = cwd, stdio = { nil, stdout, nil } }
1795
+
1796
+ local process, stdout_feed, res, did_exit = nil, {}, { cwd = cwd }, false
1797
+ local on_exit = function(exit_code)
1798
+ process:close()
1799
+
1800
+ did_exit = true
1801
+ if exit_code ~= 0 then return end
1802
+ -- Parse data about path
1803
+ local out = table.concat(stdout_feed, ''):gsub('(%z\n)+$', '')
1804
+ res.mode_bits, res.eol, res.rel_path = string.match(out, '^(%d+) (%S+) (.*)$')
1805
+ end
1806
+
1807
+ process = vim.loop.spawn('git', spawn_opts, on_exit)
1808
+ H.git_read_stream(stdout, stdout_feed)
1809
+ vim.wait(1000, function() return did_exit end, 1)
1810
+ return res
1811
+ end
1812
+
1813
+ H.git_format_patch = function(buf_id, hunks, path_data)
1814
+ local _, buf_lines = H.get_buftext(buf_id)
1815
+ local ref_lines = vim.split(H.cache[buf_id].ref_text, '\n')
1816
+
1817
+ local res = {
1818
+ string.format('diff --git a/%s b/%s', path_data.rel_path, path_data.rel_path),
1819
+ 'index 000000..000000 ' .. path_data.mode_bits,
1820
+ '--- a/' .. path_data.rel_path,
1821
+ '+++ b/' .. path_data.rel_path,
1822
+ }
1823
+
1824
+ -- Take into account changing target ref region as a result of previous hunks
1825
+ local offset = 0
1826
+ local cr_eol = path_data.eol == 'crlf' and '\r' or ''
1827
+ for _, h in ipairs(hunks) do
1828
+ -- "Add" hunks have reference line above target
1829
+ local start = h.ref_start + (h.ref_count == 0 and 1 or 0)
1830
+
1831
+ table.insert(res, string.format('@@ -%d,%d +%d,%d @@', start, h.ref_count, start + offset, h.buf_count))
1832
+ for i = h.ref_start, h.ref_start + h.ref_count - 1 do
1833
+ table.insert(res, '-' .. ref_lines[i] .. cr_eol)
1834
+ end
1835
+ for i = h.buf_start, h.buf_start + h.buf_count - 1 do
1836
+ table.insert(res, '+' .. buf_lines[i] .. cr_eol)
1837
+ end
1838
+ offset = offset + (h.buf_count - h.ref_count)
1839
+ end
1840
+
1841
+ return res
1842
+ end
1843
+
1844
+ H.git_apply_patch = function(path_data, patch)
1845
+ local stdin = vim.loop.new_pipe()
1846
+ local args = { 'apply', '--whitespace=nowarn', '--cached', '--unidiff-zero', '-' }
1847
+ local spawn_opts = { args = args, cwd = path_data.cwd, stdio = { stdin, nil, nil } }
1848
+ local process
1849
+ process = vim.loop.spawn('git', spawn_opts, function() process:close() end)
1850
+
1851
+ -- Write patch, notify that writing is finished (shutdown), and close
1852
+ for _, l in ipairs(patch) do
1853
+ stdin:write(l)
1854
+ stdin:write('\n')
1855
+ end
1856
+ stdin:shutdown(function() stdin:close() end)
1857
+ end
1858
+
1859
+ H.git_read_stream = function(stream, feed)
1860
+ local callback = function(err, data)
1861
+ if data ~= nil then return table.insert(feed, data) end
1862
+ if err then feed[1] = nil end
1863
+ stream:close()
1864
+ end
1865
+ stream:read_start(callback)
1866
+ end
1867
+
1868
+ H.git_invalidate_cache = function(cache)
1869
+ if cache == nil then return end
1870
+ pcall(vim.loop.fs_event_stop, cache.fs_event)
1871
+ pcall(vim.loop.timer_stop, cache.timer)
1872
+ end
1873
+
1874
+ -- Utilities ------------------------------------------------------------------
1875
+ H.error = function(msg) error('(mini.diff) ' .. msg, 0) end
1876
+
1877
+ H.check_type = function(name, val, ref, allow_nil)
1878
+ if type(val) == ref or (ref == 'callable' and vim.is_callable(val)) or (allow_nil and val == nil) then return end
1879
+ H.error(string.format('`%s` should be %s, not %s', name, ref, type(val)))
1880
+ end
1881
+
1882
+ H.notify = function(msg, level_name) vim.notify('(mini.diff) ' .. msg, vim.log.levels[level_name]) end
1883
+
1884
+ H.buf_ensure_loaded = function(buf_id)
1885
+ if type(buf_id) ~= 'number' or vim.api.nvim_buf_is_loaded(buf_id) then return end
1886
+ local cache_eventignore = vim.o.eventignore
1887
+ vim.o.eventignore = 'BufEnter,BufWinEnter'
1888
+ pcall(vim.fn.bufload, buf_id)
1889
+ vim.o.eventignore = cache_eventignore
1890
+ end
1891
+
1892
+ H.map = function(mode, lhs, rhs, opts)
1893
+ if lhs == '' then return end
1894
+ opts = vim.tbl_deep_extend('force', { silent = true }, opts or {})
1895
+ vim.keymap.set(mode, lhs, rhs, opts)
1896
+ end
1897
+
1898
+ H.set_extmark = function(...) pcall(vim.api.nvim_buf_set_extmark, ...) end
1899
+
1900
+ H.get_extmarks = function(...)
1901
+ local ok, res = pcall(vim.api.nvim_buf_get_extmarks, ...)
1902
+ if not ok then return {} end
1903
+ return res
1904
+ end
1905
+
1906
+ H.clear_namespace = function(...) pcall(vim.api.nvim_buf_clear_namespace, ...) end
1907
+
1908
+ H.is_buf_text = function(buf_id)
1909
+ local n = vim.api.nvim_buf_call(buf_id, function() return vim.fn.byte2line(1024) end)
1910
+ local lines = vim.api.nvim_buf_get_lines(buf_id, 0, n, false)
1911
+ return table.concat(lines, ''):find('\0') == nil
1912
+ end
1913
+
1914
+ H.get_buftext = function(buf_id)
1915
+ local lines = vim.api.nvim_buf_get_lines(buf_id, 0, -1, false)
1916
+ -- - NOTE: Appending '\n' makes more intuitive diffs at end-of-file
1917
+ local text = table.concat(lines, '\n') .. '\n'
1918
+ if not vim.bo[buf_id].bomb then return text, lines end
1919
+ local bytes = H.bom_bytes[vim.bo[buf_id].fileencoding] or ''
1920
+ lines[1] = bytes .. lines[1]
1921
+ return bytes .. text, lines
1922
+ end
1923
+
1924
+ -- Try getting buffer's full real path (after resolving symlinks)
1925
+ H.get_buf_realpath = function(buf_id) return vim.loop.fs_realpath(vim.api.nvim_buf_get_name(buf_id)) or '' end
1926
+
1927
+ -- nvim__redraw replaced nvim__buf_redraw_range during the 0.10 release cycle
1928
+ H.redraw_buffer = function(buf_id)
1929
+ vim.api.nvim__buf_redraw_range(buf_id, 0, -1)
1930
+
1931
+ -- Redraw statusline to have possible statusline component up to date
1932
+ vim.cmd('redrawstatus')
1933
+ end
1934
+ if vim.api.nvim__redraw ~= nil then
1935
+ H.redraw_buffer = function(buf_id) vim.api.nvim__redraw({ buf = buf_id, valid = true, statusline = true }) end
1936
+ end
1937
+
1938
+ return MiniDiff