-
Notifications
You must be signed in to change notification settings - Fork 222
/
Copy pathoperators.lua
1278 lines (1089 loc) · 47.6 KB
/
operators.lua
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.operators* Text edit operators
--- *MiniOperators*
---
--- MIT License Copyright (c) 2023 Evgeni Chasnovski
---
--- ==============================================================================
---
--- Features:
--- - Operators:
--- - Evaluate text and replace with output.
--- - Exchange text regions.
--- - Multiply (duplicate) text.
--- - Replace text with register.
--- - Sort text.
---
--- - Automated configurable mappings to operate on textobject, line, selection.
--- Can be disabled in favor of more control with |MiniOperators.make_mappings()|.
---
--- - All operators support |[count]| and dot-repeat.
---
--- See |MiniOperators-overview| and |MiniOperators.config| for more details.
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.operators').setup({})` (replace
--- `{}` with your `config` table). It will create global Lua table `MiniOperators`
--- which you can use for scripting or manually (with `:lua MiniOperators.*`).
---
--- See |MiniOperators.config| for available config settings.
---
--- You can override runtime config settings (but not `config.mappings`) locally
--- to buffer inside `vim.b.minioperators_config` which should have same structure
--- as `MiniOperators.config`. See |mini.nvim-buffer-local-config| for more details.
---
--- # Comparisons ~
---
--- - 'gbprod/substitute.nvim':
--- - Has "replace" and "exchange" variants, but not others from this module.
--- - Has "replace/substitute" over range functionality, while this module
--- does not by design (it is similar to |:s| functionality while not
--- offering significantly lower mental complexity).
--- - "Replace" highlights pasted text, while in this module it doesn't.
--- - "Exchange" doesn't work across buffers, while in this module it does.
---
--- - 'svermeulen/vim-subversive':
--- - Main inspiration for "replace" functionality, so they are mostly similar
--- for this operator.
--- - Has "replace/substitute" over range functionality, while this module
--- does not by design.
---
--- - 'tommcdo/vim-exchange':
--- - Main inspiration for "exchange" functionality, so they are mostly
--- similar for this operator.
--- - Doesn't work across buffers, while this module does.
---
--- - 'christoomey/vim-sort-motion':
--- - Uses |:sort| for linewise sorting, while this module uses consistent
--- sorting algorithm (by default, see |MiniOperators.default_sort_func()|).
--- - Sorting algorithm can't be customized, while this module allows this
--- (see `sort.func` in |MiniOperators.config|).
--- - For charwise region uses only commas as separators, while this module
--- can also separate by semicolon or whitespace (by default,
--- see |MiniOperators.default_sort_func()|).
---
--- # Highlight groups ~
---
--- * `MiniOperatorsExchangeFrom` - first region to exchange.
---
--- To change any highlight group, modify it directly with |:highlight|.
---
--- # Disabling ~
---
--- To disable main functionality, set `vim.g.minioperators_disable` (globally) or
--- `vim.b.minioperators_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.
--- # General overview ~
---
--- Operator defines an action that will be performed on a textobject, motion,
--- or visual selection (similar to |d|, |c|, etc.). When makes sense, it can also
--- respect supplied register (like "replace" operator).
---
--- This module implements each operator in a separate dedicated function
--- (like |MiniOperators.replace()| for "replace" operator). Each such function
--- takes `mode` as argument and acts depending on it:
---
--- - If `mode` is `nil` (or not explicitly supplied), it sets |operatorfunc|
--- to this dedicated function and returns `g@` assuming being called from
--- expression mapping. See |:map-operator| and |:map-expression| for more details.
---
--- - If `mode` is "char", "line", or "block", it acts as `operatorfunc` and performs
--- action for region between |`[| and |`]| marks.
---
--- - If `mode` is "visual", it performs action for region between |`<| and |`>| marks.
---
--- For more details about specific operator, see help for its function:
---
--- - Evaluate: |MiniOperators.evaluate()|
--- - Exchange: |MiniOperators.exchange()|
--- - Multiply: |MiniOperators.multiply()|
--- - Replace: |MiniOperators.replace()|
--- - Sort: |MiniOperators.sort()|
---
--- *MiniOperators-mappings*
--- ## Mappings ~
---
--- All operators are automatically mapped during |MiniOperators.setup()| execution.
--- Mappings keys are deduced from `prefix` field of corresponding `config` entry.
---
--- For each operator the following mappings are created:
---
--- - In Normal mode to operate on textobject. Uses `prefix` directly.
--- - In Normal mode to operate on line. Appends to `prefix` the last character.
--- This aligns with |operator-doubled| and established patterns for operators
--- with more than two characters, like |guu|, |gUU|, etc.
--- - In Visual mode to operate on visual selection. Uses `prefix` directly.
---
--- Example of default mappings for "replace":
--- - `gr` in Normal mode for operating on textobject.
--- Example of usage: `griw` replaces "inner word" with default register.
--- - `grr` in Normal mode for operating on line.
--- Example of usage: `grr` replaces current line.
--- - `gr` in Visual mode for operating on visual selection.
--- Example of usage: `viw` selects "inner word" and `gr` replaces it.
---
--- There are two suggested ways to customize mappings:
---
--- - Change `prefix` in |MiniOperators.setup()| call. For example, doing >
---
--- require('mini.operators').setup({ replace = { prefix = 'cr' } })
--- <
--- will make mappings for `cr`/`crr`/`cr` instead of `gr`/`grr`/`gr`.
---
--- - Disable automated mapping creation by supplying empty string as prefix and
--- use |MiniOperators.make_mappings()| directly. For example: >
---
--- -- Disable automated creation of "replace"
--- local operators = require('mini.operators')
--- operators.setup({ replace = { prefix = '' } })
---
--- -- Make custom mappings
--- operators.make_mappings(
--- 'replace',
--- { textobject = 'cr', line = 'crr', selection = 'cr' }
--- )
---@tag MiniOperators-overview
---@alias __operators_mode string|nil One of `nil`, `'char'`, `'line'`, `''block`, `'visual'`.
---@alias __operators_content table Table with the following fields:
--- - <lines> `(table)` - array with content lines.
--- - <submode> `(string)` - region submode. One of `'v'`, `'V'`, `'<C-v>'` (escaped).
---@diagnostic disable:undefined-field
---@diagnostic disable:discard-returns
---@diagnostic disable:unused-local
---@diagnostic disable:cast-local-type
-- Module definition ==========================================================
local MiniOperators = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniOperators.config|.
---
---@usage `require('mini.operators').setup({})` (replace `{}` with your `config` table).
MiniOperators.setup = function(config)
-- Export module
_G.MiniOperators = MiniOperators
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
-- 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 # Evaluate ~
---
--- `evaluate.prefix` is a string used to automatically infer operator mappings keys
--- during |MiniOperators.setup()|. See |MiniOperators-mappings|.
---
--- `evaluate.func` is a function used to actually evaluate text region.
--- If `nil` (default), |MiniOperators.default_evaluate_func()| is used.
---
--- This function will take content table representing selected text as input
--- and should return array of lines as output (each item per line).
--- Content table has fields `lines`, array of region lines, and `submode`,
--- one of `v`, `V`, `\22` (escaped `<C-v>`) for charwise, linewise, and blockwise.
---
--- To customize evaluation per language, set `evaluate.func` in buffer-local
--- config (`vim.b.minioperators_config`; see |mini.nvim-buffer-local-config|).
---
--- # Exchange ~
---
--- `exchange.prefix` is a string used to automatically infer operator mappings keys
--- during |MiniOperators.setup()|. See |MiniOperators-mappings|.
---
--- Note: default value "gx" overrides |netrw-gx| and |gx| / |v_gx|. If you prefer
--- using its original functionality, choose different `config.prefix`.
---
--- `exchange.reindent_linewise` is a boolean indicating whether newly put linewise
--- text should preserve indent of replaced text. In other words, if `false`,
--- regions are exchanged preserving their indents; if `true` - without them.
---
--- # Multiply ~
---
--- `multiply.prefix` is a string used to automatically infer operator mappings keys
--- during |MiniOperators.setup()|. See |MiniOperators-mappings|.
---
--- `multiply.func` is a function used to optionally update multiplied text.
--- If `nil` (default), text used as is.
---
--- Takes content table as input (see "Evaluate" section) and should return
--- array of lines as output.
---
--- # Replace ~
---
--- `replace.prefix` is a string used to automatically infer operator mappings keys
--- during |MiniOperators.setup()|. See |MiniOperators-mappings|.
---
--- `replace.reindent_linewise` is a boolean indicating whether newly put linewise
--- text should preserve indent of replaced text.
---
--- # Sort ~
---
--- `sort.prefix` is a string used to automatically infer operator mappings keys
--- during |MiniOperators.setup()|. See |MiniOperators-mappings|.
---
--- `sort.func` is a function used to actually sort text region.
--- If `nil` (default), |MiniOperators.default_sort_func()| is used.
---
--- Takes content table as input (see "Evaluate" section) and should return
--- array of lines as output.
---
--- Example of `sort.func` which asks user for custom delimiter for charwise region: >
---
--- local sort_func = function(content)
--- local opts = {}
--- if content.submode == 'v' then
--- -- Ask for delimiter to be treated as is (not as Lua pattern)
--- local delimiter = vim.fn.input('Sort delimiter: ')
--- -- Treat surrounding whitespace as part of split
--- opts.split_patterns = { '%s*' .. vim.pesc(delimiter) .. '%s*' }
--- end
--- return MiniOperators.default_sort_func(content, opts)
--- end
---
--- require('mini.operators').setup({ sort = { func = sort_func } })
MiniOperators.config = {
-- Each entry configures one operator.
-- `prefix` defines keys mapped during `setup()`: in Normal mode
-- to operate on textobject and line, in Visual - on selection.
-- Evaluate text and replace with output
evaluate = {
prefix = 'g=',
-- Function which does the evaluation
func = nil,
},
-- Exchange text regions
exchange = {
prefix = 'gx',
-- Whether to reindent new text to match previous indent
reindent_linewise = true,
},
-- Multiply (duplicate) text
multiply = {
prefix = 'gm',
-- Function which can modify text before multiplying
func = nil,
},
-- Replace text with register
replace = {
prefix = 'gr',
-- Whether to reindent new text to match previous indent
reindent_linewise = true,
},
-- Sort text
sort = {
prefix = 'gs',
-- Function which does the sort
func = nil,
}
}
--minidoc_afterlines_end
--- Evaluate text and replace with output
---
--- It replaces the region with the output of `config.evaluate.func`.
--- By default it is |MiniOperators.default_evaluate_func()| which evaluates
--- text as Lua code depending on the region submode.
---
---@param mode __operators_mode
MiniOperators.evaluate = function(mode)
if H.is_disabled() or not vim.bo.modifiable then return '' end
-- If used without arguments inside expression mapping, set it as
-- 'operatorfunc' and call it again as a result of expression mapping.
if mode == nil then
vim.o.operatorfunc = 'v:lua.MiniOperators.evaluate'
return 'g@'
end
local evaluate_func = H.get_config().evaluate.func or MiniOperators.default_evaluate_func
local data = H.get_region_data(mode)
data.reindent_linewise = true
H.apply_content_func(evaluate_func, data)
end
--- Exchange text regions
---
--- Has two-step logic:
--- - First call remembers the region as the one to be exchanged and highlights it
--- with `MiniOperatorsExchangeFrom` highlight group.
--- - Second call performs the exchange. Basically, a two substeps action:
--- "yank both regions" and replace each one with another.
---
--- Notes:
--- - Use `<C-c>` to stop exchanging after the first step.
---
--- - Exchanged regions can have different (char,line,block)-wise submodes.
---
--- - Works with most cases of intersecting regions, but not officially supported.
---
---@param mode __operators_mode
MiniOperators.exchange = function(mode)
if H.is_disabled() or not vim.bo.modifiable then return '' end
-- If used without arguments inside expression mapping, set it as
-- 'operatorfunc' and call it again as a result of expression mapping.
if mode == nil then
vim.o.operatorfunc = 'v:lua.MiniOperators.exchange'
return 'g@'
end
-- Depending on present cache data, perform exchange step
if not H.exchange_has_step_one() then
-- Store data about first region
H.cache.exchange.step_one = H.exchange_set_region_extmark(mode, true)
-- Temporarily remap `<C-c>` to stop the exchange
H.exchange_set_stop_mapping()
else
-- Store data about second region
H.cache.exchange.step_two = H.exchange_set_region_extmark(mode, false)
-- Do exchange
H.exchange_do()
-- Stop exchange
H.exchange_stop()
end
end
--- Multiply (duplicate) text
---
--- Copies a region (without affecting registers) and puts it directly after.
---
--- Notes:
--- - Supports two types of |[count]|: `[count1]gm[count2][textobject]` with default
--- `config.multiply.prefix` makes `[count1]` copies of region defined by
--- `[count2][textobject]`. Example: `2gm3aw` - 2 copies of `3aw`.
---
--- - |[count]| for "line" mapping (`gmm` by default) is treated as `[count1]` from
--- previous note.
---
--- - Advantages of using this instead of "yank" + "paste":
--- - Doesn't modify any register, while separate steps need some register to
--- hold multiplied text.
--- - In most cases separate steps would be "yank" + "move cursor" + "paste",
--- while "multiply" makes it at once.
---
---@param mode __operators_mode
MiniOperators.multiply = function(mode)
if H.is_disabled() or not vim.bo.modifiable then return '' end
-- If used without arguments inside expression mapping, set it as
-- 'operatorfunc' and call it again as a result of expression mapping.
if mode == nil then
vim.o.operatorfunc = 'v:lua.MiniOperators.multiply'
H.cache.multiply = { count = vim.v.count1 }
-- Reset count to allow two counts: first for paste, second for textobject
return vim.api.nvim_replace_termcodes('<Cmd>echon ""<CR>g@', true, true, true)
end
local count = mode == 'visual' and vim.v.count1 or H.cache.multiply.count
local data = H.get_region_data(mode)
local mark_from, mark_to, submode = data.mark_from, data.mark_to, data.submode
H.with_temp_context({ registers = { 'x', '"' } }, function()
-- Yank to temporary "x" register
local yank_data = { mark_from = mark_from, mark_to = mark_to, submode = submode, mode = mode, register = 'x' }
H.do_between_marks('y', yank_data)
-- Modify lines in "x" register
local func = H.get_config().multiply.func or function(content) return content.lines end
local x_reginfo = vim.fn.getreginfo('x')
x_reginfo.regcontents = func({ lines = x_reginfo.regcontents, submode = submode })
vim.fn.setreg('x', x_reginfo)
-- Adjust cursor for a proper paste
local ref_coords = H.multiply_get_ref_coords(mark_from, mark_to, submode)
vim.api.nvim_win_set_cursor(0, ref_coords)
-- Paste after textobject from temporary register
H.cmd_normal(count .. '"xp')
-- Adjust cursor to be at start of pasted text. Not in linewise mode as it
-- already is at first non-blank, while this moves to first column.
if submode ~= 'V' then vim.cmd('normal! `[') end
end)
end
--- Replace text with register
---
--- Notes:
--- - Supports two types of |[count]|: `[count1]gr[count2][textobject]` with default
--- `config.replace.prefix` puts `[count1]` contents of register over region defined
--- by `[count2][textobject]`. Example: `2gr3aw` - 2 register contents over `3aw`.
---
--- - |[count]| for "line" mapping (`grr` by default) is treated as `[count1]` from
--- previous note.
---
--- - Advantages of using this instead of "visually select" + "paste with |v_P|":
--- - As operator it is dot-repeatable which has cumulative gain in case of
--- multiple replacing is needed.
--- - Can automatically reindent.
---
---@param mode __operators_mode
MiniOperators.replace = function(mode)
if H.is_disabled() or not vim.bo.modifiable then return '' end
-- If used without arguments inside expression mapping, set it as
-- 'operatorfunc' and call it again as a result of expression mapping.
if mode == nil then
vim.o.operatorfunc = 'v:lua.MiniOperators.replace'
H.cache.replace = { count = vim.v.count1, register = vim.v.register }
-- Reset count to allow two counts: first for paste, second for textobject
return vim.api.nvim_replace_termcodes('<Cmd>echon ""<CR>g@', true, true, true)
end
-- Do replace
-- - Compute `count` and `register` prior getting region data because it
-- invalidates them for active Visual mode
local count = mode == 'visual' and vim.v.count1 or H.cache.replace.count
local register = mode == 'visual' and vim.v.register or H.cache.replace.register
local data = H.get_region_data(mode)
data.count = count
data.register = register
data.reindent_linewise = H.get_config().replace.reindent_linewise
H.replace_do(data)
return ''
end
--- Sort text
---
--- It replaces the region with the output of `config.sort.func`.
--- By default it is |MiniOperators.default_sort_func()| which sorts the text
--- depending on submode.
---
--- Notes:
--- - "line" mapping is charwise (as there is not much sense in sorting
--- linewise a single line). This also results into no |[count]| support.
---
---@param mode __operators_mode
MiniOperators.sort = function(mode)
if H.is_disabled() or not vim.bo.modifiable then return '' end
-- If used without arguments inside expression mapping, set it as
-- 'operatorfunc' and call it again as a result of expression mapping.
if mode == nil then
vim.o.operatorfunc = 'v:lua.MiniOperators.sort'
return 'g@'
end
local sort_func = H.get_config().sort.func or MiniOperators.default_sort_func
H.apply_content_func(sort_func, H.get_region_data(mode))
end
--- Make operator mappings
---
---@param operator_name string Name of existing operator from this module.
---@param lhs_tbl table Table with mappings keys. Should have these fields:
--- - <textobject> `(string)` - Normal mode mapping to operate on textobject.
--- - <line> `(string)` - Normal mode mapping to operate on line.
--- Usually an alias for textobject mapping followed by |_|.
--- For "sort" it operates charwise on whole line without left and right
--- whitespace (as there is not much sense in sorting linewise a single line).
--- - <selection> `(string)` - Visual mode mapping to operate on selection.
---
--- Supply empty string to not create particular mapping. Note: creating `line`
--- mapping needs `textobject` mapping to be set.
---
---@usage >
--- require('mini.operators').make_mappings(
--- 'replace',
--- { textobject = 'cr', line = 'crr', selection = 'cr' }
--- )
MiniOperators.make_mappings = function(operator_name, lhs_tbl)
-- Validate arguments
if not (type(operator_name) == 'string' and MiniOperators[operator_name] ~= nil) then
H.error('`operator_name` should be a valid operator name.')
end
local is_keys_tbl = type(lhs_tbl) == 'table'
and type(lhs_tbl.textobject) == 'string'
and type(lhs_tbl.line) == 'string'
and type(lhs_tbl.selection) == 'string'
if not is_keys_tbl then H.error('`lhs_tbl` should be a valid table of keys.') end
if lhs_tbl.line ~= '' and lhs_tbl.textobject == '' then
H.error('Creating mapping for `line` needs mapping for `textobject`.')
end
-- Make mappings
local operator_desc = operator_name:sub(1, 1):upper() .. operator_name:sub(2)
local expr_opts = { expr = true, replace_keycodes = false, desc = operator_desc .. ' operator' }
H.map('n', lhs_tbl.textobject, string.format('v:lua.MiniOperators.%s()', operator_name), expr_opts)
local rhs = lhs_tbl.textobject .. '_'
-- - Make `sort()` line mapping to be charwise
if operator_name == 'sort' then rhs = '^' .. lhs_tbl.textobject .. 'g_' end
H.map('n', lhs_tbl.line, rhs, { remap = true, desc = operator_desc .. ' line' })
local visual_rhs = string.format([[<Cmd>lua MiniOperators.%s('visual')<CR>]], operator_name)
H.map('x', lhs_tbl.selection, visual_rhs, { desc = operator_desc .. ' selection' })
end
--- Default evaluate function
---
--- Evaluate text as Lua code and return object from last line (like if last
--- line is prepended with `return` if it is not already).
---
--- Behavior depends on region submode:
---
--- - For charwise and linewise regions, text evaluated as is.
---
--- - For blockwise region, lines are evaluated per line using only first lines
--- of outputs. This allows separate execution of lines in order to provide
--- something different compared to linewise region.
---
---@param content __operators_content
MiniOperators.default_evaluate_func = function(content)
if not H.is_content(content) then H.error('`content` should be a content table.') end
local lines, submode = content.lines, content.submode
-- In non-blockwise mode return the result of the last line
if submode ~= H.submode_keys.block then return H.eval_lua_lines(lines) end
-- In blockwise selection evaluate and return each line separately
return vim.tbl_map(function(l) return H.eval_lua_lines({ l })[1] end, lines)
end
--- Default sort function
---
--- Sort text based on region submode:
---
--- - For charwise region, split by separator pattern, sort parts, merge back
--- with separators. Actual pattern is inferred based on the array of patterns
--- from `opts.split_patterns`: whichever element is present in the text is
--- used, preferring the earlier one if several are present.
--- Example: sorting "c, b; a" line with default `opts.split_patterns` results
--- into "b; a, c" as it is split only by comma.
---
--- - For linewise and blockwise regions sort lines as is.
---
--- Notes:
--- - Sort is done with |table.sort()| on an array of lines, which doesn't treat
--- whitespace or digits specially. Use |:sort| for more complicated tasks.
---
--- - Pattern is allowed to be an empty string in which case split results into
--- all characters as parts.
---
--- - Pad pattern in `split_patterns` with `%s*` to include whitespace into separator.
--- Example: line "b _ a" with "_" pattern will be sorted as " a_b " (because
--- it is split as "b ", "_", " a" ) while with "%s*_%s*" pattern it results
--- into "a _ b" (split as "b", " _ ", "a").
---
---@param content __operators_content
---@param opts table|nil Options. Possible fields:
--- - <compare_fun> `(function)` - compare function compatible with |table.sort()|.
--- Default: direct compare with `<`.
--- - <split_patterns> `(table)` - array of split Lua patterns to be used for
--- charwise submode. Order is important.
--- Default: `{ '%s*,%s*', '%s*;%s*', '%s+', '' }`.
MiniOperators.default_sort_func = function(content, opts)
if not H.is_content(content) then H.error('`content` should be a content table.') end
opts = vim.tbl_deep_extend('force', { compare_fun = nil, split_patterns = nil }, opts or {})
local compare_fun = opts.compare_fun or function(a, b) return a < b end
if not vim.is_callable(compare_fun) then H.error('`opts.compare_fun` should be callable.') end
local split_patterns = opts.split_patterns or { '%s*,%s*', '%s*;%s*', '%s+', '' }
if not vim.tbl_islist(split_patterns) then H.error('`opts.split_patterns` should be array.') end
-- Prepare lines to sort
local lines, submode = content.lines, content.submode
if submode ~= 'v' then
table.sort(lines, compare_fun)
return lines
end
local parts, seps = H.sort_charwise_split(lines, split_patterns)
table.sort(parts, compare_fun)
return H.sort_charwise_unsplit(parts, seps)
end
-- Helper data ================================================================
-- Module default config
H.default_config = vim.deepcopy(MiniOperators.config)
-- Namespaces
H.ns_id = {
exchange = vim.api.nvim_create_namespace('MiniOperatorsExchange'),
}
-- Cache for all operators
H.cache = {
exchange = {},
multiply = {},
replace = {},
}
-- Submode keys for
H.submode_keys = {
char = 'v',
line = 'V',
block = vim.api.nvim_replace_termcodes('<C-v>', true, true, true),
}
-- Helper functionality =======================================================
-- Settings -------------------------------------------------------------------
H.setup_config = function(config)
-- General idea: if some table elements are not present in user-supplied
-- `config`, take them from default config
vim.validate({ config = { config, 'table', true } })
config = vim.tbl_deep_extend('force', vim.deepcopy(H.default_config), config or {})
vim.validate({
evaluate = { config.evaluate, 'table' },
exchange = { config.exchange, 'table' },
multiply = { config.multiply, 'table' },
replace = { config.replace, 'table' },
sort = { config.sort, 'table' },
})
vim.validate({
['evaluate.prefix'] = { config.evaluate.prefix, 'string' },
['evaluate.func'] = { config.evaluate.func, 'function', true },
['exchange.prefix'] = { config.exchange.prefix, 'string' },
['exchange.reindent_linewise'] = { config.exchange.reindent_linewise, 'boolean' },
['multiply.prefix'] = { config.multiply.prefix, 'string' },
['multiply.func'] = { config.multiply.func, 'function', true },
['replace.prefix'] = { config.replace.prefix, 'string' },
['replace.reindent_linewise'] = { config.replace.reindent_linewise, 'boolean' },
['sort.prefix'] = { config.sort.prefix, 'string' },
['sort.func'] = { config.sort.func, 'function', true },
})
return config
end
H.apply_config = function(config)
MiniOperators.config = config
-- Make mappings
local map_all = function(operator_name)
-- Map only valid LHS
local prefix = config[operator_name].prefix
if type(prefix) ~= 'string' or prefix == '' then return end
local lhs_tbl = {
textobject = prefix,
line = prefix .. vim.fn.strcharpart(prefix, vim.fn.strchars(prefix) - 1, 1),
selection = prefix,
}
MiniOperators.make_mappings(operator_name, lhs_tbl)
end
map_all('evaluate')
map_all('exchange')
map_all('multiply')
map_all('replace')
map_all('sort')
end
H.is_disabled = function() return vim.g.minioperators_disable == true or vim.b.minioperators_disable == true end
H.get_config = function(config)
return vim.tbl_deep_extend('force', MiniOperators.config, vim.b.minioperators_config or {}, config or {})
end
H.create_default_hl = function()
vim.api.nvim_set_hl(0, 'MiniOperatorsExchangeFrom', { default = true, link = 'IncSearch' })
end
-- Evaluate -------------------------------------------------------------------
H.eval_lua_lines = function(lines)
-- Copy to not modify input
local lines_copy, n = vim.deepcopy(lines), #lines
lines_copy[n] = (lines_copy[n]:find('^%s*return%s+') == nil and 'return ' or '') .. lines_copy[n]
local str_to_eval = table.concat(lines_copy, '\n')
-- Allow returning tuple with any value(s) being `nil`
return H.inspect_objects(assert(loadstring(str_to_eval))())
end
H.inspect_objects = function(...)
local objects = {}
-- Not using `{...}` because it removes `nil` input
for i = 1, select('#', ...) do
local v = select(i, ...)
table.insert(objects, vim.inspect(v))
end
return vim.split(table.concat(objects, '\n'), '\n')
end
-- Exchange -------------------------------------------------------------------
H.exchange_do = function()
local step_one, step_two = H.cache.exchange.step_one, H.cache.exchange.step_two
-- Do nothing if regions are the same
if H.exchange_is_same_steps(step_one, step_two) then return end
-- Save temporary registers
local reg_one, reg_two = vim.fn.getreginfo('a'), vim.fn.getreginfo('b')
-- Create step temporary contexts (data that should not change)
local context_one = { buf_id = step_one.buf_id, marks = { 'x', 'y' }, registers = { '"' } }
local context_two = { buf_id = step_two.buf_id, marks = { 'x', 'y' }, registers = { '"' } }
-- Put regions into registers. NOTE: do it before actual exchange to allow
-- intersecting regions.
local populating_register = function(step, register)
return function()
H.exchange_set_step_marks(step, { 'x', 'y' })
local yank_data =
{ mark_from = 'x', mark_to = 'y', submode = step.submode, mode = step.mode, register = register }
H.do_between_marks('y', yank_data)
end
end
H.with_temp_context(context_one, populating_register(step_one, 'a'))
H.with_temp_context(context_two, populating_register(step_two, 'b'))
-- Sequentially replace
local replacing = function(step, register)
return function()
H.exchange_set_step_marks(step, { 'x', 'y' })
local replace_data = {
count = 1,
mark_from = 'x',
mark_to = 'y',
mode = step.mode,
register = register,
reindent_linewise = H.get_config().exchange.reindent_linewise,
submode = step.submode,
}
H.replace_do(replace_data)
end
end
H.with_temp_context(context_one, replacing(step_one, 'b'))
H.with_temp_context(context_two, replacing(step_two, 'a'))
-- Restore temporary registers
vim.fn.setreg('a', reg_one)
vim.fn.setreg('b', reg_two)
end
H.exchange_has_step_one = function()
local step_one = H.cache.exchange.step_one
if type(step_one) ~= 'table' then return false end
if not vim.api.nvim_buf_is_valid(step_one.buf_id) then
H.exchange_stop()
return false
end
return true
end
H.exchange_set_region_extmark = function(mode, add_highlight)
local ns_id = H.ns_id.exchange
-- Compute regular marks for target region
local region_data = H.get_region_data(mode)
local submode = region_data.submode
local markcoords_from, markcoords_to = H.get_mark(region_data.mark_from), H.get_mark(region_data.mark_to)
-- Compute extmark's range for target region
local extmark_from = { markcoords_from[1] - 1, markcoords_from[2] }
local extmark_to = { markcoords_to[1] - 1, H.get_next_char_bytecol(markcoords_to) }
-- Adjust for visual selection in case of 'selection=exclusive'
if region_data.mark_to == '>' and vim.o.selection == 'exclusive' then extmark_to[2] = extmark_to[2] - 1 end
-- - Tweak columns for linewise marks
if submode == 'V' then
extmark_from[2] = 0
extmark_to[2] = vim.fn.col({ extmark_to[1] + 1, '$' }) - 1
end
-- Set extmark to represent region. Add highlighting inside of it only if
-- needed and not in blockwise submode (can't highlight that way).
local buf_id = vim.api.nvim_get_current_buf()
local extmark_hl_group
if add_highlight and submode ~= H.submode_keys.block then extmark_hl_group = 'MiniOperatorsExchangeFrom' end
local extmark_opts = {
end_row = extmark_to[1],
end_col = extmark_to[2],
hl_group = extmark_hl_group,
-- Using this gravity is better for handling empty lines in linewise mode
end_right_gravity = mode == 'line',
}
local region_extmark_id = vim.api.nvim_buf_set_extmark(buf_id, ns_id, extmark_from[1], extmark_from[2], extmark_opts)
-- - Possibly add highlighting for blockwise mode
if add_highlight and extmark_hl_group == nil then
-- Highlighting blockwise region needs full register type with width
local opts = { regtype = H.exchange_get_blockwise_regtype(markcoords_from, markcoords_to) }
vim.highlight.range(buf_id, ns_id, 'MiniOperatorsExchangeFrom', extmark_from, extmark_to, opts)
end
-- Return data to cache
return { buf_id = buf_id, mode = mode, submode = submode, extmark_id = region_extmark_id }
end
H.exchange_get_region_extmark = function(step)
return vim.api.nvim_buf_get_extmark_by_id(step.buf_id, H.ns_id.exchange, step.extmark_id, { details = true })
end
H.exchange_set_step_marks = function(step, mark_names)
local extmark_details = H.exchange_get_region_extmark(step)
H.set_mark(mark_names[1], { extmark_details[1] + 1, extmark_details[2] })
-- Unadjust for visual selection in case of 'selection=exclusive'
local should_unadjust = step.mode == 'visual' and vim.o.selection == 'exclusive'
local col_offset = should_unadjust and 1 or 0
H.set_mark(mark_names[2], { extmark_details[3].end_row + 1, extmark_details[3].end_col - 1 + col_offset })
end
H.exchange_get_blockwise_regtype = function(markcoords_from, markcoords_to)
local f = function()
-- Yank into "z" register and return its blockwise type
H.set_mark('x', markcoords_from)
H.set_mark('y', markcoords_to)
local yank_data = { mark_from = 'x', mark_to = 'y', submode = H.submode_keys.block, mode = 'block', register = 't' }
H.do_between_marks('y', yank_data)
return vim.fn.getregtype('t')
end
return H.with_temp_context({ buf_id = 0, marks = { 'x', 'y' }, registers = { 't' } }, f)
end
H.exchange_stop = function()
H.exchange_del_stop_mapping()
local cur, ns_id = H.cache.exchange, H.ns_id.exchange
if cur.step_one ~= nil then pcall(vim.api.nvim_buf_clear_namespace, cur.step_one.buf_id, ns_id, 0, -1) end
if cur.step_two ~= nil then pcall(vim.api.nvim_buf_clear_namespace, cur.step_two.buf_id, ns_id, 0, -1) end
H.cache.exchange = {}
end
H.exchange_set_stop_mapping = function()
local lhs = '<C-c>'
H.cache.exchange.stop_restore_map_data = vim.fn.maparg(lhs, 'n', false, true)
vim.keymap.set('n', lhs, H.exchange_stop, { desc = 'Stop exchange' })
end
H.exchange_del_stop_mapping = function()
local map_data = H.cache.exchange.stop_restore_map_data
if map_data == nil then return end
-- Try restore previous mapping if it was set. NOTE: Neovim<0.8 doesn't have
-- `mapset()`, so resort to deleting.
if vim.tbl_count(map_data) > 0 and vim.fn.has('nvim-0.8') == 1 then
vim.fn.mapset('n', false, map_data)
else
vim.keymap.del('n', map_data.lhs or '<C-c>')
end
end
H.exchange_is_same_steps = function(step_one, step_two)
if step_one.buf_id ~= step_two.buf_id or step_one.submode ~= step_two.submode then return false end
-- Region's start and end should be the same
local one, two = H.exchange_get_region_extmark(step_one), H.exchange_get_region_extmark(step_two)
return one[1] == two[1] and one[2] == two[2] and one[3].end_row == two[3].end_row and one[3].end_col == two[3].end_col
end
-- Multiply -------------------------------------------------------------------
H.multiply_get_ref_coords = function(mark_from, mark_to, submode)
local markcoords_from, markcoords_to = H.get_mark(mark_from), H.get_mark(mark_to)
if mark_to == '>' and vim.o.selection == 'exclusive' then markcoords_to[2] = markcoords_to[2] - 1 end
if submode ~= H.submode_keys.block then return markcoords_to end
-- In blockwise selection go to top right corner (allowing for presence of
-- multibyte characters)
local row = math.min(markcoords_from[1], markcoords_to[1])
if vim.fn.has('nvim-0.8') == 0 then
-- Neovim<0.8 doesn't have `virtcol2col()`
local col = math.max(markcoords_from[2], markcoords_to[2])
return { row, col - 1 }
end
-- - "from"/"to" may not only be "top-left"/"bottom-right" but also
-- "top-right" and "bottom-left"
local virtcol_from = vim.fn.virtcol({ markcoords_from[1], markcoords_from[2] + 1 })
local virtcol_to = vim.fn.virtcol({ markcoords_to[1], markcoords_to[2] + 1 })
local virtcol = math.max(virtcol_from, virtcol_to)
local col = vim.fn.virtcol2col(0, row, virtcol)
return { row, col - 1 }
end
-- Replace --------------------------------------------------------------------
--- Delete region between two marks and paste from register
---
---@param data table Fields:
--- - <count> (optional) - Number of times to paste.
--- - <mark_from> - Name of "from" mark.
--- - <mark_to> - Name of "to" mark.
--- - <mode> - Operator mode. One of 'visual', 'char', 'line', 'block'.
--- - <register> - Name of register from which to paste.
--- - <submode> - Region submode. One of 'v', 'V', '\22'.
---@private
H.replace_do = function(data)
-- NOTE: Ideally, implementation would leverage "Visually select - press `P`"
-- approach, but it has issues with dot-repeat. The `cancel_redo()` approach
-- doesn't work probably because `P` implementation uses more than one
-- dot-repeat overwrite.
local register, submode = data.register, data.submode
local mark_from, mark_to = data.mark_from, data.mark_to
-- Do nothing with empty/unknown register
local register_type = H.get_reg_type(register)
if register_type == '' then H.error('Register ' .. vim.inspect(register) .. ' is empty or unknown.') end
-- Determine if region is at edge which is needed for the correct paste key
local from_line, from_col = unpack(H.get_mark(mark_from))
local to_line, to_col = unpack(H.get_mark(mark_to))
local edge_to_col = vim.fn.col({ to_line, '$' }) - 1 - (vim.o.selection == 'exclusive' and 0 or 1)
local is_edge_line = submode == 'V' and to_line == vim.fn.line('$')
local is_edge_col = submode ~= 'V' and to_col == edge_to_col
local is_edge = is_edge_line or is_edge_col
local covers_linewise_all_buffer = is_edge_line and from_line == 1
-- Compute current indent if needed
local init_indent
local should_reindent = data.reindent_linewise and data.submode == 'V' and vim.o.equalprg == ''
if should_reindent then init_indent = H.get_region_indent(mark_from, mark_to) end
-- Delete region to black whole register
-- - Delete single character in blockwise submode with inclusive motion.
-- See https://github.com/neovim/neovim/issues/24613
local is_blockwise_single_cell = submode == H.submode_keys.block and from_line == to_line and from_col == to_col
local forced_motion = is_blockwise_single_cell and 'v' or submode