forked from nvim-mini/mini.nvim
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdiff.lua
More file actions
1981 lines (1727 loc) · 78.9 KB
/
diff.lua
File metadata and controls
1981 lines (1727 loc) · 78.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
--- *mini.diff* Work with diff hunks
--- *MiniDiff*
---
--- MIT License Copyright (c) 2024 Evgeni Chasnovski
---
--- ==============================================================================
---
--- Features:
---
--- - Visualize difference between buffer text and its configurable reference
--- interactively (updates as you type). This is done per line showing whether
--- it is inside added, changed, or deleted part of difference (called hunk).
--- Visualization can be with customizable colored signs or line numbers.
---
--- - Special toggleable overlay view with more hunk details inside text area.
--- See |MiniDiff.toggle_overlay()|.
---
--- - Completely configurable per buffer source(s) of reference text used to keep
--- it up to date and define interactions with it. Can be array of sources which
--- are attempted to attach in order. See |MiniDiff-source-specification|.
--- By default uses Git source. See |MiniDiff.gen_source.git()|.
---
--- - Configurable mappings to manage diff hunks:
--- - Apply and reset hunks inside region (selected visually or with
--- a dot-repeatable operator).
--- - "Hunk range under cursor" textobject to be used as operator target.
--- - Navigate to first/previous/next/last hunk. See |MiniDiff.goto_hunk()|.
---
--- - Supports three diff sources:
--- - |MiniDiff.gen_source.git()|: Use git repository as the source to diff
--- the current buffer.
--- - |MiniDiff.gen_source.mercurial()|: Use mercurial repository as the
--- source to diff the current buffer.
--- - |MiniDiff.gen_source.save()|: Diff with respect to the file on disk.
---
--- What it doesn't do:
---
--- - Provide functionality to work directly with Git outside of visualizing
--- and staging (applying) hunks with (default) Git source. In particular,
--- unstaging hunks is not supported. See |MiniDiff.gen_source.git()|.
---
--- Sources with more details:
--- - |MiniDiff-overview|
--- - |MiniDiff-source-specification|
--- - |MiniDiff-hunk-specification|
--- - |MiniDiff-diff-summary|
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.diff').setup({})` (replace
--- `{}` with your `config` table). It will create global Lua table `MiniDiff`
--- which you can use for scripting or manually (with `:lua MiniDiff.*`).
---
--- See |MiniDiff.config| for `config` structure and default values.
---
--- You can override runtime config settings locally to buffer inside
--- `vim.b.minidiff_config` which should have same structure as
--- `MiniDiff.config`. See |mini.nvim-buffer-local-config| for more details.
---
--- # Comparisons ~
---
--- - 'lewis6991/gitsigns.nvim':
--- - Main inspiration for this module, so there are many similarities.
--- - Can display only Git hunks, while this module has extensible design.
--- - Provides more functionality to work with Git outside of hunks.
--- This module does not (by design).
---
--- # Highlight groups ~
---
--- * `MiniDiffSignAdd` - "add" hunk lines visualization.
--- * `MiniDiffSignChange` - "change" hunk lines visualization.
--- * `MiniDiffSignDelete` - "delete" hunk lines visualization.
--- * `MiniDiffOverAdd` - added buffer text shown in overlay.
--- * `MiniDiffOverChange` - changed reference text shown in overlay.
--- * `MiniDiffOverChangeBuf` - changed buffer text shown in overlay.
--- * `MiniDiffOverContext` - context of a change shown in reference overlay.
--- * `MiniDiffOverContextBuf` - context of a change shown in buffer overlay.
--- * `MiniDiffOverDelete` - deleted reference text shown in overlay.
---
--- To change any highlight group, modify it directly with |:highlight|.
---
--- # Disabling ~
---
--- To temporarily disable features without relying on |MiniDiff.disable()|,
--- set `vim.g.minidiff_disable` (globally) or `vim.b.minidiff_disable` (for
--- a buffer) to `true`. Considering high number of different scenarios and
--- customization intentions, writing exact rules for disabling module's
--- functionality is left to user.
--- See |mini.nvim-disabling-recipes| for common recipes.
--- # Diffs and hunks ~
---
--- The "diff" (short for "difference") is a result of computing how two text
--- strings differ from one another. This is done on per line basis, i.e. the
--- goal is to compute sequences of lines common to both files, interspersed
--- with groups of differing lines (called "hunks").
---
--- Although computing diff is a general concept (used on its own, in Git, etc.),
--- this module computes difference between current text in a buffer and some
--- reference text which is kept up to date specifically for that buffer.
--- For example, default reference text is computed as file content in Git index.
--- This can be customized in `config.source` (see |MiniDiff-source-specification|).
---
--- *MiniDiff-hunk-specification*
--- Hunk describes two sets (one from buffer text, one - from reference) of
--- consecutive lines which are different. In this module hunk is stored as
--- a table with the following fields:
---
--- - <buf_start> `(number)` - start of hunk buffer lines. First line is 1.
--- Can be 0 if first reference lines are deleted.
---
--- - <buf_count> `(number)` - number of consecutive buffer lines. Can be 0 in
--- case reference lines are deleted.
---
--- - <ref_start> `(number)` - start of hunk reference lines. First line is 1.
--- Can be 0 if lines are added before first reference line.
---
--- - <ref_count> `(number)` - number of consecutive reference lines. Can be 0 in
--- case buffer lines are added.
---
--- - <type> `(string)` - hunk type. Can be one of:
--- - "add" - lines are present in buffer but absent in reference.
--- - "change" - lines are present in both buffer and reference.
--- - "delete" - lines are absent in buffer but present in reference.
---
--- # Life cycle ~
---
--- - When entering proper (not already enabled, valid, showing text) buffer,
--- it is attempted to be enabled for diff processing.
--- - During enabling, attempt attaching the source. This should set up how
--- reference text is kept up to date.
--- - On every text change, diff computation is scheduled in debounced fashion
--- after customizable delay (200 ms by default).
--- - After the diff is computed, do the following:
--- - Update visualization based on configurable style: either by placing
--- colored text in sign column or coloring line numbers. Colors for both
--- styles are defined per hunk type in corresponding `MiniDiffSign*`
--- highlight group (see |MiniDiff|) and sign text for "sign" style can
--- be configured in `view.signs` of |MiniDiff.config|.
--- - Update overlay view (if it is enabled).
--- - Update `vim.b.minidiff_summary` and `vim.b.minidiff_summary_string`
--- buffer-local variables. These can be used, for example, in statusline.
--- *MiniDiff-update-event*
--- - Trigger `MiniDiffUpdated` `User` event. See |MiniDiff-diff-summary| for
--- example of how to use it.
---
--- Notes:
--- - Use |:edit| to reset (disable and re-enable) current buffer.
--- - To work with BOM bytes, set 'bomb' and have `ucs-bom` in 'fileencodings'.
---
--- # Overlay ~
---
--- Along with basic visualization, there is a special view called "overlay".
--- Although it is meant for temporary overview of diff details and can be
--- manually toggled via |MiniDiff.toggle_overlay()|, text can be changed with
--- overlay reacting accordingly.
---
--- It shows more diff details inside text area:
---
--- - Added buffer lines are highlighted with `MiniDiffOverAdd` highlight group.
---
--- - Deleted reference lines are shown as virtual lines and highlighted with
--- `MiniDiffOverDelete` highlight group.
---
--- - "Change" hunks with equal number of buffer/reference lines show "word diff".
--- This is usually the case when `options.linematch` is enabled (as by default).
--- Reference line is shown next to its buffer counterpart. Changed parts are
--- highlighted with `MiniDiffOverChange` and `MiniDiffOverChangeBuf` in reference
--- and buffer lines. The rest of lines have `MiniDiffOverContext`
--- and `MiniDiffOverContextBuf` highlighting.
---
--- Change with unequal number of buffer/reference lines is shown with reference
--- part as virtual lines highlighted with `MiniDiffOverChange` group.
--- Corresponding buffer lines are treated as context for the change and are
--- highlighted with `MiniDiffOverContextBuf` group.
---
--- Notes:
--- - Word diff has non-zero context width. This means if changed characters
--- are close enough, whole range between them is also colored. This usually
--- reduces visual noise.
--- - Virtual lines above line 1 (like deleted or changed lines) need manual
--- scroll to become visible (with |CTRL-Y|).
---
--- # Mappings ~
---
--- This module provides mappings for common actions with diffs, like:
--- - Apply and reset hunks.
--- - "Hunk range under cursor" textobject.
--- - Go to first/previous/next/last hunk range.
---
--- Examples:
--- - `vip` followed by `gh` / `gH` applies/resets hunks inside current paragraph.
--- Same can be achieved in operator form `ghip` / `gHip`, which has the
--- advantage of being dot-repeatable (see |single-repeat|).
--- - `gh_` / `gH_` applies/resets current line (even if it is not a full hunk).
--- - `ghgh` / `gHgh` applies/resets hunk range under cursor.
--- - `dgh` deletes hunk range under cursor.
--- - `[H` / `[h` / `]h` / `]H` navigate cursor to the first / previous / next / last
--- hunk range of the current buffer.
---
--- Mappings for some functionality are assumed to be done manually.
--- See |MiniDiff.operator()|.
---
--- # Buffer-local variables ~
--- *MiniDiff-diff-summary*
--- Each enabled buffer has the following buffer-local variables which can be
--- used in custom statusline to show an overview of hunks in current buffer:
---
--- - `vim.b.minidiff_summary` is a table with the following fields:
--- - `source_name` - name of the active source.
--- - `n_ranges` - number of hunk ranges (sequences of contiguous hunks).
--- - `add` - number of added lines.
--- - `change` - number of changed lines.
--- - `delete` - number of deleted lines.
---
--- - `vim.b.minidiff_summary_string` is a string representation of summary
--- with a fixed format. It is expected to be used as is. To achieve
--- different formatting, use `vim.b.minidiff_summary` to construct one.
--- The best way to do this is by overriding `vim.b.minidiff_summary_string`
--- in the callback for |MiniDiff-update-event| event: >lua
---
--- local format_summary = function(data)
--- local summary = vim.b[data.buf].minidiff_summary
--- local t = {}
--- if summary.add > 0 then table.insert(t, '+' .. summary.add) end
--- if summary.change > 0 then table.insert(t, '~' .. summary.change) end
--- if summary.delete > 0 then table.insert(t, '-' .. summary.delete) end
--- vim.b[data.buf].minidiff_summary_string = table.concat(t, ' ')
--- end
--- local au_opts = { pattern = 'MiniDiffUpdated', callback = format_summary }
--- vim.api.nvim_create_autocmd('User', au_opts)
--- <
---@tag MiniDiff-overview
---@alias __diff_buf_id number Target buffer identifier. Default: 0 for current buffer.
---@diagnostic disable:undefined-field
---@diagnostic disable:discard-returns
---@diagnostic disable:unused-local
---@diagnostic disable:cast-local-type
---@diagnostic disable:undefined-doc-name
---@diagnostic disable:luadoc-miss-type-name
-- Module definition ==========================================================
local MiniDiff = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniDiff.config|.
---
---@usage >lua
--- require('mini.diff').setup() -- use default config
--- -- OR
--- require('mini.diff').setup({}) -- replace {} with your config table
--- <
MiniDiff.setup = function(config)
-- Export module
_G.MiniDiff = MiniDiff
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
-- Define behavior
H.create_autocommands()
for _, buf_id in ipairs(vim.api.nvim_list_bufs()) do
H.auto_enable({ buf = buf_id })
end
-- Create default highlighting
H.create_default_hl()
end
--stylua: ignore
--- Module config
---
--- Default values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
---@text # View ~
---
--- `config.view` contains settings for how diff hunks are visualized.
--- Example of using custom signs: >lua
---
--- require('mini.diff').setup({
--- view = {
--- style = 'sign',
--- signs = { add = '+', change = '~', delete = '-' },
--- },
--- })
--- <
--- `view.style` is a string defining visualization style. Can be one of "sign"
--- (as a colored sign in a |sign-column|) or "number" (colored line number).
--- Default: "number" if |number| option is enabled, "sign" otherwise.
--- Note: with "sign" style it is usually better to have |signcolumn| always shown.
---
--- `view.signs` is a table with one or two character strings used as signs for
--- corresponding ("add", "change", "delete") hunks.
--- Default: all hunks use "▒" character resulting in a contiguous colored lines.
---
--- `view.priority` is a number with priority used for visualization and
--- overlay |extmarks|.
--- Default: 199 which is one less than `user` in |vim.hl.priorities| (on Neovim<0.11
--- see |vim.highlight.priorities|) to have higher priority than automated
--- extmarks but not as in user enabled ones.
---
--- *MiniDiff-source-specification*
--- # Source ~
---
--- `config.source` is a table with single source or array of them. Single source
--- defines how reference text is managed in a particular buffer. Sources in array
--- are attempted to attach in order; call |MiniDiff.disable()| if none attaches.
---
--- A single source table can have the following fields:
---
--- - <attach> `(function)` - callable which defines how and when reference text
--- is updated inside a particular buffer. It is used inside |MiniDiff.enable()|
--- with a buffer identifier as a single argument.
---
--- Should execute logic which results into calling |MiniDiff.set_ref_text()|
--- when reference text for buffer needs to be updated. Like inside callback
--- for an |autocommand| or file watcher (see |watch-file|).
---
--- For example, default Git source watches when ".git/index" file is changed
--- and computes reference text as the one from Git index for current file.
---
--- Can return `false` to indicate that attach has failed. If attach fail can
--- not be inferred immediately (for example, due to asynchronous execution),
--- should explicitly call |MiniDiff.fail_attch()| with appropriate arguments.
--- This is important to properly process array of sources.
---
--- No default value, should be always supplied.
---
--- - <name> `(string|nil)` - source name. String `"unknown"` is used if not supplied.
---
--- - <detach> `(function|nil)` - callable with cleanup action to be done when
--- buffer is disabled. It is called inside |MiniDiff.disable()| with a buffer
--- identifier as a single argument.
---
--- If not supplied, nothing is done during detaching.
---
--- - <apply_hunks> `(function|nil)` - callable which defines how hunks are applied.
--- It is called with buffer identifier as first argument and array of hunks
--- (see |MiniDiff-hunk-specification|) as second. It should eventually update
--- reference text: either by explicitly calling |MiniDiff.set_ref_text()| or
--- performing action triggering its call.
---
--- For example, default Git source computes patch based on the hunks and
--- applies it inside file's git repo.
---
--- If not supplied, applying hunks throws an error.
---
--- Default: a single |MiniDiff.gen_source.git()|.
---
--- # Delay ~
---
--- `config.delay` contains settings for delays in asynchronous processes.
---
--- `delay.text_change` is a number (in ms) defining how long to wait after latest
--- text change (in debounced fashion) before updating diff and visualization.
--- Default: 200.
---
--- # Mappings ~
---
--- `config.mappings` contains keys which are mapped during |MiniDiff.setup()|.
---
--- `mappings.apply` keys can be used to apply hunks inside visual/operator region.
--- What exactly "apply hunks" means depends on the source and its `apply_hunks()`.
--- For example, in default Git source it means stage hunks.
---
--- `mappings.reset` keys can be used to reset hunks inside visual/operator region.
--- Reset means replacing buffer text in region with corresponding reference text.
---
--- `mappings.textobject` keys define "hunk range under cursor" textobject
--- which can be used in Operator-pending mode as target for operator (like
--- |d|, |y|, apply/reset hunks, etc.). It is also set up in Visual mode if
--- keys do not conflict with `mappings.apply` and `mappings.reset`.
--- "Hunk range" is used in a sense that contiguous (back-to-back) hunks are
--- considered as parts of a same hunk range.
---
--- `mappings.goto_first` / `mappings.goto_prev` / `mappings.goto_next` /
--- `mappings.goto_last` keys can be used to navigate to first / previous / next /
--- last hunk range in the current buffer.
---
--- # Options ~
---
--- `config.options` contains various customization options.
---
--- `options.algorithm` is a string defining which diff algorithm to use.
--- Default: "histogram". See |vim.diff()| for possible values.
---
--- `options.indent_heuristic` is a boolean defining whether to use indent
--- heuristic for a (possibly) more naturally aligned hunks.
--- Default: `true`.
---
--- `options.linematch` is a number defining hunk size for which a second
--- stage diff is executed for a better aligned and more granular hunks.
--- Default: 60. See |vim.diff()| and 'diffopt' for more details.
---
--- `options.wrap_goto` is a boolean indicating whether to wrap around edges during
--- hunk navigation (with |MiniDiff.goto_hunk()| or `goto_*` mappings). Like if
--- cursor is after the last hunk, going "next" will put cursor on the first hunk.
--- Default: `false`.
MiniDiff.config = {
-- Options for how hunks are visualized
view = {
-- Visualization style. Possible values are 'sign' and 'number'.
-- Default: 'number' if line numbers are enabled, 'sign' otherwise.
style = vim.go.number and 'number' or 'sign',
-- Signs used for hunks with 'sign' view
signs = { add = '▒', change = '▒', delete = '▒' },
-- Priority of used visualization extmarks
priority = 199,
},
-- Source(s) for how reference text is computed/updated/etc
-- Uses content from Git index by default
source = nil,
-- Delays (in ms) defining asynchronous processes
delay = {
-- How much to wait before update following every text change
text_change = 200,
},
-- Module mappings. Use `''` (empty string) to disable one.
mappings = {
-- Apply hunks inside a visual/operator region
apply = 'gh',
-- Reset hunks inside a visual/operator region
reset = 'gH',
-- Hunk range textobject to be used inside operator
-- Works also in Visual mode if mapping differs from apply and reset
textobject = 'gh',
-- Go to hunk range in corresponding direction
goto_first = '[H',
goto_prev = '[h',
goto_next = ']h',
goto_last = ']H',
},
-- Various options
options = {
-- Diff algorithm. See `:h vim.diff()`.
algorithm = 'histogram',
-- Whether to use "indent heuristic". See `:h vim.diff()`.
indent_heuristic = true,
-- The amount of second-stage diff to align lines
linematch = 60,
-- Whether to wrap around edges during hunk navigation
wrap_goto = false,
},
}
--minidoc_afterlines_end
--- Enable diff processing in buffer
---
---@param buf_id __diff_buf_id
MiniDiff.enable = function(buf_id)
buf_id = H.validate_buf_id(buf_id)
-- Don't enable more than once
if H.is_buf_enabled(buf_id) or H.is_disabled(buf_id) then return end
-- Ensure buffer is loaded (to have up to date lines returned)
H.buf_ensure_loaded(buf_id)
-- Register enabled buffer with cached data for performance
H.update_buf_cache(buf_id)
-- Add buffer watchers
vim.api.nvim_buf_attach(buf_id, false, {
-- Called on every text change (`:h nvim_buf_lines_event`)
on_lines = function(_, _, _, from_line, _, to_line)
local buf_cache = H.cache[buf_id]
-- Properly detach if diffing is disabled
if buf_cache == nil then return true end
H.schedule_diff_update(buf_id, buf_cache.config.delay.text_change)
end,
-- Called when buffer content is changed outside of current session
on_reload = function() H.schedule_diff_update(buf_id, 0) end,
-- Called when buffer is unloaded from memory (`:h nvim_buf_detach_event`),
-- **including** `:edit` command
on_detach = function() MiniDiff.disable(buf_id) end,
})
-- Add buffer autocommands
H.setup_buf_autocommands(buf_id)
-- Try attaching source after all necessary watchers are set up. It is needed
-- to still have them set up if first source of many returned `false`.
local attach_output = H.get_active_source(H.cache[buf_id]).attach(buf_id)
if attach_output == false then MiniDiff.fail_attach(buf_id) end
end
--- Disable diff processing in buffer
---
---@param buf_id __diff_buf_id
MiniDiff.disable = function(buf_id)
buf_id = H.validate_buf_id(buf_id)
local buf_cache = H.cache[buf_id]
if buf_cache == nil then return end
H.cache[buf_id] = nil
pcall(vim.api.nvim_del_augroup_by_id, buf_cache.augroup)
vim.b[buf_id].minidiff_summary, vim.b[buf_id].minidiff_summary_string = nil, nil
H.clear_all_diff(buf_id)
pcall(H.get_active_source(buf_cache).detach, buf_id)
end
--- Toggle diff processing in buffer
---
--- Enable if disabled, disable if enabled.
---
---@param buf_id __diff_buf_id
MiniDiff.toggle = function(buf_id)
buf_id = H.validate_buf_id(buf_id)
if H.is_buf_enabled(buf_id) then return MiniDiff.disable(buf_id) end
return MiniDiff.enable(buf_id)
end
--- Toggle overlay view in buffer
---
---@param buf_id __diff_buf_id
MiniDiff.toggle_overlay = function(buf_id)
buf_id = H.validate_buf_id(buf_id)
local buf_cache = H.cache[buf_id]
if buf_cache == nil then H.error(string.format('Buffer %d is not enabled.', buf_id)) end
buf_cache.overlay = not buf_cache.overlay
H.clear_all_diff(buf_id)
H.schedule_diff_update(buf_id, 0)
end
--- Export hunks
---
--- Get and convert hunks from current/all buffers. Example of using it: >lua
---
--- -- Set quickfix list from all available hunks
--- vim.fn.setqflist(MiniDiff.export('qf'))
--- <
---@param format string Output format. Currently only `'qf'` value is supported.
---@param opts table|nil Options. Possible fields:
--- - <scope> `(string)` - scope defining from which buffers to use hunks.
--- One of "all" (all enabled buffers) or "current".
---
---@return table Result of export. Depends on the `format`:
--- - If "qf", an array compatible with |setqflist()| and |setloclist()|.
MiniDiff.export = function(format, opts)
opts = vim.tbl_deep_extend('force', { scope = 'all' }, opts or {})
if format == 'qf' then return H.export_qf(opts) end
H.error('`format` should be one of "qf".')
end
--- Get buffer data
---
---@param buf_id __diff_buf_id
---
---@return table|nil Table with buffer diff data or `nil` if buffer is not enabled.
--- Table has the following fields:
--- - <config> `(table)` - config used for this particular buffer.
--- - <hunks> `(table)` - array of hunks. See |MiniDiff-hunk-specification|.
--- - <overlay> `(boolean)` - whether an overlay view is shown.
--- - <ref_text> `(string|nil)` - current value of reference text. Lines are
--- separated with newline character (`'\n'`). Can be `nil` indicating that
--- reference text was not yet set (for example, if source did not yet react).
--- - <summary> `(table)` - overall diff summary. See |MiniDiff-diff-summary|.
MiniDiff.get_buf_data = function(buf_id)
buf_id = H.validate_buf_id(buf_id)
local buf_cache = H.cache[buf_id]
if buf_cache == nil then return nil end
return vim.deepcopy({
config = buf_cache.config,
hunks = buf_cache.hunks,
overlay = buf_cache.overlay,
ref_text = buf_cache.ref_text,
summary = buf_cache.summary,
})
end
--- Set reference text for the buffer
---
--- Note: this will call |MiniDiff.enable()| for target buffer if it is not
--- already enabled.
---
---@param buf_id __diff_buf_id
---@param text string|table New reference text. Either a string with `\n` used to
--- separate lines or array of lines. Use empty table to unset current
--- reference text (results into no hunks shown). Default: `{}`.
--- Note: newline character is appended at the end (if it is not there already)
--- for better diffs.
MiniDiff.set_ref_text = function(buf_id, text)
buf_id = H.validate_buf_id(buf_id)
if not (type(text) == 'table' or type(text) == 'string') then H.error('`text` should be either string or array.') end
if type(text) == 'table' then text = #text > 0 and table.concat(text, '\n') or nil end
-- Enable if not already enabled
if not H.is_buf_enabled(buf_id) then MiniDiff.enable(buf_id) end
if not H.is_buf_enabled(buf_id) then H.error('Can not set reference text for not enabled buffer.') end
-- Appending '\n' makes more intuitive diffs at end-of-file
if text ~= nil and string.sub(text, -1) ~= '\n' then text = text .. '\n' end
if text == nil then
H.clear_all_diff(buf_id)
vim.cmd('redraw')
end
-- Immediately update diff
H.cache[buf_id].ref_text = text
H.schedule_diff_update(buf_id, 0)
end
--- Generate builtin sources
---
--- This is a table with function elements. Call to actually get source.
--- Examples: >lua
---
--- local diff = require('mini.diff')
---
--- -- Single `save` source
--- diff.setup({ source = diff.gen_source.save() })
---
--- -- Multiple sources (attempted to attach in order)
--- diff.setup({ source = {
--- diff.gen_source.git(),
--- diff.gen_source.mercurial(),
--- diff.gen_source.save()
--- } })
--- <
MiniDiff.gen_source = {}
--- Git source
---
--- Default source. Uses file text from Git index as reference. This results in:
--- - "Add" hunks represent text present in current buffer, but not in index.
--- - "Change" hunks represent modified text already present in index.
--- - "Delete" hunks represent text deleted from index.
---
--- Applying hunks means staging, a.k.a adding to index.
--- Notes:
--- - Requires Git version at least 2.38.0.
--- - There is no capability for unstaging hunks. Use full Git client for that.
---
---@return table Source. See |MiniDiff-source-specification|.
MiniDiff.gen_source.git = function()
local attach = function(buf_id)
-- Try attaching to a buffer only once
if H.vcs_cache[buf_id] ~= nil then return false end
-- - Possibly resolve symlinks to get data from the original repo
local path = H.get_buf_realpath(buf_id)
if path == '' then return false end
H.vcs_cache[buf_id] = {}
local opts = {
command = 'git',
vcs_dir_cmd_args = { 'rev-parse', '--path-format=absolute', '--git-dir' },
index_name = 'index',
vcs_file_test_spawn_args_fn = function(local_path)
local basename = vim.fn.fnamemodify(local_path, ':t')
return { 'show', ':0:./' .. basename }
end,
}
H.vcs_start_watching_index(buf_id, path, opts)
end
local detach = function(buf_id)
local cache = H.vcs_cache[buf_id]
H.vcs_cache[buf_id] = nil
H.vcs_invalidate_cache(cache)
end
local apply_hunks = function(buf_id, hunks)
local path_data = H.git_get_path_data(H.get_buf_realpath(buf_id))
if path_data == nil or path_data.rel_path == nil then return end
local patch = H.git_format_patch(buf_id, hunks, path_data)
H.git_apply_patch(path_data, patch)
end
return { name = 'git', attach = attach, detach = detach, apply_hunks = apply_hunks }
end
--- Merurial source
---
--- Uses file text from mercurial's `dirstate` as reference. This results in:
--- - "Add" hunks represent text present in current buffer, but not in
--- mercurial repo.
--- - "Change" hunks represent modified text not in mercurial repo.
--- - "Delete" hunks represent text deleted from repo.
---
--- Notes:
--- - Requires Git version at least 6.9.4.
---
---@return table Source. See |MiniDiff-source-specification|.
MiniDiff.gen_source.mercurial = function()
local attach = function(buf_id)
-- Try attaching to a buffer only once
if H.vcs_cache[buf_id] ~= nil then return false end
-- - Possibly resolve symlinks to get data from the original repo
local path = H.get_buf_realpath(buf_id)
if path == '' then return false end
H.vcs_cache[buf_id] = {}
local opts = {
command = 'hg',
vcs_dir_cmd_args = { 'root', '--template', '{reporoot}/.hg' },
index_name = 'dirstate',
vcs_file_test_spawn_args_fn = function(local_path)
local basename = vim.fn.fnamemodify(local_path, ':t')
return { 'cat', basename }
end,
}
H.vcs_start_watching_index(buf_id, path, opts)
end
local detach = function(buf_id)
local cache = H.vcs_cache[buf_id]
H.vcs_cache[buf_id] = nil
H.vcs_invalidate_cache(cache)
end
return { name = 'mercurial', attach = attach, detach = detach }
end
--- "Do nothing" source
---
--- Allows buffers to be enabled while not setting any reference text.
--- Use this if the goal is to rely on manual |MiniDiff.set_ref_text()| calls.
---
---@return table Source. See |MiniDiff-source-specification|.
MiniDiff.gen_source.none = function()
return { name = 'none', attach = function() end }
end
--- Latest save source
---
--- Uses text at latest save as the reference. This results into diff showing
--- difference after the latest save.
---
---@return table Source. See |MiniDiff-source-specification|.
MiniDiff.gen_source.save = function()
local augroups = {}
local attach = function(buf_id)
local augroup = vim.api.nvim_create_augroup('MiniDiffSourceSaveBuffer' .. buf_id, { clear = true })
augroups[buf_id] = augroup
local set_ref = function()
if vim.bo[buf_id].modified then return end
MiniDiff.set_ref_text(buf_id, H.get_buftext(buf_id))
end
-- Autocommand are more efficient than file watcher as it doesn't read disk
local au_opts = { group = augroup, buffer = buf_id, callback = set_ref, desc = 'Set reference text after save' }
vim.api.nvim_create_autocmd({ 'BufWritePost', 'FileChangedShellPost' }, au_opts)
set_ref()
end
local detach = function(buf_id) pcall(vim.api.nvim_del_augroup_by_id, augroups[buf_id]) end
return { name = 'save', attach = attach, detach = detach }
end
--- Perform action on hunks in region
---
--- Compute hunks inside a target region (even for hunks only partially inside it)
--- and perform apply/reset/yank operation on them.
---
--- The "yank" action yanks all reference lines of target hunks into
--- a specified register (should be one of |registers|).
---
--- Notes:
--- - Whether hunk is inside a region is computed based on position of its
--- buffer lines.
--- - If "change" or "delete" is only partially inside a target region, all
--- reference lines are used in computed "intersection" hunk.
---
--- Used directly in `config.mappings.apply` and `config.mappings.reset`.
--- Usually there is no need to use this function manually.
--- See |MiniDiff.operator()| for how to set up a mapping for "yank".
---
---@param buf_id __diff_buf_id
---@param action string One of "apply", "reset", "yank".
---@param opts table|nil Options. Possible fields:
--- - <line_start> `(number)` - start line of the region. Default: 1.
--- - <line_end> `(number)` - start line of the region. Default: last buffer line.
--- - <register> `(string)` - register to yank reference lines into.
--- Default: |v:register|.
MiniDiff.do_hunks = function(buf_id, action, opts)
buf_id = H.validate_buf_id(buf_id)
local buf_cache = H.cache[buf_id]
if buf_cache == nil then H.error(string.format('Buffer %d is not enabled.', buf_id)) end
if type(buf_cache.ref_text) ~= 'string' then H.error(string.format('Buffer %d has no reference text.', buf_id)) end
if not (action == 'apply' or action == 'reset' or action == 'yank') then
H.error('`action` should be one of "apply", "reset", "yank".')
end
local default_opts = { line_start = 1, line_end = vim.api.nvim_buf_line_count(buf_id), register = vim.v.register }
opts = vim.tbl_deep_extend('force', default_opts, opts or {})
local line_start, line_end = H.validate_target_lines(buf_id, opts.line_start, opts.line_end)
if type(opts.register) ~= 'string' then H.error('`opts.register` should be string.') end
local hunks = H.get_hunks_in_range(buf_cache.hunks, line_start, line_end)
if #hunks == 0 then return H.notify('No hunks to ' .. action, 'INFO') end
if action == 'apply' then H.get_active_source(buf_cache).apply_hunks(buf_id, hunks) end
if action == 'reset' then H.reset_hunks(buf_id, hunks) end
if action == 'yank' then H.yank_hunks_ref(buf_cache.ref_text, hunks, opts.register) end
end
--- Go to hunk range in current buffer
---
---@param direction string One of "first", "prev", "next", "last".
---@param opts table|nil Options. A table with fields:
--- - <n_times> `(number)` - Number of times to advance. Default: |v:count1|.
--- - <line_start> `(number)` - Line number to start from for directions
--- "prev" and "next". Default: cursor line.
--- - <wrap> `(boolean)` - Whether to wrap around edges.
--- Default: `options.wrap` value of the config.
MiniDiff.goto_hunk = function(direction, opts)
local buf_id = vim.api.nvim_get_current_buf()
local buf_cache = H.cache[buf_id]
if buf_cache == nil then H.error(string.format('Buffer %d is not enabled.', buf_id)) end
if not vim.tbl_contains({ 'first', 'prev', 'next', 'last' }, direction) then
H.error('`direction` should be one of "first", "prev", "next", "last".')
end
local default_wrap = buf_cache.config.options.wrap_goto
local default_opts = { n_times = vim.v.count1, line_start = vim.fn.line('.'), wrap = default_wrap }
opts = vim.tbl_deep_extend('force', default_opts, opts or {})
if not (type(opts.n_times) == 'number' and opts.n_times >= 1) then
H.error('`opts.n_times` should be positive number.')
end
if type(opts.line_start) ~= 'number' then H.error('`opts.line_start` should be number.') end
if type(opts.wrap) ~= 'boolean' then H.error('`opts.wrap` should be boolean.') end
-- Prepare ranges to iterate.
local ranges = H.get_contiguous_hunk_ranges(buf_cache.hunks)
if #ranges == 0 then return H.notify('No hunks to go to', 'INFO') end
-- Iterate
local res_ind, did_wrap = H.iterate_hunk_ranges(ranges, direction, opts)
if res_ind == nil then return H.notify('No hunk ranges in direction ' .. vim.inspect(direction), 'INFO') end
local res_line = ranges[res_ind].from
if did_wrap then H.notify('Wrapped around edge in direction ' .. vim.inspect(direction), 'INFO') end
-- Add to jumplist
vim.cmd([[normal! m']])
-- Jump
local _, col = vim.fn.getline(res_line):find('^%s*')
vim.api.nvim_win_set_cursor(0, { res_line, col })
-- Open just enough folds
vim.cmd('normal! zv')
end
--- Perform action over region
---
--- Perform action over region defined by marks. Used in mappings.
---
--- Example of a mapping to yank reference lines of hunk range under cursor
--- (assuming default 'config.mappings.textobject'): >lua
---
--- local rhs = function() return MiniDiff.operator('yank') .. 'gh' end
--- vim.keymap.set('n', 'ghy', rhs, { expr = true, remap = true })
--- <
---@param mode string One of "apply", "reset", "yank", or the ones used in |g@|.
MiniDiff.operator = function(mode)
local buf_id = vim.api.nvim_get_current_buf()
if H.is_disabled(buf_id) then return '' end
if mode == 'apply' or mode == 'reset' or mode == 'yank' then
H.operator_cache = { action = mode, win_view = vim.fn.winsaveview(), register = vim.v.register }
vim.o.operatorfunc = 'v:lua.MiniDiff.operator'
return 'g@'
end
local cache = H.operator_cache
-- NOTE: Using `[` / `]` marks also works in Visual mode as because it is
-- executed as part of `g@`, which treats visual selection as a result of
-- Operator-pending mode mechanics (for which visual selection is allowed to
-- define motion/textobject). The downside is that it sets 'operatorfunc',
-- but the upside is that it is "dot-repeatable" (for relative selection).
local opts = { line_start = vim.fn.line("'["), line_end = vim.fn.line("']"), register = cache.register }
if opts.line_end < opts.line_start then return H.notify('Not a proper textobject', 'INFO') end
MiniDiff.do_hunks(buf_id, cache.action, opts)
-- Restore window view for "apply" (as buffer text should not have changed)
if cache.action == 'apply' and cache.win_view ~= nil then
vim.fn.winrestview(cache.win_view)
-- NOTE: Restore only once because during dot-repeat it is not up to date
cache.win_view = nil
end
return ''
end
--- Select hunk range textobject
---
--- Selects all contiguous lines adjacent to cursor line which are in any (not
--- necessarily same) hunk (if cursor line itself is in hunk).
--- Used in default mappings.
MiniDiff.textobject = function()
local buf_id = vim.api.nvim_get_current_buf()
local buf_cache = H.cache[buf_id]
if buf_cache == nil or H.is_disabled(buf_id) then H.error('Current buffer is not enabled.') end
-- Get hunk range under cursor
local cur_line = vim.fn.line('.')
local regions, cur_region = H.get_contiguous_hunk_ranges(buf_cache.hunks), nil
for _, r in ipairs(regions) do
if r.from <= cur_line and cur_line <= r.to then cur_region = r end
end
if cur_region == nil then return H.notify('No hunk range under cursor', 'INFO') end
-- Select target region
local is_visual = vim.tbl_contains({ 'v', 'V', '\22' }, vim.fn.mode())
if is_visual then vim.cmd('normal! \27') end
vim.cmd(string.format('normal! %dGV%dG', cur_region.from, cur_region.to))
end
--- Indicate source attach fail
---
--- Try to attach next source; if there is none - call |MiniDiff.disable()|.
---
---@param buf_id integer Buffer identifier for which attach has failed.
MiniDiff.fail_attach = function(buf_id)
buf_id = H.validate_buf_id(buf_id)
-- Do nothing if there was no attempt to enable
local buf_cache = H.cache[buf_id]
if buf_cache == nil then return end
-- If no next source, disable buffer without calling any of `detach`
if buf_cache.source_id >= #buf_cache.source then
H.cache[buf_id].source_id = math.huge
return MiniDiff.disable(buf_id)
end
-- Try attaching next source
buf_cache.source_id = buf_cache.source_id + 1
local attach_output = H.get_active_source(buf_cache).attach(buf_id)
if attach_output == false then MiniDiff.fail_attach(buf_id) end
end
-- Helper data ================================================================
-- Module default config
H.default_config = MiniDiff.config
H.default_source = { MiniDiff.gen_source.git() }
-- Timers
H.timer_diff_update = vim.loop.new_timer()
-- Namespaces per highlighter name
H.ns_id = {
viz = vim.api.nvim_create_namespace('MiniDiffViz'),
overlay = vim.api.nvim_create_namespace('MiniDiffOverlay'),
}
-- Cache of buffers waiting for debounced diff update
H.bufs_to_update = {}
-- Cache per enabled buffer
H.cache = {}
-- Cache per buffer for attached `git` source
H.vcs_cache = {}
-- Cache for operator
H.operator_cache = {}
-- Common extmark data for supported styles
--stylua: ignore
H.style_extmark_data = {
sign = { hl_group_prefix = 'MiniDiffSign', field = 'sign_hl_group' },
number = { hl_group_prefix = 'MiniDiffSign', field = 'number_hl_group' },
}
-- Suffix for overlay virtual lines to be highlighted as full line
H.overlay_suffix = string.rep(' ', vim.o.columns)
-- Flag for whether to invalidate extmarks
H.extmark_invalidate = vim.fn.has('nvim-0.10') == 1 and true or nil
-- Flag for whether to handle virtual lines overflow
H.extmark_virt_lines_overflow = vim.fn.has('nvim-0.11') == 1 and 'scroll' or nil